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()