From 7645cef613204f224c16df27bf8d7fc255b9dc03 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Sat, 17 Jan 2026 11:38:54 -0600 Subject: [PATCH] Allows you to install encryption keys in intercoms and station-bounced radios (#94874) ## About The Pull Request *All* radios can have an encryption key installed This includes intercoms and station-bounced radios image These radios are *not* normally freerange, meaning to tune into channels added by an encryption key, a new button in the radio UI has been added - "Tune", which tunes you directly into the channel. This even works with the Syndie key, giving the intercom access to Syndie channel and the ability to hear all encrypted channels. However, it will *not* work with a Binary key. Likewise, any key with language understanding on it will do nothing. I think **theoretically** we can have intercepted messages in alternate languages check to see if they have a key of that language with (to relay a translation instead), but that sounds like a lot of work... Also this means you can now speak on encrypted frequencies (like departmental ones) via non-subspace means - ie, when telecomms is disabled. ## Why It's Good For The Game To be completely honest, I haven't fully considered the repercussions of such a change. Someone just offhandedly mentioned that if they could tune into a dept. radio via subspace, they would try playing without a headset, and I thought that sounded fun. Otherwise, this opens up stuff like departmental intercom systems - which sounds cool - and in the future we could possibly de-hardcode freerange settings (like the command intercom). ## Changelog :cl: Melbert add: All radios (intercoms and station-bounced) can have encryption keys installed in them, rather than solely headsets and cyborgs. add: Existing screwdriver interactions for station-bounced radios and intercoms have been moved to right click. However, intercoms will still default to the old behavior if they have no key installed. qol: Screentips for radios. /:cl: --- .../objects/items/devices/radio/headset.dm | 67 ++++---- .../objects/items/devices/radio/intercom.dm | 28 +++- .../game/objects/items/devices/radio/radio.dm | 143 +++++++++++------- .../jobs/job_types/security_officer.dm | 2 - strings/tips.txt | 5 +- tgui/packages/tgui/interfaces/Radio.tsx | 35 +++-- 6 files changed, 175 insertions(+), 105 deletions(-) diff --git a/code/game/objects/items/devices/radio/headset.dm b/code/game/objects/items/devices/radio/headset.dm index 6206003e3ac..2bbe6baf7fd 100644 --- a/code/game/objects/items/devices/radio/headset.dm +++ b/code/game/objects/items/devices/radio/headset.dm @@ -433,52 +433,45 @@ GLOBAL_LIST_INIT(channel_tokens, list( . = ..() make_syndie() -/obj/item/radio/headset/screwdriver_act(mob/living/user, obj/item/tool) - if(keyslot || keyslot2) - for(var/ch_name in channels) - SSradio.remove_object(src, GLOB.default_radio_channels[ch_name]) - secure_radio_connections[ch_name] = null +/obj/item/radio/headset/Exited(atom/movable/gone, direction) + . = ..() + if(gone == keyslot2) + keyslot2 = null + if(!QDELING(src)) + recalculateChannels() - if(keyslot) - user.put_in_hands(keyslot) - keyslot = null - if(keyslot2) - user.put_in_hands(keyslot2) - keyslot2 = null - - recalculateChannels() - to_chat(user, span_notice("You pop out the encryption keys in the headset.")) - - else - to_chat(user, span_warning("This headset doesn't have any unique encryption keys! How useless...")) - tool.play_tool_sound(src, 10) - return TRUE - -/obj/item/radio/headset/attackby(obj/item/W, mob/user, list/modifiers, list/attack_modifiers) - if(!istype(W, /obj/item/encryptionkey)) - return ..() - - if(keyslot && keyslot2) - to_chat(user, span_warning("The headset can't hold another key!")) +/obj/item/radio/headset/remove_keys(mob/living/user) + . = ..() + if(!keyslot2) return - if(!keyslot) - if(!user.transferItemToLoc(W, src)) - return - keyslot = W - else - if(!user.transferItemToLoc(W, src)) - return - keyslot2 = W + . += keyslot2 + user.put_in_hands(keyslot2) // null via Exited +/obj/item/radio/headset/install_key(mob/living/user, obj/item/encryptionkey/key) + if(!keyslot) + return ..() + + if(keyslot2) + loc.balloon_alert(user, "cannot hold a third key!") + return ITEM_INTERACT_BLOCKING + + if(!user.transferItemToLoc(key, src)) + loc.balloon_alert(user, "cannot install!") + return ITEM_INTERACT_BLOCKING + + keyslot2 = key recalculateChannels() + playsound(src, 'sound/machines/click.ogg', 50, TRUE) + loc.balloon_alert(user, "encryption key installed") + return ITEM_INTERACT_SUCCESS /obj/item/radio/headset/recalculateChannels() . = ..() if(keyslot2) - for(var/ch_name in keyslot2.channels) - if(!(ch_name in src.channels)) - LAZYSET(channels, ch_name, keyslot2.channels[ch_name]) + for(var/channel_name in keyslot2.channels) + if(!(channel_name in channels)) + channels[channel_name] = keyslot2.channels[channel_name] special_channels |= keyslot2.special_channels diff --git a/code/game/objects/items/devices/radio/intercom.dm b/code/game/objects/items/devices/radio/intercom.dm index 526c21bde15..7dcf0f1c98a 100644 --- a/code/game/objects/items/devices/radio/intercom.dm +++ b/code/game/objects/items/devices/radio/intercom.dm @@ -63,7 +63,19 @@ else . += span_notice("It has a frequency lock set to [frequency/10].") -/obj/item/radio/intercom/screwdriver_act(mob/living/user, obj/item/tool) +/obj/item/radio/intercom/add_context(atom/source, list/context, obj/item/held_item, mob/user) + . = ..() + if(held_item?.tool_behaviour == TOOL_SCREWDRIVER) + context[SCREENTIP_CONTEXT_RMB] = unscrewed ? "Secure to wall" : "Unscrew from wall" + context[SCREENTIP_CONTEXT_LMB] = isnull(keyslot) ? context[SCREENTIP_CONTEXT_RMB] : "Remove encryption key" // sometimes same behavior + . = CONTEXTUAL_SCREENTIP_SET + + if(held_item?.tool_behaviour == TOOL_WRENCH && unscrewed) + context[SCREENTIP_CONTEXT_RMB] = "Detach from wall" + context[SCREENTIP_CONTEXT_LMB] = context[SCREENTIP_CONTEXT_LMB] // same behavior + . = CONTEXTUAL_SCREENTIP_SET + +/obj/item/radio/intercom/screwdriver_act_secondary(mob/living/user, obj/item/tool) if(unscrewed) user.visible_message(span_notice("[user] starts tightening [src]'s screws..."), span_notice("You start screwing in [src]...")) if(tool.use_tool(src, user, 30, volume=50)) @@ -76,19 +88,27 @@ user.visible_message(span_notice("[user] loosens [src]'s screws!"), span_notice("You unscrew [src], loosening it from the wall.")) unscrewed = TRUE update_appearance(UPDATE_OVERLAYS) - return TRUE + return ITEM_INTERACT_SUCCESS + +/obj/item/radio/intercom/screwdriver_act(mob/living/user, obj/item/tool) + if(isnull(keyslot)) + return screwdriver_act_secondary(user, tool) + return ..() /obj/item/radio/intercom/wrench_act(mob/living/user, obj/item/tool) - . = TRUE if(!unscrewed) to_chat(user, span_warning("You need to unscrew [src] from the wall first!")) - return + return ITEM_INTERACT_BLOCKING user.visible_message(span_notice("[user] starts unsecuring [src]..."), span_notice("You start unsecuring [src]...")) tool.play_tool_sound(src) if(tool.use_tool(src, user, 80)) user.visible_message(span_notice("[user] unsecures [src]!"), span_notice("You detach [src] from the wall.")) playsound(src, 'sound/items/deconstruct.ogg', 50, TRUE) deconstruct(TRUE) + return ITEM_INTERACT_SUCCESS + +/obj/item/radio/intercom/wrench_act_secondary(mob/living/user, obj/item/tool) + return wrench_act(user, tool) /** * Override attack_tk_grab instead of attack_tk because we actually want attack_tk's diff --git a/code/game/objects/items/devices/radio/radio.dm b/code/game/objects/items/devices/radio/radio.dm index 9245f00a0ed..86c15936fff 100644 --- a/code/game/objects/items/devices/radio/radio.dm +++ b/code/game/objects/items/devices/radio/radio.dm @@ -99,13 +99,11 @@ /obj/item/radio/Initialize(mapload) set_wires(new /datum/wires/radio(src)) - secure_radio_connections = list() . = ..() if(ispath(keyslot)) keyslot = new keyslot() - for(var/ch_name in channels) - secure_radio_connections[ch_name] = add_radio(src, GLOB.default_radio_channels[ch_name]) + recalculateChannels() perform_update_icon = FALSE set_listening(listening) @@ -118,7 +116,7 @@ update_appearance(UPDATE_ICON) AddElement(/datum/element/empprotection, EMP_PROTECT_WIRES) - + register_context() // No subtypes if(type != /obj/item/radio) return @@ -132,6 +130,16 @@ QDEL_NULL(keyslot) return ..() +/obj/item/radio/add_context(atom/source, list/context, obj/item/held_item, mob/user) + . = ..() + if(held_item?.tool_behaviour == TOOL_SCREWDRIVER) + context[SCREENTIP_CONTEXT_LMB] = "Remove encryption key" + context[SCREENTIP_CONTEXT_RMB] = unscrewed ? "Screw in" : "Unscrew" + . = CONTEXTUAL_SCREENTIP_SET + if(istype(held_item, /obj/item/encryptionkey)) + context[SCREENTIP_CONTEXT_LMB] = "Install encryption key" + . = CONTEXTUAL_SCREENTIP_SET + /obj/item/radio/on_saboteur(datum/source, disrupt_duration) . = ..() if(broadcasting) //no broadcasting but it can still be used to send radio messages. @@ -155,7 +163,7 @@ if(!(channel_name in channels)) channels[channel_name] = keyslot.channels[channel_name] - special_channels = keyslot.special_channels + special_channels |= keyslot.special_channels for(var/channel_name in channels) secure_radio_connections[channel_name] = add_radio(src, GLOB.default_radio_channels[channel_name]) @@ -163,12 +171,17 @@ if(!listening) remove_radio_all(src) -// Used for cyborg override /obj/item/radio/proc/resetChannels() + for(var/ch_name in channels) + SSradio.remove_object(src, GLOB.default_radio_channels[ch_name]) + channels = list() secure_radio_connections = list() special_channels = NONE + if(!freerange && (frequency > MAX_FREQ || frequency < MIN_FREQ)) + frequency = FREQ_COMMON + ///goes through all radio channels we should be listening for and readds them to the global list /obj/item/radio/proc/readd_listening_radio_channels() for(var/channel_name in channels) @@ -494,16 +507,26 @@ if("frequency") if(freqlock != RADIO_FREQENCY_UNLOCKED) return - var/tune = params["tune"] + var/tune = 0 var/adjust = text2num(params["adjust"]) if(adjust) tune = frequency + adjust * 10 - . = TRUE - else if(text2num(tune) != null) - tune = tune * 10 - . = TRUE - if(.) + else if(tune) + tune *= 10 + if(tune) set_frequency(sanitize_frequency(tune, freerange, (special_channels & RADIO_SPECIAL_SYNDIE))) + . = TRUE + + if("tune_to_channel") + if(freqlock != RADIO_FREQENCY_UNLOCKED) + return + var/channel = params["channel"] + if(!(channel in channels)) + return + // bypasses frequency range, force tunes to a specific encrypted channel + set_frequency(GLOB.default_radio_channels[channel]) + . = TRUE + if("listen") set_listening(!listening) . = TRUE @@ -561,15 +584,66 @@ /obj/item/radio/item_interaction(mob/living/user, obj/item/tool, list/modifiers) if(user.combat_mode && tool.tool_behaviour == TOOL_SCREWDRIVER) return screwdriver_act(user, tool) - return ..() + if(istype(tool, /obj/item/encryptionkey)) + return install_key(user, tool) + return NONE -/obj/item/radio/screwdriver_act(mob/living/user, obj/item/tool) +/obj/item/radio/screwdriver_act_secondary(mob/living/user, obj/item/tool) add_fingerprint(user) unscrewed = !unscrewed + tool.play_tool_sound(src, 10) if(unscrewed) - to_chat(user, span_notice("The radio can now be attached and modified!")) + to_chat(user, span_notice("[src] can now be attached and modified!")) else - to_chat(user, span_notice("The radio can no longer be modified or attached!")) + to_chat(user, span_notice("[src] can no longer be modified or attached!")) + return ITEM_INTERACT_SUCCESS + +/obj/item/radio/screwdriver_act(mob/living/user, obj/item/tool) + var/list/removed_keys = remove_keys(user) + if(length(removed_keys) > 1) + to_chat(user, span_notice("You remove the encryption keys from [src].")) + else if(length(removed_keys) == 1) + to_chat(user, span_notice("You remove [removed_keys[1]] from [src].")) + else + to_chat(user, span_warning("[src] doesn't have any unique encryption keys! How useless...")) + tool.play_tool_sound(src, 10) + return TRUE + +/obj/item/radio/Exited(atom/movable/gone, direction) + . = ..() + if(gone == keyslot) + keyslot = null + if(!QDELING(src)) + recalculateChannels() + +/// Attempts to put all keys in the radio into the user's hands +/// Returns a list of the removed keys +/obj/item/radio/proc/remove_keys(mob/living/user) + . = list() + if(!keyslot) + return + + . += keyslot + user.put_in_hands(keyslot) // null via Exited + +/// Attempts to install the given encryption key into the radio +/obj/item/radio/proc/install_key(mob/living/user, obj/item/encryptionkey/key) + if(keyslot) + loc.balloon_alert(user, "cannot hold a second key!") + return ITEM_INTERACT_BLOCKING + if(freqlock) + loc.balloon_alert(user, "keyslot is locked!") + return ITEM_INTERACT_BLOCKING + + if(!user.transferItemToLoc(key, src)) + loc.balloon_alert(user, "cannot install!") + return ITEM_INTERACT_BLOCKING + + keyslot = key + recalculateChannels() + playsound(src, 'sound/machines/click.ogg', 50, TRUE) + loc.balloon_alert(user, "encryption key installed") + return ITEM_INTERACT_SUCCESS /obj/item/radio/emp_act(severity) . = ..() @@ -632,41 +706,6 @@ . = ..() set_frequency(FREQ_SYNDICATE) -/obj/item/radio/borg/screwdriver_act(mob/living/user, obj/item/tool) - if(!keyslot) - loc.balloon_alert(user, "no encryption keys!") - return - - for(var/ch_name in channels) - SSradio.remove_object(src, GLOB.default_radio_channels[ch_name]) - secure_radio_connections[ch_name] = null - - if (!user.put_in_hands(keyslot)) - keyslot.forceMove(drop_location()) - - keyslot = null - recalculateChannels() - loc.balloon_alert(user, "encryption key removed") - return ..() - -/obj/item/radio/borg/item_interaction(mob/living/user, obj/item/tool, list/modifiers) - if (!istype(tool, /obj/item/encryptionkey)) - return NONE - - if(keyslot) - loc.balloon_alert(user, "cannot hold another key!") - return ITEM_INTERACT_BLOCKING - - if(!user.transferItemToLoc(tool, src)) - loc.balloon_alert(user, "cannot install!") - return ITEM_INTERACT_BLOCKING - - keyslot = tool - recalculateChannels() - playsound(src, 'sound/machines/click.ogg', 50, TRUE) - loc.balloon_alert(user, "encryption key installed") - return ITEM_INTERACT_SUCCESS - /obj/item/radio/off // Station bounced radios, their only difference is spawning with the speakers off, this was made to help the lag. dog_fashion = /datum/dog_fashion/back @@ -734,6 +773,8 @@ canhear_range = 3 // In case you want to map it in/spawn it for some reason +/obj/item/radio/toy + /obj/item/radio/toy/Initialize(mapload) . = ..() make_silly() diff --git a/code/modules/jobs/job_types/security_officer.dm b/code/modules/jobs/job_types/security_officer.dm index 983d7a56c5a..78ea30f1fc1 100644 --- a/code/modules/jobs/job_types/security_officer.dm +++ b/code/modules/jobs/job_types/security_officer.dm @@ -272,8 +272,6 @@ GLOBAL_LIST_EMPTY(security_officer_distribution) /obj/item/radio/headset/headset_sec/alt/department/Initialize(mapload) . = ..() set_wires(new/datum/wires/radio(src)) - secure_radio_connections = list() - recalculateChannels() /obj/item/radio/headset/headset_sec/alt/department/engi keyslot = /obj/item/encryptionkey/headset_sec diff --git a/strings/tips.txt b/strings/tips.txt index 57d7ca1dce4..d832de900b3 100644 --- a/strings/tips.txt +++ b/strings/tips.txt @@ -227,7 +227,7 @@ As the Head of Security, you can call for executions or forced cyborgization, bu As the Malfunctioning AI, look into flooding the station with plasma fires to kill off large portions of the crew, letting you pick off the remaining few with space suits who escaped. As the Malfunctioning AI, you can shunt to an APC if the situation gets bad. This disables your doomsday device if it is active. As the Malfunctioning AI, you should either order your cyborgs to dismantle the robotics console or blow it up yourself in order to protect them. -As the Mime, you can use :r and :l to speak through your ventriloquist dummy. +As the Mime, you can use :R / .R or :L / .L to speak through a ventriloquist dummy, depending on which hand you are holding it in. As the Mime, your invisible wall power blocks people as well as projectiles. You can use it in a pinch to delay your pursuer. As the Mime, your oath of silence is your source of power. Breaking it robs you of your powers and of your honor. As the Quartermaster, be sure to check the manifests on crates you receive to make sure all the info is correct. If there's a mistake, stamp the manifest DENIED and send it back in a crate with the items untouched for a refund! @@ -320,8 +320,11 @@ You can right click someone with wire cutters, jaws of life, and box cutters to You can screwdriver any non-chemical grenade to shorten fuses from 5 seconds, to 3 seconds, to instant boom! Nobody ever expects this. You can spray a fire extinguisher, throw items or fire a gun while floating through space to change your direction. Simply fire opposite to where you want to go. You can swap floor tiles by holding a crowbar in one hand and a stack of tiles in the other. +You can use :I / .I to speak through any intercom within arm's reach. This allows you to talk into it without manually turning on its microphone, and even works if you are in critical condition (but not unconscious)! +You can use :L / .L or :R / .R to speak through an item being held in your left or right hand respectively. This allows you to talk into a station-bounced radio without manually turning on its microphone, or even to pretend to be a toy! You can use a machine in the vault to deposit cash or rob Cargo's department funds. You can use a multitool in your hand to track the area's local APC. +You can use a screwdriver to remove an encryption key from a radio headset. The key can then be installed into your own headset, an intercom, or even a station-bounced radio, allowing you to eavesdrop on secure communications. You can use an upgraded microwave to charge your PDA! You don't need to destroy a Spacecoin machine to make your funds stop draining. Swiping your ID on it will stop the withdrawal. You'll quickly lose your interest in the game if you play to win and kill. If you find yourself doing this, take a step back and talk to people - it's a much better experience! diff --git a/tgui/packages/tgui/interfaces/Radio.tsx b/tgui/packages/tgui/interfaces/Radio.tsx index 168acab435d..a99c34e69e0 100644 --- a/tgui/packages/tgui/interfaces/Radio.tsx +++ b/tgui/packages/tgui/interfaces/Radio.tsx @@ -16,7 +16,7 @@ import { RADIO_CHANNELS } from '../constants'; import { Window } from '../layouts'; type RadioData = { - freqlock: number; + freqlock: BooleanLike; frequency: number; minFrequency: number; maxFrequency: number; @@ -26,7 +26,7 @@ type RadioData = { useCommand: BooleanLike; subspace: BooleanLike; subspaceSwitchable: BooleanLike; - channels: string[]; + channels: Record; radio_noises: number; }; @@ -54,15 +54,13 @@ export const Radio = (props) => { })); // Calculate window height let height = 133; - if (subspace) { - if (channels.length > 0) { - height += channels.length * 25 + 8; - } else { - height += 24; - } + if (channels.length > 0) { + height += channels.length * 25 + 8; + } else if (subspace) { + height += 24; } return ( - +
@@ -143,7 +141,7 @@ export const Radio = (props) => { stepPixelSize={10} /> - {!!subspace && ( + {(!!subspace || channels.length > 0) && ( {channels.length === 0 && ( @@ -163,6 +161,23 @@ export const Radio = (props) => { }) } /> + {!subspace && !freqlock && ( + + )} ))}