From 4c820ea0aa96a4867e687454a955ebe66f66cdfc Mon Sep 17 00:00:00 2001 From: Iajret Creature <122297233+Steals-The-PRs@users.noreply.github.com> Date: Mon, 29 Apr 2024 07:16:53 +0300 Subject: [PATCH] [MIRROR] bug fixes and code refactor for AI, malf or otherwise (#2222) (#3096) * bug fixes and code refactor for AI, malf or otherwise (#82590) ## About The Pull Request I was trying to fix a bug with ejecting from mechs as malf AI and the more I looked the worse it seemed to get? So I'm putting in this PR with the intent to refactor AI code to not be a Byzantine nightmare of new objects referencing each other incompletely or with buggy behavior. Finished PR for #82579 because I didn't want to clutter the comments with commits of me trying to fix shit with git restore and revert ## Why It's Good For The Game Fixes #81877 Fixes #82524 Mech dominating now just works off (and integrates with) similar code for APC shunting The cores left behind by AIs shunting or controlling mechs now properly reference the AI instead of only the other way around Some of these refactors slightly change how malf works; I think most of it was unintended behavior in the first place, let me know in review if not ## Changelog The code for AIs remoting out of their shell has been refactored. :cl: fix: Mech domination now properly integrates with shunting. fix: Combat upgraded AIs no longer get two buggy malf ability pickers if they also become malfunctioning refactor: Refactored most of the functionality around malf AI shunting, mech control /:cl: --------- * bug fixes and code refactor for AI, malf or otherwise --------- Co-authored-by: NovaBot <154629622+NovaBot13@users.noreply.github.com> Co-authored-by: Joshua Kidder <49173900+Metekillot@users.noreply.github.com> Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com> --- code/game/objects/structures/ai_core.dm | 26 +++++++++- code/modules/antagonists/malf_ai/malf_ai.dm | 2 + code/modules/mob/living/silicon/ai/ai.dm | 17 +++++-- .../mob/living/silicon/ai/ai_defense.dm | 3 +- code/modules/mob/living/silicon/ai/ai_say.dm | 17 ------- code/modules/mob/living/silicon/ai/death.dm | 4 +- code/modules/power/apc/apc_attack.dm | 14 ++++-- code/modules/power/apc/apc_main.dm | 7 ++- code/modules/power/apc/apc_malf.dm | 33 ++++++------ code/modules/vehicles/mecha/_mecha.dm | 4 +- .../vehicles/mecha/mecha_ai_interaction.dm | 2 + .../vehicles/mecha/mecha_mob_interaction.dm | 50 +++++++++++++------ 12 files changed, 113 insertions(+), 66 deletions(-) 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 4ee14ad9245..9d02d2ff72f 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 33215f90032..490c249890d 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 2aed0bbb449..344454c8a1c 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 425cad5d821..9d88adcd65c 100644 --- a/code/modules/power/apc/apc_attack.dm +++ b/code/modules/power/apc/apc_attack.dm @@ -121,13 +121,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 1f3c0cbaaaf..2940fcf6243 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))