Files
Arrow768 14b6339d5c TGUI Migration (for everything remaining except the hardsuit) (#22404)
```
  - refactor: "Updates the Computer Fabricator to TGUI"
  - refactor: "Updates the Instruments to TGUI"
  - refactor: "Updates the Geo Scanner to TGUI"
  - refactor: "Updates the Stacking Machine to TGUI"
  - refactor: "Updates the Robotics Console to TGUI"
  - refactor: "Updates the Requests Console to TGUI"
  - refactor: "Updates the Synthesizer/Instrument main panel to TGUI"
  - refactor: "Updates the Nuclear Bomb control panel to TGUI"
  - rscadd: "Requests Console messages are now typed (Assistance Request, Supply Request, Information, Reply); the type is shown in the recipient's log, audible alert, and PDA notification."
  - rscadd: "Reconstructs the Synthesizer's Virtual Environment Editor (was broken since added — its NanoUI template never existed). The Custom environment preset that depends on it is gated behind a new musical_config.can_use_custom flag (default off, matching prior behavior)."
  - bugfix: "Fixes the Autolathe UI re-opening after every print job."
  - bugfix: "Fixes the Song Editor's currently-playing line indicator not advancing during playback."
  - bugfix: "Fixes a class of bugs where TGUI windows for the Air Alarm, Portable Turret, Mining Vendor, Mineral Processor, Tank Dispenser, Helm Console, all Chemistry machines, and Suit Cycler would force themselves back open after every action, even when the player had closed them."
  - bugfix: "Fixes the spatial sound system silently dropping every sound that used the 'None' (-1) environment id or a 23-element custom environment list. The validator was rejecting both as invalid, so synthesizer notes played with Custom or None mode produced no audible output at all."
  - admin: "Adds two requests consoles and two fax machines to the bridge on the runtime test map."

```

Claude Opus 4.7 has been used during the creation of this PR

---------

Signed-off-by: Arrow768 <1331699+Arrow768@users.noreply.github.com>
Co-authored-by: Werner <Arrow768@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Batrachophreno <Batrochophreno@gmail.com>
2026-05-26 17:57:26 +00:00

325 lines
11 KiB
Plaintext

//This is the combination of logic pertaining music
//An atom should use the logic and call it as it wants
/datum/real_instrument
var/datum/instrument/instruments
var/datum/sound_player/player
var/datum/instrument_ui/song_editor/song_editor
var/datum/instrument_ui/usage_info/usage_info
var/maximum_lines
var/maximum_line_length
var/obj/owner
var/datum/instrument_ui/env_editor/env_editor
var/datum/instrument_ui/echo_editor/echo_editor
/datum/real_instrument/New(obj/who, datum/sound_player/how, datum/instrument/what)
player = how
owner = who
maximum_lines = GLOB.musical_config.max_lines
maximum_line_length = GLOB.musical_config.max_line_length
instruments = what //This can be a list, or it can also not be one
/// Builds the data list consumed by the Synthesizer TGUI. Called from each instrument atom's ui_data().
/datum/real_instrument/proc/build_ui_data()
return list(
"playback" = list(
"playing" = src.player.song.playing,
"autorepeat" = src.player.song.autorepeat,
"wait" = src.player.wait != null
),
"basic_options" = list(
"cur_instrument" = src.player.song.instrument_data.name,
"volume" = src.player.volume,
"bpm" = round(600 / src.player.song.tempo),
"transposition" = src.player.song.transposition,
"octave_range" = list(
"min" = src.player.song.octave_range_min,
"max" = src.player.song.octave_range_max
)
),
"advanced_options" = list(
"all_environments" = GLOB.musical_config.all_environments,
"selected_environment" = GLOB.musical_config.id_to_environment(src.player.virtual_environment_selected),
"apply_echo" = src.player.apply_echo,
"can_use_custom" = GLOB.musical_config.can_use_custom
),
"sustain" = list(
"linear_decay_active" = src.player.song.linear_decay,
"sustain_timer" = src.player.song.sustain_timer,
"soft_coeff" = src.player.song.soft_coeff
),
"show" = list(
"playback" = src.player.song.lines.len > 0,
"custom_env_options" = GLOB.musical_config.is_custom_env(src.player.virtual_environment_selected),
"env_settings" = GLOB.musical_config.env_settings_available
),
"status" = list(
"channels" = src.player.song.available_channels,
"events" = src.player.event_manager.events.len,
"max_channels" = GLOB.musical_config.channels_per_instrument,
"max_events" = GLOB.musical_config.max_events
)
)
/// Dispatches a TGUI action sent from the Synthesizer interface. Returns TRUE if handled.
/datum/real_instrument/proc/handle_ui_act(action, list/params, mob/user)
var/value = text2num(params["value"])
switch(action)
if("tempo")
src.player.song.tempo = src.player.song.sanitize_tempo(src.player.song.tempo + value * world.tick_lag)
return TRUE
if("play")
src.player.song.playing = value
if(src.player.song.playing)
GLOB.instrument_synchronizer.raise_event(player.actual_instrument)
src.player.song.play_song(user)
return TRUE
if("wait")
if(value)
src.player.wait = WEAKREF(user)
else
src.player.wait = null
return TRUE
if("newsong")
src.player.song.lines.Cut()
src.player.song.tempo = src.player.song.sanitize_tempo(5) // default 120 BPM
return TRUE
if("import")
var/t = ""
do
t = html_encode(input(user, "Please paste the entire song, formatted:", "[owner.name]", t) as message)
if(!CanInteractWith(user, owner, GLOB.physical_state))
return TRUE
if(length(t) >= 2 * src.maximum_lines * src.maximum_line_length)
var/cont = input(user, "Your message is too long! Would you like to continue editing it?", "", "yes") in list("yes", "no")
if(!CanInteractWith(user, owner, GLOB.physical_state))
return TRUE
if(cont == "no")
break
while(length(t) > 2 * src.maximum_lines * src.maximum_line_length)
if(length(t))
src.player.song.lines = splittext(t, "\n")
if(copytext(src.player.song.lines[1], 1, 6) == "BPM: ")
if(text2num(copytext(src.player.song.lines[1], 6)) != 0)
src.player.song.tempo = src.player.song.sanitize_tempo(600 / text2num(copytext(src.player.song.lines[1], 6)))
src.player.song.lines.Cut(1, 2)
else
src.player.song.tempo = src.player.song.sanitize_tempo(5)
else
src.player.song.tempo = src.player.song.sanitize_tempo(5) // default 120 BPM
if(src.player.song.lines.len > maximum_lines)
to_chat(user, "Too many lines!")
src.player.song.lines.Cut(maximum_lines + 1)
var/linenum = 1
for(var/l in src.player.song.lines)
if(length(l) > maximum_line_length)
to_chat(user, "Line [linenum] too long!")
src.player.song.lines.Remove(l)
else
linenum++
return TRUE
if("show_song_editor")
if(!src.song_editor)
src.song_editor = new(host = src.owner, song = src.player.song)
src.song_editor.ui_interact(user)
return TRUE
if("show_usage")
if(!src.usage_info)
src.usage_info = new(owner, src.player)
src.usage_info.ui_interact(user)
return TRUE
if("volume")
src.player.volume = min(max(min(player.volume + value, 100), 0), player.max_volume)
return TRUE
if("transposition")
src.player.song.transposition = max(min(player.song.transposition + value, GLOB.musical_config.highest_transposition), GLOB.musical_config.lowest_transposition)
return TRUE
if("min_octave")
src.player.song.octave_range_min = max(min(player.song.octave_range_min + value, GLOB.musical_config.highest_octave), GLOB.musical_config.lowest_octave)
src.player.song.octave_range_max = max(player.song.octave_range_max, player.song.octave_range_min)
return TRUE
if("max_octave")
src.player.song.octave_range_max = max(min(player.song.octave_range_max + value, GLOB.musical_config.highest_octave), GLOB.musical_config.lowest_octave)
src.player.song.octave_range_min = min(player.song.octave_range_max, player.song.octave_range_min)
return TRUE
if("sustain_timer")
src.player.song.sustain_timer = max(min(player.song.sustain_timer + value, GLOB.musical_config.longest_sustain_timer), 1)
return TRUE
if("soft_coeff")
var/new_coeff = input(user, "from [GLOB.musical_config.gentlest_drop] to [GLOB.musical_config.steepest_drop]") as num
if(!CanInteractWith(user, owner, GLOB.physical_state))
return TRUE
new_coeff = round(min(max(new_coeff, GLOB.musical_config.gentlest_drop), GLOB.musical_config.steepest_drop), 0.001)
src.player.song.soft_coeff = new_coeff
return TRUE
if("instrument")
if(!islist(instruments))
return TRUE
var/list/as_list = instruments
var/list/categories = list()
for(var/key in as_list)
var/datum/instrument/instrument = as_list[key]
categories |= instrument.category
var/category = input(user, "Choose a category") as null|anything in categories
if(!CanInteractWith(user, owner, GLOB.physical_state))
return TRUE
var/list/instruments_available = list()
for(var/key in as_list)
var/datum/instrument/instrument = as_list[key]
if(instrument.category == category)
instruments_available += key
var/new_instrument = input(user, "Choose an instrument") as null|anything in instruments_available
if(!CanInteractWith(user, owner, GLOB.physical_state))
return TRUE
if(new_instrument)
src.player.song.instrument_data = as_list[new_instrument]
return TRUE
if("autorepeat")
src.player.song.autorepeat = value
return TRUE
if("decay")
src.player.song.linear_decay = value
return TRUE
if("echo")
src.player.apply_echo = value
return TRUE
if("show_env_editor")
if(GLOB.musical_config.env_settings_available)
if(!src.env_editor)
src.env_editor = new(src.player)
src.env_editor.ui_interact(user)
else
to_chat(user, "Virtual environment is disabled")
return TRUE
if("show_echo_editor")
if(!src.echo_editor)
src.echo_editor = new(src.player)
src.echo_editor.ui_interact(user)
return TRUE
if("select_env")
if(value in -1 to 26)
var/new_id = round(value)
if(GLOB.musical_config.is_custom_env(new_id) && !GLOB.musical_config.can_use_custom)
return TRUE
src.player.virtual_environment_selected = new_id
return TRUE
return FALSE
/datum/real_instrument/Destroy()
if(islist(instruments))
var/list/aslist = instruments
QDEL_LIST_ASSOC_VAL(aslist)
else
QDEL_NULL(instruments)
QDEL_NULL(player)
QDEL_NULL(song_editor)
QDEL_NULL(usage_info)
owner = null
QDEL_NULL(env_editor)
QDEL_NULL(echo_editor)
return ..()
/obj/structure/synthesized_instrument
var/datum/real_instrument/real_instrument
icon = 'icons/obj/musician.dmi'
//Initialization data
var/list/datum/instrument/instruments = list()
var/path = /datum/instrument
var/sound_player = /datum/sound_player
/obj/structure/synthesized_instrument/Initialize()
. = ..()
for(var/type in typesof(path))
var/datum/instrument/new_instrument = new type
if(!new_instrument.id) continue
new_instrument.create_full_sample_deviation_map()
src.instruments[new_instrument.name] = new_instrument
src.real_instrument = new /datum/real_instrument(src, new sound_player(src, instruments[pick(instruments)]), instruments)
/obj/structure/synthesized_instrument/Destroy()
QDEL_NULL(real_instrument)
QDEL_LIST(instruments)
return ..()
/obj/structure/synthesized_instrument/attack_hand(mob/user)
src.interact(user)
/obj/structure/synthesized_instrument/interact(mob/user) // CONDITIONS ..(user) that shit in subclasses
src.ui_interact(user)
/obj/structure/synthesized_instrument/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "Synthesizer", name)
ui.open()
/obj/structure/synthesized_instrument/ui_data(mob/user)
return real_instrument.build_ui_data()
/obj/structure/synthesized_instrument/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
return real_instrument.handle_ui_act(action, params, usr)
/obj/structure/synthesized_instrument/proc/shouldStopPlaying(mob/user)
return 0
////////////////////////
//DEVICE VERSION
////////////////////////
/obj/item/synthesized_instrument
var/datum/real_instrument/real_instrument
icon = 'icons/obj/musician.dmi'
var/list/datum/instrument/instruments = list()
var/path = /datum/instrument
var/sound_player = /datum/sound_player
/obj/item/synthesized_instrument/Initialize()
. = ..()
for(var/type in typesof(path))
var/datum/instrument/new_instrument = new type
if(!new_instrument.id) continue
new_instrument.create_full_sample_deviation_map()
src.instruments[new_instrument.name] = new_instrument
src.real_instrument = new /datum/real_instrument(src, new sound_player(src, instruments[pick(instruments)]), instruments)
/obj/item/synthesized_instrument/Destroy()
QDEL_NULL(src.real_instrument)
if(islist(instruments))
var/list/as_list = instruments
for(var/key in as_list)
qdel(as_list[key])
instruments = null
. = ..()
/obj/item/synthesized_instrument/attack_self(mob/user)
src.interact(user)
/obj/item/synthesized_instrument/interact(mob/user) // CONDITIONS ..(user) that shit in subclasses
src.ui_interact(user)
/obj/item/synthesized_instrument/ui_interact(mob/user, datum/tgui/ui)
if(!real_instrument)
return
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "Synthesizer", name)
ui.open()
/obj/item/synthesized_instrument/ui_data(mob/user)
return real_instrument.build_ui_data()
/obj/item/synthesized_instrument/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
if(!real_instrument)
return
return real_instrument.handle_ui_act(action, params, usr)
/obj/item/synthesized_instrument/proc/shouldStopPlaying(mob/user)
return !(src && in_range(src, user))