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

<img width="472" height="294" alt="image"
src="https://github.com/user-attachments/assets/8fe8880b-2872-4030-9e42-ced4ce529b31"
/>

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

🆑 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.
/🆑
This commit is contained in:
MrMelbert
2026-01-17 10:38:54 -07:00
committed by GitHub
parent 312f3761b0
commit 7645cef613
6 changed files with 175 additions and 105 deletions
@@ -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
@@ -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
+92 -51
View File
@@ -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()
@@ -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
+4 -1
View File
@@ -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!
+25 -10
View File
@@ -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<string, BooleanLike>;
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 (
<Window width={376} height={height}>
<Window width={380} height={height}>
<Window.Content>
<Section>
<LabeledList>
@@ -143,7 +141,7 @@ export const Radio = (props) => {
stepPixelSize={10}
/>
</LabeledList.Item>
{!!subspace && (
{(!!subspace || channels.length > 0) && (
<LabeledList.Item label="Channels">
{channels.length === 0 && (
<Box inline color="bad">
@@ -163,6 +161,23 @@ export const Radio = (props) => {
})
}
/>
{!subspace && !freqlock && (
<Button
icon="walkie-talkie"
ml={1}
disabled={
RADIO_CHANNELS.find((c) => c.name === channel.name)
?.freq === frequency
}
onClick={() =>
act('tune_to_channel', {
channel: channel.name,
})
}
>
Tune
</Button>
)}
</Box>
))}
</Stack>