diff --git a/code/game/objects/structures/ai_core.dm b/code/game/objects/structures/ai_core.dm index 6207a4031c1..5c219aaa4a9 100644 --- a/code/game/objects/structures/ai_core.dm +++ b/code/game/objects/structures/ai_core.dm @@ -12,6 +12,8 @@ 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) . = ..() @@ -58,11 +60,20 @@ update_appearance() /obj/structure/ai_core/Destroy() + if(istype(remote_ai)) + remote_ai.break_core_link() + 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 @@ -157,6 +168,8 @@ return ITEM_INTERACT_SUCCESS /obj/structure/ai_core/attackby(obj/item/tool, mob/living/user, params) + if(remote_ai) + to_chat(remote_ai, span_danger("CORE TAMPERING DETECTED!")) if(!anchored) if(tool.tool_behaviour == TOOL_WELDER) if(state != EMPTY_CORE) @@ -295,8 +308,17 @@ if(tool.tool_behaviour == TOOL_CROWBAR && core_mmi) tool.play_tool_sound(src) balloon_alert(user, "removed [AI_CORE_BRAIN(core_mmi)]") - core_mmi.forceMove(loc) - return + 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) diff --git a/code/modules/antagonists/malf_ai/malf_ai.dm b/code/modules/antagonists/malf_ai/malf_ai.dm index 095ccbc1b75..d44403df84a 100644 --- a/code/modules/antagonists/malf_ai/malf_ai.dm +++ b/code/modules/antagonists/malf_ai/malf_ai.dm @@ -173,6 +173,8 @@ to_chat(malf_ai, "Your radio has been upgraded! Use :t to speak on an encrypted channel with Syndicate Agents!") + if(malf_ai.malf_picker) + return malf_ai.add_malf_picker() diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm index 292b0f1d7c1..8e7ff5ff45d 100644 --- a/code/modules/mob/living/silicon/ai/ai.dm +++ b/code/modules/mob/living/silicon/ai/ai.dm @@ -41,12 +41,13 @@ var/shunted = FALSE //1 if the AI is currently shunted. Used to differentiate between shunted and ghosted/braindead var/obj/machinery/ai_voicechanger/ai_voicechanger = null // reference to machine that holds the voicechanger var/malfhacking = FALSE // More or less a copy of the above var, so that malf AIs can hack and still get new cyborgs -- NeoFite + /// List of hacked APCs + var/list/hacked_apcs = list() var/malf_cooldown = 0 //Cooldown var for malf modules, stores a worldtime + cooldown var/obj/machinery/power/apc/malfhack var/explosive = FALSE //does the AI explode when it dies? - var/mob/living/silicon/ai/parent var/camera_light_on = FALSE var/list/obj/machinery/camera/lit_cameras = list() @@ -439,6 +440,10 @@ 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) @@ -947,6 +952,9 @@ module_picker.ui_interact(owner) /mob/living/silicon/ai/proc/add_malf_picker() + if (malf_picker) + stack_trace("Attempted to give malf AI malf picker to \[[src]\], who already has a malf picker.") + return to_chat(src, "In the top left corner of the screen you will find the Malfunction Modules button, where you can purchase various abilities, from upgraded surveillance to station ending doomsday devices.") to_chat(src, "You are also capable of hacking APCs, which grants you more points to spend on your Malfunction powers. The drawback is that a hacked APC will give you away if spotted by the crew. Hacking an APC takes 60 seconds.") view_core() //A BYOND bug requires you to be viewing your core before your verbs update @@ -1024,13 +1032,16 @@ malf_ai_datum.update_static_data_for_all_viewers() else //combat software AIs use a different UI malf_picker.update_static_data_for_all_viewers() - - apc.malfai = parent || src + if(apc.malfai) // another malf hacked this one; counter-hack! + to_chat(apc.malfai, span_warning("An adversarial subroutine has counter-hacked [apc]!")) + apc.malfai.hacked_apcs -= apc + apc.malfai = src apc.malfhack = TRUE apc.locked = TRUE apc.coverlocked = TRUE apc.flicker_hacked_icon() apc.set_hacked_hud() + hacked_apcs += apc playsound(get_turf(src), 'sound/machines/ding.ogg', 50, TRUE, ignore_walls = FALSE) to_chat(src, "Hack complete. [apc] is now under your exclusive control.") diff --git a/code/modules/mob/living/silicon/ai/ai_defense.dm b/code/modules/mob/living/silicon/ai/ai_defense.dm index 1148b639d01..a80b84b95aa 100644 --- a/code/modules/mob/living/silicon/ai/ai_defense.dm +++ b/code/modules/mob/living/silicon/ai/ai_defense.dm @@ -2,6 +2,7 @@ /mob/living/silicon/ai/attackby(obj/item/W, mob/user, params) if(istype(W, /obj/item/ai_module)) var/obj/item/ai_module/MOD = W + disconnect_shell() if(!mind) //A player mind is required for law procs to run antag checks. to_chat(user, span_warning("[src] is entirely unresponsive!")) return @@ -137,7 +138,7 @@ return ITEM_INTERACT_SUCCESS balloon_alert(src, "neural network being disconnected...") balloon_alert(user, "disconnecting neural network...") - if(!tool.use_tool(src, user, (stat == DEAD ? 40 SECONDS : 5 SECONDS))) + if(!tool.use_tool(src, user, (stat == DEAD ? 5 SECONDS : 40 SECONDS))) return ITEM_INTERACT_SUCCESS if(IS_MALF_AI(src)) to_chat(user, span_userdanger("The voltage inside the wires rises dramatically!")) diff --git a/code/modules/mob/living/silicon/ai/ai_say.dm b/code/modules/mob/living/silicon/ai/ai_say.dm index 2a8b8e6c4a0..4eb56b42384 100644 --- a/code/modules/mob/living/silicon/ai/ai_say.dm +++ b/code/modules/mob/living/silicon/ai/ai_say.dm @@ -1,20 +1,3 @@ -/mob/living/silicon/ai/say( - message, - bubble_type, - list/spans = list(), - sanitize = TRUE, - datum/language/language, - ignore_spam = FALSE, - forced, - filterproof = FALSE, - message_range = 7, - datum/saymode/saymode, - list/message_mods = list(), -) - if(istype(parent) && parent.stat != DEAD) //If there is a defined "parent" AI, it is actually an AI, and it is alive, anything the AI tries to say is said by the parent instead. - return parent.say(arglist(args)) - return ..() - /mob/living/silicon/ai/compose_track_href(atom/movable/speaker, namepart) var/mob/M = speaker.GetSource() if(M) diff --git a/code/modules/mob/living/silicon/ai/death.dm b/code/modules/mob/living/silicon/ai/death.dm index 03824857c4e..79e14d649fc 100644 --- a/code/modules/mob/living/silicon/ai/death.dm +++ b/code/modules/mob/living/silicon/ai/death.dm @@ -1,4 +1,4 @@ -/mob/living/silicon/ai/death(gibbed) +/mob/living/silicon/ai/death(gibbed, drop_mmi = TRUE) if(stat == DEAD) return @@ -33,7 +33,7 @@ ShutOffDoomsdayDevice() - if(gibbed) + if(gibbed && drop_mmi) make_mmi_drop_and_transfer() if(explosive) diff --git a/code/modules/power/apc/apc_attack.dm b/code/modules/power/apc/apc_attack.dm index b57f3465f0d..acaaac1bd3e 100644 --- a/code/modules/power/apc/apc_attack.dm +++ b/code/modules/power/apc/apc_attack.dm @@ -110,13 +110,17 @@ return TRUE if(!HAS_SILICON_ACCESS(user)) return TRUE + . = TRUE var/mob/living/silicon/ai/AI = user var/mob/living/silicon/robot/robot = user - if(aidisabled || malfhack && istype(malfai) && ((istype(AI) && (malfai != AI && malfai != AI.parent)) || (istype(robot) && (robot in malfai.connected_robots)))) - if(!loud) - balloon_alert(user, "it's disabled!") - return FALSE - return TRUE + if(istype(AI) || istype(robot)) + if(aidisabled) + . = FALSE + else if(istype(malfai) && (malfai != AI || !(robot in malfai.connected_robots))) + . = FALSE + if (!. && !loud) + balloon_alert(user, "it's disabled!") + return . /obj/machinery/power/apc/proc/set_broken() if(machine_stat & BROKEN) diff --git a/code/modules/power/apc/apc_main.dm b/code/modules/power/apc/apc_main.dm index 3e09ff89205..ae30a350496 100644 --- a/code/modules/power/apc/apc_main.dm +++ b/code/modules/power/apc/apc_main.dm @@ -228,8 +228,11 @@ find_and_hang_on_wall() /obj/machinery/power/apc/Destroy() - if(malfai && operating) - malfai.malf_picker.processing_time = clamp(malfai.malf_picker.processing_time - 10, 0, 1000) + if(malfai) + if(operating) + malfai.malf_picker.processing_time = clamp(malfai.malf_picker.processing_time - 10, 0, 1000) + malfai.hacked_apcs -= src + malfai = null disconnect_from_area() QDEL_NULL(alarm_manager) if(occupier) diff --git a/code/modules/power/apc/apc_malf.dm b/code/modules/power/apc/apc_malf.dm index dee0d95de4d..55152d8e01d 100644 --- a/code/modules/power/apc/apc_malf.dm +++ b/code/modules/power/apc/apc_malf.dm @@ -1,7 +1,7 @@ /obj/machinery/power/apc/proc/get_malf_status(mob/living/silicon/ai/malf) if(!istype(malf) || !malf.malf_picker) return APC_AI_NO_MALF - if(malfai != (malf.parent || malf)) + if(malfai != malf) return APC_AI_NO_HACK if(occupier == malf) return APC_AI_HACK_SHUNT_HERE @@ -12,7 +12,7 @@ /obj/machinery/power/apc/proc/malfhack(mob/living/silicon/ai/malf) if(!istype(malf)) return - if(get_malf_status(malf) != 1) + if(get_malf_status(malf) != APC_AI_HACK_NO_SHUNT || get_malf_status(malf) != APC_AI_NO_HACK) return if(malf.malfhacking) to_chat(malf, span_warning("You are already hacking an APC!")) @@ -37,18 +37,16 @@ if(!is_station_level(z)) return malf.ShutOffDoomsdayDevice() - occupier = new /mob/living/silicon/ai(src, malf.laws.copy_lawset(), malf) //DEAR GOD WHY? //IKR???? - occupier.adjustOxyLoss(malf.getOxyLoss()) + 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.linked_core.remote_ai = malf // note that we do not set the deactivated core's core_mmi.brainmob + malf.forceMove(src) // move INTO the APC, not to its tile if(!findtext(occupier.name, "APC Copy")) occupier.name = "[malf.name] APC Copy" - if(malf.parent) - occupier.parent = malf.parent - else - occupier.parent = malf malf.shunted = TRUE occupier.eyeobj.name = "[occupier.name] (AI Eye)" - if(malf.parent) - qdel(malf) + occupier.eyeobj.forceMove(src.loc) for(var/obj/item/pinpointer/nuke/disk_pinpointers in GLOB.pinpointer_list) disk_pinpointers.switch_mode_to(TRACK_MALF_AI) //Pinpointer will track the shunted AI var/datum/action/innate/core_return/return_action = new @@ -58,12 +56,11 @@ /obj/machinery/power/apc/proc/malfvacate(forced) if(!occupier) return - if(occupier.parent && occupier.parent.stat != DEAD) - occupier.mind.transfer_to(occupier.parent) - occupier.parent.shunted = FALSE - occupier.parent.setOxyLoss(occupier.getOxyLoss()) - occupier.parent.cancel_camera() - qdel(occupier) + if(occupier.linked_core) + occupier.shunted = FALSE + occupier.forceMove(occupier.linked_core.loc) + qdel(occupier.linked_core) + occupier.cancel_camera() return to_chat(occupier, span_danger("Primary core damaged, unable to return core processes.")) if(forced) @@ -89,7 +86,7 @@ if(!occupier.mind || !occupier.client) to_chat(user, span_warning("[occupier] is either inactive or destroyed!")) return FALSE - if(!occupier.parent.stat) + if(occupier.linked_core) //if they have an active linked_core, they can't be transferred from an APC to_chat(user, span_warning("[occupier] is refusing all attempts at transfer!") ) return FALSE if(transfer_in_progress) @@ -127,7 +124,7 @@ to_chat(occupier, span_notice("Transfer complete! You've been stored in [user]'s [card.name].")) occupier.forceMove(card) card.AI = occupier - occupier.parent.shunted = FALSE + occupier.shunted = FALSE occupier.cancel_camera() occupier = null transfer_in_progress = FALSE diff --git a/code/modules/vehicles/mecha/_mecha.dm b/code/modules/vehicles/mecha/_mecha.dm index 9d059ba7e22..ca1728f3fc0 100644 --- a/code/modules/vehicles/mecha/_mecha.dm +++ b/code/modules/vehicles/mecha/_mecha.dm @@ -274,7 +274,7 @@ /// and gets deleted with the mech. However, they do remain in .contents var/list/potential_occupants = contents | occupants for(var/mob/buggy_ejectee in potential_occupants) - mob_exit(buggy_ejectee, silent = TRUE) + mob_exit(buggy_ejectee, silent = TRUE, forced = TRUE) if(LAZYLEN(flat_equipment)) for(var/obj/item/mecha_parts/mecha_equipment/equip as anything in flat_equipment) @@ -328,7 +328,7 @@ for(var/mob/living/occupant as anything in occupants) if(isAI(occupant)) var/mob/living/silicon/ai/ai = occupant - if(!ai.linked_core) // we probably shouldnt gib AIs with a core + if(!ai.linked_core && !ai.can_shunt) // we probably shouldnt gib AIs with a core or shunting abilities unlucky_ai = occupant ai.investigate_log("has been gibbed by having their mech destroyed.", INVESTIGATE_DEATHS) ai.gib(DROP_ALL_REMAINS) //No wreck, no AI to recover diff --git a/code/modules/vehicles/mecha/mecha_ai_interaction.dm b/code/modules/vehicles/mecha/mecha_ai_interaction.dm index 9ae35d8ff4b..3a681cac97d 100644 --- a/code/modules/vehicles/mecha/mecha_ai_interaction.dm +++ b/code/modules/vehicles/mecha/mecha_ai_interaction.dm @@ -67,6 +67,7 @@ if(AI_MECH_HACK) //Called by AIs on the mech AI.linked_core = new /obj/structure/ai_core/deactivated(AI.loc) + AI.linked_core.remote_ai = AI 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!")) @@ -101,6 +102,7 @@ AI.eyeobj?.RegisterSignal(src, COMSIG_MOVABLE_MOVED, TYPE_PROC_REF(/mob/camera/ai_eye, update_visibility)) AI.controlled_equipment = src AI.remote_control = src + AI.ShutOffDoomsdayDevice() to_chat(AI, AI.can_dominate_mechs ? span_greenannounce("Takeover of [name] complete! You are now loaded onto the onboard computer. Do not attempt to leave the station sector!") :\ span_notice("You have been uploaded to a mech's onboard computer.")) to_chat(AI, "Use Middle-Mouse or the action button in your HUD to toggle equipment safety. Clicks with safety enabled will pass AI commands.") diff --git a/code/modules/vehicles/mecha/mecha_mob_interaction.dm b/code/modules/vehicles/mecha/mecha_mob_interaction.dm index d16e4af1541..e72d5505cb6 100644 --- a/code/modules/vehicles/mecha/mecha_mob_interaction.dm +++ b/code/modules/vehicles/mecha/mecha_mob_interaction.dm @@ -119,19 +119,29 @@ //stop listening to this signal, as the static update is now handled by the eyeobj's setLoc AI.eyeobj?.UnregisterSignal(src, COMSIG_MOVABLE_MOVED) AI.eyeobj?.forceMove(newloc) //kick the eye out as well - if(forced)//This should only happen if there are multiple AIs in a round, and at least one is Malf. + if(forced) + AI.controlled_equipment = null + AI.remote_control = null if(!AI.linked_core) //if the victim AI has no core - AI.investigate_log("has been gibbed by being forced out of their mech by another AI.", INVESTIGATE_DEATHS) - AI.gib(DROP_ALL_REMAINS) //If one Malf decides to steal a mech from another AI (even other Malfs!), they are destroyed, as they have nowhere to go when replaced. - AI = null - mecha_flags &= ~SILICON_PILOT - return + if (!AI.can_shunt || !length(AI.hacked_apcs)) + AI.investigate_log("has been gibbed by being forced out of their mech.", INVESTIGATE_DEATHS) + /// If an AI with no core (and no shunting abilities) gets forced out of their mech + /// (in a way that isn't handled by the normal handling of their mech being destroyed) + /// we gib 'em here, too. + AI.gib(DROP_ALL_REMAINS) + AI = null + mecha_flags &= ~SILICON_PILOT + return + else + var/obj/machinery/power/apc/emergency_shunt_apc = pick(AI.hacked_apcs) + emergency_shunt_apc.malfoccupy(AI) //get shunted into a random APC (you don't get to choose which) + AI = null + mecha_flags &= ~SILICON_PILOT + return + newloc = get_turf(AI.linked_core) + qdel(AI.linked_core) + AI.forceMove(newloc) else - if(!AI.linked_core) - if(!silent) - to_chat(AI, span_userdanger("Inactive core destroyed. Unable to return.")) - AI.linked_core = null - return if(!silent) to_chat(AI, span_notice("Returning to core...")) AI.controlled_equipment = null @@ -139,6 +149,7 @@ mob_container = AI newloc = get_turf(AI.linked_core) qdel(AI.linked_core) + AI.forceMove(newloc) else if(isliving(M)) mob_container = M else @@ -186,9 +197,20 @@ /obj/vehicle/sealed/mecha/container_resist_act(mob/living/user) if(isAI(user)) var/mob/living/silicon/ai/AI = user - if(!AI.can_shunt) - to_chat(AI, span_notice("You can't leave a mech after dominating it!.")) - return FALSE + if(!AI.linked_core) + to_chat(AI, span_userdanger("Inactive core destroyed. Unable to return.")) + if(!AI.can_shunt || !AI.hacked_apcs.len) + to_chat(AI, span_warning("[AI.can_shunt ? "No hacked APCs available." : "No shunting capabilities."]")) + return + var/confirm = tgui_alert(AI, "Shunt to a random APC? You won't have anywhere else to go!", "Confirm Emergency Shunt", list("Yes", "No")) + if(confirm == "Yes") + /// Mechs with open cockpits can have the pilot shot by projectiles, or EMPs may destroy the AI inside + /// Alternatively, destroying the mech will shunt the AI if they can shunt, or a deadeye wizard can hit + /// them with a teleportation bolt + if (AI.stat == DEAD || AI.loc != src) + return + mob_exit(AI, forced = TRUE) + return to_chat(user, span_notice("You begin the ejection procedure. Equipment is disabled during this process. Hold still to finish ejecting.")) is_currently_ejecting = TRUE if(do_after(user, has_gravity() ? exit_delay : 0 , target = src))