From 91b42cecfe2985ac8e6b743ec9683ee824fb9ae4 Mon Sep 17 00:00:00 2001 From: Wildkins Date: Thu, 26 Feb 2026 13:38:24 -0500 Subject: [PATCH] TGUI Radio (#21917) updates radio / headset UI, inspired heavily by TG's version ![eRcywUYQFD](https://github.com/user-attachments/assets/45a89b9c-c985-4758-b7e6-95b9801d486c) --- .../game/objects/items/devices/radio/radio.dm | 239 ++++++++---------- .../file_system/programs/pai/radio.dm | 2 +- html/changelogs/johnwildkins-radio.yml | 13 + nano/templates/radio_basic.tmpl | 120 --------- tgui/packages/tgui/interfaces/Radio.tsx | 181 +++++++++++++ tgui/packages/tgui/interfaces/pAIRadio.tsx | 15 +- .../tgui/styles/interfaces/Radio.scss | 53 ++++ tgui/packages/tgui/styles/main.scss | 1 + 8 files changed, 360 insertions(+), 264 deletions(-) create mode 100644 html/changelogs/johnwildkins-radio.yml delete mode 100644 nano/templates/radio_basic.tmpl create mode 100644 tgui/packages/tgui/interfaces/Radio.tsx create mode 100644 tgui/packages/tgui/styles/interfaces/Radio.scss diff --git a/code/game/objects/items/devices/radio/radio.dm b/code/game/objects/items/devices/radio/radio.dm index fae5ed34135..644fb5be84e 100644 --- a/code/game/objects/items/devices/radio/radio.dm +++ b/code/game/objects/items/devices/radio/radio.dm @@ -231,31 +231,91 @@ var/global/list/default_interrogation_channels = list( return ui_interact(user) -/obj/item/radio/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1) - var/data[0] +/obj/item/radio/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "Radio", "[name]") + ui.open() +/obj/item/radio/ui_data(mob/user) + var/list/data = list() data["mic_status"] = broadcasting data["speaker"] = listening - data["freq"] = format_frequency(frequency) - data["default_freq"] = format_frequency(default_frequency) - data["rawfreq"] = num2text(frequency) + data["freq"] = frequency + data["default_freq"] = default_frequency + data["rawfreq"] = frequency data["mic_cut"] = (wires.is_cut(WIRE_TRANSMIT) || wires.is_cut(WIRE_SIGNAL)) data["spk_cut"] = (wires.is_cut(WIRE_RECEIVE) || wires.is_cut(WIRE_SIGNAL)) - var/list/chanlist = list_channels(user) - if(islist(chanlist) && chanlist.len) - data["chan_list"] = chanlist - data["chan_list_len"] = chanlist.len + var/list/chl = list_channels(user) + if(LAZYLEN(chl)) + data["channels"] = chl if(syndie) - data["useSyndMode"] = 1 + data["syndie"] = TRUE - ui = SSnanoui.try_update_ui(user, src, ui_key, ui, data, force_open) - if(!ui) - ui = new(user, src, ui_key, "radio_basic.tmpl", "[name]", 400, 430) - ui.set_initial_data(data) - ui.open() + return data + +/obj/item/radio/ui_static_data(mob/user) + var/list/data = list() + + data["min_freq"] = PUBLIC_LOW_FREQ + data["max_freq"] = PUBLIC_HIGH_FREQ + + return data + +/obj/item/radio/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + if(..()) + return TRUE + + if(action == "start_track") + var/mob/target = locate(params["target"]) + var/mob/living/silicon/ai/A = locate(params["ai"]) + if(A && target) + A.ai_actual_track(target) + . = TRUE + + else if (action == "set_freq") + var/new_freq = text2num(params["freq"]) + if ((new_freq < PUBLIC_LOW_FREQ || new_freq > PUBLIC_HIGH_FREQ)) + new_freq = sanitize_frequency(new_freq) + set_frequency(new_freq) + if(hidden_uplink) + if(hidden_uplink.check_trigger(usr, frequency, traitor_frequency)) + ui.close() + . = TRUE + else if (action == "toggle_talk") + set_broadcasting(!broadcasting) + . = TRUE + else if (action == "toggle_listen") + var/channel_name = num2text(params["channel_name"]) + if(channel_name == "0") + set_listening(!listening) + else + channel_name = reverseradiochannels[channel_name] + if (channels[channel_name] & FREQ_LISTENING) + channels[channel_name] &= ~FREQ_LISTENING + else + channels[channel_name] |= FREQ_LISTENING + . = TRUE + else if (action == "spec_freq") + var/freq = params["freq"] + if(has_channel_access(usr, freq)) + set_frequency(text2num(freq)) + . = TRUE + else if (action == "reset_freq") + if(default_frequency) + set_frequency(default_frequency) + . = TRUE + + if (params["nowindow"]) + return TRUE + + if(.) + update_icon() + if(clicksound && iscarbon(usr)) + playsound(loc, clicksound, clickvol) /obj/item/radio/proc/setupRadioDescription(var/additional_radio_desc) var/radio_text = "" @@ -284,9 +344,9 @@ var/global/list/default_interrogation_channels = list( for(var/ch_name in channels) var/chan_stat = channels[ch_name] - var/listening = !!(chan_stat & FREQ_LISTENING) != 0 + var/listening = chan_stat & FREQ_LISTENING - dat.Add(list(list("chan" = ch_name, "display_name" = ch_name, "secure_channel" = 1, "sec_channel_listen" = !listening, "chan_span" = frequency_span_class(radiochannels[ch_name])))) + dat.Add(list(list("chan" = radiochannels[ch_name], "display_name" = ch_name, "secure_channel" = 1, "sec_channel_listen" = listening, "chan_span" = frequency_span_class(radiochannels[ch_name])))) return dat @@ -294,7 +354,7 @@ var/global/list/default_interrogation_channels = list( var/dat[0] for(var/internal_chan in internal_channels) if(has_channel_access(user, internal_chan)) - dat.Add(list(list("chan" = internal_chan, "display_name" = get_frequency_name(text2num(internal_chan)), "chan_span" = frequency_span_class(text2num(internal_chan))))) + dat.Add(list(list("chan" = text2num(internal_chan), "display_name" = get_frequency_name(text2num(internal_chan)), "chan_span" = frequency_span_class(text2num(internal_chan))))) return dat @@ -327,67 +387,11 @@ var/global/list/default_interrogation_channels = list( Speaker: [list ? "Engaged" : "Disengaged"]
"} -/obj/item/radio/CanUseTopic() +/obj/item/radio/ui_status(mob/user, datum/ui_state/state) if(!on) - return STATUS_CLOSE + return UI_CLOSE return ..() -/obj/item/radio/CouldUseTopic(var/mob/user) - ..() - if(clicksound && iscarbon(user)) - playsound(loc, clicksound, clickvol) - -/obj/item/radio/Topic(href, href_list) - if(..()) - return TRUE - - usr.set_machine(src) - if (href_list["track"]) - var/mob/target = locate(href_list["track"]) - var/mob/living/silicon/ai/A = locate(href_list["track2"]) - if(A && target) - A.ai_actual_track(target) - . = TRUE - - else if (href_list["freq"]) - var/new_frequency = (frequency + text2num(href_list["freq"])) - if ((new_frequency < PUBLIC_LOW_FREQ || new_frequency > PUBLIC_HIGH_FREQ)) - new_frequency = sanitize_frequency(new_frequency) - set_frequency(new_frequency) - if(hidden_uplink) - if(hidden_uplink.check_trigger(usr, frequency, traitor_frequency)) - usr << browse(null, "window=radio") - . = TRUE - else if (href_list["talk"]) - set_broadcasting(!broadcasting) - . = TRUE - else if (href_list["listen"]) - var/chan_name = href_list["ch_name"] - if (!chan_name) - set_listening(!listening) - else - if (channels[chan_name] & FREQ_LISTENING) - channels[chan_name] &= ~FREQ_LISTENING - else - channels[chan_name] |= FREQ_LISTENING - . = TRUE - else if(href_list["spec_freq"]) - var freq = href_list["spec_freq"] - if(has_channel_access(usr, freq)) - set_frequency(text2num(freq)) - . = TRUE - else if(href_list["reset_freq"]) - if(default_frequency) - set_frequency(default_frequency) - . = TRUE - - if(href_list["nowindow"]) // here for pAIs, maybe others will want it, idk - return TRUE - - if(.) - SSnanoui.update_uis(src) - update_icon() - /obj/item/radio/proc/autosay(var/message, var/from, var/channel) //BS12 EDIT var/datum/radio_frequency/connection = null if(channel && channels && channels.len > 0) @@ -672,71 +676,36 @@ var/global/list/default_interrogation_channels = list( setupRadioDescription() return -/obj/item/radio/borg/Topic(href, href_list) +/obj/item/radio/borg/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) if(..()) - return 1 - if (href_list["mode"]) - var/enable_subspace_transmission = text2num(href_list["mode"]) - if(enable_subspace_transmission != subspace_transmission) - subspace_transmission = !subspace_transmission - if(subspace_transmission) - to_chat(usr, SPAN_NOTICE("Subspace Transmission is enabled")) - else - to_chat(usr, SPAN_NOTICE("Subspace Transmission is disabled")) + return TRUE + if (action == "mode") + subspace_transmission = !subspace_transmission + to_chat(usr, SPAN_NOTICE("Subspace Transmission [subspace_transmission ? "enabled" : "disabled"].")) + if(subspace_transmission) + recalculateChannels() + else + channels = list() + . = TRUE + if (action == "shutup") // Toggle loudspeaker mode, AKA everyone around you hearing your radio. + shut_up = !shut_up + canhear_range = shut_up ? 0 : 3 + to_chat(usr, SPAN_NOTICE("Loudspeaker [shut_up ? "disabled" : "enabled"].")) + . = TRUE - if(subspace_transmission == 0)//Simple as fuck, clears the channel list to prevent talking/listening over them if subspace transmission is disabled - channels = list() - else - recalculateChannels() - . = 1 - if (href_list["shutup"]) // Toggle loudspeaker mode, AKA everyone around you hearing your radio. - var/do_shut_up = text2num(href_list["shutup"]) - if(do_shut_up != shut_up) - shut_up = !shut_up - if(shut_up) - canhear_range = 0 - to_chat(usr, SPAN_NOTICE("Loadspeaker disabled.")) - else - canhear_range = 3 - to_chat(usr, SPAN_NOTICE("Loadspeaker enabled.")) - . = 1 +/obj/item/radio/borg/ui_data(mob/user) + var/list/data = ..() - if(.) - SSnanoui.update_uis(src) - -/obj/item/radio/borg/interact(mob/user as mob) - if(!on) - return - - . = ..() - -/obj/item/radio/borg/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1) - var/data[0] - - data["mic_status"] = broadcasting - data["speaker"] = listening - data["freq"] = format_frequency(frequency) - data["default_freq"] = format_frequency(default_frequency) - data["rawfreq"] = num2text(frequency) - - var/list/chanlist = list_channels(user) - if(islist(chanlist) && chanlist.len) - data["chan_list"] = chanlist - data["chan_list_len"] = chanlist.len - - if(syndie) - data["useSyndMode"] = 1 - - data["has_loudspeaker"] = 1 data["loudspeaker"] = !shut_up - data["has_subspace"] = 1 data["subspace"] = subspace_transmission - ui = SSnanoui.try_update_ui(user, src, ui_key, ui, data, force_open) - if(!ui) - ui = new(user, src, ui_key, "radio_basic.tmpl", "[name]", 400, 430) - ui.set_initial_data(data) - ui.open() + return data + +/obj/item/radio/borg/ui_static_data(mob/user) + var/list/data = ..() + data["has_loudspeaker"] = TRUE + data["has_subspace"] = TRUE + return data /obj/item/radio/proc/config(op) if(SSradio) diff --git a/code/modules/modular_computers/file_system/programs/pai/radio.dm b/code/modules/modular_computers/file_system/programs/pai/radio.dm index 6fe22519019..06fa59fa678 100644 --- a/code/modules/modular_computers/file_system/programs/pai/radio.dm +++ b/code/modules/modular_computers/file_system/programs/pai/radio.dm @@ -54,5 +54,5 @@ if(action == "radio_range") //fuck nanoUI and fuck vueui two-way bullshit, and fuck topic hooking host.radio.canhear_range = params["radio_range"] else - host.radio.Topic(action, params) + host.radio.ui_act(action, params, ui, state) . = TRUE diff --git a/html/changelogs/johnwildkins-radio.yml b/html/changelogs/johnwildkins-radio.yml new file mode 100644 index 00000000000..01dac4d69ca --- /dev/null +++ b/html/changelogs/johnwildkins-radio.yml @@ -0,0 +1,13 @@ +# Your name. +author: JohnWildkins + +# Optional: Remove this file after generating master changelog. Useful for PR changelogs that won't get used again. +delete-after: True + +# Any changes you've made. See valid prefix list above. +# INDENT WITH TWO SPACES. NOT TABS. SPACES. +# SCREW THIS UP AND IT WON'T WORK. +# Also, this gets changed to [] after reading. Just remove the brackets when you add new shit. +# Please surround your changes in double quotes ("). It works without them, but if you use certain characters it screws up compiling. The quotes will not show up in the changelog. +changes: + - rscadd: "Converted radio/headset interface to TGUI." diff --git a/nano/templates/radio_basic.tmpl b/nano/templates/radio_basic.tmpl deleted file mode 100644 index ceb02b3644a..00000000000 --- a/nano/templates/radio_basic.tmpl +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - -{{if data.useSyndMode}} - {{:helper.syndicateMode()}} -{{/if}} - -
-
- Microphone -
-
- {{if data.mic_cut}} - {{:helper.link('On', null, null, 'disabled')}} - {{:helper.link('Off', null, null, 'disabled')}} - {{else}} - {{:helper.link('On', null, {'talk' : 0}, data.mic_status ? 'selected' : null)}} - {{:helper.link('Off', null, {'talk' : 1}, data.mic_status ? null : 'selected')}} - {{/if}} -
-
- -
-
- Speaker -
-
- {{if data.spk_cut}} - {{:helper.link('On', null, null, 'disabled')}} - {{:helper.link('Off', null, null, 'disabled')}} - {{else}} - {{:helper.link('On', null, {'listen' : 0}, data.speaker ? 'selected' : null)}} - {{:helper.link('Off', null, {'listen' : 1}, data.speaker ? null : 'selected')}} - {{/if}} -
-
- -{{if data.has_subspace}} -
-
- Subspace Transmission: -
-
- {{:helper.link('On', null, {'mode' : 1}, data.subspace ? 'selected' : null)}} - {{:helper.link('Off', null, {'mode' : 0}, data.subspace ? null : 'selected')}} -
-
-{{/if}} - -{{if data.has_loudspeaker}} -
-
- Loudspeaker: -
-
- {{:helper.link('On', null, {'shutup' : 0}, data.loudspeaker ? 'selected' : null)}} - {{:helper.link('Off', null, {'shutup' : 1}, data.loudspeaker ? null : 'selected')}} -
-
-{{/if}} - -
-
- Frequency: {{:data.freq}} -
-
- {{:helper.link('--', null, {'freq' : -10})}} - {{:helper.link('-', null, {'freq' : -2})}} - {{:helper.link('+', null, {'freq' : 2})}} - {{:helper.link('++', null, {'freq' : 10})}} - {{if data.default_freq}} - {{:helper.link(data.default_freq, null, {'default_freq' : 1})}} - {{/if}} -
-
- -{{if data.chan_list_len >= 1}} -

