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

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

---------

Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com>
This commit is contained in:
Y0SH1M4S73R
2025-09-03 23:25:50 -07:00
committed by GitHub
co-authored by Ghom
parent 473157f915
commit f8511ea227
21 changed files with 416 additions and 122 deletions
@@ -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)
@@ -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"
+3
View File
@@ -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
+1
View File
@@ -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,
+6
View File
@@ -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)
+5 -5
View File
@@ -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)
@@ -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))
+3 -2
View File
@@ -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)
+5 -51
View File
@@ -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
+5
View File
@@ -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
+1 -1
View File
@@ -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.")
+1 -1
View File
@@ -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")
@@ -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)
+1 -1
View File
@@ -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."))
@@ -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
+5 -1
View File
@@ -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
+2 -2
View File
@@ -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)
@@ -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
+211 -53
View File
@@ -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
@@ -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)
+2
View File
@@ -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"