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<