From f8511ea227c9527bb59da3ac8d992f1ee01646e8 Mon Sep 17 00:00:00 2001 From: Y0SH1M4S73R Date: Thu, 4 Sep 2025 02:25:50 -0400 Subject: [PATCH] You can install B.O.R.I.S. in circuits with MMI components (+other MMI component changes) (#92002) ## About The Pull Request This PR adds the ability to install a B.O.R.I.S. in a circuit that contains an MMI component. These circuits can then be remotely connected to by an AI by clicking on them or anything they are inside of. To indicate that a circuit allows remote AI connection, an indicator is given to the circuit and anything containing it. Additionally: - Refactors the MMI component to use `item_interaction`, since it was pertinent. - You cannot insert an MMI/B.O.R.I.S. into a locked circuit. - You can no longer hotswap MMIs/B.O.R.I.S.es - you must manually eject the inserted one. Let me know what changelog labels I should use for the hotswap removal and the prevention of insertion into locked circuits. ## Why It's Good For The Game If you can put an MMI or posibrain in a circuit, why not allow an AI to use it using a B.O.R.I.S.? ## Changelog :cl: add: B.O.R.I.S.es can be installed inside of integrated circuits with MMI components, allowing an AI to remotely interface with them the same way an MMI or posibrain could. refactor: The MMI component now uses item interaction behavior for inserting MMIs/B.O.R.I.S.es. /:cl: --------- Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com> --- .../signals_atom/signals_atom_attack.dm | 2 + .../signals_mob/signals_mob_silicon.dm | 2 + code/__DEFINES/traits/declarations.dm | 3 + code/_globalvars/traits/_traits.dm | 1 + code/_onclick/ai.dm | 6 + code/datums/components/connect_containers.dm | 10 +- .../shuttle_move_deferred_checks.dm | 72 +++++ code/datums/components/stationloving.dm | 5 +- code/datums/components/usb_port.dm | 56 +--- code/game/atom/alternate_appearance.dm | 5 + code/game/machinery/computer/aifixer.dm | 2 +- code/game/objects/items/devices/aicard.dm | 2 +- .../game/objects/items/devices/aicard_evil.dm | 2 +- code/game/objects/structures/ai_core.dm | 2 +- .../bitrunning/components/avatar_gear.dm | 6 +- code/modules/mob/living/silicon/ai/ai.dm | 6 +- code/modules/mod/mod_ai.dm | 4 +- .../vehicles/mecha/mecha_ai_interaction.dm | 4 +- code/modules/wiremod/components/action/mmi.dm | 264 ++++++++++++++---- .../component_boris_circuit_container.dm | 82 ++++++ tgstation.dme | 2 + 21 files changed, 416 insertions(+), 122 deletions(-) create mode 100644 code/datums/components/shuttle_move_deferred_checks.dm create mode 100644 code/modules/wiremod/dcs_components/component_boris_circuit_container.dm diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_attack.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_attack.dm index 3ac9c088605..bf1f2eff3ab 100644 --- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_attack.dm +++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_attack.dm @@ -51,6 +51,8 @@ #define COMSIG_ATOM_ATTACK_ROBOT "atom_attack_robot" /// from base of atom/attack_robot_secondary(): (mob/user) #define COMSIG_ATOM_ATTACK_ROBOT_SECONDARY "atom_attack_robot_secondary" +/// from base of atom/attack_ai(): (mob/user, params) +#define COMSIG_ATOM_ATTACK_AI "atom_attack_ai" ///from relay_attackers element: (atom/attacker, attack_flags) #define COMSIG_ATOM_WAS_ATTACKED "atom_was_attacked" ///Called before a atom gets something tilted on them. If [COMPONENT_IMMUNE_TO_TILT_AND_CRUSH] is returned in a signal, the atom will be unaffected: (atom/target, atom/source) 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 aee6f2df79b..564465dfcf2 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_silicon.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_silicon.dm @@ -17,3 +17,5 @@ #define COMSIG_SILICON_AI_OCCUPY_APC "AI_occupy_apc" ///called when an AI vacates an APC #define COMSIG_SILICON_AI_VACATE_APC "AI_vacate_apc" +///called when an AI's control is toggled +#define COMSIG_SILICON_AI_SET_CONTROL_DISABLED "AI_set_control_disabled" diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm index 92c34ed34e3..47fd640341b 100644 --- a/code/__DEFINES/traits/declarations.dm +++ b/code/__DEFINES/traits/declarations.dm @@ -1562,4 +1562,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// Trait that allows an item to perform holy rites akin to a nullrod #define TRAIT_NULLROD_ITEM "nullrod_item" +/// Trait specifying that an AI has a remote connection to an integrated circuit +#define TRAIT_CONNECTED_TO_CIRCUIT "connected_to_circuit" + // END TRAIT DEFINES diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm index e6ee72fd897..142d9948c82 100644 --- a/code/_globalvars/traits/_traits.dm +++ b/code/_globalvars/traits/_traits.dm @@ -228,6 +228,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_COAGULATING" = TRAIT_COAGULATING, "TRAIT_COLORBLIND" = TRAIT_COLORBLIND, "TRAIT_COMBAT_MODE_LOCK" = TRAIT_COMBAT_MODE_LOCK, + "TRAIT_CONNECTED_TO_CIRCUIT" = TRAIT_CONNECTED_TO_CIRCUIT, "TRAIT_CORPSELOCKED" = TRAIT_CORPSELOCKED, "TRAIT_CRITICAL_CONDITION" = TRAIT_CRITICAL_CONDITION, "TRAIT_CULT_HALO" = TRAIT_CULT_HALO, diff --git a/code/_onclick/ai.dm b/code/_onclick/ai.dm index 3e41d019fad..1f5c943d454 100644 --- a/code/_onclick/ai.dm +++ b/code/_onclick/ai.dm @@ -78,6 +78,8 @@ set_waypoint(A) return + if(SEND_SIGNAL(A, COMSIG_ATOM_ATTACK_AI, src, params) & COMPONENT_CANCEL_ATTACK_CHAIN) + return A.attack_ai(src) /* @@ -87,9 +89,13 @@ it functions and re-insert it above. */ /mob/living/silicon/ai/UnarmedAttack(atom/A, proximity_flag, list/modifiers) + if(SEND_SIGNAL(A, COMSIG_ATOM_ATTACK_AI, src) & COMPONENT_CANCEL_ATTACK_CHAIN) + return A.attack_ai(src) /mob/living/silicon/ai/RangedAttack(atom/A) + if(SEND_SIGNAL(A, COMSIG_ATOM_ATTACK_AI, src) & COMPONENT_CANCEL_ATTACK_CHAIN) + return A.attack_ai(src) /atom/proc/attack_ai(mob/user) diff --git a/code/datums/components/connect_containers.dm b/code/datums/components/connect_containers.dm index 59eabcf6658..e04656a0cc2 100644 --- a/code/datums/components/connect_containers.dm +++ b/code/datums/components/connect_containers.dm @@ -1,6 +1,6 @@ /// This component behaves similar to connect_loc_behalf, but it's nested and hooks a signal onto all MOVABLES containing this atom. /datum/component/connect_containers - dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS + dupe_mode = COMPONENT_DUPE_SELECTIVE /// An assoc list of signal -> procpath to register to the loc this object is on. var/list/connections @@ -22,13 +22,13 @@ set_tracked(null) return ..() -/datum/component/connect_containers/InheritComponent(datum/component/component, original, atom/movable/tracked, list/connections) +/datum/component/connect_containers/CheckDupeComponent(datum/component/connect_containers/new_component, atom/movable/tracked, list/connections) // Not equivalent. Checks if they are not the same list via shallow comparison. if(!compare_list(src.connections, connections)) - stack_trace("connect_containers component attached to [parent] tried to inherit another connect_containers component with different connections") - return + return FALSE // Different set of connections. if(src.tracked != tracked) - set_tracked(tracked) + set_tracked(tracked) // Different target for the same set of connections, track the new target. + return TRUE // No new component. /datum/component/connect_containers/proc/set_tracked(atom/movable/new_tracked) if(tracked) diff --git a/code/datums/components/shuttle_move_deferred_checks.dm b/code/datums/components/shuttle_move_deferred_checks.dm new file mode 100644 index 00000000000..e7268fe98c0 --- /dev/null +++ b/code/datums/components/shuttle_move_deferred_checks.dm @@ -0,0 +1,72 @@ +// This component allows for certain movement checks, particularly those involving linked atoms existing on the same z-level, to be deferred when a shuttle moves. +/datum/component/shuttle_move_deferred_checks + dupe_mode = COMPONENT_DUPE_SOURCES + /// An list of targets to listen for the movements of + var/list/targets = list() + + /// The check to call on the parent when a target moves. Can be the name of a proc on the parent, or a `/datum/callback`. + var/check + + /// A list of each target currently being moved by a shuttle - if this list is not empty, checks will not be run. + var/list/moving_targets = list() + +/datum/component/shuttle_move_deferred_checks/Initialize(check) + . = ..() + if(!check) + return COMPONENT_INCOMPATIBLE + src.check = check + +/datum/component/shuttle_move_deferred_checks/Destroy(force) + targets = null + check = null + moving_targets = null + return ..() + +/datum/component/shuttle_move_deferred_checks/on_source_add(source, check) + . = ..() + var/atom/movable/movable = locate(source) + if(!istype(movable) || (check != src.check)) + return COMPONENT_INCOMPATIBLE + targets += movable + RegisterSignal(movable, COMSIG_MOVABLE_MOVED, PROC_REF(on_target_moved)) + RegisterSignal(movable, COMSIG_ATOM_BEFORE_SHUTTLE_MOVE, PROC_REF(before_target_shuttle_move)) + RegisterSignal(movable, COMSIG_ATOM_AFTER_SHUTTLE_MOVE, PROC_REF(after_target_shuttle_move)) + RegisterSignal(movable, COMSIG_QDELETING, PROC_REF(on_target_deleted)) + +/datum/component/shuttle_move_deferred_checks/on_source_remove(source) + var/atom/movable/movable = locate(source) + if(!istype(movable)) + return + targets -= movable + moving_targets -= movable + UnregisterSignal(movable, list(COMSIG_MOVABLE_MOVED, COMSIG_ATOM_BEFORE_SHUTTLE_MOVE, COMSIG_ATOM_AFTER_SHUTTLE_MOVE, COMSIG_QDELETING)) + if(!length(moving_targets) && length(targets)) + call_check() + return ..() + +/datum/component/shuttle_move_deferred_checks/proc/call_check() + if(istype(check, /datum/callback)) + var/datum/callback/callback_check = check + callback_check.Invoke() + else + call(parent, check)() + +/datum/component/shuttle_move_deferred_checks/proc/on_target_moved(atom/movable/source, atom/old_loc, dir, forced, list/old_locs) + SIGNAL_HANDLER + if(length(moving_targets)) + return + call_check() + +/datum/component/shuttle_move_deferred_checks/proc/before_target_shuttle_move(atom/source) + SIGNAL_HANDLER + moving_targets |= source + +/datum/component/shuttle_move_deferred_checks/proc/after_target_shuttle_move(atom/source) + SIGNAL_HANDLER + moving_targets -= source + if(!length(moving_targets)) + call_check() + +/datum/component/shuttle_move_deferred_checks/proc/on_target_deleted(datum/source) + SIGNAL_HANDLER + on_source_remove(REF(source)) diff --git a/code/datums/components/stationloving.dm b/code/datums/components/stationloving.dm index 8b59717da70..ce83014323c 100644 --- a/code/datums/components/stationloving.dm +++ b/code/datums/components/stationloving.dm @@ -6,6 +6,7 @@ var/disallow_soul_imbue = TRUE /// If FALSE, prevents parent from being qdel'd unless it's a force = TRUE qdel. var/allow_item_destruction = FALSE + var/datum/weakref/connect_ref /datum/component/stationloving/Initialize(inform_admins = FALSE, allow_item_destruction = FALSE) if(!ismovable(parent)) @@ -28,7 +29,7 @@ COMSIG_MOVABLE_MOVED = PROC_REF(on_parent_moved), SIGNAL_ADDTRAIT(TRAIT_SECLUDED_LOCATION) = PROC_REF(on_loc_secluded), ) - AddComponent(/datum/component/connect_containers, parent, loc_connections) + connect_ref = WEAKREF(AddComponent(/datum/component/connect_containers, parent, loc_connections)) /datum/component/stationloving/UnregisterFromParent() UnregisterSignal(parent, list( @@ -39,7 +40,7 @@ COMSIG_MOVABLE_MOVED, )) - qdel(GetComponent(/datum/component/connect_containers)) + qdel(connect_ref) /datum/component/stationloving/InheritComponent(datum/component/stationloving/newc, original, inform_admins, allow_death) if (original) diff --git a/code/datums/components/usb_port.dm b/code/datums/components/usb_port.dm index b5a370d12c6..a6ec5ffcf78 100644 --- a/code/datums/components/usb_port.dm +++ b/code/datums/components/usb_port.dm @@ -1,8 +1,3 @@ -/// Range checking is being deferred because the component's parent is being moved by a shuttle -#define PARENT_DEFERRED (1<<0) -/// Range checking is being deferred because the circuit shell being tracked is being moved by a shuttle -#define PHYSICAL_OBJECT_DEFERRED (1<<1) - /// Opens up a USB port that can be connected to by circuits, creating registerable circuit components /datum/component/usb_port dupe_mode = COMPONENT_DUPE_UNIQUE @@ -24,9 +19,6 @@ /// The current physical object that the beam is connected to and listens to. var/atom/movable/physical_object - /// Used to prevent range checking during shuttle movement, which moves atoms en-masse. - var/defer_range_checks = 0 - /// An extra callback to invoke when registering this component with its parent. Can be a proc name or a callback datum. var/extra_registration_callback @@ -64,11 +56,9 @@ /datum/component/usb_port/RegisterWithParent() RegisterSignal(parent, COMSIG_ATOM_USB_CABLE_TRY_ATTACH, PROC_REF(on_atom_usb_cable_try_attach)) - RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved)) - RegisterSignal(parent, COMSIG_ATOM_BEFORE_SHUTTLE_MOVE, PROC_REF(before_parent_shuttle_move)) - RegisterSignal(parent, COMSIG_ATOM_AFTER_SHUTTLE_MOVE, PROC_REF(after_parent_shuttle_move)) RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine)) RegisterSignal(parent, COMSIG_MOVABLE_CIRCUIT_LOADED, PROC_REF(on_load)) + AddComponentFrom(REF(parent), /datum/component/shuttle_move_deferred_checks, PROC_REF(on_moved)) for(var/obj/item/circuit_component/component as anything in circuit_components) component.register_usb_parent(parent) @@ -83,12 +73,10 @@ /datum/component/usb_port/UnregisterFromParent() UnregisterSignal(parent, list( COMSIG_ATOM_USB_CABLE_TRY_ATTACH, - COMSIG_MOVABLE_MOVED, - COMSIG_ATOM_BEFORE_SHUTTLE_MOVE, - COMSIG_ATOM_AFTER_SHUTTLE_MOVE, COMSIG_ATOM_EXAMINE, COMSIG_MOVABLE_CIRCUIT_LOADED, )) + RemoveComponentSource(REF(parent), /datum/component/shuttle_move_deferred_checks) for(var/obj/item/circuit_component/component as anything in circuit_components) component.unregister_usb_parent(parent) @@ -148,12 +136,8 @@ return SEND_SIGNAL(src, COMSIG_USB_PORT_UNREGISTER_PHYSICAL_OBJECT, physical_object) - UnregisterSignal(physical_object, list( - COMSIG_MOVABLE_MOVED, - COMSIG_ATOM_BEFORE_SHUTTLE_MOVE, - COMSIG_ATOM_AFTER_SHUTTLE_MOVE, - COMSIG_ATOM_EXAMINE, - )) + UnregisterSignal(physical_object, COMSIG_ATOM_EXAMINE) + RemoveComponentSource(REF(physical_object), /datum/component/shuttle_move_deferred_checks) /datum/component/usb_port/proc/attach_circuit_components(obj/item/integrated_circuit/circuitboard) for(var/obj/item/circuit_component/component as anything in circuit_components) @@ -228,9 +212,7 @@ var/atom/atom_parent = parent usb_cable_beam = atom_parent.Beam(new_physical_object, "usb_cable_beam", 'icons/obj/science/circuits.dmi') - RegisterSignal(new_physical_object, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved)) - RegisterSignal(new_physical_object, COMSIG_ATOM_BEFORE_SHUTTLE_MOVE, PROC_REF(before_physical_object_shuttle_move)) - RegisterSignal(new_physical_object, COMSIG_ATOM_AFTER_SHUTTLE_MOVE, PROC_REF(after_physical_object_shuttle_move)) + AddComponentFrom(REF(new_physical_object), /datum/component/shuttle_move_deferred_checks, PROC_REF(on_moved)) RegisterSignal(new_physical_object, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine_shell)) SEND_SIGNAL(src, COMSIG_USB_PORT_REGISTER_PHYSICAL_OBJECT, new_physical_object) physical_object = new_physical_object @@ -246,9 +228,6 @@ if (isnull(attached_circuit)) return - if (defer_range_checks) - return - if (IN_GIVEN_RANGE(attached_circuit, parent, USB_CABLE_MAX_RANGE)) return @@ -289,28 +268,3 @@ usb_cable_ref = null QDEL_NULL(usb_cable_beam) - -/datum/component/usb_port/proc/before_parent_shuttle_move() - SIGNAL_HANDLER - - defer_range_checks |= PARENT_DEFERRED - -/datum/component/usb_port/proc/before_physical_object_shuttle_move() - SIGNAL_HANDLER - - defer_range_checks |= PHYSICAL_OBJECT_DEFERRED - -/datum/component/usb_port/proc/after_parent_shuttle_move() - SIGNAL_HANDLER - - defer_range_checks &= ~PARENT_DEFERRED - on_moved() - -/datum/component/usb_port/proc/after_physical_object_shuttle_move() - SIGNAL_HANDLER - - defer_range_checks &= ~PHYSICAL_OBJECT_DEFERRED - on_moved() - -#undef PARENT_DEFERRED -#undef PHYSICAL_OBJECT_DEFERRED diff --git a/code/game/atom/alternate_appearance.dm b/code/game/atom/alternate_appearance.dm index 6df75d2508a..1bc5ae0d641 100644 --- a/code/game/atom/alternate_appearance.dm +++ b/code/game/atom/alternate_appearance.dm @@ -171,6 +171,11 @@ GLOBAL_LIST_EMPTY(active_alternate_appearances) return TRUE return FALSE +/datum/atom_hud/alternate_appearance/basic/ais + +/datum/atom_hud/alternate_appearance/basic/ais/mobShouldSee(mob/M) + return isAI(M) + /datum/atom_hud/alternate_appearance/basic/observers add_ghost_version = FALSE //just in case, to prevent infinite loops diff --git a/code/game/machinery/computer/aifixer.dm b/code/game/machinery/computer/aifixer.dm index 0b28775a5b8..6c6b41da72c 100644 --- a/code/game/machinery/computer/aifixer.dm +++ b/code/game/machinery/computer/aifixer.dm @@ -115,7 +115,7 @@ return AI.forceMove(src) occupier = AI - AI.control_disabled = TRUE + AI.set_control_disabled(TRUE) AI.radio_enabled = FALSE to_chat(AI, span_alert("You have been uploaded to a stationary terminal. Sadly, there is no remote access from here.")) to_chat(user, "[span_notice("Transfer successful")]: [AI.name] ([rand(1000,9999)].exe) installed and executed successfully. Local copy has been removed.") diff --git a/code/game/objects/items/devices/aicard.dm b/code/game/objects/items/devices/aicard.dm index 47875695bfb..759790ab155 100644 --- a/code/game/objects/items/devices/aicard.dm +++ b/code/game/objects/items/devices/aicard.dm @@ -141,7 +141,7 @@ wipe_ai() . = TRUE if("wireless") - AI.control_disabled = !AI.control_disabled + AI.set_control_disabled(!AI.control_disabled) to_chat(AI, span_warning("[src]'s wireless port has been [AI.control_disabled ? "disabled" : "enabled"]!")) . = TRUE if("radio") diff --git a/code/game/objects/items/devices/aicard_evil.dm b/code/game/objects/items/devices/aicard_evil.dm index d59df54073a..b63ecf25870 100644 --- a/code/game/objects/items/devices/aicard_evil.dm +++ b/code/game/objects/items/devices/aicard_evil.dm @@ -71,7 +71,7 @@ capture_ai(new_ai, user) var/obj/structure/ai_core/deactivated/detritus = locate() in get_turf(src) qdel(detritus) - AI.control_disabled = FALSE + AI.set_control_disabled(FALSE) AI.radio_enabled = TRUE do_sparks(4, TRUE, src) playsound(src, 'sound/machines/chime.ogg', 25, TRUE) diff --git a/code/game/objects/structures/ai_core.dm b/code/game/objects/structures/ai_core.dm index 8fcfca6f6ea..6eee27ce24e 100644 --- a/code/game/objects/structures/ai_core.dm +++ b/code/game/objects/structures/ai_core.dm @@ -460,7 +460,7 @@ That prevents a few funky behaviors. return //Transferring a carded AI to a core. if(interaction == AI_TRANS_FROM_CARD) - AI.control_disabled = FALSE + 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.")) diff --git a/code/modules/bitrunning/components/avatar_gear.dm b/code/modules/bitrunning/components/avatar_gear.dm index be7ede816fe..09a336f4bd0 100644 --- a/code/modules/bitrunning/components/avatar_gear.dm +++ b/code/modules/bitrunning/components/avatar_gear.dm @@ -5,6 +5,8 @@ var/datum/callback/load_callback /// Weakref to the human we are currently carried by var/datum/weakref/tracked_human_ref + /// Weakref to the connect_containers component that tracks the human + var/datum/weakref/connect_ref /datum/component/loads_avatar_gear/Initialize(datum/callback/load_callback) if(!isitem(parent)) @@ -18,14 +20,14 @@ var/static/list/loc_connections = list( COMSIG_ATOM_ENTERING = PROC_REF(on_entered_loc), ) - AddComponent(/datum/component/connect_containers, parent, loc_connections) + connect_ref = WEAKREF(AddComponent(/datum/component/connect_containers, parent, loc_connections)) /datum/component/loads_avatar_gear/UnregisterFromParent() UnregisterSignal(parent, list( COMSIG_ATOM_ENTERING, )) - qdel(GetComponent(/datum/component/connect_containers)) + qdel(connect_ref) /datum/component/loads_avatar_gear/Destroy(force) load_callback = null diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm index 75564b0448d..32811c61f80 100644 --- a/code/modules/mob/living/silicon/ai/ai.dm +++ b/code/modules/mob/living/silicon/ai/ai.dm @@ -781,7 +781,7 @@ var/obj/structure/ai_core/new_core = new /obj/structure/ai_core/deactivated(loc, posibrain_inside)//Spawns a deactivated terminal at AI location. new_core.circuit.battery = battery ai_restore_power()//So the AI initially has power. - control_disabled = TRUE //Can't control things remotely if you're stuck in a card! + set_control_disabled(TRUE) //Can't control things remotely if you're stuck in a card! radio_enabled = FALSE //No talking on the built-in radio for you either! forceMove(card) card.AI = src @@ -1098,6 +1098,10 @@ return ai_voicechanger.say_name return +/mob/living/silicon/ai/proc/set_control_disabled(control_disabled) + SEND_SIGNAL(src, COMSIG_SILICON_AI_SET_CONTROL_DISABLED, control_disabled) + src.control_disabled = control_disabled + #undef HOLOGRAM_CHOICE_CHARACTER #undef CHARACTER_TYPE_SELF #undef CHARACTER_TYPE_CREWMEMBER diff --git a/code/modules/mod/mod_ai.dm b/code/modules/mod/mod_ai.dm index 269eb4a96bb..461cb8c4f86 100644 --- a/code/modules/mod/mod_ai.dm +++ b/code/modules/mod/mod_ai.dm @@ -45,7 +45,7 @@ /// Place an AI in control of your suit functions /obj/item/mod/control/proc/ai_enter_mod(mob/living/silicon/ai/new_ai) - new_ai.control_disabled = FALSE + new_ai.set_control_disabled(FALSE) new_ai.radio_enabled = TRUE new_ai.ai_restore_power() new_ai.cancel_camera() @@ -58,7 +58,7 @@ /obj/item/mod/control/proc/ai_exit_mod(obj/item/aicard/card) var/mob/living/silicon/ai/old_ai = ai_assistant old_ai.ai_restore_power()//So the AI initially has power. - old_ai.control_disabled = TRUE + old_ai.set_control_disabled(TRUE) old_ai.radio_enabled = FALSE old_ai.disconnect_shell() old_ai.forceMove(card) diff --git a/code/modules/vehicles/mecha/mecha_ai_interaction.dm b/code/modules/vehicles/mecha/mecha_ai_interaction.dm index 032b1d5ddcc..02bc2c89953 100644 --- a/code/modules/vehicles/mecha/mecha_ai_interaction.dm +++ b/code/modules/vehicles/mecha/mecha_ai_interaction.dm @@ -64,7 +64,7 @@ return //User sat on the selection window and things changed. AI.ai_restore_power()//So the AI initially has power. - AI.control_disabled = TRUE + AI.set_control_disabled(TRUE) AI.radio_enabled = FALSE AI.disconnect_shell() remove_occupant(AI) @@ -104,7 +104,7 @@ if((LAZYLEN(occupants) >= max_occupants) || dna_lock) //Normal AIs cannot steal mechs! to_chat(user, span_warning("Access denied. [name] is [LAZYLEN(occupants) >= max_occupants ? "currently fully occupied" : "secured with a DNA lock"].")) return - AI.control_disabled = FALSE + AI.set_control_disabled(FALSE) AI.radio_enabled = TRUE 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 diff --git a/code/modules/wiremod/components/action/mmi.dm b/code/modules/wiremod/components/action/mmi.dm index 3a8cc85b00b..0a7b6a38ae8 100644 --- a/code/modules/wiremod/components/action/mmi.dm +++ b/code/modules/wiremod/components/action/mmi.dm @@ -1,5 +1,18 @@ #define MMI_MESSAGE_COOLDOWN (0.1 SECONDS) +/datum/action/innate/mmi_comp_disconnect + name = "Disconnect from remote circuit" + desc = "Stop controlling an integrated circuit" + button_icon = 'icons/mob/actions/actions_AI.dmi' + button_icon_state = "ai_core" + +/datum/action/innate/mmi_comp_disconnect/Trigger(trigger_flags) + . = ..() + if(!.) + return + var/obj/item/circuit_component/mmi/mmi_comp = target + mmi_comp.remove_occupant() + /** * # Man-Machine Interface Component * @@ -7,7 +20,7 @@ */ /obj/item/circuit_component/mmi display_name = "Man-Machine Interface" - desc = "A component that allows MMI to enter shells to send output signals." + desc = "A component that allows an MMI or B.O.R.I.S. module to be inserted into the shell, allowing a brain or artificial intelligence to output signals." category = "Action" circuit_flags = CIRCUIT_FLAG_REFUSE_MODULE @@ -34,15 +47,36 @@ /// Called when the MMI right clicks. var/datum/port/output/secondary_attack - /// The current MMI card + /// The current MMI/posibrain var/obj/item/mmi/brain + /// The current B.O.R.I.S. module + var/obj/item/borg/upgrade/ai/boris + + /// The brainmob or AI currently controlling to the circuit + var/mob/living/occupant + + /// The action used to allow a connected AI to disconnect + var/datum/action/innate/mmi_comp_disconnect/disconnect_action + /// Maximum length of the message that can be sent to the MMI var/max_length = 300 /// Cooldown for when the next message can be sent to the MMI. COOLDOWN_DECLARE(message_cooldown) + /// These two component weakrefs are needed because GetComponent is not reliable for components that are DUPE_ALLOWED or DUPE_SELECTED + + /// A reference to the connect_containers component that handles making boris circuits and things containing them clickable by AIs to connect + var/datum/weakref/boris_circuit_container_connections + + /// A reference to the connect_containers component that handles when a connected AI or something containing it moves + var/datum/weakref/connected_ai_container_connections + +/obj/item/circuit_component/mmi/Initialize(mapload) + . = ..() + disconnect_action = new(src) + /obj/item/circuit_component/mmi/populate_ports() message = add_input_port("Message", PORT_TYPE_STRING) send = add_input_port("Send Message", PORT_TYPE_SIGNAL) @@ -58,90 +92,214 @@ clicked_atom = add_output_port("Target Entity", PORT_TYPE_ATOM) /obj/item/circuit_component/mmi/Destroy() - remove_current_brain() + remove_occupant_item() + QDEL_NULL(disconnect_action) return ..() /obj/item/circuit_component/mmi/input_received(datum/port/input/port) - if(!brain) + if(!brain && !boris) return if(COMPONENT_TRIGGERED_BY(eject, port)) - remove_current_brain() + remove_occupant_item() if(COMPONENT_TRIGGERED_BY(send, port)) if(!message.value || !COOLDOWN_FINISHED(src, message_cooldown)) return var/msg_str = copytext(html_encode(message.value), 1, max_length) - var/mob/living/target = brain.brainmob - if(!target) + if(!occupant) return - to_chat(target, "[span_bold("You hear a message in your ear: ")][msg_str]") + if(isAI(occupant)) + to_chat(occupant, "[span_boldnotice("Message from remote circuit: ")][span_notice(msg_str)]") + else + to_chat(occupant, "[span_bold("You hear a message: ")][msg_str]") COOLDOWN_START(src, message_cooldown, MMI_MESSAGE_COOLDOWN) - /obj/item/circuit_component/mmi/register_shell(atom/movable/shell) . = ..() - RegisterSignal(shell, COMSIG_ATOM_ATTACKBY, PROC_REF(handle_attack_by)) + RegisterSignal(shell, COMSIG_ATOM_ITEM_INTERACTION, PROC_REF(handle_interaction)) /obj/item/circuit_component/mmi/unregister_shell(atom/movable/shell) - UnregisterSignal(shell, COMSIG_ATOM_ATTACKBY) - remove_current_brain() + UnregisterSignal(shell, list(COMSIG_ATOM_ITEM_INTERACTION)) + remove_occupant_item() return ..() -/obj/item/circuit_component/mmi/proc/handle_attack_by(atom/movable/shell, obj/item/item, mob/living/attacker) +/obj/item/circuit_component/mmi/proc/handle_interaction(atom/movable/shell, mob/living/user, obj/item/item) SIGNAL_HANDLER + var/obj/item/mmi/target_mmi + var/mob/living/new_occupant + var/obj/item/borg/upgrade/ai/target_boris if(istype(item, /obj/item/mmi)) - var/obj/item/mmi/target_mmi = item + target_mmi = item if(!target_mmi.brainmob) - return - add_mmi(item) - return COMPONENT_NO_AFTERATTACK - -/obj/item/circuit_component/mmi/proc/add_mmi(obj/item/mmi/to_add) - remove_current_brain() - - to_add.forceMove(src) - if(to_add.brainmob) - update_mmi_mob(to_add, null, to_add.brainmob) - brain = to_add - RegisterSignal(to_add, COMSIG_QDELETING, PROC_REF(remove_current_brain)) - RegisterSignal(to_add, COMSIG_MOVABLE_MOVED, PROC_REF(mmi_moved)) - -/obj/item/circuit_component/mmi/proc/mmi_moved(atom/movable/mmi) - SIGNAL_HANDLER - - if(mmi.loc != src) - remove_current_brain() - -/obj/item/circuit_component/mmi/proc/remove_current_brain() - SIGNAL_HANDLER - if(!brain) + shell.balloon_alert(user, "no consciousness detected!") + return ITEM_INTERACT_FAILURE + new_occupant = target_mmi.brainmob + else if(istype(item, /obj/item/borg/upgrade/ai)) + target_boris = item + else return + var/datum/component/shell/shell_comp = shell.GetComponent(/datum/component/shell) + if(shell_comp.locked) + shell.balloon_alert(user, "locked!") + return ITEM_INTERACT_FAILURE + if(brain || boris) + shell.balloon_alert(user, "already has brain!") + return ITEM_INTERACT_FAILURE + if(!user.transferItemToLoc(item, src)) + return ITEM_INTERACT_FAILURE + if(target_mmi) + brain = target_mmi + set_occupant(new_occupant) + if(target_boris) + boris = target_boris + register_boris_circuit(shell) + RegisterSignal(item, COMSIG_QDELETING, PROC_REF(remove_occupant_item)) + RegisterSignal(item, COMSIG_MOVABLE_MOVED, PROC_REF(occupant_item_moved)) - if(brain.brainmob) - update_mmi_mob(brain, brain.brainmob) - UnregisterSignal(brain, list( +/obj/item/circuit_component/mmi/proc/register_boris_circuit(atom/movable/shell) + var/static/list/connections = list(COMSIG_MOVABLE_MOVED = PROC_REF(boris_shell_or_container_moved)) + boris_circuit_container_connections = WEAKREF(AddComponent(/datum/component/connect_containers, src, connections)) + for(var/atom/movable/location as anything in get_nested_locs(shell) + shell) + location.AddComponentFrom(REF(src), /datum/component/boris_circuit_container) + AddComponentFrom(REF(location), /datum/component/shuttle_move_deferred_checks, PROC_REF(post_movement_checks)) + +/obj/item/circuit_component/mmi/proc/unregister_boris_circuit(atom/movable/shell) + QDEL_NULL(boris_circuit_container_connections) + for(var/atom/movable/location as anything in get_nested_locs(shell) + shell) + location.RemoveComponentSource(REF(src), /datum/component/boris_circuit_container) + RemoveComponentSource(REF(location), /datum/component/shuttle_move_deferred_checks) + +/obj/item/circuit_component/mmi/proc/boris_shell_or_container_moved(atom/movable/shell_or_container, atom/old_loc) + SIGNAL_HANDLER + if(isturf(old_loc) && isturf(shell_or_container.loc)) + return + var/list/old_locs = list() + if(ismovable(old_loc)) + old_locs = get_nested_locs(old_loc) + old_loc + var/list/new_locs = get_nested_locs(shell_or_container) + for(var/atom/movable/loc_exited as anything in old_locs - new_locs) + loc_exited.RemoveComponentSource(REF(src), /datum/component/boris_circuit_container) + RemoveComponentSource(REF(loc_exited), /datum/component/shuttle_move_deferred_checks) + for(var/atom/movable/loc_entered as anything in new_locs - old_locs) + loc_entered.AddComponentFrom(REF(src), /datum/component/boris_circuit_container) + AddComponentFrom(REF(loc_entered), /datum/component/shuttle_move_deferred_checks, PROC_REF(post_movement_checks)) + +/obj/item/circuit_component/mmi/proc/occupant_item_moved(atom/movable/occupant_item) + SIGNAL_HANDLER + + if(occupant_item.loc != src) + remove_occupant_item(occupant_item) + +/obj/item/circuit_component/mmi/proc/remove_occupant_item(obj/item/removing) + SIGNAL_HANDLER + if(!removing) + removing = brain + if(!removing) + removing = boris + if(!removing) + return + if(istype(removing, /obj/item/mmi)) + brain = null + if(istype(removing, /obj/item/borg/upgrade/ai)) + boris = null + unregister_boris_circuit(parent.shell) + remove_occupant() + + UnregisterSignal(removing, list( COMSIG_QDELETING, COMSIG_MOVABLE_MOVED )) - if(brain.loc == src) - brain.forceMove(drop_location()) - brain = null + if(removing.loc == src) + removing.forceMove(drop_location()) -/obj/item/circuit_component/mmi/proc/update_mmi_mob(datum/source, mob/living/old_mmi, mob/living/new_mmi) +/obj/item/circuit_component/mmi/proc/confirm_ai_connect(mob/living/silicon/ai/user, atom/movable/shell) + var/confirmation = tgui_alert(user, "Connect to [shell]?", buttons = list("Yes", "No")) + if(confirmation != "Yes") + return + if(QDELETED(src) || QDELETED(user) || QDELETED(shell) || !parent?.shell || !user.can_interact_with(shell) || !boris) + return + do_ai_connect(user, shell) + +/obj/item/circuit_component/mmi/proc/do_ai_connect(mob/living/silicon/ai/user, atom/movable/shell) + if(occupant) + if(occupant != user) + shell.balloon_alert(user, "occupied!") + return + set_occupant(user) + +/obj/item/circuit_component/mmi/proc/set_occupant(mob/living/new_occupant) + new_occupant.remote_control = src + RegisterSignal(new_occupant, COMSIG_MOB_CLICKON, PROC_REF(handle_occupant_attack)) + RegisterSignal(new_occupant, COMSIG_QDELETING, PROC_REF(remove_occupant)) + occupant = new_occupant + if(!isAI(new_occupant)) + return + ADD_TRAIT(new_occupant, TRAIT_CONNECTED_TO_CIRCUIT, REF(src)) + var/mob/living/silicon/ai = new_occupant + ai.reset_perspective(src) + // Perspective gets reset whenever multicam is ended, which happens whenever an AI gets incapacitated or carded. + // This could change in the future, so we also register other signal handlers. + RegisterSignals(ai, list(COMSIG_MOB_RESET_PERSPECTIVE, SIGNAL_ADDTRAIT(TRAIT_INCAPACITATED)), PROC_REF(remove_occupant)) + RegisterSignal(ai, COMSIG_SILICON_AI_SET_CONTROL_DISABLED, PROC_REF(on_control_toggled)) + var/static/list/connections = list(COMSIG_MOVABLE_MOVED = PROC_REF(occupant_or_container_moved)) + connected_ai_container_connections = WEAKREF(AddComponent(/datum/component/connect_containers, ai, connections)) + for(var/atom/movable/location as anything in get_nested_locs(ai) + ai) + AddComponentFrom(REF(location), /datum/component/shuttle_move_deferred_checks, PROC_REF(post_movement_checks)) + disconnect_action.Grant(ai) + to_chat(ai, span_notice("Established connection with remote circuit.")) + +/obj/item/circuit_component/mmi/proc/occupant_or_container_moved(atom/movable/occupant_or_container, atom/old_loc) SIGNAL_HANDLER - if(old_mmi) - old_mmi.remote_control = null - UnregisterSignal(old_mmi, COMSIG_MOB_CLICKON) - if(new_mmi) - new_mmi.remote_control = src - RegisterSignal(new_mmi, COMSIG_MOB_CLICKON, PROC_REF(handle_mmi_attack)) + if(isturf(old_loc) && isturf(occupant_or_container.loc)) + return + var/list/old_locs = list() + if(ismovable(old_loc)) + old_locs = get_nested_locs(old_loc) + old_loc + var/list/new_locs = get_nested_locs(occupant_or_container) + for(var/atom/movable/loc_exited as anything in old_locs - new_locs) + RemoveComponentSource(REF(loc_exited), /datum/component/shuttle_move_deferred_checks) + for(var/atom/movable/loc_entered as anything in new_locs - old_locs) + AddComponentFrom(REF(loc_entered), /datum/component/shuttle_move_deferred_checks, PROC_REF(post_movement_checks)) + +/obj/item/circuit_component/mmi/proc/post_movement_checks() + SIGNAL_HANDLER + var/mob/living/silicon/ai/ai = occupant + if(!istype(ai)) + return + if(!ai.can_interact_with(parent.shell)) + remove_occupant() + +/obj/item/circuit_component/mmi/proc/on_control_toggled(datum/_source, control_disabled) + SIGNAL_HANDLER + if(control_disabled) + remove_occupant() + +/obj/item/circuit_component/mmi/proc/remove_occupant() + SIGNAL_HANDLER + if(!occupant) + return + if(isAI(occupant)) + REMOVE_TRAIT(occupant, TRAIT_CONNECTED_TO_CIRCUIT, REF(src)) + var/mob/living/silicon/ai/ai = occupant + if(!ai.eyeobj) + ai.create_eye() + disconnect_action.Remove(ai) + UnregisterSignal(ai, list(COMSIG_MOB_RESET_PERSPECTIVE, SIGNAL_ADDTRAIT(TRAIT_INCAPACITATED), COMSIG_SILICON_AI_SET_CONTROL_DISABLED)) + QDEL_NULL(connected_ai_container_connections) + for(var/atom/movable/location as anything in get_nested_locs(ai) + ai) + RemoveComponentSource(REF(location), /datum/component/shuttle_move_deferred_checks) + ai.reset_perspective(null) + to_chat(ai, span_notice("Disconnected from remote circuit.")) + occupant.remote_control = null + UnregisterSignal(occupant, list(COMSIG_MOB_CLICKON, COMSIG_QDELETING)) + occupant = null /obj/item/circuit_component/mmi/relaymove(mob/living/user, direct) - if(user != brain.brainmob) + if(user != occupant) return ..() if(direct & NORTH) @@ -155,7 +313,7 @@ return TRUE -/obj/item/circuit_component/mmi/proc/handle_mmi_attack(mob/living/source, atom/target, list/modifiers, list/attack_modifiers) +/obj/item/circuit_component/mmi/proc/handle_occupant_attack(mob/living/source, atom/target, list/modifiers, list/attack_modifiers) SIGNAL_HANDLER if(modifiers[RIGHT_CLICK]) clicked_atom.set_output(target) @@ -174,7 +332,7 @@ /obj/item/circuit_component/mmi/removed_from(obj/item/integrated_circuit/removed_from) REMOVE_TRAIT(removed_from, TRAIT_COMPONENT_MMI, REF(src)) - remove_current_brain() + remove_occupant_item() return ..() #undef MMI_MESSAGE_COOLDOWN diff --git a/code/modules/wiremod/dcs_components/component_boris_circuit_container.dm b/code/modules/wiremod/dcs_components/component_boris_circuit_container.dm new file mode 100644 index 00000000000..c64437d21c0 --- /dev/null +++ b/code/modules/wiremod/dcs_components/component_boris_circuit_container.dm @@ -0,0 +1,82 @@ +/datum/component/boris_circuit_container + dupe_mode = COMPONENT_DUPE_SOURCES + var/list/obj/item/circuit_component/mmi/mmi_components + var/datum/weakref/indicator_weakref + +/datum/component/boris_circuit_container/Initialize() + . = ..() + if(!ismovable(parent)) + return COMPONENT_INCOMPATIBLE + +/datum/component/boris_circuit_container/Destroy(force) + mmi_components = null + return ..() + +/datum/component/boris_circuit_container/RegisterWithParent() + . = ..() + RegisterSignal(parent, COMSIG_ATOM_ATTACK_AI, PROC_REF(on_ai_click)) + var/atom/parent_atom = parent + var/image/indicator_image = image('icons/mob/huds/hud.dmi', parent_atom, "hudtracking", pixel_x = 8) + SET_PLANE_EXPLICIT(indicator_image, ABOVE_LIGHTING_PLANE, parent_atom) + indicator_weakref = WEAKREF(parent_atom.add_alt_appearance( + /datum/atom_hud/alternate_appearance/basic/ais, + "boris_circuit_container_[REF(src)]", + indicator_image, + NONE + )) + +/datum/component/boris_circuit_container/UnregisterFromParent() + QDEL_NULL(indicator_weakref) + return ..() + +/datum/component/boris_circuit_container/on_source_add(source) + . = ..() + var/obj/item/circuit_component/mmi/source_comp = locate(source) + if(!istype(source_comp)) + return COMPONENT_INCOMPATIBLE + LAZYADD(mmi_components, source) + +/datum/component/boris_circuit_container/on_source_remove(source) + var/obj/item/circuit_component/mmi/source_comp = locate(source) + if(source_comp) + LAZYREMOVE(mmi_components, source_comp) + return ..() + +/datum/component/boris_circuit_container/proc/on_ai_click(atom/movable/source, mob/user) + SIGNAL_HANDLER + if(!length(mmi_components)) + return + if(HAS_TRAIT(user, TRAIT_CONNECTED_TO_CIRCUIT)) + return + if(length(mmi_components) == 1) + var/obj/item/circuit_component/mmi/mmi_comp = mmi_components[1] + if(mmi_comp.parent.shell == source) + INVOKE_ASYNC(mmi_comp, TYPE_PROC_REF(/obj/item/circuit_component/mmi, confirm_ai_connect), user, source) + return COMPONENT_CANCEL_ATTACK_CHAIN + INVOKE_ASYNC(src, PROC_REF(select_circuit), source, user) + return COMPONENT_CANCEL_ATTACK_CHAIN + +/datum/component/boris_circuit_container/proc/select_circuit(atom/movable/source, mob/user) + var/list/choices = list() + var/list/choice_map = list() + var/source_in_choices = FALSE + for(var/obj/item/circuit_component/mmi/mmi_component as anything in mmi_components) + if(mmi_component.occupant) + continue + var/atom/movable/shell = mmi_component.parent.shell + choices[shell] = shell + choice_map[shell] = mmi_component + if(shell == source) + source_in_choices = TRUE + if(!source_in_choices) + choices[source] = source + var/atom/movable/choice = show_radial_menu(user, source, choices, user_space = TRUE) + if(QDELETED(choice) || QDELETED(user) || !user.can_interact_with(choice)) + return + if(choice == source && !source_in_choices) + choice.attack_ai(user) + return + var/obj/item/circuit_component/mmi/choice_comp = choice_map[choice] + if(QDELETED(choice_comp) || choice_comp.parent?.shell != choice || !choice_comp.boris) + return + choice_comp.do_ai_connect(user, choice) diff --git a/tgstation.dme b/tgstation.dme index 9f513a1e8bc..345871915cb 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1268,6 +1268,7 @@ #include "code\datums\components\shovel_hands.dm" #include "code\datums\components\shrink.dm" #include "code\datums\components\shuttle_cling.dm" +#include "code\datums\components\shuttle_move_deferred_checks.dm" #include "code\datums\components\sign_language.dm" #include "code\datums\components\simple_access.dm" #include "code\datums\components\simple_bodycam.dm" @@ -6669,6 +6670,7 @@ #include "code\modules\wiremod\datatypes\composite\composite.dm" #include "code\modules\wiremod\datatypes\composite\list.dm" #include "code\modules\wiremod\dcs_components\component_add_port.dm" +#include "code\modules\wiremod\dcs_components\component_boris_circuit_container.dm" #include "code\modules\wiremod\dcs_components\component_wirenet_connection.dm" #include "code\modules\wiremod\preset\hello_world.dm" #include "code\modules\wiremod\preset\speech_relay.dm"