From 9a16063ba16e769d3dcbb023f5dc0f9ab83ea2b4 Mon Sep 17 00:00:00 2001 From: Leland Kemble <70413276+lelandkemble@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:18:38 -0500 Subject: [PATCH] Allows you to deploy, undeploy, activate & deactivate other people's MODsuits via strip menu (#95250) ## About The Pull Request Allows deployment, undeployment, activation and deactivation of other people's MODsuits via stripping buttons. Deployment and undeployment is the same as if you were right clicking the button, i.e. undeployment if you have any parts active, deployment otherwise. Each action takes the MOD's `strip_delay` as its time to complete. DNA lock modules' consequences do not trigger on anybody if the `user` is not the `wearer`. They still don't deploy unless the acting agent is the right dna. ## Why It's Good For The Game Currently, there are two ways to get a MODsuit to activate or deploy on another person without that person's involvement. An very rare PDA app and sacrificing an AI. That's unfortunate, given as MODsuits are the most used space protection. If you're forced to take off someone's MODsuit in order to provide some sort of medical care, you are unable to then put it back on them to prevent additional space-wrought damage. You can't ~waste~ use a MODsuit to protect a incapacitated prisoner, or force a springlocked MODsuit on a potential victim. It feels like a missed avenue of interaction. The PDA app is still powerful, for better reasons, as is the AI. Not to imply anyone was using AIs for this. ## Changelog :cl: add: You can now deploy, undeploy, activate & deactivate other people's MODsuit via the strip menu /:cl: --- code/modules/mod/mod_activation.dm | 14 +++-- code/modules/mod/mod_control.dm | 66 ++++++++++++++++++++ code/modules/mod/modules/modules_ninja.dm | 2 +- code/modules/mod/modules/modules_timeline.dm | 6 +- tgui/packages/tgui/interfaces/StripMenu.tsx | 19 ++++++ 5 files changed, 99 insertions(+), 8 deletions(-) diff --git a/code/modules/mod/mod_activation.dm b/code/modules/mod/mod_activation.dm index 2935aeb0913..c3674801e36 100644 --- a/code/modules/mod/mod_activation.dm +++ b/code/modules/mod/mod_activation.dm @@ -50,11 +50,7 @@ balloon_alert(user, "currently [active ? "unsealing" : "sealing"]!") playsound(src, 'sound/machines/scanner/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE) return FALSE - var/deploy = TRUE - for(var/obj/item/part as anything in get_parts()) - if(part.loc != src) - deploy = FALSE - break + var/deploy = check_retracted() wearer.visible_message(span_notice("[wearer]'s [src] [deploy ? "deploys" : "retracts"] its parts with a mechanical hiss."), span_notice("[src] [deploy ? "deploys" : "retracts"] its parts with a mechanical hiss."), span_hear("You hear a mechanical hiss.")) @@ -325,4 +321,12 @@ for(var/obj/item/part as anything in get_parts()) deploy(null, part, instant = TRUE) +/// Checks if the suit is fully retracted, with no parts outside +/obj/item/mod/control/proc/check_retracted() + for(var/obj/item/part as anything in get_parts()) + if(part.loc != src) + return FALSE + return TRUE + + #undef MOD_ACTIVATION_STEP_FLAGS diff --git a/code/modules/mod/mod_control.dm b/code/modules/mod/mod_control.dm index 397ca5c4d19..e2554be5646 100644 --- a/code/modules/mod/mod_control.dm +++ b/code/modules/mod/mod_control.dm @@ -100,8 +100,12 @@ locked = TRUE new_core?.install(src) update_speed() + RegisterSignal(src, COMSIG_ATOM_EXITED, PROC_REF(on_exit)) RegisterSignal(src, COMSIG_SPEED_POTION_APPLIED, PROC_REF(on_potion)) + RegisterSignal(src, COMSIG_ITEM_GET_STRIPPABLE_ALT_ACTIONS, PROC_REF(get_strippable_alternate_actions)) + RegisterSignal(src, COMSIG_ITEM_STRIPPABLE_ALT_ACTION, PROC_REF(do_strippable_action)) + for(var/obj/item/mod/module/module as anything in theme.inbuilt_modules) module = new module(src) install(module) @@ -402,6 +406,68 @@ icon_state = "[skin]-[base_icon_state][active ? "-sealed" : ""]" return ..() +/obj/item/mod/control/proc/get_strippable_alternate_actions(obj/item/source, atom/owner, mob/user, list/alt_actions) + SIGNAL_HANDLER + if(active) + alt_actions += "deactivate_mod" + else + alt_actions += "activate_mod" + if(check_retracted()) + alt_actions += "deploy" + else + alt_actions += "undeploy" + + +/obj/item/mod/control/proc/do_strippable_action(obj/item/source, atom/owner, mob/user, action_key) + SIGNAL_HANDLER + if(!isliving(user)) + return NONE + switch(action_key) + + if("deploy", "undeploy") + owner.visible_message( + span_warning("[user] tries to [action_key] [owner]'s [src]..."), + span_userdanger("[user] is trying to [action_key] your [src]!"), + blind_message = span_hear("You hear rustling."), + ignored_mobs = user, + ) + INVOKE_ASYNC(src, PROC_REF(attempt_strip_deploy), owner, user, action_key) + return COMPONENT_ALT_ACTION_DONE + + if("activate_mod", "deactivate_mod") + owner.visible_message( + span_warning("[user] tries to press [owner]'s [src]'s power button..."), + span_userdanger("[user] is trying to press your [src]'s power button!"), + blind_message = span_hear("You hear rustling."), + ignored_mobs = user, + ) + INVOKE_ASYNC(src, PROC_REF(attempt_strip_activate), owner, user) + return COMPONENT_ALT_ACTION_DONE + + else + return NONE + +/obj/item/mod/control/proc/attempt_strip_deploy(atom/owner, mob/user, message) + if(!do_after(user, strip_delay, owner)) + return + owner.visible_message( + span_warning("[user] [message]s [owner]'s [src]."), + span_userdanger("[user] [message]s your [src]!"), + ignored_mobs = user, + ) + quick_deploy(user) + +/obj/item/mod/control/proc/attempt_strip_activate(atom/owner, mob/user) + if(!do_after(user, strip_delay, owner)) + return + owner.visible_message( + span_warning("[user] presses [owner]'s [src]'s power button."), + span_userdanger("[user] presses your [src]'s power button!"), + ignored_mobs = user, + ) + toggle_activate(user) + + /obj/item/mod/control/proc/get_parts(all = FALSE) . = list() for(var/key in mod_parts) diff --git a/code/modules/mod/modules/modules_ninja.dm b/code/modules/mod/modules/modules_ninja.dm index ae72a93df3a..04df80eb540 100644 --- a/code/modules/mod/modules/modules_ninja.dm +++ b/code/modules/mod/modules/modules_ninja.dm @@ -779,7 +779,7 @@ /obj/item/mod/module/dna_lock/reinforced/on_mod_activation(datum/source, mob/user) . = ..() - if(. != MOD_CANCEL_ACTIVATE || !isliving(user)) + if(. != MOD_CANCEL_ACTIVATE || !isliving(user) || user != mod.wearer) return if(mod.ai_assistant == user) to_chat(mod.ai_assistant, span_danger("fATaL EERRoR: 381200-*#00CODE BLUE\nAI INTErFERenCE DEtECted\nACTi0N DISrEGArdED")) diff --git a/code/modules/mod/modules/modules_timeline.dm b/code/modules/mod/modules/modules_timeline.dm index 3bc992ecfe6..bfca6e421b7 100644 --- a/code/modules/mod/modules/modules_timeline.dm +++ b/code/modules/mod/modules/modules_timeline.dm @@ -39,10 +39,12 @@ /obj/item/mod/module/eradication_lock/proc/on_mod_activation(datum/source, mob/user) SIGNAL_HANDLER - if(true_owner_ckey && user.ckey != true_owner_ckey) + if(!true_owner_ckey || user.ckey == true_owner_ckey) + return NONE + if(user == mod.wearer) to_chat(mod.wearer, span_userdanger("\"MODsuit compromised by timeline inhabitant! Eradicating...\"")) new /obj/structure/chrono_field(user.loc, user) - return MOD_CANCEL_ACTIVATE + return MOD_CANCEL_ACTIVATE ///Signal fired when the modsuit tries removing a module. /obj/item/mod/module/eradication_lock/proc/on_mod_removal(datum/source, mob/user) diff --git a/tgui/packages/tgui/interfaces/StripMenu.tsx b/tgui/packages/tgui/interfaces/StripMenu.tsx index a0975730f63..480de878b40 100644 --- a/tgui/packages/tgui/interfaces/StripMenu.tsx +++ b/tgui/packages/tgui/interfaces/StripMenu.tsx @@ -88,6 +88,25 @@ const ALTERNATE_ACTIONS: Record = { icon: 'ribbon', text: 'Strip accessory', }, + + deploy: { + icon: 'plus', + text: 'Deploy MOD', + }, + + undeploy: { + icon: 'minus', + text: 'Undeploy MOD', + }, + + activate_mod: { + icon: 'power-off', + text: 'Activate MOD', + }, + deactivate_mod: { + icon: 'power-off', + text: 'Deactivate MOD', + }, }; const SLOTS: Record<