Skip to content

Commit

Permalink
[MIRROR] bug fixes and code refactor for AI, malf or otherwise (#2222) (
Browse files Browse the repository at this point in the history
#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 <[email protected]>
Co-authored-by: Joshua Kidder <[email protected]>
Co-authored-by: MrMelbert <[email protected]>
  • Loading branch information
4 people authored Apr 29, 2024
1 parent 08b11b0 commit 4c820ea
Show file tree
Hide file tree
Showing 12 changed files with 113 additions and 66 deletions.
26 changes: 24 additions & 2 deletions code/game/objects/structures/ai_core.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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)
. = ..()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions code/modules/antagonists/malf_ai/malf_ai.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand Down
17 changes: 14 additions & 3 deletions code/modules/mob/living/silicon/ai/ai.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.")

Expand Down
3 changes: 2 additions & 1 deletion code/modules/mob/living/silicon/ai/ai_defense.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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!"))
Expand Down
17 changes: 0 additions & 17 deletions code/modules/mob/living/silicon/ai/ai_say.dm
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
4 changes: 2 additions & 2 deletions code/modules/mob/living/silicon/ai/death.dm
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/mob/living/silicon/ai/death(gibbed)
/mob/living/silicon/ai/death(gibbed, drop_mmi = TRUE)
if(stat == DEAD)
return

Expand Down Expand Up @@ -33,7 +33,7 @@

ShutOffDoomsdayDevice()

if(gibbed)
if(gibbed && drop_mmi)
make_mmi_drop_and_transfer()

if(explosive)
Expand Down
14 changes: 9 additions & 5 deletions code/modules/power/apc/apc_attack.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 5 additions & 2 deletions code/modules/power/apc/apc_main.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
33 changes: 15 additions & 18 deletions code/modules/power/apc/apc_malf.dm
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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!"))
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions code/modules/vehicles/mecha/_mecha.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions code/modules/vehicles/mecha/mecha_ai_interaction.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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!"))
Expand Down Expand Up @@ -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>")
50 changes: 36 additions & 14 deletions code/modules/vehicles/mecha/mecha_mob_interaction.dm
Original file line number Diff line number Diff line change
Expand Up @@ -119,26 +119,37 @@
//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
AI.remote_control = null
mob_container = AI
newloc = get_turf(AI.linked_core)
qdel(AI.linked_core)
AI.forceMove(newloc)
else if(isliving(M))
mob_container = M
else
Expand Down Expand Up @@ -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))
Expand Down

0 comments on commit 4c820ea

Please sign in to comment.