diff --git a/_maps/RandomRuins/SpaceRuins/oldstation.dmm b/_maps/RandomRuins/SpaceRuins/oldstation.dmm
index a857a39a40a..180a0eb6d41 100644
--- a/_maps/RandomRuins/SpaceRuins/oldstation.dmm
+++ b/_maps/RandomRuins/SpaceRuins/oldstation.dmm
@@ -288,7 +288,9 @@
/turf/open/floor/iron,
/area/ruin/space/ancientstation/charlie/bridge)
"bA" = (
-/obj/structure/ai_core/deactivated,
+/obj/structure/ai_core{
+ state = 5
+ },
/turf/open/floor/iron/dark,
/area/ruin/space/ancientstation/delta/ai)
"bC" = (
diff --git a/code/__DEFINES/construction/structures.dm b/code/__DEFINES/construction/structures.dm
index e48dc078ca4..874978a8cc9 100644
--- a/code/__DEFINES/construction/structures.dm
+++ b/code/__DEFINES/construction/structures.dm
@@ -1,12 +1,12 @@
//Defines for construction states
//ai core defines
-#define EMPTY_CORE 0
-#define CIRCUIT_CORE 1
-#define SCREWED_CORE 2
-#define CABLED_CORE 3
-#define GLASS_CORE 4
-#define AI_READY_CORE 5
+#define CORE_STATE_EMPTY 0
+#define CORE_STATE_CIRCUIT 1
+#define CORE_STATE_SCREWED 2
+#define CORE_STATE_CABLED 3
+#define CORE_STATE_GLASSED 4
+#define CORE_STATE_FINISHED 5
//girder construction states
#define GIRDER_NORMAL 0
diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_silicon.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_silicon.dm
index 564465dfcf2..f68b0840205 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_silicon.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_silicon.dm
@@ -8,10 +8,6 @@
#define COMSIG_BORG_HUG_HANDLED 1
///called from /mob/living/silicon/attack_hand proc
#define COMSIG_MOB_PAT_BORG "mob_pat_borg"
-///called when someone is inquiring about an AI's linked core
-#define COMSIG_SILICON_AI_CORE_STATUS "AI_core_status"
- #define COMPONENT_CORE_ALL_GOOD (1<<0)
- #define COMPONENT_CORE_DISCONNECTED (1<<1)
///called when an AI (malf or perhaps combat upgraded or some other circumstance that has them inhabit
///an APC) enters an APC
#define COMSIG_SILICON_AI_OCCUPY_APC "AI_occupy_apc"
diff --git a/code/game/machinery/ai_core/_core.dm b/code/game/machinery/ai_core/_core.dm
new file mode 100644
index 00000000000..50fb2a9c732
--- /dev/null
+++ b/code/game/machinery/ai_core/_core.dm
@@ -0,0 +1,247 @@
+#define AI_CORE_BRAIN(X) X.braintype == "Android" ? "brain" : "MMI"
+
+/obj/structure/ai_core
+ name = "\improper AI core"
+ desc = "The framework for an artificial intelligence core."
+ icon = 'icons/mob/silicon/ai.dmi'
+ icon_state = "build_0"
+ base_icon_state = "build_"
+ density = TRUE
+ anchored = FALSE
+ max_integrity = 500
+ var/state = CORE_STATE_EMPTY
+ var/datum/ai_laws/laws
+ var/obj/item/circuitboard/aicore/circuit
+ var/obj/item/mmi/core_mmi
+
+/obj/structure/ai_core/Initialize(mapload, state = src.state, obj/item/mmi/core_mmi = null)
+ . = ..()
+ laws = new
+ laws.set_laws_config()
+
+ if(core_mmi && state < CORE_STATE_CABLED)
+ stack_trace("supplied a core_mmi as constructor argument, but core state wouldn't have accepted it!")
+ state = CORE_STATE_FINISHED // just in case...
+ src.state = state
+ if(state >= CORE_STATE_CIRCUIT)
+ circuit = new(src)
+ if(state >= CORE_STATE_CABLED)
+ if(!core_mmi)
+ core_mmi = new /obj/item/mmi(src)
+ core_mmi.brain = new(core_mmi)
+ core_mmi.brain.organ_flags |= ORGAN_FROZEN
+ core_mmi.set_brainmob(new /mob/living/brain())
+ core_mmi.brainmob.container = core_mmi
+ core_mmi.update_appearance()
+ core_mmi.forceMove(src)
+ src.core_mmi = core_mmi
+ set_anchored(TRUE)
+
+ update_appearance(UPDATE_ICON_STATE)
+
+/obj/structure/ai_core/update_icon_state()
+ if(state != CORE_STATE_FINISHED)
+ icon_state = "[base_icon_state][state]"
+ if(state == CORE_STATE_CABLED && core_mmi)
+ icon_state += "b"
+ else
+ icon_state = "ai-empty"
+ return ..()
+
+/obj/structure/ai_core/Exited(atom/movable/gone, direction)
+ . = ..()
+ if(gone == circuit)
+ circuit = null
+ if((state != CORE_STATE_GLASSED) && (state != CORE_STATE_FINISHED))
+ state = CORE_STATE_EMPTY
+ update_appearance()
+ if(gone == core_mmi)
+ core_mmi = null
+ update_appearance()
+
+/obj/structure/ai_core/atom_deconstruct(disassembled = TRUE)
+ if(state >= CORE_STATE_GLASSED)
+ new /obj/item/stack/sheet/rglass(drop_location(), 2)
+ if(state >= CORE_STATE_CABLED)
+ new /obj/item/stack/cable_coil(drop_location(), 5)
+ core_mmi?.forceMove(drop_location())
+ circuit?.forceMove(drop_location())
+ new /obj/item/stack/sheet/plasteel(drop_location(), 4)
+
+/obj/structure/ai_core/Destroy()
+ QDEL_NULL(circuit)
+ QDEL_NULL(core_mmi)
+ QDEL_NULL(laws)
+ return ..()
+
+/obj/structure/ai_core/examine(mob/user)
+ . = ..()
+ . += span_notice("It has some bolts that look [anchored ? "tightened" : "loosened"].")
+
+ switch(state)
+ if(CORE_STATE_EMPTY)
+ . += span_notice("There is a slot for a circuit board, the frame can be melted down.")
+ if(CORE_STATE_CIRCUIT)
+ . += span_notice("The circuit board can be screwed into place or pried out.")
+ if(CORE_STATE_SCREWED)
+ . += span_notice("The frame can be wired, the circuit board can be unfastened.")
+ if(CORE_STATE_CABLED)
+ if(!core_mmi)
+ . += span_notice("There are wires which could be hooked up to an MMI or positronic brain, or cut.")
+ else
+ var/accept_laws = TRUE
+ if(core_mmi.laws.id != DEFAULT_AI_LAWID || !core_mmi.brainmob || !core_mmi.brainmob?.mind)
+ accept_laws = FALSE
+ . += span_notice("There is a slot for a reinforced glass panel, the [AI_CORE_BRAIN(core_mmi)] could be pried out.[accept_laws ? " A law module can be swiped across." : ""]")
+ if(CORE_STATE_GLASSED)
+ . += span_notice("The monitor [core_mmi?.brainmob?.mind && !suicide_check() ? "and neural interface " : ""]can be screwed in, the panel can be pried out.")
+ if(CORE_STATE_FINISHED)
+ . += span_notice("The monitor's connection can be cut[core_mmi?.brainmob?.mind && !suicide_check() ? " the neural interface can be screwed in." : "."]")
+
+/obj/structure/ai_core/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
+ if(state < CORE_STATE_FINISHED)
+ return construction_item_interaction(user, tool, modifiers)
+
+ return NONE
+
+/// Exists to be used for callbacks.
+/obj/structure/ai_core/proc/check_state(state_to_check)
+ return (state == state_to_check)
+
+/obj/structure/ai_core/latejoin_inactive
+ name = "networked AI core"
+ desc = "This AI core is connected by bluespace transmitters to NTNet, allowing for an AI personality to be downloaded to it on the fly mid-shift."
+ anchored = TRUE
+ state = CORE_STATE_FINISHED
+ var/available = TRUE
+ var/safety_checks = TRUE
+ var/active = TRUE
+
+/obj/structure/ai_core/latejoin_inactive/Initialize(mapload, state, posibrain)
+ . = ..()
+ GLOB.latejoin_ai_cores += src
+
+/obj/structure/ai_core/latejoin_inactive/Destroy()
+ GLOB.latejoin_ai_cores -= src
+ return ..()
+
+/obj/structure/ai_core/latejoin_inactive/examine(mob/user)
+ . = ..()
+ . += "Its transmitter seems to be [active? "on" : "off"]."
+ . += span_notice("You could [active? "deactivate" : "activate"] it with a multitool.")
+
+/obj/structure/ai_core/latejoin_inactive/proc/is_available() //If people still manage to use this feature to spawn-kill AI latejoins ahelp them.
+ if(!available)
+ return FALSE
+ if(!safety_checks)
+ return TRUE
+ if(!active)
+ return FALSE
+ var/turf/T = get_turf(src)
+ var/area/A = get_area(src)
+ if(!(A.area_flags & BLOBS_ALLOWED))
+ return FALSE
+ if(!A.power_equip)
+ return FALSE
+ if(!SSmapping.level_trait(T.z,ZTRAIT_STATION))
+ return FALSE
+ if(!isfloorturf(T))
+ return FALSE
+ return TRUE
+
+/obj/structure/ai_core/latejoin_inactive/multitool_act(mob/living/user, obj/item/tool)
+ if(user.combat_mode)
+ return NONE
+
+ if(!tool.use_tool(src, user, 0 SECONDS, 0, 50))
+ return ITEM_INTERACT_BLOCKING
+
+ active = !active
+ balloon_alert(user, "[active ? "activated" : "deactivated"] transmitters")
+ return ITEM_INTERACT_SUCCESS
+
+/obj/structure/ai_core/proc/ai_structure_to_mob()
+ var/mob/living/brain/the_brainmob = core_mmi.brainmob
+ if(!the_brainmob.mind || suicide_check())
+ return FALSE
+ the_brainmob.mind.remove_antags_for_borging()
+ if(!the_brainmob.mind.has_ever_been_ai)
+ SSblackbox.record_feedback("amount", "ais_created", 1)
+ var/mob/living/silicon/ai/ai_mob = null
+
+ if(core_mmi.overrides_aicore_laws)
+ ai_mob = new /mob/living/silicon/ai(loc, core_mmi.laws, the_brainmob)
+ core_mmi.laws = null //MMI's law datum is being donated, so we need the MMI to let it go or the GC will eat it
+ else
+ ai_mob = new /mob/living/silicon/ai(loc, laws, the_brainmob)
+ laws = null //we're giving the new AI this datum, so let's not delete it when we qdel(src) 5 lines from now
+
+ var/datum/antagonist/malf_ai/malf_datum = IS_MALF_AI(ai_mob)
+ if(malf_datum)
+ malf_datum.add_law_zero()
+
+ if(!isnull(the_brainmob.client))
+ ai_mob.set_gender(the_brainmob.client)
+ if(core_mmi.force_replace_ai_name)
+ ai_mob.fully_replace_character_name(ai_mob.name, core_mmi.replacement_ai_name())
+ ai_mob.posibrain_inside = core_mmi.braintype == "Android"
+ deadchat_broadcast(" has been brought online at [get_area_name(ai_mob, format_text = TRUE)].", span_name("[ai_mob]"), follow_target = ai_mob, message_type = DEADCHAT_ANNOUNCEMENT)
+ qdel(src)
+ return ai_mob
+
+/// Quick proc to call to see if the brainmob inside of us has suicided. Returns TRUE if we have, FALSE in any other scenario.
+/obj/structure/ai_core/proc/suicide_check()
+ if(isnull(core_mmi) || isnull(core_mmi.brainmob))
+ return FALSE
+ return HAS_TRAIT(core_mmi.brainmob, TRAIT_SUICIDED)
+
+/*
+This is a good place for AI-related object verbs so I'm sticking it here.
+If adding stuff to this, don't forget that an AI need to cancel_camera() whenever it physically moves to a different location.
+That prevents a few funky behaviors.
+*/
+//The type of interaction, the player performing the operation, the AI itself, and the card object, if any.
+
+
+/atom/proc/transfer_ai(interaction, mob/user, mob/living/silicon/ai/AI, obj/item/aicard/card)
+ SHOULD_CALL_PARENT(TRUE)
+ if(istype(card))
+ if(card.flush)
+ to_chat(user, span_alert("ERROR: AI flush is in progress, cannot execute transfer protocol."))
+ return FALSE
+ return TRUE
+
+/obj/structure/ai_core/transfer_ai(interaction, mob/user, mob/living/silicon/ai/AI, obj/item/aicard/card)
+ if(state != CORE_STATE_FINISHED || !..())
+ return
+ if(core_mmi && core_mmi.brainmob)
+ if(core_mmi.brainmob.mind)
+ to_chat(user, span_warning("[src] already contains an active mind!"))
+ return
+ else if(suicide_check())
+ to_chat(user, span_warning("[AI_CORE_BRAIN(core_mmi)] installed in [src] is completely useless!"))
+ return
+ //Transferring a carded AI to a core.
+ if(interaction == AI_TRANS_FROM_CARD)
+ AI.set_control_disabled(FALSE)
+ AI.radio_enabled = TRUE
+ AI.forceMove(loc) // to replace the terminal.
+ to_chat(AI, span_notice("You have been uploaded to a stationary terminal. Remote device connection restored."))
+ to_chat(user, "[span_boldnotice("Transfer successful")]: [AI.name] ([rand(1000,9999)].exe) installed and executed successfully. Local copy has been removed.")
+ card.AI = null
+ AI.battery = circuit.battery
+ AI.posibrain_inside = isnull(core_mmi) || core_mmi.braintype == "Android"
+ qdel(src)
+ else //If for some reason you use an empty card on an empty AI terminal.
+ to_chat(user, span_alert("There is no AI loaded on this terminal."))
+
+/obj/item/circuitboard/aicore
+ name = "AI core (AI Core Board)" //Well, duh, but best to be consistent
+ var/battery = 200 //backup battery for when the AI loses power. Copied to/from AI mobs when carding, and placed here to avoid recharge via deconning the core
+
+/obj/item/circuitboard/aicore/Initialize(mapload)
+ . = ..()
+ if(mapload && HAS_TRAIT(SSstation, STATION_TRAIT_HUMAN_AI))
+ return INITIALIZE_HINT_QDEL
+
+#undef AI_CORE_BRAIN
diff --git a/code/game/machinery/ai_core/core_construction.dm b/code/game/machinery/ai_core/core_construction.dm
new file mode 100644
index 00000000000..df1a36782da
--- /dev/null
+++ b/code/game/machinery/ai_core/core_construction.dm
@@ -0,0 +1,253 @@
+#define AI_CORE_BRAIN(X) X.braintype == "Android" ? "brain" : "MMI"
+#define UPDATE_STATE(new_state) state = new_state; update_appearance(UPDATE_ICON_STATE)
+#define CHECK_STATE_CALLBACK(maintained_state) CALLBACK(src, PROC_REF(check_state), maintained_state)
+
+/obj/structure/ai_core/welder_act(mob/living/user, obj/item/tool)
+ if(state != CORE_STATE_EMPTY)
+ balloon_alert(user, "frame has to be empty!")
+ return ITEM_INTERACT_BLOCKING
+
+ if(!tool.tool_start_check(user, 1))
+ return ITEM_INTERACT_BLOCKING
+
+ if(!tool.use_tool(src, user, 2 SECONDS, 1, 50, CHECK_STATE_CALLBACK(CORE_STATE_EMPTY)))
+ return ITEM_INTERACT_BLOCKING
+
+ deconstruct(TRUE)
+ return ITEM_INTERACT_SUCCESS
+
+/obj/structure/ai_core/wrench_act(mob/living/user, obj/item/tool)
+ if(state >= CORE_STATE_FINISHED)
+ set_anchored(TRUE) //teehee
+ balloon_alert(user, "can't unanchor!")
+ return ITEM_INTERACT_BLOCKING
+
+ default_unfasten_wrench(user, tool)
+ return ITEM_INTERACT_SUCCESS
+
+/obj/structure/ai_core/screwdriver_act(mob/living/user, obj/item/tool)
+ switch(state)
+ if(CORE_STATE_EMPTY)
+ balloon_alert(user, "nothing to screw in there!")
+ return ITEM_INTERACT_BLOCKING
+ if(CORE_STATE_CIRCUIT)
+ if(!tool.use_tool(src, user, 0 SECONDS, 0, 50, CHECK_STATE_CALLBACK(CORE_STATE_CIRCUIT)))
+ return ITEM_INTERACT_BLOCKING
+ balloon_alert(user, "board secured")
+ UPDATE_STATE(CORE_STATE_SCREWED)
+ return ITEM_INTERACT_SUCCESS
+ if(CORE_STATE_SCREWED)
+ if(!tool.use_tool(src, user, 0 SECONDS, 0, 50, CHECK_STATE_CALLBACK(CORE_STATE_SCREWED)))
+ return ITEM_INTERACT_BLOCKING
+ balloon_alert(user, "board unsecured")
+ UPDATE_STATE(CORE_STATE_CIRCUIT)
+ return ITEM_INTERACT_SUCCESS
+ if(CORE_STATE_CABLED)
+ balloon_alert(user, "can't reach the board!")
+ return ITEM_INTERACT_BLOCKING
+ if(CORE_STATE_GLASSED)
+ if(!anchored)
+ balloon_alert(user, "isn't anchored!")
+ return ITEM_INTERACT_BLOCKING
+ if(!tool.use_tool(src, user, 0 SECONDS, 0, 50, CHECK_STATE_CALLBACK(CORE_STATE_GLASSED)))
+ return ITEM_INTERACT_BLOCKING
+ if(suicide_check())
+ balloon_alert(user, "processor is completely useless!")
+ return ITEM_INTERACT_BLOCKING
+
+ var/atom/movable/alert_source = src
+ if(core_mmi.brainmob?.mind)
+ alert_source = ai_structure_to_mob() || alert_source
+ else
+ UPDATE_STATE(CORE_STATE_FINISHED)
+ alert_source.balloon_alert(user, "connected monitor[core_mmi?.brainmob?.mind ? " and neural network" : ""]")
+ return ITEM_INTERACT_SUCCESS
+ if(CORE_STATE_FINISHED)
+ if(!core_mmi?.brainmob?.mind || suicide_check())
+ balloon_alert(user, "processor is inactive!")
+ return ITEM_INTERACT_BLOCKING
+
+ if(!anchored)
+ balloon_alert(user, "anchor it first!")
+ return ITEM_INTERACT_BLOCKING
+
+ balloon_alert(user, "connecting neural network...")
+ if(!tool.use_tool(src, user, 10 SECONDS, 0, 50, CHECK_STATE_CALLBACK(CORE_STATE_FINISHED)))
+ return ITEM_INTERACT_BLOCKING
+
+ var/atom/movable/alert_source = ai_structure_to_mob()
+ if(!alert_source)
+ balloon_alert(user, "processor is inactive!")
+ return ITEM_INTERACT_BLOCKING
+
+ alert_source.balloon_alert(user, "connected neural network")
+ return ITEM_INTERACT_SUCCESS
+
+/obj/structure/ai_core/crowbar_act(mob/living/user, obj/item/tool)
+ switch(state)
+ if(CORE_STATE_EMPTY)
+ balloon_alert(user, "nothing to pry out!")
+ return ITEM_INTERACT_BLOCKING
+ if(CORE_STATE_CIRCUIT)
+ if(!tool.use_tool(src, user, 0 SECONDS, 0, 50, CHECK_STATE_CALLBACK(CORE_STATE_CIRCUIT)))
+ return ITEM_INTERACT_BLOCKING
+
+ circuit.forceMove(drop_location())
+ UPDATE_STATE(CORE_STATE_EMPTY)
+ return ITEM_INTERACT_SUCCESS
+ if(CORE_STATE_SCREWED)
+ balloon_alert(user, "won't budge!")
+ return ITEM_INTERACT_BLOCKING
+ if(CORE_STATE_CABLED)
+ if(!core_mmi)
+ balloon_alert(user, "nothing to pry out!")
+ return ITEM_INTERACT_BLOCKING
+ if(!tool.use_tool(src, user, 0 SECONDS, 0, 50, CHECK_STATE_CALLBACK(CORE_STATE_CABLED)) || !core_mmi)
+ return ITEM_INTERACT_BLOCKING
+
+ core_mmi.forceMove(drop_location())
+ UPDATE_STATE(CORE_STATE_CABLED)
+ return ITEM_INTERACT_SUCCESS
+ if(CORE_STATE_GLASSED)
+ if(!tool.use_tool(src, user, 0 SECONDS, 0, 50, CHECK_STATE_CALLBACK(CORE_STATE_GLASSED)))
+ return ITEM_INTERACT_BLOCKING
+
+ new /obj/item/stack/sheet/rglass(drop_location(), 2)
+ UPDATE_STATE(CORE_STATE_CABLED)
+ return ITEM_INTERACT_SUCCESS
+ if(CORE_STATE_FINISHED)
+ balloon_alert(user, "display is on!")
+ return ITEM_INTERACT_SUCCESS
+
+/obj/structure/ai_core/wirecutter_act(mob/living/user, obj/item/tool)
+ switch(state)
+ if(CORE_STATE_EMPTY to CORE_STATE_CIRCUIT)
+ balloon_alert(user, "nothing to cut!")
+ return ITEM_INTERACT_BLOCKING
+ if(CORE_STATE_CABLED)
+ if(core_mmi)
+ balloon_alert(user, "[AI_CORE_BRAIN(core_mmi)] in the way!")
+ return ITEM_INTERACT_BLOCKING
+
+ if(!tool.use_tool(src, user, 0 SECONDS, 0, 50, CHECK_STATE_CALLBACK(CORE_STATE_CABLED)) || core_mmi)
+ return ITEM_INTERACT_BLOCKING
+
+ new /obj/item/stack/cable_coil(drop_location(), 5)
+ UPDATE_STATE(CORE_STATE_SCREWED)
+ return ITEM_INTERACT_SUCCESS
+ if(CORE_STATE_GLASSED)
+ balloon_alert(user, "nothing left to cut!")
+ return ITEM_INTERACT_BLOCKING
+ if(CORE_STATE_FINISHED)
+ if(!tool.use_tool(src, user, 0 SECONDS, 0, 50, CHECK_STATE_CALLBACK(CORE_STATE_FINISHED)))
+ return ITEM_INTERACT_BLOCKING
+
+ UPDATE_STATE(CORE_STATE_GLASSED)
+ return ITEM_INTERACT_SUCCESS
+
+/// Handles the interaction chain the same as item_interaction. Exists to isolate construction behaviour from other item behaviour.
+/obj/structure/ai_core/proc/construction_item_interaction(mob/living/user, obj/item/tool, list/modifiers)
+ if(istype(tool, /obj/item/circuitboard/aicore))
+ return install_board(user, tool) ? ITEM_INTERACT_SUCCESS : ITEM_INTERACT_BLOCKING
+
+ if(istype(tool, /obj/item/stack/cable_coil))
+ return add_cabling(user, tool) ? ITEM_INTERACT_SUCCESS : ITEM_INTERACT_BLOCKING
+
+ if(istype(tool, /obj/item/mmi))
+ return install_mmi(user, tool) ? ITEM_INTERACT_SUCCESS : ITEM_INTERACT_BLOCKING
+ if(istype(tool, /obj/item/ai_module))
+ return update_laws(user, tool) ? ITEM_INTERACT_SUCCESS : ITEM_INTERACT_BLOCKING
+ if(istype(tool, /obj/item/stack/sheet/rglass))
+ return install_glass(user, tool) ? ITEM_INTERACT_SUCCESS : ITEM_INTERACT_BLOCKING
+
+ return NONE
+
+/obj/structure/ai_core/proc/install_board(mob/living/user, obj/item/circuitboard/aicore/circuit)
+ if(state != CORE_STATE_EMPTY)
+ return FALSE
+ if(!user.transferItemToLoc(circuit, src))
+ return FALSE
+
+ playsound(src, 'sound/items/deconstruct.ogg', 50, TRUE)
+ src.circuit = circuit
+ UPDATE_STATE(CORE_STATE_CIRCUIT)
+ return TRUE
+
+/obj/structure/ai_core/proc/add_cabling(mob/living/user, obj/item/stack/cable_coil/cable)
+ if(state != CORE_STATE_SCREWED)
+ return FALSE
+
+ if(cable.get_amount() < 5)
+ balloon_alert(user, "not enough [cable::name]!")
+ return FALSE
+
+ balloon_alert(user, "adding cable...")
+ if(!cable.use_tool(src, user, 2 SECONDS, 5, 50, CHECK_STATE_CALLBACK(CORE_STATE_SCREWED)))
+ return FALSE
+
+ UPDATE_STATE(CORE_STATE_CABLED)
+ return TRUE
+
+/obj/structure/ai_core/proc/install_mmi(mob/living/user, obj/item/mmi/mmi)
+ if(state != CORE_STATE_CABLED)
+ return FALSE
+
+ if(!mmi.brain_check(user))
+ var/wants_install = (tgui_alert(user, "This [AI_CORE_BRAIN(mmi)] is inactive, would you like to make an inactive AI?", "Installing AI [AI_CORE_BRAIN(mmi)]", list("Yes", "No")) == "Yes")
+ if(!wants_install)
+ return FALSE
+ if(QDELETED(src) || QDELETED(user) || QDELETED(mmi) || !user.is_holding(mmi) || !Adjacent(user))
+ return FALSE
+ if(mmi.brainmob && HAS_TRAIT(mmi.brainmob, TRAIT_SUICIDED))
+ balloon_alert(user, "[AI_CORE_BRAIN(mmi)] is useless!")
+ return FALSE
+ else
+ var/mob/living/brain/mmi_brainmob = mmi.brainmob
+ if(!CONFIG_GET(flag/allow_ai) || (mmi_brainmob && is_banned_from(mmi_brainmob.ckey, JOB_AI)))
+ if(!QDELETED(src) && !QDELETED(user) && !QDELETED(mmi) && user.is_holding(mmi) && Adjacent(user))
+ balloon_alert(user, "[mmi] won't fit!")
+ return FALSE
+
+ if(state != CORE_STATE_CABLED)
+ return FALSE
+ if(!user.transferItemToLoc(mmi, src))
+ return FALSE
+
+ core_mmi = mmi
+ UPDATE_STATE(CORE_STATE_CABLED)
+ return TRUE
+
+/obj/structure/ai_core/proc/update_laws(mob/living/user, obj/item/ai_module/module)
+ if(!core_mmi)
+ balloon_alert(user, "no brain installed!")
+ return FALSE
+ if(!core_mmi.brainmob || !core_mmi.brainmob?.mind || suicide_check())
+ balloon_alert(user, "[AI_CORE_BRAIN(core_mmi)] is inactive!")
+ return FALSE
+ if(core_mmi.laws.id != DEFAULT_AI_LAWID)
+ balloon_alert(user, "[AI_CORE_BRAIN(core_mmi)] already has set laws!")
+ return FALSE
+
+ module.install(laws, user)
+ return TRUE
+
+/obj/structure/ai_core/proc/install_glass(mob/living/user, obj/item/stack/sheet/rglass/glass)
+ if(state != CORE_STATE_CABLED)
+ return FALSE
+
+ if(!core_mmi)
+ balloon_alert(user, "needs a processor!")
+ return FALSE
+ if(glass.get_amount() < 2)
+ balloon_alert(user, "not enough [glass::name]!")
+ return FALSE
+
+ if(!glass.use_tool(src, user, 2 SECONDS, 2, 50, CHECK_STATE_CALLBACK(CORE_STATE_CABLED)) || !core_mmi)
+ return FALSE
+
+ UPDATE_STATE(CORE_STATE_GLASSED)
+ return TRUE
+
+#undef CHECK_STATE_CALLBACK
+#undef UPDATE_STATE
+#undef AI_CORE_BRAIN
diff --git a/code/game/objects/items/devices/aicard_evil.dm b/code/game/objects/items/devices/aicard_evil.dm
index b63ecf25870..0c9f0c6f9c4 100644
--- a/code/game/objects/items/devices/aicard_evil.dm
+++ b/code/game/objects/items/devices/aicard_evil.dm
@@ -69,7 +69,7 @@
// Transfer the AI from the core we created into the card, then delete the core
capture_ai(new_ai, user)
- var/obj/structure/ai_core/deactivated/detritus = locate() in get_turf(src)
+ var/obj/structure/ai_core/detritus = locate() in get_turf(src)
qdel(detritus)
AI.set_control_disabled(FALSE)
AI.radio_enabled = TRUE
diff --git a/code/game/objects/structures/ai_core.dm b/code/game/objects/structures/ai_core.dm
deleted file mode 100644
index 6eee27ce24e..00000000000
--- a/code/game/objects/structures/ai_core.dm
+++ /dev/null
@@ -1,484 +0,0 @@
-#define AI_CORE_BRAIN(X) X.braintype == "Android" ? "brain" : "MMI"
-
-/obj/structure/ai_core
- density = TRUE
- anchored = FALSE
- name = "\improper AI core"
- icon = 'icons/mob/silicon/ai.dmi'
- icon_state = "0"
- desc = "The framework for an artificial intelligence core."
- max_integrity = 500
- var/state = EMPTY_CORE
- var/datum/ai_laws/laws
- var/obj/item/circuitboard/aicore/circuit
- var/obj/item/mmi/core_mmi
- /// only used in cases of AIs piloting mechs or shunted malf AIs, possible later use cases
- var/mob/living/silicon/ai/remote_ai = null
-
-/obj/structure/ai_core/Initialize(mapload)
- . = ..()
- laws = new
- laws.set_laws_config()
-
-/obj/structure/ai_core/examine(mob/user)
- . = ..()
- if(!anchored)
- if(state != EMPTY_CORE)
- . += span_notice("It has some bolts that could be tightened.")
- else
- . += span_notice("It has some bolts that could be tightened. The frame can be melted down.")
- else
- switch(state)
- if(EMPTY_CORE)
- . += span_notice("There is a slot for a circuit board, its bolts can be loosened.")
- if(CIRCUIT_CORE)
- . += span_notice("The circuit board can be screwed into place or pried out.")
- if(SCREWED_CORE)
- . += span_notice("The frame can be wired, the circuit board can be unfastened.")
- if(CABLED_CORE)
- if(!core_mmi)
- . += span_notice("There are wires which could be hooked up to an MMI or positronic brain, or cut.")
- else
- var/accept_laws = TRUE
- if(core_mmi.laws.id != DEFAULT_AI_LAWID || !core_mmi.brainmob || !core_mmi.brainmob?.mind)
- accept_laws = FALSE
- . += span_notice("There is a slot for a reinforced glass panel, the [AI_CORE_BRAIN(core_mmi)] could be pried out.[accept_laws ? " A law module can be swiped across." : ""]")
- if(GLASS_CORE)
- . += span_notice("The monitor [core_mmi?.brainmob?.mind && !suicide_check() ? "and neural interface " : ""]can be screwed in, the panel can be pried out.")
- if(AI_READY_CORE)
- . += span_notice("The monitor's connection can be cut[core_mmi?.brainmob?.mind && !suicide_check() ? " the neural interface can be screwed in." : "."]")
-
-/obj/structure/ai_core/Exited(atom/movable/gone, direction)
- . = ..()
- if(gone == circuit)
- circuit = null
- if((state != GLASS_CORE) && (state != AI_READY_CORE))
- state = EMPTY_CORE
- update_appearance()
- if(gone == core_mmi)
- core_mmi = null
- update_appearance()
-
-/obj/structure/ai_core/Destroy()
- if(istype(remote_ai))
- remote_ai = null
- QDEL_NULL(circuit)
- QDEL_NULL(core_mmi)
- QDEL_NULL(laws)
- return ..()
-
-/obj/structure/ai_core/take_damage(damage_amount, damage_type, damage_flag, sound_effect, attack_dir, armour_penetration)
- . = ..()
- if(. > 0 && istype(remote_ai))
- to_chat(remote_ai, span_danger("Your core is under attack!"))
-
-
-/obj/structure/ai_core/deactivated
- icon_state = "ai-empty"
- anchored = TRUE
- state = AI_READY_CORE
- var/mob/living/silicon/ai/attached_ai
-
-/obj/structure/ai_core/deactivated/Initialize(mapload, skip_mmi_creation = FALSE, posibrain = FALSE, linked_ai)
- . = ..()
- circuit = new(src)
- if(linked_ai)
- attached_ai = linked_ai
- if(skip_mmi_creation)
- return
- if(posibrain)
- core_mmi = new/obj/item/mmi/posibrain(src, /* autoping = */ FALSE)
- else
- core_mmi = new(src)
- core_mmi.brain = new(core_mmi)
- core_mmi.update_appearance()
-
-/obj/structure/ai_core/deactivated/Destroy()
- if(attached_ai)
- attached_ai.linked_core = null
- attached_ai = null
- . = ..()
-
-/obj/structure/ai_core/deactivated/proc/disable_doomsday(datum/source)
- SIGNAL_HANDLER
- attached_ai.ShutOffDoomsdayDevice()
-
-/obj/structure/ai_core/latejoin_inactive
- name = "networked AI core"
- desc = "This AI core is connected by bluespace transmitters to NTNet, allowing for an AI personality to be downloaded to it on the fly mid-shift."
- icon_state = "ai-empty"
- anchored = TRUE
- state = AI_READY_CORE
- var/available = TRUE
- var/safety_checks = TRUE
- var/active = TRUE
-
-/obj/structure/ai_core/latejoin_inactive/Initialize(mapload)
- . = ..()
- circuit = new(src)
- core_mmi = new(src)
- core_mmi.brain = new(core_mmi)
- core_mmi.update_appearance()
- GLOB.latejoin_ai_cores += src
-
-/obj/structure/ai_core/latejoin_inactive/Destroy()
- GLOB.latejoin_ai_cores -= src
- return ..()
-
-/obj/structure/ai_core/latejoin_inactive/examine(mob/user)
- . = ..()
- . += "Its transmitter seems to be [active? "on" : "off"]."
- . += span_notice("You could [active? "deactivate" : "activate"] it with a multitool.")
-
-/obj/structure/ai_core/latejoin_inactive/proc/is_available() //If people still manage to use this feature to spawn-kill AI latejoins ahelp them.
- if(!available)
- return FALSE
- if(!safety_checks)
- return TRUE
- if(!active)
- return FALSE
- var/turf/T = get_turf(src)
- var/area/A = get_area(src)
- if(!(A.area_flags & BLOBS_ALLOWED))
- return FALSE
- if(!A.power_equip)
- return FALSE
- if(!SSmapping.level_trait(T.z,ZTRAIT_STATION))
- return FALSE
- if(!isfloorturf(T))
- return FALSE
- return TRUE
-
-/obj/structure/ai_core/latejoin_inactive/attackby(obj/item/tool, mob/user, list/modifiers, list/attack_modifiers)
- if(tool.tool_behaviour == TOOL_MULTITOOL)
- active = !active
- to_chat(user, span_notice("You [active? "activate" : "deactivate"] \the [src]'s transmitters."))
- return
- return ..()
-
-/obj/structure/ai_core/wrench_act(mob/living/user, obj/item/tool)
- . = ..()
- default_unfasten_wrench(user, tool)
- return ITEM_INTERACT_SUCCESS
-
-/obj/structure/ai_core/screwdriver_act(mob/living/user, obj/item/tool)
- . = ..()
- if(state == AI_READY_CORE)
- if(!core_mmi)
- balloon_alert(user, "no brain installed!")
- return ITEM_INTERACT_SUCCESS
- else if(!core_mmi.brainmob?.mind || suicide_check())
- balloon_alert(user, "brain is inactive!")
- return ITEM_INTERACT_SUCCESS
- else
- balloon_alert(user, "connecting neural network...")
- if(!tool.use_tool(src, user, 10 SECONDS))
- return ITEM_INTERACT_SUCCESS
- if(!ai_structure_to_mob())
- return ITEM_INTERACT_SUCCESS
- balloon_alert(user, "connected neural network")
- return ITEM_INTERACT_SUCCESS
-
-/obj/structure/ai_core/attackby(obj/item/tool, mob/living/user, list/modifiers, list/attack_modifiers)
- if(remote_ai)
- to_chat(remote_ai, span_danger("CORE TAMPERING DETECTED!"))
- if(!anchored)
- if(tool.tool_behaviour == TOOL_WELDER)
- if(state != EMPTY_CORE)
- balloon_alert(user, "core must be empty to deconstruct it!")
- return
-
- if(!tool.tool_start_check(user, amount=1))
- return
-
- balloon_alert(user, "deconstructing frame...")
- if(tool.use_tool(src, user, 20, volume=50) && state == EMPTY_CORE)
- balloon_alert(user, "deconstructed frame")
- deconstruct(TRUE)
- return
- else
- if(!user.combat_mode)
- balloon_alert(user, "bolt it down first!")
- return
- else
- return ..()
- else
- switch(state)
- if(EMPTY_CORE)
- if(istype(tool, /obj/item/circuitboard/aicore))
- if(!user.transferItemToLoc(tool, src))
- return
- playsound(loc, 'sound/items/deconstruct.ogg', 50, TRUE)
- balloon_alert(user, "circuit board inserted")
- update_appearance()
- state = CIRCUIT_CORE
- circuit = tool
- return
- if(CIRCUIT_CORE)
- if(tool.tool_behaviour == TOOL_SCREWDRIVER)
- tool.play_tool_sound(src)
- balloon_alert(user, "board screwed into place")
- state = SCREWED_CORE
- update_appearance()
- return
- if(tool.tool_behaviour == TOOL_CROWBAR)
- tool.play_tool_sound(src)
- balloon_alert(user, "circuit board removed")
- state = EMPTY_CORE
- circuit.forceMove(loc)
- return
- if(SCREWED_CORE)
- if(tool.tool_behaviour == TOOL_SCREWDRIVER && circuit)
- tool.play_tool_sound(src)
- balloon_alert(user, "circuit board unfastened")
- state = CIRCUIT_CORE
- update_appearance()
- return
- if(istype(tool, /obj/item/stack/cable_coil))
- var/obj/item/stack/cable_coil/C = tool
- if(C.get_amount() >= 5)
- playsound(loc, 'sound/items/deconstruct.ogg', 50, TRUE)
- balloon_alert(user, "adding cables to frame...")
- if(do_after(user, 2 SECONDS, target = src) && state == SCREWED_CORE && C.use(5))
- balloon_alert(user, "added cables to frame.")
- state = CABLED_CORE
- update_appearance()
- else
- balloon_alert(user, "need five lengths of cable!")
- return
- if(CABLED_CORE)
- if(tool.tool_behaviour == TOOL_WIRECUTTER)
- if(core_mmi)
- balloon_alert(user, "remove the [AI_CORE_BRAIN(core_mmi)] first!")
- else
- tool.play_tool_sound(src)
- balloon_alert(user, "cables removed")
- state = SCREWED_CORE
- update_appearance()
- new /obj/item/stack/cable_coil(drop_location(), 5)
- return
-
- if(istype(tool, /obj/item/stack/sheet/rglass))
- if(!core_mmi)
- balloon_alert(user, "add a brain first!")
- return
- var/obj/item/stack/sheet/rglass/G = tool
- if(G.get_amount() >= 2)
- playsound(loc, 'sound/items/deconstruct.ogg', 50, TRUE)
- balloon_alert(user, "adding glass panel...")
- if(do_after(user, 2 SECONDS, target = src) && state == CABLED_CORE && G.use(2))
- balloon_alert(user, "added glass panel")
- state = GLASS_CORE
- update_appearance()
- else
- balloon_alert(user, "need two sheets of reinforced glass!")
- return
-
- if(istype(tool, /obj/item/ai_module))
- if(!core_mmi)
- balloon_alert(user, "no brain installed!")
- return
- if(!core_mmi.brainmob || !core_mmi.brainmob?.mind || suicide_check())
- balloon_alert(user, "[AI_CORE_BRAIN(core_mmi)] is inactive!")
- return
- if(core_mmi.laws.id != DEFAULT_AI_LAWID)
- balloon_alert(user, "[AI_CORE_BRAIN(core_mmi)] already has set laws!")
- return
- var/obj/item/ai_module/module = tool
- module.install(laws, user)
- return
-
- if(istype(tool, /obj/item/mmi) && !core_mmi)
- var/obj/item/mmi/M = tool
- if(!M.brain_check(user))
- var/install = tgui_alert(user, "This [AI_CORE_BRAIN(M)] is inactive, would you like to make an inactive AI?", "Installing AI [AI_CORE_BRAIN(M)]", list("Yes", "No"))
- if(install != "Yes")
- return
- if(M.brainmob && HAS_TRAIT(M.brainmob, TRAIT_SUICIDED))
- to_chat(user, span_warning("[M.name] is completely useless!"))
- return
- if(!user.transferItemToLoc(M, src))
- return
- core_mmi = M
- balloon_alert(user, "added [AI_CORE_BRAIN(core_mmi)] to frame")
- update_appearance()
- return
-
- var/mob/living/brain/B = M.brainmob
- if(!CONFIG_GET(flag/allow_ai) || (is_banned_from(B.ckey, JOB_AI) && !QDELETED(src) && !QDELETED(user) && !QDELETED(M) && !QDELETED(user) && Adjacent(user)))
- if(!QDELETED(M))
- to_chat(user, span_warning("This [M.name] does not seem to fit!"))
- return
- if(!user.transferItemToLoc(M,src))
- return
-
- core_mmi = M
- balloon_alert(user, "added [AI_CORE_BRAIN(core_mmi)] to frame")
- update_appearance()
- return
-
- if(tool.tool_behaviour == TOOL_CROWBAR && core_mmi)
- tool.play_tool_sound(src)
- balloon_alert(user, "removed [AI_CORE_BRAIN(core_mmi)]")
- if(remote_ai)
- var/mob/living/silicon/ai/remoted_ai = remote_ai
- remoted_ai.break_core_link()
- if(!IS_MALF_AI(remoted_ai))
- //don't pull back shunted malf AIs
- remoted_ai.death(gibbed = TRUE, drop_mmi = FALSE)
- ///the drop_mmi param determines whether the MMI is dropped at their current location
- ///which in this case would be somewhere else, so we drop their MMI at the core instead
- remoted_ai.make_mmi_drop_and_transfer(core_mmi, src)
- core_mmi.forceMove(loc) //if they're malf, just drops a blank MMI, or if it's an incomplete shell
- return //it drops the mmi that was put in before it was finished
-
- if(GLASS_CORE)
- if(tool.tool_behaviour == TOOL_CROWBAR)
- tool.play_tool_sound(src)
- balloon_alert(user, "removed glass panel")
- state = CABLED_CORE
- update_appearance()
- new /obj/item/stack/sheet/rglass(loc, 2)
- return
-
- if(tool.tool_behaviour == TOOL_SCREWDRIVER)
- if(suicide_check())
- to_chat(user, span_warning("The brain installed is completely useless."))
- return
- tool.play_tool_sound(src)
-
- var/atom/alert_source = src
- if(core_mmi.brainmob?.mind)
- alert_source = ai_structure_to_mob() || alert_source
- else
- state = AI_READY_CORE
- update_appearance()
- alert_source.balloon_alert(user, "connected monitor[core_mmi?.brainmob?.mind ? " and neural network" : ""]")
- return
-
- if(AI_READY_CORE)
- if(istype(tool, /obj/item/aicard))
- return //handled by /obj/structure/ai_core/transfer_ai()
-
- if(tool.tool_behaviour == TOOL_WIRECUTTER)
- tool.play_tool_sound(src)
- balloon_alert(user, "disconnected monitor")
- state = GLASS_CORE
- update_appearance()
- return
- return ..()
-
-/obj/structure/ai_core/proc/ai_structure_to_mob()
- var/mob/living/brain/the_brainmob = core_mmi.brainmob
- if(!the_brainmob.mind || suicide_check())
- return FALSE
- the_brainmob.mind.remove_antags_for_borging()
- if(!the_brainmob.mind.has_ever_been_ai)
- SSblackbox.record_feedback("amount", "ais_created", 1)
- var/mob/living/silicon/ai/ai_mob = null
-
- if(core_mmi.overrides_aicore_laws)
- ai_mob = new /mob/living/silicon/ai(loc, core_mmi.laws, the_brainmob)
- core_mmi.laws = null //MMI's law datum is being donated, so we need the MMI to let it go or the GC will eat it
- else
- ai_mob = new /mob/living/silicon/ai(loc, laws, the_brainmob)
- laws = null //we're giving the new AI this datum, so let's not delete it when we qdel(src) 5 lines from now
-
- var/datum/antagonist/malf_ai/malf_datum = IS_MALF_AI(ai_mob)
- if(malf_datum)
- malf_datum.add_law_zero()
-
- if(!isnull(the_brainmob.client))
- ai_mob.set_gender(the_brainmob.client)
- if(core_mmi.force_replace_ai_name)
- ai_mob.fully_replace_character_name(ai_mob.name, core_mmi.replacement_ai_name())
- ai_mob.posibrain_inside = core_mmi.braintype == "Android"
- deadchat_broadcast(" has been brought online at [get_area_name(ai_mob, format_text = TRUE)].", span_name("[ai_mob]"), follow_target = ai_mob, message_type = DEADCHAT_ANNOUNCEMENT)
- qdel(src)
- return ai_mob
-
-/obj/structure/ai_core/update_icon_state()
- switch(state)
- if(EMPTY_CORE)
- icon_state = "0"
- if(CIRCUIT_CORE)
- icon_state = "1"
- if(SCREWED_CORE)
- icon_state = "2"
- if(CABLED_CORE)
- if(core_mmi)
- icon_state = "3b"
- else
- icon_state = "3"
- if(GLASS_CORE)
- icon_state = "4"
- if(AI_READY_CORE)
- icon_state = "ai-empty"
- return ..()
-
-/obj/structure/ai_core/atom_deconstruct(disassembled = TRUE)
- if(state >= GLASS_CORE)
- new /obj/item/stack/sheet/rglass(loc, 2)
- if(state >= CABLED_CORE)
- new /obj/item/stack/cable_coil(loc, 5)
- if(circuit)
- circuit.forceMove(loc)
- circuit = null
- new /obj/item/stack/sheet/plasteel(loc, 4)
-
-/// Quick proc to call to see if the brainmob inside of us has suicided. Returns TRUE if we have, FALSE in any other scenario.
-/obj/structure/ai_core/proc/suicide_check()
- if(isnull(core_mmi) || isnull(core_mmi.brainmob))
- return FALSE
- return HAS_TRAIT(core_mmi.brainmob, TRAIT_SUICIDED)
-
-/*
-This is a good place for AI-related object verbs so I'm sticking it here.
-If adding stuff to this, don't forget that an AI need to cancel_camera() whenever it physically moves to a different location.
-That prevents a few funky behaviors.
-*/
-//The type of interaction, the player performing the operation, the AI itself, and the card object, if any.
-
-
-/atom/proc/transfer_ai(interaction, mob/user, mob/living/silicon/ai/AI, obj/item/aicard/card)
- SHOULD_CALL_PARENT(TRUE)
- if(istype(card))
- if(card.flush)
- to_chat(user, span_alert("ERROR: AI flush is in progress, cannot execute transfer protocol."))
- return FALSE
- return TRUE
-
-/obj/structure/ai_core/transfer_ai(interaction, mob/user, mob/living/silicon/ai/AI, obj/item/aicard/card)
- if(state != AI_READY_CORE || !..())
- return
- if(core_mmi && core_mmi.brainmob)
- if(core_mmi.brainmob.mind)
- to_chat(user, span_warning("[src] already contains an active mind!"))
- return
- else if(suicide_check())
- to_chat(user, span_warning("[AI_CORE_BRAIN(core_mmi)] installed in [src] is completely useless!"))
- return
- //Transferring a carded AI to a core.
- if(interaction == AI_TRANS_FROM_CARD)
- AI.set_control_disabled(FALSE)
- AI.radio_enabled = TRUE
- AI.forceMove(loc) // to replace the terminal.
- to_chat(AI, span_notice("You have been uploaded to a stationary terminal. Remote device connection restored."))
- to_chat(user, "[span_boldnotice("Transfer successful")]: [AI.name] ([rand(1000,9999)].exe) installed and executed successfully. Local copy has been removed.")
- card.AI = null
- AI.battery = circuit.battery
- AI.posibrain_inside = isnull(core_mmi) || core_mmi.braintype == "Android"
- qdel(src)
- else //If for some reason you use an empty card on an empty AI terminal.
- to_chat(user, span_alert("There is no AI loaded on this terminal."))
-
-/obj/item/circuitboard/aicore
- name = "AI core (AI Core Board)" //Well, duh, but best to be consistent
- var/battery = 200 //backup battery for when the AI loses power. Copied to/from AI mobs when carding, and placed here to avoid recharge via deconning the core
-
-/obj/item/circuitboard/aicore/Initialize(mapload)
- . = ..()
- if(mapload && HAS_TRAIT(SSstation, STATION_TRAIT_HUMAN_AI))
- return INITIALIZE_HINT_QDEL
-
-#undef AI_CORE_BRAIN
diff --git a/code/modules/antagonists/malf_ai/malf_ai.dm b/code/modules/antagonists/malf_ai/malf_ai.dm
index f448670eeda..d35a00f8b7c 100644
--- a/code/modules/antagonists/malf_ai/malf_ai.dm
+++ b/code/modules/antagonists/malf_ai/malf_ai.dm
@@ -40,7 +40,6 @@
malfunction_flavor = strings(MALFUNCTION_FLAVOR_FILE, employer)
add_law_zero()
- RegisterSignal(owner.current, COMSIG_SILICON_AI_CORE_STATUS, PROC_REF(core_status))
if(malf_sound)
owner.current.playsound_local(get_turf(owner.current), malf_sound, 100, FALSE, pressure_affected = FALSE, use_reverb = FALSE)
owner.current.grant_language(/datum/language/codespeak, source = LANGUAGE_MALF)
@@ -57,7 +56,6 @@
malf_ai.remove_malf_abilities()
QDEL_NULL(malf_ai.malf_picker)
- UnregisterSignal(owner, COMSIG_SILICON_AI_CORE_STATUS)
return ..()
/// Generates a complete set of malf AI objectives up to the traitor objective limit.
@@ -271,14 +269,6 @@
return malf_ai_icon
-/datum/antagonist/malf_ai/proc/core_status(datum/source)
- SIGNAL_HANDLER
-
- var/mob/living/silicon/ai/malf_owner = owner.current
- if(malf_owner?.linked_core)
- return COMPONENT_CORE_ALL_GOOD
- return COMPONENT_CORE_DISCONNECTED
-
//Subtype of Malf AI datum, used for one of the traitor final objectives
/datum/antagonist/malf_ai/infected
name = "Infected AI"
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index 329a5f29823..67aa473c47e 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -5,7 +5,7 @@
/mob/living/silicon/ai/Initialize(mapload, datum/ai_laws/L, mob/target_ai)
. = ..()
if(!target_ai) //If there is no player/brain inside.
- new/obj/structure/ai_core/deactivated(loc) //New empty terminal.
+ new/obj/structure/ai_core(loc, CORE_STATE_FINISHED) //New empty terminal.
return INITIALIZE_HINT_QDEL //Delete AI.
ADD_TRAIT(src, TRAIT_NO_TELEPORT, AI_ANCHOR_TRAIT)
@@ -152,7 +152,7 @@
current = null
bot_ref = null
controlled_equipment = null
- linked_core = null
+ break_core_link()
apc_override = null
if(ai_voicechanger)
ai_voicechanger.owner = null
@@ -363,53 +363,45 @@
status_flags &= ~CANPUSH //we dont want the core to be push-able when anchored
ADD_TRAIT(src, TRAIT_NO_TELEPORT, AI_ANCHOR_TRAIT)
+/// Creates an MMI of the AI based on its configuration.
+/mob/living/silicon/ai/proc/make_mmi(atom/destination) as /obj/item/mmi
+ RETURN_TYPE(/obj/item/mmi)
+ //FIXME: this code is really bad, we shouldn't be doing most of this ourselves. MMI code needs a good refactoring....
+ var/obj/item/mmi/copied_mmi
+ if(posibrain_inside)
+ copied_mmi = new /obj/item/mmi/posibrain(destination, FALSE)
+ copied_mmi.name = "[initial(copied_mmi.name)] ([real_name])"
+ else
+ copied_mmi = new /obj/item/mmi(destination)
+ copied_mmi.name = "[initial(copied_mmi.name)]: [real_name]"
+ copied_mmi.brain = new /obj/item/organ/brain(copied_mmi)
+ copied_mmi.brain.organ_flags |= ORGAN_FROZEN
+ copied_mmi.brain.name = "[real_name]'s brain"
+ copied_mmi.set_brainmob(new /mob/living/brain(copied_mmi))
+ copied_mmi.brainmob.container = copied_mmi
+
+ copied_mmi.brainmob.name = real_name
+ copied_mmi.brainmob.real_name = real_name
+ copied_mmi.brainmob.gender = gender
+
+ var/suicided = HAS_TRAIT(src, TRAIT_SUICIDED)
+ copied_mmi.brainmob.set_suicide(suicided)
+ copied_mmi.brain?.suicided = suicided // we can't guarantee that the MMI has a brain... sigh
+
+ if(copied_mmi.brainmob.stat == DEAD && !suicided)
+ copied_mmi.brainmob.set_stat(CONSCIOUS)
+
+ copied_mmi.update_appearance()
+ return copied_mmi
+
/mob/living/silicon/ai/proc/ai_mob_to_structure()
disconnect_shell()
ShutOffDoomsdayDevice()
- var/obj/structure/ai_core/deactivated/ai_core = new(get_turf(src), /* skip_mmi_creation = */ TRUE)
- if(make_mmi_drop_and_transfer(ai_core.core_mmi, the_core = ai_core))
- qdel(src)
+ var/obj/structure/ai_core/ai_core = new(get_turf(src), CORE_STATE_FINISHED, make_mmi())
+ mind?.transfer_to(ai_core.core_mmi.brainmob)
+ qdel(src)
return ai_core
-/mob/living/silicon/ai/proc/break_core_link()
- to_chat(src, span_danger("Your core has been destroyed!"))
- linked_core = null
-
-/mob/living/silicon/ai/proc/make_mmi_drop_and_transfer(obj/item/mmi/the_mmi, the_core)
- var/mmi_type
- if(posibrain_inside)
- mmi_type = new/obj/item/mmi/posibrain(src, /* autoping = */ FALSE)
- else
- mmi_type = new/obj/item/mmi(src)
- if(hack_software)
- new/obj/item/malf_upgrade(get_turf(src))
- the_mmi = mmi_type
- the_mmi.brain = new /obj/item/organ/brain(the_mmi)
- the_mmi.brain.organ_flags |= ORGAN_FROZEN
- the_mmi.brain.name = "[real_name]'s brain"
- the_mmi.name = "[initial(the_mmi.name)]: [real_name]"
- the_mmi.set_brainmob(new /mob/living/brain(the_mmi))
- the_mmi.brainmob.name = src.real_name
- the_mmi.brainmob.real_name = src.real_name
- the_mmi.brainmob.container = the_mmi
- the_mmi.brainmob.gender = src.gender
-
- var/has_suicided_trait = HAS_TRAIT(src, TRAIT_SUICIDED)
- the_mmi.brainmob.set_suicide(has_suicided_trait)
- the_mmi.brain.suicided = has_suicided_trait
- if(the_core)
- var/obj/structure/ai_core/core = the_core
- core.core_mmi = the_mmi
- the_mmi.forceMove(the_core)
- else
- the_mmi.forceMove(get_turf(src))
- if(the_mmi.brainmob.stat == DEAD && !has_suicided_trait)
- the_mmi.brainmob.set_stat(CONSCIOUS)
- if(mind)
- mind.transfer_to(the_mmi.brainmob)
- the_mmi.update_appearance()
- return TRUE
-
/mob/living/silicon/ai/Topic(href, href_list)
..()
if(usr != src)
@@ -702,7 +694,7 @@
if(!istype(apc))
to_chat(owner, span_notice("You are already in your Main Core."))
return
- if(SEND_SIGNAL(owner, COMSIG_SILICON_AI_CORE_STATUS) & COMPONENT_CORE_ALL_GOOD)
+ if(astype(owner, /mob/living/silicon/ai)?.linked_core)
apc.malfvacate()
else
to_chat(owner, span_danger("Linked core not detected!"))
@@ -786,7 +778,7 @@
balloon_alert(user, "no intelligence detected!") // average tg coder am i right
return
ShutOffDoomsdayDevice()
- var/obj/structure/ai_core/new_core = new /obj/structure/ai_core/deactivated(loc, posibrain_inside)//Spawns a deactivated terminal at AI location.
+ var/obj/structure/ai_core/new_core = new /obj/structure/ai_core(loc, CORE_STATE_FINISHED, make_mmi())
new_core.circuit.battery = battery
ai_restore_power()//So the AI initially has power.
set_control_disabled(TRUE) //Can't control things remotely if you're stuck in a card!
@@ -1109,6 +1101,99 @@
SEND_SIGNAL(src, COMSIG_SILICON_AI_SET_CONTROL_DISABLED, control_disabled)
src.control_disabled = control_disabled
+
+/// Establishes a "core link" with a supplied core structure.
+/// This will register multiple signals and give the AI a strong reference to it.
+/// See [proc/resolve_core_link] or [proc/break_core_link] for ways to end the connection.
+/mob/living/silicon/ai/proc/create_core_link(obj/structure/ai_core/core)
+ if(linked_core) //uh oh
+ break_core_link(linked_core)
+ linked_core = core
+
+ //this block is kind of sketchy, but I don't think this should cause any problems
+ qdel(core.core_mmi)
+ core.core_mmi = make_mmi(core)
+
+ RegisterSignals(linked_core, list(COMSIG_ATOM_DESTRUCTION, COMSIG_QDELETING), PROC_REF(on_core_destroyed))
+ RegisterSignals(linked_core, list(
+ COMSIG_ATOM_ITEM_INTERACTION,
+ COMSIG_ATOM_TOOL_ACT(TOOL_CROWBAR),
+ COMSIG_ATOM_TOOL_ACT(TOOL_WRENCH),
+ COMSIG_ATOM_TOOL_ACT(TOOL_WELDER),
+ COMSIG_ATOM_TOOL_ACT(TOOL_WIRECUTTER),
+ COMSIG_ATOM_TOOL_ACT(TOOL_SCREWDRIVER),
+ ), PROC_REF(on_core_item_interaction))
+ RegisterSignal(linked_core, COMSIG_ATOM_TAKE_DAMAGE, PROC_REF(on_core_take_damage))
+ RegisterSignal(linked_core, COMSIG_ATOM_EXITED, PROC_REF(on_core_exited))
+
+/// Elegantly closes the AI's link to a core structure,
+/// moving them to its location and cleaning it up. This is generally what you want to call.
+/// Prefer calling [proc/break_core_link] directly if the connection is meant to be suddenly severed.
+/mob/living/silicon/ai/proc/resolve_core_link()
+ if(!linked_core) //oh no bro
+ CRASH("tried to resolve a core link with no core!!!!")
+
+ forceMove(linked_core.loc)
+ var/obj/structure/ai_core/unlinked_core = linked_core
+ break_core_link()
+ qdel(unlinked_core)
+ cancel_camera()
+
+/// Handles unregistering the AI from its core. The core itself will not be cleaned up.
+/// Prefer calling [proc/resolve_core_link] if the connection is being closed elegantly.
+/mob/living/silicon/ai/proc/break_core_link()
+ if(!linked_core)
+ return
+
+ UnregisterSignal(linked_core, list(
+ COMSIG_QDELETING, COMSIG_ATOM_DESTRUCTION, //on_core_destroyed
+ //on_core_item_interaction
+ COMSIG_ATOM_ITEM_INTERACTION,
+ COMSIG_ATOM_TOOL_ACT(TOOL_CROWBAR),
+ COMSIG_ATOM_TOOL_ACT(TOOL_WRENCH),
+ COMSIG_ATOM_TOOL_ACT(TOOL_WELDER),
+ COMSIG_ATOM_TOOL_ACT(TOOL_WIRECUTTER),
+ COMSIG_ATOM_TOOL_ACT(TOOL_SCREWDRIVER),
+ COMSIG_ATOM_TAKE_DAMAGE, //on_core_take_damage
+ COMSIG_ATOM_EXITED, //on_core_exited
+ ))
+ linked_core = null
+
+/mob/living/silicon/ai/proc/on_core_item_interaction(datum/source, mob/living/user, obj/item/tool, list/processing_recipes)
+ SIGNAL_HANDLER
+ if(user.combat_mode)
+ return NONE
+
+ to_chat(src, span_danger("CORE TAMPERING DETECTED!"))
+ return NONE
+
+/mob/living/silicon/ai/proc/on_core_take_damage(datum/source, damage_taken, ...)
+ SIGNAL_HANDLER
+
+ if(damage_taken > 0)
+ to_chat(src, span_danger("CORE DAMAGE DETECTED!"))
+ return NONE
+
+/mob/living/silicon/ai/proc/on_core_destroyed(datum/source, damage_flag)
+ SIGNAL_HANDLER
+
+ to_chat(src, span_danger("Your core has been destroyed!"))
+ ShutOffDoomsdayDevice()
+ break_core_link()
+
+/mob/living/silicon/ai/proc/on_core_exited(datum/source, atom/movable/gone, direction)
+ SIGNAL_HANDLER
+
+ if(istype(gone, /obj/item/mmi))
+ var/obj/item/mmi/mmi_gone = gone
+ on_core_destroyed(source, NONE)
+ if(!IS_MALF_AI(src)) //don't pull back shunted malf AIs
+ death(gibbed = TRUE, drop_mmi = FALSE)
+ ///the drop_mmi param determines whether the MMI is dropped at their current location
+ ///which in this case would be somewhere else, so we drop their MMI at the core instead
+ mind?.transfer_to(mmi_gone.brainmob)
+ qdel(src)
+
#undef HOLOGRAM_CHOICE_CHARACTER
#undef CHARACTER_TYPE_SELF
#undef CHARACTER_TYPE_CREWMEMBER
diff --git a/code/modules/mob/living/silicon/ai/ai_defines.dm b/code/modules/mob/living/silicon/ai/ai_defines.dm
index de631e7b867..30201c9e703 100644
--- a/code/modules/mob/living/silicon/ai/ai_defines.dm
+++ b/code/modules/mob/living/silicon/ai/ai_defines.dm
@@ -122,8 +122,8 @@
/* REMOTE CONTROL */
/// Equipment that the AI is controlling remotely, to determine whether to relaymove or use the AI eye
VAR_FINAL/obj/controlled_equipment
- /// AI core that this AI is linked to, used when put into an exosuit
- VAR_FINAL/obj/structure/ai_core/deactivated/linked_core
+ /// AI core that this AI is linked to. See [proc/create_core_link] [proc/resolve_core_link] [proc/break_core_link]
+ VAR_FINAL/obj/structure/ai_core/linked_core
/// Robot that this AI is currently using
VAR_FINAL/mob/living/silicon/robot/deployed_shell
/// Action to deploy to a shell from a list of options
diff --git a/code/modules/mob/living/silicon/ai/death.dm b/code/modules/mob/living/silicon/ai/death.dm
index aad3df39d80..6f76f3294a9 100644
--- a/code/modules/mob/living/silicon/ai/death.dm
+++ b/code/modules/mob/living/silicon/ai/death.dm
@@ -34,7 +34,8 @@
ShutOffDoomsdayDevice()
if(gibbed && drop_mmi)
- make_mmi_drop_and_transfer()
+ var/obj/item/mmi/loose_cpu = make_mmi(get_turf(src))
+ mind?.transfer_to(loose_cpu.brainmob)
if(explodes_on_death)
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(explosion), loc, 3, 6, 12, null, 15), 1 SECONDS)
@@ -42,7 +43,5 @@
SSblackbox.ReportDeath(src)
/mob/living/silicon/ai/proc/ShutOffDoomsdayDevice()
- if(nuking)
- nuking = FALSE
- if(doomsday_device)
- qdel(doomsday_device)
+ nuking = FALSE
+ QDEL_NULL(doomsday_device)
diff --git a/code/modules/power/apc/apc_malf.dm b/code/modules/power/apc/apc_malf.dm
index c4cafb9d5e7..eb0526e32ca 100644
--- a/code/modules/power/apc/apc_malf.dm
+++ b/code/modules/power/apc/apc_malf.dm
@@ -45,8 +45,7 @@
malf.ShutOffDoomsdayDevice()
occupier = malf
if (isturf(malf.loc)) // create a deactivated AI core if the AI isn't coming from an emergency mech shunt
- malf.linked_core = new /obj/structure/ai_core/deactivated(malf.loc)
- malf.linked_core.remote_ai = malf // note that we do not set the deactivated core's core_mmi.brainmob
+ malf.create_core_link(new /obj/structure/ai_core(malf.loc, CORE_STATE_FINISHED, malf.make_mmi()))
malf.forceMove(src) // move INTO the APC, not to its tile
if(!findtext(occupier.name, "APC Copy"))
occupier.name = "[malf.name] APC Copy"
@@ -74,9 +73,7 @@
return
if(occupier.linked_core)
occupier.shunted = FALSE
- occupier.forceMove(occupier.linked_core.loc)
- qdel(occupier.linked_core)
- occupier.cancel_camera()
+ occupier.resolve_core_link()
occupier = null
else
stack_trace("An AI: [occupier] has vacated an APC with no linked core and without being gibbed.")
diff --git a/code/modules/vehicles/mecha/_mecha.dm b/code/modules/vehicles/mecha/_mecha.dm
index b666a013479..44690288175 100644
--- a/code/modules/vehicles/mecha/_mecha.dm
+++ b/code/modules/vehicles/mecha/_mecha.dm
@@ -332,6 +332,7 @@
var/mob/living/silicon/ai/unlucky_ai
for(var/mob/living/occupant as anything in occupants)
if(isAI(occupant))
+ //FIXME: Nothiing about this block works
var/mob/living/silicon/ai/ai = occupant
if(!ai.linked_core && !ai.can_shunt) // we probably shouldnt gib AIs with a core or shunting abilities
unlucky_ai = occupant
diff --git a/code/modules/vehicles/mecha/mecha_ai_interaction.dm b/code/modules/vehicles/mecha/mecha_ai_interaction.dm
index 02bc2c89953..7a13e423bb4 100644
--- a/code/modules/vehicles/mecha/mecha_ai_interaction.dm
+++ b/code/modules/vehicles/mecha/mecha_ai_interaction.dm
@@ -78,10 +78,7 @@
return
if(AI_MECH_HACK) //Called by AIs on the mech
- var/obj/structure/ai_core/deactivated/deactivated_core = new(AI.loc, FALSE, FALSE, AI)
- AI.linked_core = deactivated_core
- AI.linked_core.RegisterSignal(deactivated_core, COMSIG_ATOM_DESTRUCTION, TYPE_PROC_REF(/obj/structure/ai_core/deactivated, disable_doomsday)) //Protect that core! The structure goes bye-bye when we re-shunt back in so no need for cleanup.
- AI.linked_core.remote_ai = AI
+ AI.create_core_link(new /obj/structure/ai_core(AI.loc, CORE_STATE_FINISHED, AI.make_mmi()))
if(AI.can_dominate_mechs && LAZYLEN(occupants)) //Oh, I am sorry, were you using that?
to_chat(AI, span_warning("Occupants detected! Forced ejection initiated!"))
to_chat(occupants, span_danger("You have been forcibly ejected!"))
diff --git a/code/modules/vehicles/mecha/mecha_mob_interaction.dm b/code/modules/vehicles/mecha/mecha_mob_interaction.dm
index f5bf2fed81c..57250ecb437 100644
--- a/code/modules/vehicles/mecha/mecha_mob_interaction.dm
+++ b/code/modules/vehicles/mecha/mecha_mob_interaction.dm
@@ -111,7 +111,9 @@
brain_mob.log_message("was put into [src] by [key_name(user)]", LOG_GAME, log_globally = FALSE)
return TRUE
+
/obj/vehicle/sealed/mecha/mob_exit(mob/M, silent = FALSE, randomstep = FALSE, forced = FALSE)
+ // FIXME: this code is really bad (shocker). Needs a refactor
var/atom/movable/mob_container
var/turf/newloc = get_turf(src)
if(ishuman(M))
@@ -147,9 +149,7 @@
if(!forced && !silent)
to_chat(AI, span_notice("Returning to core..."))
mecha_flags &= ~SILICON_PILOT
- newloc = get_turf(AI.linked_core)
- qdel(AI.linked_core)
- AI.forceMove(newloc)
+ AI.resolve_core_link()
if(forced)
to_chat(AI, span_danger("ZZUZULU.ERR--ERRR-NEUROLOG-- PERCEP--- DIST-B**@"))
for(var/count in 1 to 5)
diff --git a/icons/mob/silicon/ai.dmi b/icons/mob/silicon/ai.dmi
index df9059fb09e..be9060ecd55 100644
Binary files a/icons/mob/silicon/ai.dmi and b/icons/mob/silicon/ai.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index f4204593f57..e14e3913392 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -2251,6 +2251,8 @@
#include "code\game\machinery\wall_healer.dm"
#include "code\game\machinery\washing_machine.dm"
#include "code\game\machinery\wishgranter.dm"
+#include "code\game\machinery\ai_core\_core.dm"
+#include "code\game\machinery\ai_core\core_construction.dm"
#include "code\game\machinery\camera\camera.dm"
#include "code\game\machinery\camera\camera_construction.dm"
#include "code\game\machinery\camera\motion.dm"
@@ -2869,7 +2871,6 @@
#include "code\game\objects\items\tools\painter\airlock_painter.dm"
#include "code\game\objects\items\tools\painter\decal_painter.dm"
#include "code\game\objects\items\tools\painter\paintable_decals.dm"
-#include "code\game\objects\structures\ai_core.dm"
#include "code\game\objects\structures\aliens.dm"
#include "code\game\objects\structures\bedsheet_bin.dm"
#include "code\game\objects\structures\billboard.dm"
diff --git a/tools/UpdatePaths/Scripts/93873_aicore_refactor.txt b/tools/UpdatePaths/Scripts/93873_aicore_refactor.txt
new file mode 100644
index 00000000000..aa15c612a0f
--- /dev/null
+++ b/tools/UpdatePaths/Scripts/93873_aicore_refactor.txt
@@ -0,0 +1 @@
+/obj/structure/ai_core/deactivated/@SUBTYPES : /obj/structure/ai_core/@SUBTYPES{@OLD;state=5}