[MIRROR] bug fixes and code refactor for AI, malf or otherwise (#27499)

* 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.
🆑
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
/🆑

---------

Co-authored-by: MrMelbert <51863163+MrMelbert@ users.noreply.github.com>

* bug fixes and code refactor for AI, malf or otherwise

---------

Co-authored-by: Joshua Kidder <49173900+Metekillot@users.noreply.github.com>
Co-authored-by: MrMelbert <51863163+MrMelbert@ users.noreply.github.com>
This commit is contained in:
SkyratBot
2024-04-29 23:50:24 +01:00
committed by GitHub
co-authored by MrMelbert Joshua Kidder
parent 155d75ee38
commit c66bf3a16b
12 changed files with 113 additions and 66 deletions
+24 -2
View File
@@ -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)
@@ -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()
+14 -3
View File
@@ -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.")
@@ -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!"))
@@ -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)
+2 -2
View File
@@ -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)
+9 -5
View File
@@ -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)
+5 -2
View File
@@ -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)
+15 -18
View File
@@ -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
+2 -2
View File
@@ -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
@@ -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, "<span class='reallybig boldnotice'>Use Middle-Mouse or the action button in your HUD to toggle equipment safety. Clicks with safety enabled will pass AI commands.</span>")
@@ -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))