Files
Alexis 21b4095dfd [MDB IGNORE] [IDB IGNORE] Upstream Sync - 04/17/2026 (#5453)
Upstream 04/17/2026

fixes https://github.com/Bubberstation/Bubberstation/issues/5549

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: tgstation-ci[bot] <179393467+tgstation-ci[bot]@users.noreply.github.com>
Co-authored-by: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com>
Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
Co-authored-by: Rhials <28870487+Rhials@users.noreply.github.com>
Co-authored-by: rageguy505 <54517726+rageguy505@users.noreply.github.com>
Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
Co-authored-by: John Willard <53777086+JohnFulpWillard@users.noreply.github.com>
Co-authored-by: Aliceee2ch <160794176+Aliceee2ch@users.noreply.github.com>
Co-authored-by: Time-Green <7501474+Time-Green@users.noreply.github.com>
Co-authored-by: Tsar-Salat <62388554+Tsar-Salat@users.noreply.github.com>
Co-authored-by: SmArtKar <44720187+SmArtKar@users.noreply.github.com>
Co-authored-by: Maxipat <108554989+Maxipat112@users.noreply.github.com>
Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: SimplyLogan <47579821+loganuk@users.noreply.github.com>
Co-authored-by: loganuk <fakeemail123@aol.com>
Co-authored-by: Leland Kemble <70413276+lelandkemble@users.noreply.github.com>
Co-authored-by: FalloutFalcon <86381784+FalloutFalcon@users.noreply.github.com>
Co-authored-by: Roxy <75404941+TealSeer@users.noreply.github.com>
Co-authored-by: Lucy <lucy@absolucy.moe>
Co-authored-by: siliconOpossum <138069572+siliconOpossum@users.noreply.github.com>
Co-authored-by: Isratosh <Isratosh@hotmail.com>
Co-authored-by: TheRyeGuyWhoWillNowDie <70169560+TheRyeGuyWhoWillNowDie@users.noreply.github.com>
Co-authored-by: Neocloudy <88008002+Neocloudy@users.noreply.github.com>
Co-authored-by: Alexander V. <volas@ya.ru>
Co-authored-by: ElGitificador <168473461+ElGitificador@users.noreply.github.com>
Co-authored-by: Twaticus <46540570+Twaticus@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bloop <13398309+vinylspiders@users.noreply.github.com>
Co-authored-by: Cameron Lennox <killer65311@gmail.com>
Co-authored-by: Tim <timothymtorres@gmail.com>
Co-authored-by: Iamgoofball <iamgoofball@gmail.com>
Co-authored-by: Layzu666 <121319428+Layzu666@users.noreply.github.com>
Co-authored-by: Arturlang <24881678+Arturlang@users.noreply.github.com>
Co-authored-by: _0Steven <42909981+00-Steven@users.noreply.github.com>
Co-authored-by: mrmanlikesbt <99309552+mrmanlikesbt@users.noreply.github.com>
Co-authored-by: Ben10Omintrix <138636438+Ben10Omintrix@users.noreply.github.com>
Co-authored-by: John F. Kennedy <54908920+MacaroniCritter@users.noreply.github.com>
Co-authored-by: Cursor <102828457+theselfish@users.noreply.github.com>
Co-authored-by: Josh <josh.adam.powell@gmail.com>
Co-authored-by: Josh Powell <josh.powell@softwire.com>
Co-authored-by: Yobrocharlie <Charliemiller5617@gmail.com>
Co-authored-by: Hardly3D <66234359+Hardly3D@users.noreply.github.com>
Co-authored-by: shayoki <96078776+shayoki@users.noreply.github.com>
Co-authored-by: LT3 <83487515+lessthnthree@users.noreply.github.com>
2026-05-16 00:56:00 +02:00

339 lines
13 KiB
Plaintext

#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(mob/clicker, trigger_flags)
. = ..()
if(!.)
return
var/obj/item/circuit_component/mmi/mmi_comp = target
mmi_comp.remove_occupant()
/**
* # Man-Machine Interface Component
*
* Allows an MMI to be inserted into a shell, allowing it to be linked up. Requires a shell.
*/
/obj/item/circuit_component/mmi
display_name = "Man-Machine Interface"
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
/// The message to send to the MMI in the shell.
var/datum/port/input/message
/// Sends the current MMI a message
var/datum/port/input/send
/// Ejects the current MMI
var/datum/port/input/eject
/// Called when the MMI tries moving north
var/datum/port/output/north
/// Called when the MMI tries moving east
var/datum/port/output/east
/// Called when the MMI tries moving south
var/datum/port/output/south
/// Called when the MMI tries moving west
var/datum/port/output/west
/// Returns what the MMI last clicked on.
var/datum/port/output/clicked_atom
/// Called when the MMI clicks.
var/datum/port/output/attack
/// Called when the MMI right clicks.
var/datum/port/output/secondary_attack
/// 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)
eject = add_input_port("Eject", PORT_TYPE_SIGNAL)
north = add_output_port("North", PORT_TYPE_SIGNAL)
east = add_output_port("East", PORT_TYPE_SIGNAL)
south = add_output_port("South", PORT_TYPE_SIGNAL)
west = add_output_port("West", PORT_TYPE_SIGNAL)
attack = add_output_port("Attack", PORT_TYPE_SIGNAL)
secondary_attack = add_output_port("Secondary Attack", PORT_TYPE_SIGNAL)
clicked_atom = add_output_port("Target Entity", PORT_TYPE_ATOM)
/obj/item/circuit_component/mmi/Destroy()
remove_occupant_item()
QDEL_NULL(disconnect_action)
return ..()
/obj/item/circuit_component/mmi/input_received(datum/port/input/port)
if(!brain && !boris)
return
if(COMPONENT_TRIGGERED_BY(eject, port))
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)
if(!occupant)
return
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_ITEM_INTERACTION, PROC_REF(handle_interaction))
/obj/item/circuit_component/mmi/unregister_shell(atom/movable/shell)
UnregisterSignal(shell, list(COMSIG_ATOM_ITEM_INTERACTION))
remove_occupant_item()
return ..()
/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))
target_mmi = item
if(!target_mmi.brainmob)
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))
/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(removing.loc == src)
removing.forceMove(drop_location())
/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(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 != occupant)
return ..()
if(direct & NORTH)
north.set_output(COMPONENT_SIGNAL)
if(direct & WEST)
west.set_output(COMPONENT_SIGNAL)
if(direct & EAST)
east.set_output(COMPONENT_SIGNAL)
if(direct & SOUTH)
south.set_output(COMPONENT_SIGNAL)
return TRUE
/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)
secondary_attack.set_output(COMPONENT_SIGNAL)
. = COMSIG_MOB_CANCEL_CLICKON
else if(modifiers[LEFT_CLICK] && !modifiers[SHIFT_CLICK] && !modifiers[ALT_CLICK] && !modifiers[CTRL_CLICK])
clicked_atom.set_output(target)
attack.set_output(COMPONENT_SIGNAL)
. = COMSIG_MOB_CANCEL_CLICKON
/obj/item/circuit_component/mmi/add_to(obj/item/integrated_circuit/add_to)
. = ..()
if(HAS_TRAIT(add_to, TRAIT_COMPONENT_MMI))
return FALSE
ADD_TRAIT(add_to, TRAIT_COMPONENT_MMI, REF(src))
/obj/item/circuit_component/mmi/removed_from(obj/item/integrated_circuit/removed_from)
REMOVE_TRAIT(removed_from, TRAIT_COMPONENT_MMI, REF(src))
remove_occupant_item()
return ..()
#undef MMI_MESSAGE_COOLDOWN