Files
Alexis 21b4095dfd [MDB IGNORE] [IDB IGNORE] Upstream Sync - 04/17/2026 (#5453)
Upstream 04/17/2026

fixes https://github.com/Bubberstation/Bubberstation/issues/5549

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: tgstation-ci[bot] <179393467+tgstation-ci[bot]@users.noreply.github.com>
Co-authored-by: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com>
Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
Co-authored-by: Rhials <28870487+Rhials@users.noreply.github.com>
Co-authored-by: rageguy505 <54517726+rageguy505@users.noreply.github.com>
Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
Co-authored-by: John Willard <53777086+JohnFulpWillard@users.noreply.github.com>
Co-authored-by: Aliceee2ch <160794176+Aliceee2ch@users.noreply.github.com>
Co-authored-by: Time-Green <7501474+Time-Green@users.noreply.github.com>
Co-authored-by: Tsar-Salat <62388554+Tsar-Salat@users.noreply.github.com>
Co-authored-by: SmArtKar <44720187+SmArtKar@users.noreply.github.com>
Co-authored-by: Maxipat <108554989+Maxipat112@users.noreply.github.com>
Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: SimplyLogan <47579821+loganuk@users.noreply.github.com>
Co-authored-by: loganuk <fakeemail123@aol.com>
Co-authored-by: Leland Kemble <70413276+lelandkemble@users.noreply.github.com>
Co-authored-by: FalloutFalcon <86381784+FalloutFalcon@users.noreply.github.com>
Co-authored-by: Roxy <75404941+TealSeer@users.noreply.github.com>
Co-authored-by: Lucy <lucy@absolucy.moe>
Co-authored-by: siliconOpossum <138069572+siliconOpossum@users.noreply.github.com>
Co-authored-by: Isratosh <Isratosh@hotmail.com>
Co-authored-by: TheRyeGuyWhoWillNowDie <70169560+TheRyeGuyWhoWillNowDie@users.noreply.github.com>
Co-authored-by: Neocloudy <88008002+Neocloudy@users.noreply.github.com>
Co-authored-by: Alexander V. <volas@ya.ru>
Co-authored-by: ElGitificador <168473461+ElGitificador@users.noreply.github.com>
Co-authored-by: Twaticus <46540570+Twaticus@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bloop <13398309+vinylspiders@users.noreply.github.com>
Co-authored-by: Cameron Lennox <killer65311@gmail.com>
Co-authored-by: Tim <timothymtorres@gmail.com>
Co-authored-by: Iamgoofball <iamgoofball@gmail.com>
Co-authored-by: Layzu666 <121319428+Layzu666@users.noreply.github.com>
Co-authored-by: Arturlang <24881678+Arturlang@users.noreply.github.com>
Co-authored-by: _0Steven <42909981+00-Steven@users.noreply.github.com>
Co-authored-by: mrmanlikesbt <99309552+mrmanlikesbt@users.noreply.github.com>
Co-authored-by: Ben10Omintrix <138636438+Ben10Omintrix@users.noreply.github.com>
Co-authored-by: John F. Kennedy <54908920+MacaroniCritter@users.noreply.github.com>
Co-authored-by: Cursor <102828457+theselfish@users.noreply.github.com>
Co-authored-by: Josh <josh.adam.powell@gmail.com>
Co-authored-by: Josh Powell <josh.powell@softwire.com>
Co-authored-by: Yobrocharlie <Charliemiller5617@gmail.com>
Co-authored-by: Hardly3D <66234359+Hardly3D@users.noreply.github.com>
Co-authored-by: shayoki <96078776+shayoki@users.noreply.github.com>
Co-authored-by: LT3 <83487515+lessthnthree@users.noreply.github.com>
2026-05-16 00:56:00 +02:00

799 lines
29 KiB
Plaintext

#define FREQ_LISTENING (1<<0)
/obj/item/radio
icon = 'icons/obj/devices/voice.dmi'
name = "station bounced radio"
icon_state = "walkietalkie"
inhand_icon_state = "walkietalkie"
lefthand_file = 'icons/mob/inhands/items/devices_lefthand.dmi'
righthand_file = 'icons/mob/inhands/items/devices_righthand.dmi'
worn_icon_state = "radio"
desc = "A basic handheld radio that communicates with local telecommunication networks."
dog_fashion = /datum/dog_fashion/back
interaction_flags_atom = parent_type::interaction_flags_atom | INTERACT_ATOM_ALLOW_USER_LOCATION | INTERACT_ATOM_IGNORE_MOBILITY
sound_vary = TRUE
pickup_sound = SFX_GENERIC_DEVICE_PICKUP
drop_sound = SFX_GENERIC_DEVICE_DROP
obj_flags = CONDUCTS_ELECTRICITY
slot_flags = ITEM_SLOT_BELT
throw_speed = 3
throw_range = 7
w_class = WEIGHT_CLASS_SMALL
custom_materials = list(/datum/material/iron=SMALL_MATERIAL_AMOUNT * 0.75, /datum/material/glass=SMALL_MATERIAL_AMOUNT * 0.25)
///if FALSE, broadcasting and listening don't matter and this radio shouldn't do anything
VAR_PRIVATE/on = TRUE
///the "default" radio frequency this radio is set to, listens and transmits to this frequency by default. wont work if the channel is encrypted
VAR_PRIVATE/frequency = FREQ_COMMON
/// Whether the radio will transmit dialogue it hears nearby into its radio channel.
VAR_PRIVATE/broadcasting = FALSE
/// Whether the radio is currently receiving radio messages from its radio frequencies.
VAR_PRIVATE/listening = TRUE
//the below three vars are used to track listening and broadcasting should they be forced off for whatever reason but "supposed" to be active
//eg player sets the radio to listening, but an emp or whatever turns it off, its still supposed to be activated but was forced off,
//when it wears off it sets listening to should_be_listening
///used for tracking what broadcasting should be in the absence of things forcing it off, eg its set to broadcast but gets emp'd temporarily
var/should_be_broadcasting = FALSE
///used for tracking what listening should be in the absence of things forcing it off, eg its set to listen but gets emp'd temporarily
var/should_be_listening = TRUE
/// Both the range around the radio in which mobs can hear what it receives and the range the radio can hear
var/canhear_range = 3
/// Tracks the number of EMPs currently stacked.
var/emped = 0
/// Whether wires are accessible. Toggleable by screwdrivering.
var/unscrewed = FALSE
/// If true, the radio has access to the full spectrum.
var/freerange = FALSE
/// If true, the radio transmits and receives on subspace exclusively.
var/subspace_transmission = FALSE
/// If true, subspace_transmission can be toggled at will.
var/subspace_switchable = FALSE
/// Frequency lock to stop the user from untuning specialist radios.
var/freqlock = RADIO_FREQENCY_UNLOCKED
/// Locks the keyslot to prevent removing the encryption key from specialist radios.
var/keylock = RADIO_KEYSLOT_UNLOCKED
/// If true, broadcasts will be large and BOLD.
var/use_command = FALSE
/// If true, use_command can be toggled at will.
var/command = FALSE
/// Does it play radio noise?
var/radio_noise = TRUE
///makes anyone who is talking through this anonymous.
var/anonymize = FALSE
/// Encryption key handling
var/obj/item/encryptionkey/keyslot
/// Flags for which "special" radio networks should be accessible
var/special_channels = NONE
/// associative list of the encrypted radio channels this radio is currently set to listen/broadcast to, of the form: list(channel name = TRUE or FALSE)
var/list/channels
/// lazy associative list of the encrypted radio channels this radio can listen/broadcast to, of the form: list(channel name = channel frequency)
var/list/secure_radio_connections = null
/// overlay when speaker is on
var/overlay_speaker_idle = "s_idle"
/// overlay when receiving a message
var/overlay_speaker_active = "s_active"
/// overlay when mic is on
var/overlay_mic_idle = "m_idle"
/// overlay when speaking a message (is displayed simultaneously with speaker_active)
var/overlay_mic_active = "m_active"
/// When set to FALSE, will avoid calling update_icon() in set_broadcasting and co.
/// Used to save time on updating icon several times over initialization.
VAR_PRIVATE/perform_update_icon = TRUE
/// If TRUE, will set the icon in initializations.
VAR_PRIVATE/should_update_icon = FALSE
/// A very brief cooldown to prevent regular radio sounds from overlapping.
COOLDOWN_DECLARE(audio_cooldown)
/// A very brief cooldown to prevent "important" radio sounds from overlapping.
COOLDOWN_DECLARE(important_audio_cooldown)
/obj/item/radio/Initialize(mapload)
set_wires(new /datum/wires/radio(src))
. = ..()
if(ispath(keyslot))
keyslot = new keyslot(src)
recalculateChannels()
perform_update_icon = FALSE
set_listening(listening)
set_broadcasting(broadcasting)
set_frequency(sanitize_frequency(frequency, freerange, (special_channels & RADIO_SPECIAL_SYNDIE)))
set_on(on)
perform_update_icon = TRUE
if (should_update_icon)
update_appearance(UPDATE_ICON)
AddElement(/datum/element/empprotection, EMP_PROTECT_WIRES)
register_context()
// No subtypes
if(type != /obj/item/radio)
return
AddElement(/datum/element/slapcrafting, string_list(list(/datum/crafting_recipe/improv_explosive)))
if(prob(check_holidays(APRIL_FOOLS) ? 50 : 0.5)) // Extremely rare chance to replace a normal radio with a toy one, because it's funny
make_silly()
/obj/item/radio/Destroy()
remove_radio_all(src) //Just to be sure
if(istype(keyslot))
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.
set_broadcasting(FALSE)
return TRUE
/obj/item/radio/proc/set_frequency(new_frequency)
SEND_SIGNAL(src, COMSIG_RADIO_NEW_FREQUENCY, args)
remove_radio(src, frequency)
if(new_frequency)
frequency = new_frequency
if(listening && on)
add_radio(src, new_frequency)
/obj/item/radio/proc/recalculateChannels()
resetChannels()
if(keyslot)
for(var/channel_name in keyslot.channels)
if(!(channel_name in channels))
channels[channel_name] = keyslot.channels[channel_name]
special_channels |= keyslot.special_channels
for(var/channel_name in channels)
LAZYSET(secure_radio_connections, channel_name, add_radio(src, GLOB.default_radio_channels[channel_name]))
if(!listening)
remove_radio_all(src)
/obj/item/radio/proc/resetChannels()
for(var/ch_name in channels)
SSradio.remove_object(src, GLOB.default_radio_channels[ch_name])
channels = list()
LAZYNULL(secure_radio_connections)
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)
add_radio(src, GLOB.default_radio_channels[channel_name])
add_radio(src, frequency)
/obj/item/radio/proc/make_syndie() // Turns normal radios into Syndicate radios!
qdel(keyslot)
keyslot = new /obj/item/encryptionkey/syndicate()
special_channels |= RADIO_SPECIAL_SYNDIE
recalculateChannels()
/obj/item/radio/interact(mob/user)
if(unscrewed && !isAI(user))
wires.interact(user)
add_fingerprint(user)
else
..()
//simple getters only because i NEED to enforce complex setter use for these vars for caching purposes but VAR_PROTECTED requires getter usage as well.
//if another decorator is made that doesn't require getters feel free to nuke these and change these vars over to that
///simple getter for the on variable. necessary due to VAR_PROTECTED
/obj/item/radio/proc/is_on()
return on
///simple getter for the frequency variable. necessary due to VAR_PROTECTED
/obj/item/radio/proc/get_frequency()
return frequency
///simple getter for the broadcasting variable. necessary due to VAR_PROTECTED
/obj/item/radio/proc/get_broadcasting()
return broadcasting
///simple getter for the listening variable. necessary due to VAR_PROTECTED
/obj/item/radio/proc/get_listening()
return listening
//now for setters for the above protected vars
/**
* setter for the listener var, adds or removes this radio from the global radio list if we are also on
*
* * new_listening - the new value we want to set listening to
* * actual_setting - whether or not the radio is supposed to be listening, sets should_be_listening to the new listening value if true, otherwise just changes listening
*/
/obj/item/radio/proc/set_listening(new_listening, actual_setting = TRUE)
listening = new_listening
if(actual_setting)
should_be_listening = listening
if(listening && on)
readd_listening_radio_channels()
else if(!listening)
remove_radio_all(src)
if (perform_update_icon && !isnull(overlay_speaker_idle))
update_icon()
else if (!perform_update_icon)
should_update_icon = TRUE
/**
* setter for broadcasting that makes us not hearing sensitive if not broadcasting and hearing sensitive if broadcasting
* hearing sensitive in this case only matters for the purposes of listening for words said in nearby tiles, talking into us directly bypasses hearing
*
* * new_broadcasting- the new value we want to set broadcasting to
* * actual_setting - whether or not the radio is supposed to be broadcasting, sets should_be_broadcasting to the new value if true, otherwise just changes broadcasting
*/
/obj/item/radio/proc/set_broadcasting(new_broadcasting, actual_setting = TRUE)
broadcasting = new_broadcasting
if(actual_setting)
should_be_broadcasting = broadcasting
if(broadcasting && on) //we don't need hearing sensitivity if we aren't broadcasting, because talk_into doesn't care about hearing
become_hearing_sensitive(INNATE_TRAIT)
else if(!broadcasting)
lose_hearing_sensitivity(INNATE_TRAIT)
if (perform_update_icon && !isnull(overlay_mic_idle))
update_icon()
else if (!perform_update_icon)
should_update_icon = TRUE
///setter for the on var that sets both broadcasting and listening to off or whatever they were supposed to be
/obj/item/radio/proc/set_on(new_on)
on = new_on
if(on)
set_broadcasting(should_be_broadcasting)//set them to whatever they're supposed to be
set_listening(should_be_listening)
else
set_broadcasting(FALSE, actual_setting = FALSE)//fake set them to off
set_listening(FALSE, actual_setting = FALSE)
/obj/item/radio/talk_into(atom/movable/talking_movable, message, channel, list/spans, datum/language/language, list/message_mods)
if(SEND_SIGNAL(talking_movable, COMSIG_MOVABLE_USING_RADIO, src) & COMPONENT_CANNOT_USE_RADIO)
return NONE
if(SEND_SIGNAL(src, COMSIG_RADIO_NEW_MESSAGE, talking_movable, message, channel) & COMPONENT_CANNOT_USE_RADIO)
return NONE
if(!spans)
spans = list(talking_movable.speech_span)
if(!language)
language = talking_movable.get_selected_language()
INVOKE_ASYNC(src, PROC_REF(talk_into_impl), talking_movable, message, channel, LAZYLISTDUPLICATE(spans), language, LAZYLISTDUPLICATE(message_mods))
return ITALICS | REDUCE_RANGE
/**
* Handles talking into the radio
*
* Unlike most speech related procs, spans and message_mods are not guaranteed to be lists
*
* * talking_movable - the atom that is talking
* * message - the message to be spoken
* * channel - the channel to be spoken on
* * spans - the spans to be used, lazylist
* * language - the language to be spoken in. (Should) never be null
* * message_mods - the message mods to be used, lazylist
*/
/obj/item/radio/proc/talk_into_impl(atom/movable/talking_movable, message, channel, list/spans, datum/language/language, list/message_mods)
if(!on)
return // the device has to be on
if(!talking_movable || !message)
return
if(wires.is_cut(WIRE_TX)) // Permacell and otherwise tampered-with radios
return
if(!talking_movable.try_speak(message, ignore_spam = TRUE, filterproof = TRUE))
return
if(use_command)
spans |= SPAN_COMMAND
var/radio_message = message
if(LAZYACCESS(message_mods, WHISPER_MODE))
// Radios don't pick up whispers very well
radio_message = stars(radio_message)
spans |= SPAN_ITALICS
flick_overlay_view(overlay_mic_active, 5 SECONDS)
/*
Roughly speaking, radios attempt to make a subspace transmission (which
is received, processed, and rebroadcast by the telecomms satellite) and
if that fails, they send a mundane radio transmission.
Headsets cannot send/receive mundane transmissions, only subspace.
Syndicate radios can hear transmissions on all well-known frequencies.
CentCom radios can hear the CentCom frequency no matter what.
*/
// From the channel, determine the frequency and get a reference to it.
var/freq
if(channel && channels && channels.len > 0)
if(channel == MODE_DEPARTMENT)
channel = channels[1]
freq = LAZYACCESS(secure_radio_connections, channel)
if (!channels[channel]) // if the channel is turned off, don't broadcast
return
else
freq = frequency
channel = null
// Nearby active jammers prevent the message from transmitting
if(is_within_radio_jammer_range(src) && !(special_channels & RADIO_SPECIAL_SYNDIE))
return
// Determine the identity information which will be attached to the signal.
var/atom/movable/virtualspeaker/speaker = new(null, talking_movable, src)
// Construct the signal
var/datum/signal/subspace/vocal/signal = new(src, freq, speaker, language, radio_message, spans, message_mods)
// Independent radios, on the CentCom frequency, reach all independent radios
if ((special_channels & RADIO_SPECIAL_CENTCOM) && (freq == FREQ_CENTCOM || freq == FREQ_STATUS_DISPLAYS || freq == FREQ_FACTION || freq == FREQ_CYBERSUN || freq == FREQ_INTERDYNE || freq == FREQ_GUILD || freq == FREQ_TARKON || freq == FREQ_TERRAGOV)) //SKYRAT EDIT CHANGE - FACTION, MAPPING, TERRAGOV
signal.data["compression"] = 0
signal.transmission_method = TRANSMISSION_SUPERSPACE
signal.levels = list(0)
signal.broadcast()
return
if(isliving(talking_movable))
var/mob/living/talking_living = talking_movable
var/volume_modifier = (talking_living.client?.prefs.read_preference(/datum/preference/numeric/volume/sound_radio_noise))
if(radio_noise && !HAS_TRAIT(talking_living, TRAIT_DEAF) && volume_modifier && signal.frequency != FREQ_COMMON && !LAZYACCESS(message_mods, MODE_SEQUENTIAL) && COOLDOWN_FINISHED(src, audio_cooldown))
COOLDOWN_START(src, audio_cooldown, 0.5 SECONDS)
var/sound/radio_noise = sound('sound/items/radio/radio_talk.ogg', volume = volume_modifier)
radio_noise.frequency = get_rand_frequency_low_range()
SEND_SOUND(talking_living, radio_noise)
// All radios make an attempt to use the subspace system first
signal.send_to_receivers()
// If the radio is subspace-only, that's all it can do
if (subspace_transmission)
return
// Non-subspace radios will check in a couple of seconds, and if the signal
// was never received, send a mundane broadcast (no headsets).
addtimer(CALLBACK(src, PROC_REF(backup_transmission), signal), 2 SECONDS)
/obj/item/radio/proc/backup_transmission(datum/signal/subspace/vocal/signal)
var/turf/T = get_turf(src)
if (signal.data["done"] && (T.z in signal.levels))
return
// Okay, the signal was never processed, send a mundane broadcast.
signal.data["compression"] = 0
signal.transmission_method = TRANSMISSION_RADIO
signal.levels = SSmapping.get_connected_levels(T)
signal.broadcast()
/obj/item/radio/Hear(atom/movable/speaker, message_language, raw_message, radio_freq, radio_freq_name, radio_freq_color, list/spans, list/message_mods = list(), message_range)
. = ..()
if(radio_freq || !broadcasting || get_dist(src, speaker) > canhear_range || message_mods[MODE_RELAY])
return
// BUBBER Edit Start - the worst snowflake code for vore
if(istype(speaker.loc, /obj/vore_belly))
var/obj/vore_belly/VB = speaker.loc
if(VB.muffles_radio)
return
// BUBBER Edit End
var/list/filtered_mods = list()
if (message_mods[MODE_SING])
filtered_mods[MODE_SING] = message_mods[MODE_SING]
if (message_mods[WHISPER_MODE])
filtered_mods[WHISPER_MODE] = message_mods[WHISPER_MODE]
if (message_mods[SAY_MOD_VERB])
filtered_mods[SAY_MOD_VERB] = message_mods[SAY_MOD_VERB]
if (message_mods[MODE_CUSTOM_SAY_EMOTE])
filtered_mods[MODE_CUSTOM_SAY_EMOTE] = message_mods[MODE_CUSTOM_SAY_EMOTE]
filtered_mods[MODE_CUSTOM_SAY_ERASE_INPUT] = message_mods[MODE_CUSTOM_SAY_ERASE_INPUT]
if(message_mods[RADIO_EXTENSION] == MODE_L_HAND || message_mods[RADIO_EXTENSION] == MODE_R_HAND)
// try to avoid being heard double
if (loc == speaker && ismob(speaker))
var/mob/mob_speaker = speaker
var/idx = mob_speaker.get_held_index_of_item(src)
// left hands are odd slots
if (idx && (idx % 2) == (message_mods[RADIO_EXTENSION] == MODE_L_HAND))
return
talk_into(speaker, raw_message, spans=spans, language=message_language, message_mods=filtered_mods)
/// Checks if this radio can receive on the given frequency.
/obj/item/radio/proc/can_receive(input_frequency, list/levels)
// deny checks
if (levels != RADIO_NO_Z_LEVEL_RESTRICTION)
var/turf/position = get_turf(src)
if(!position || !(position.z in levels))
return FALSE
if (input_frequency == FREQ_SYNDICATE && !(special_channels & RADIO_SPECIAL_SYNDIE))
return FALSE
// allow checks: are we listening on that frequency?
if (input_frequency == frequency)
return TRUE
for(var/ch_name in channels)
if(channels[ch_name] & FREQ_LISTENING)
if(GLOB.default_radio_channels[ch_name] == text2num(input_frequency) || special_channels & RADIO_SPECIAL_SYNDIE)
return TRUE
return FALSE
/obj/item/radio/proc/on_receive_message(list/data)
SEND_SIGNAL(src, COMSIG_RADIO_RECEIVE_MESSAGE, data)
flick_overlay_view(overlay_speaker_active, 5 SECONDS)
if(!isliving(loc))
return
var/mob/living/holder = loc
var/volume_modifier = (holder.client?.prefs.read_preference(/datum/preference/numeric/volume/sound_radio_noise))
if(!radio_noise || HAS_TRAIT(holder, TRAIT_DEAF) || !holder.client?.prefs.read_preference(/datum/preference/numeric/volume/sound_radio_noise))
return
var/list/spans = data["spans"]
if(COOLDOWN_FINISHED(src, audio_cooldown))
COOLDOWN_START(src, audio_cooldown, 0.5 SECONDS)
var/sound/radio_receive = sound('sound/items/radio/radio_receive.ogg', volume = volume_modifier)
radio_receive.frequency = get_rand_frequency_low_range()
SEND_SOUND(holder, radio_receive)
if((SPAN_COMMAND in spans) && COOLDOWN_FINISHED(src, important_audio_cooldown))
COOLDOWN_START(src, important_audio_cooldown, 0.5 SECONDS)
var/sound/radio_important = sound('sound/items/radio/radio_important.ogg', volume = volume_modifier)
radio_important.frequency = get_rand_frequency_low_range()
SEND_SOUND(holder, radio_important)
/obj/item/radio/ui_state(mob/user)
return GLOB.inventory_state
/obj/item/radio/ui_interact(mob/user, datum/tgui/ui, datum/ui_state/state)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "Radio", name)
if(state)
ui.set_state(state)
ui.open()
/obj/item/radio/ui_data(mob/user)
var/list/data = list()
data["broadcasting"] = broadcasting
data["listening"] = listening
data["frequency"] = frequency
data["minFrequency"] = freerange ? MIN_FREE_FREQ : MIN_FREQ
data["maxFrequency"] = freerange ? MAX_FREE_FREQ : MAX_FREQ
data["freqlock"] = freqlock != RADIO_FREQENCY_UNLOCKED
data["channels"] = list()
for(var/channel in channels)
data["channels"][channel] = channels[channel] & FREQ_LISTENING
data["command"] = command
data["useCommand"] = use_command
data["subspace"] = subspace_transmission
data["subspaceSwitchable"] = subspace_switchable
data["headset"] = FALSE
data["radio_noises"] = (user.client?.prefs.read_preference(/datum/preference/numeric/volume/sound_radio_noise))
return data
/obj/item/radio/ui_act(action, params, datum/tgui/ui)
. = ..()
if(.)
return
var/mob/user = ui.user
switch(action)
if("frequency")
if(freqlock != RADIO_FREQENCY_UNLOCKED)
return
var/tune = 0
var/adjust = text2num(params["adjust"])
if(adjust)
tune = frequency + adjust * 10
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
if("broadcast")
set_broadcasting(!broadcasting)
. = TRUE
if("channel")
var/channel = params["channel"]
if(!(channel in channels))
return
if(channels[channel] & FREQ_LISTENING)
channels[channel] &= ~FREQ_LISTENING
else
channels[channel] |= FREQ_LISTENING
. = TRUE
if("command")
use_command = !use_command
. = TRUE
if("subspace")
if(subspace_switchable)
subspace_transmission = !subspace_transmission
if(!subspace_transmission)
channels = list()
else
recalculateChannels()
. = TRUE
if("set_radio_volume")
if(!user.client)
return
user.client.prefs.write_preference(GLOB.preference_entries[/datum/preference/numeric/volume/sound_radio_noise], params["volume"])
//let them know what it'll sound like
//we get their read prefs instead of just taking the params beacuse write_preference is what handles ensuring
//there's no href exploits.
var/volume_modifier = (user.client.prefs.read_preference(/datum/preference/numeric/volume/sound_radio_noise))
SEND_SOUND(user, sound('sound/items/radio/radio_receive.ogg', volume = volume_modifier))
/obj/item/radio/examine(mob/user)
. = ..()
if (frequency && in_range(src, user))
. += span_notice("It is set to broadcast over the [span_radio("[frequency/10]")] frequency.")
if (unscrewed)
. += span_notice("It can be attached and modified.")
else
. += span_notice("It cannot be modified or attached.")
/obj/item/radio/update_overlays()
. = ..()
if(unscrewed)
return
if(broadcasting && overlay_mic_idle)
. += overlay_mic_idle
if(listening && overlay_speaker_idle)
. += overlay_speaker_idle
/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)
if(istype(tool, /obj/item/encryptionkey))
return install_key(user, tool)
return NONE
/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("[src] can now be attached and modified!"))
else
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)
switch(keylock)
if(RADIO_KEYSLOT_LOCKED)
to_chat(user, span_warning("The screws locking [src]'s keyslot are stripped, and can't be removed."))
return ITEM_INTERACT_BLOCKING
if(RADIO_KEYSLOT_EMAGGABLE_LOCK)
to_chat(user, span_warning("The screws locking [src]'s keyslot are fastened tight, and likely can't be removed without some kind of magnet..."))
return ITEM_INTERACT_BLOCKING
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 || keylock)
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)
. = ..()
if (. & EMP_PROTECT_SELF)
return
emped++ //There's been an EMP; better count it
var/curremp = emped //Remember which EMP this was
if (listening && ismob(loc)) // if the radio is turned on and on someone's person they notice
to_chat(loc, span_warning("\The [src] overloads."))
for (var/ch_name in channels)
channels[ch_name] = 0
set_on(FALSE)
addtimer(CALLBACK(src, PROC_REF(end_emp_effect), curremp), 20 SECONDS)
/obj/item/radio/suicide_act(mob/living/user)
user.visible_message(span_suicide("[user] starts bouncing [src] off [user.p_their()] head! It looks like [user.p_theyre()] trying to commit suicide!"))
return BRUTELOSS
/obj/item/radio/proc/end_emp_effect(curremp)
if(emped != curremp) //Don't fix it if it's been EMP'd again
return FALSE
emped = FALSE
set_on(TRUE)
return TRUE
/obj/item/radio/proc/make_silly()
name = "\improper Little-Crew: Assistant's First Radio"
icon_state = "walkieian"
desc = "A Little-Crew branded toy radio in the shape of a lovable pet. After Little-Crew HQ was hit with a Donksoft Nuke, these have become collector's items!"
overlay_speaker_idle = null
overlay_speaker_active = null
overlay_mic_idle = null
overlay_mic_active = null
///////////////////////////////
//////////Borg Radios//////////
///////////////////////////////
//Giving borgs their own radio to have some more room to work with -Sieve
/obj/item/radio/borg
name = "cyborg radio"
subspace_transmission = TRUE
subspace_switchable = TRUE
dog_fashion = null
canhear_range = 0
/obj/item/radio/borg/resetChannels()
. = ..()
if (!iscyborg(loc))
return
var/mob/living/silicon/robot/borg = loc
for(var/ch_name in borg.model.radio_channels)
channels[ch_name] = TRUE
/obj/item/radio/borg/syndicate
special_channels = RADIO_SPECIAL_SYNDIE
keyslot = /obj/item/encryptionkey/syndicate
/obj/item/radio/borg/syndicate/Initialize(mapload)
. = ..()
set_frequency(FREQ_SYNDICATE)
/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
/obj/item/radio/off/Initialize(mapload)
. = ..()
set_listening(FALSE)
// RADIOS USED BY BROADCASTING
/obj/item/radio/entertainment
desc = "You should not hold this."
canhear_range = 7
freerange = TRUE
freqlock = RADIO_FREQENCY_LOCKED
radio_noise = FALSE
/obj/item/radio/entertainment/Initialize(mapload)
. = ..()
set_frequency(FREQ_ENTERTAINMENT)
/obj/item/radio/entertainment/speakers // Used inside of the entertainment monitors, not to be used as a actual item
should_be_listening = TRUE
should_be_broadcasting = FALSE
/obj/item/radio/entertainment/speakers/proc/toggle_mute()
should_be_listening = !should_be_listening
/obj/item/radio/entertainment/speakers/Initialize(mapload)
. = ..()
set_broadcasting(FALSE)
set_listening(TRUE)
wires?.cut(WIRE_TX)
/obj/item/radio/entertainment/speakers/on_receive_message(list/data)
playsound(src, SFX_MUFFLED_SPEECH, 60, TRUE, -4, ignore_walls = FALSE, volume_preference = /datum/preference/numeric/volume/sound_radio_noise)
return ..()
/obj/item/radio/entertainment/speakers/physical // Can be used as a physical item
name = "entertainment radio"
desc = "A portable one-way radio permanently tuned into entertainment frequency."
icon_state = "radio"
inhand_icon_state = "radio"
worn_icon_state = "radio"
overlay_speaker_idle = "radio_s_idle"
overlay_speaker_active = "radio_s_active"
overlay_mic_idle = "radio_m_idle"
overlay_mic_active = "radio_m_active"
/obj/item/radio/entertainment/microphone // Used inside of a broadcast camera, not to be used as a actual item
should_be_listening = FALSE
should_be_broadcasting = TRUE
/obj/item/radio/entertainment/microphone/Initialize(mapload)
. = ..()
set_broadcasting(TRUE)
set_listening(FALSE)
wires?.cut(WIRE_RX)
/obj/item/radio/entertainment/microphone/physical // Can be used as a physical item
name = "microphone"
desc = "No comments."
icon = 'icons/obj/service/broadcast.dmi'
icon_state = "microphone"
inhand_icon_state = "microphone"
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()
#undef FREQ_LISTENING