Channels

-
- {{for data.chan_list}} -
- {{:value.display_name}} -
-
- {{if value.secure_channel}} - {{:helper.link('On', null, {'ch_name' : value.chan, 'listen' : value.sec_channel_listen}, value.sec_channel_listen ? null : 'selected')}} - {{:helper.link('Off', null, {'ch_name' : value.chan, 'listen' : value.sec_channel_listen}, value.sec_channel_listen ? 'selected' : null)}} - {{else}} - {{:helper.link('Switch', null, {'spec_freq' : value.chan}, data.rawfreq == value.chan ? 'selected' : null)}} - {{/if}} -
- {{/for}} -{{/if}} diff --git a/tgui/packages/tgui/interfaces/Radio.tsx b/tgui/packages/tgui/interfaces/Radio.tsx new file mode 100644 index 00000000000..2709397e1ba --- /dev/null +++ b/tgui/packages/tgui/interfaces/Radio.tsx @@ -0,0 +1,181 @@ +import { toFixed } from 'common/math'; +import { BooleanLike } from 'common/react'; +import { useBackend } from '../backend'; +import { Section, LabeledList, Button, Box, NumberInput } from '../components'; +import { Window } from '../layouts'; + +type Channel = { + chan: number; + display_name: string; + chan_span: string; + secure_channel?: BooleanLike; + sec_channel_listen?: BooleanLike; +}; + +type RadioData = { + mic_status: BooleanLike; + speaker: BooleanLike; + freq: number; + default_freq: number; + rawfreq: number; + + min_freq: number; + max_freq: number; + + mic_cut: BooleanLike; + spk_cut: BooleanLike; + + channels?: Channel[]; + + syndie?: BooleanLike; + + has_loudspeaker?: BooleanLike; + has_subspace?: BooleanLike; + loudspeaker?: BooleanLike; + subspace?: BooleanLike; +}; + +export const Radio = (props, context) => { + const { act, data } = useBackend(context); + const { + mic_status, + speaker, + freq, + default_freq, + rawfreq, + min_freq, + max_freq, + has_loudspeaker, + loudspeaker, + has_subspace, + subspace, + channels, + mic_cut, + spk_cut, + } = data; + const tunedChannel = channels?.find((channel) => channel.chan === freq); + // Calculate window height + let height = 133; + if (channels && channels.length > 0) { + height += channels.length * 30 + 8; + } else { + height += 24; + } + + return ( + + +
+ + + toFixed(value, 1)} + onChange={(_, value) => + act('set_freq', { + freq: value * 10, + }) + } + /> + {default_freq && ( + +
+ {channels && ( +
+ + {channels.map((channel) => ( + + {channel.display_name} + + } + > + {channel.secure_channel ? ( +
+ )} +
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/pAIRadio.tsx b/tgui/packages/tgui/interfaces/pAIRadio.tsx index b810dfbbbaa..e802710bfea 100644 --- a/tgui/packages/tgui/interfaces/pAIRadio.tsx +++ b/tgui/packages/tgui/interfaces/pAIRadio.tsx @@ -27,26 +27,26 @@ export const pAIRadio = (props, context) => {