Refactor jukebox, jukebox datum, now jukebox audio is positional. And it fully respects deaf people. (#81135)

## About The Pull Request

- Jukebox is refactored into a datum that the rave visor and the jukebox
uses.

- Jukebox UI is now typescript. 

- How the Jukebox delivers sound to players has been rewritten. 
- Now it adjusts the sound's position in accordance to where the
listener is.
- This implementation was loosely inspired by that done by Baystation
half a decade ago, so kudos to them.
   - Additionally, being deafened will temporarily mute the jukebox. 

- And sorry, in refactoring this I snuck in one tiny feature. 
- You can now toggle looping on Jukeboxes to play the song
foreeeverrrrr.

## Why It's Good For The Game

It sounds wayyyyyy better. Overhead isn't even that bad, though it could
be tested on a live server to make sure.


https://github.com/tgstation/tgstation/assets/51863163/ec1321b6-bf1c-4c33-9663-83f2c23a4277

## Changelog

🆑 Melbert
refactor: Jukebox has been refactored. Jukebox music now updates as the
player moves, mutes when the player is deafened, and overall sounds
wayyy better. You can also now toggle song repeat on jukeboxes.
/🆑
This commit is contained in:
MrMelbert
2024-01-30 18:54:56 -06:00
committed by GitHub
parent b2efe359cf
commit 7ffc073b33
6 changed files with 612 additions and 260 deletions
@@ -150,7 +150,6 @@
},
/obj/effect/turf_decal/siding/brown,
/obj/machinery/jukebox{
active = 1;
req_access = null
},
/turf/open/floor/iron,
+406
View File
@@ -0,0 +1,406 @@
/// Checks if the mob has jukebox muted in their preferences
#define IS_PREF_MUTED(mob) (!isnull(mob.client) && !mob.client.prefs.read_preference(/datum/preference/toggle/sound_jukebox))
// Reasons for appling STATUS_MUTE to a mob's sound status
/// The mob is deaf
#define MUTE_DEAF (1<<0)
/// The mob has disabled jukeboxes in their preferences
#define MUTE_PREF (1<<1)
/// The mob is out of range of the jukebox
#define MUTE_RANGE (1<<2)
/**
* ## Jukebox datum
*
* Plays music to nearby mobs when hosted in a movable or a turf.
*/
/datum/jukebox
/// Atom that hosts the jukebox. Can be a turf or a movable.
VAR_FINAL/atom/parent
/// List of /datum/tracks we can play. Set via get_songs().
VAR_FINAL/list/songs = list()
/// Current song track selected
VAR_FINAL/datum/track/selection
/// Current song datum playing
VAR_FINAL/sound/active_song_sound
/// Whether the jukebox requires a connect_range component to check for new listeners
VAR_PROTECTED/requires_range_check = TRUE
/// Assoc list of all mobs listening to the jukebox to their sound status.
VAR_PRIVATE/list/mob/listeners = list()
/// Volume of the songs played. Also serves as the max volume.
/// Do not set directly, use set_new_volume() instead.
VAR_PROTECTED/volume = 50
/// Range at which the sound plays to players, can also be a view "XxY" string
VAR_PROTECTED/sound_range
/// How far away horizontally from the jukebox can you be before you stop hearing it
VAR_PRIVATE/x_cutoff
/// How far away vertically from the jukebox can you be before you stop hearing it
VAR_PRIVATE/z_cutoff
/// Whether the music loops when done.
/// If FALSE, you must handle ending music yourself.
var/sound_loops = FALSE
/datum/jukebox/New(atom/new_parent)
if(!ismovable(new_parent) && !isturf(new_parent))
stack_trace("[type] created on non-turf or non-movable: [new_parent ? "[new_parent] ([new_parent.type])" : "null"])")
qdel(src)
return
parent = new_parent
if(isnull(sound_range))
sound_range = world.view
var/list/worldviewsize = getviewsize(sound_range)
x_cutoff = ceil(worldviewsize[1] * 1.25 / 2) // * 1.25 gives us some extra range to fade out with
z_cutoff = ceil(worldviewsize[2] * 1.25 / 2) // and / 2 is because world view is the whole screen, and we want the centre
if(requires_range_check)
var/static/list/connections = list(COMSIG_ATOM_ENTERED = PROC_REF(check_new_listener))
AddComponent(/datum/component/connect_range, parent, connections, max(x_cutoff, z_cutoff))
songs = init_songs()
if(length(songs))
selection = songs[pick(songs)]
RegisterSignal(parent, COMSIG_ENTER_AREA, PROC_REF(on_enter_area))
RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved))
RegisterSignal(parent, COMSIG_QDELETING, PROC_REF(parent_delete))
/datum/jukebox/Destroy()
unlisten_all()
parent = null
selection = null
songs.Cut()
active_song_sound = null
return ..()
/// When our parent is deleted, we should go too.
/datum/jukebox/proc/parent_delete(datum/source)
SIGNAL_HANDLER
qdel(src)
/**
* Initializes the track list.
*
* By default, this loads all tracks from the config datum.
*
* Returns
* * An assoc list of track names to /datum/track. Track names must be unique.
*/
/datum/jukebox/proc/init_songs()
return load_songs_from_config()
/// Loads the config sounds once, and returns a copy of them.
/datum/jukebox/proc/load_songs_from_config()
var/static/list/config_songs
if(isnull(config_songs))
config_songs = list()
var/list/tracks = flist("[global.config.directory]/jukebox_music/sounds/")
for(var/track_file in tracks)
var/datum/track/new_track = new()
new_track.song_path = file("[global.config.directory]/jukebox_music/sounds/[track_file]")
var/list/track_data = splittext(track_file, "+")
if(length(track_data) != 3)
continue
new_track.song_name = track_data[1]
new_track.song_length = text2num(track_data[2])
new_track.song_beat = text2num(track_data[3])
config_songs[new_track.song_name] = new_track
if(!length(config_songs))
var/datum/track/default/default_track = new()
config_songs[default_track.song_name] = default_track
// returns a copy so it can mutate if desired.
return config_songs.Copy()
/**
* Returns a set of general data relating to the jukebox for use in TGUI.
*
* Returns
* * A list of UI data
*/
/datum/jukebox/proc/get_ui_data()
var/list/data = list()
var/list/songs_data = list()
for(var/song_name in songs)
var/datum/track/one_song = songs[song_name]
UNTYPED_LIST_ADD(songs_data, list( \
"name" = song_name, \
"length" = DisplayTimeText(one_song.song_length), \
"beat" = one_song.song_beat, \
))
data["active"] = !!active_song_sound
data["songs"] = songs_data
data["track_selected"] = selection?.song_name
data["looping"] = sound_loops
data["volume"] = volume
return data
/**
* Sets the sound's range to a new value. This can be a number or a view size string "XxY".
* Then updates any mobs listening to it.
*/
/datum/jukebox/proc/set_sound_range(new_range)
if(sound_range == new_range)
return
sound_range = new_range
var/list/worldviewsize = getviewsize(sound_range)
x_cutoff = ceil(worldviewsize[1] / 2)
z_cutoff = ceil(worldviewsize[2] / 2)
update_all()
/**
* Sets the sound's volume to a new value.
* Then updates any mobs listening to it.
*/
/datum/jukebox/proc/set_new_volume(new_vol)
new_vol = clamp(new_vol, 0, initial(volume))
if(volume == new_vol)
return
volume = new_vol
if(!active_song_sound)
return
active_song_sound.volume = volume
update_all()
/// Sets volume to the maximum possible value, the initial volume value.
/datum/jukebox/proc/set_volume_to_max()
set_new_volume(initial(volume))
/**
* Sets the sound's environment to a new value.
* Then updates any mobs listening to it.
*/
/datum/jukebox/proc/set_new_environment(new_env)
if(!active_song_sound || active_song_sound.environment == new_env)
return
active_song_sound.environment = new_env
update_all()
/// Helper to stop the music for all mobs listening to the music.
/datum/jukebox/proc/unlisten_all()
for(var/mob/listening as anything in listeners)
deregister_listener(listening)
active_song_sound = null
/// Helper to update all mobs currently listening to the music.
/datum/jukebox/proc/update_all()
for(var/mob/listening as anything in listeners)
update_listener(listening)
/// Helper to kickstart the music for all mobs in hearing range of the jukebox.
/datum/jukebox/proc/start_music()
for(var/mob/nearby in hearers(sound_range, parent))
register_listener(nearby)
/// Helper to get all mobs currently, ACTIVELY listening to the jukebox.
/datum/jukebox/proc/get_active_listeners()
var/list/all_listeners = list()
for(var/mob/listener as anything in listeners)
if(listeners[listener] & SOUND_MUTE)
continue
all_listeners += listener
return all_listeners
/// Registers the passed mob as a new listener to the jukebox.
/datum/jukebox/proc/register_listener(mob/new_listener)
PROTECTED_PROC(TRUE)
listeners[new_listener] = NONE
RegisterSignal(new_listener, COMSIG_QDELETING, PROC_REF(listener_deleted))
if(isnull(new_listener.client))
RegisterSignal(new_listener, COMSIG_MOB_LOGIN, PROC_REF(listener_login))
return
RegisterSignal(new_listener, COMSIG_MOVABLE_MOVED, PROC_REF(listener_moved))
RegisterSignals(new_listener, list(SIGNAL_ADDTRAIT(TRAIT_DEAF), SIGNAL_REMOVETRAIT(TRAIT_DEAF)), PROC_REF(listener_deaf))
if(HAS_TRAIT(new_listener, TRAIT_DEAF) || IS_PREF_MUTED(new_listener))
listeners[new_listener] |= SOUND_MUTE
if(isnull(active_song_sound))
var/area/juke_area = get_area(parent)
active_song_sound = sound(selection.song_path)
active_song_sound.channel = CHANNEL_JUKEBOX
active_song_sound.priority = 255
active_song_sound.falloff = 2
active_song_sound.volume = volume
active_song_sound.y = 1
active_song_sound.environment = juke_area.sound_environment || SOUND_ENVIRONMENT_NONE
active_song_sound.repeat = sound_loops
update_listener(new_listener)
// if you have a sound with status SOUND_UPDATE,
// and try to play it to a client who is not listening to the sound already,
// it will not work.
// so we only add this status AFTER the first update, which plays the first sound.
// and after that it's fine to keep it on the sound so it updates as the x/z does.
listeners[new_listener] |= SOUND_UPDATE
/// Deregisters mobs on deletion.
/datum/jukebox/proc/listener_deleted(mob/source)
SIGNAL_HANDLER
deregister_listener(source)
/// Updates the sound's position on mob movement.
/datum/jukebox/proc/listener_moved(mob/source)
SIGNAL_HANDLER
update_listener(source)
/// Allows mobs who are clientless when the music starts to hear it when they log in.
/datum/jukebox/proc/listener_login(mob/source)
SIGNAL_HANDLER
deregister_listener(source)
register_listener(source)
/// Updates the sound's mute status when the mob's deafness updates.
/datum/jukebox/proc/listener_deaf(mob/source)
SIGNAL_HANDLER
if(HAS_TRAIT(source, TRAIT_DEAF))
listeners[source] |= SOUND_MUTE
else if(!unmute_listener(source, MUTE_DEAF))
return
update_listener(source)
/**
* Unmutes the passed mob's sound from the passed reason.
*
* Arguments
* * mob/listener - The mob to unmute.
* * reason - The reason to unmute them for. Can be a combination of MUTE_DEAF, MUTE_PREF, MUTE_RANGE.
*/
/datum/jukebox/proc/unmute_listener(mob/listener, reason)
// We need to check everything BUT the reason we're unmuting for
// Because if we're muted for a different reason we don't wanna touch it
reason = ~reason
if((reason & MUTE_DEAF) && HAS_TRAIT(listener, TRAIT_DEAF))
return FALSE
if((reason & MUTE_PREF) && IS_PREF_MUTED(listener))
return FALSE
if(reason & MUTE_RANGE)
var/turf/sound_turf = get_turf(parent)
var/turf/listener_turf = get_turf(listener)
if(isnull(sound_turf) || isnull(listener_turf))
return FALSE
if(sound_turf.z != listener_turf.z)
return FALSE
if(abs(sound_turf.x - listener_turf.x) > x_cutoff)
return FALSE
if(abs(sound_turf.y - listener_turf.y) > z_cutoff)
return FALSE
listeners[listener] &= ~SOUND_MUTE
return TRUE
/// Deregisters the passed mob as a listener to the jukebox, stopping the music.
/datum/jukebox/proc/deregister_listener(mob/no_longer_listening)
PROTECTED_PROC(TRUE)
listeners -= no_longer_listening
no_longer_listening.stop_sound_channel(CHANNEL_JUKEBOX)
UnregisterSignal(no_longer_listening, list(
COMSIG_MOB_LOGIN,
COMSIG_QDELETING,
COMSIG_MOVABLE_MOVED,
SIGNAL_ADDTRAIT(TRAIT_DEAF),
SIGNAL_REMOVETRAIT(TRAIT_DEAF),
))
/// Updates the passed mob's sound in according to their position and status.
/datum/jukebox/proc/update_listener(mob/listener)
PROTECTED_PROC(TRUE)
active_song_sound.status = listeners[listener] || NONE
var/turf/sound_turf = get_turf(parent)
var/turf/listener_turf = get_turf(listener)
if(isnull(sound_turf) || isnull(listener_turf)) // ??
active_song_sound.x = 0
active_song_sound.z = 0
else if(sound_turf.z != listener_turf.z) // Could MAYBE model multi-z jukeboxes but that's too complex for now
listeners[listener] |= SOUND_MUTE
else
// keep in mind sound XYZ is different to world XYZ. sound +-z = world +-y
var/new_x = sound_turf.x - listener_turf.x
var/new_z = sound_turf.y - listener_turf.y
if((abs(new_x) > x_cutoff || abs(new_z) > z_cutoff))
listeners[listener] |= SOUND_MUTE
else if(listeners[listener] & SOUND_MUTE)
unmute_listener(listener, MUTE_RANGE)
active_song_sound.x = new_x
active_song_sound.z = new_z
SEND_SOUND(listener, active_song_sound)
/// When the jukebox moves, we need to update all listeners.
/datum/jukebox/proc/on_moved(datum/source, ...)
SIGNAL_HANDLER
update_all()
/// When the jukebox enters a new area entirely, we need to update the environment to the new area's.
/datum/jukebox/proc/on_enter_area(datum/source, area/area_to_register)
SIGNAL_HANDLER
set_new_environment(area_to_register.sound_environment || SOUND_ENVIRONMENT_NONE)
/// Check for new mobs entering the jukebox's range.
/datum/jukebox/proc/check_new_listener(datum/source, atom/movable/entered)
SIGNAL_HANDLER
if(isnull(active_song_sound))
return
if(!ismob(entered))
return
if(entered in listeners)
return
register_listener(entered)
/**
* Subtype which only plays the music to the mob you pass in via start_music().
*
* Multiple mobs can still listen at once, but you must register them all manually via start_music().
*/
/datum/jukebox/single_mob
requires_range_check = FALSE
/datum/jukebox/single_mob/start_music(mob/solo_listener)
register_listener(solo_listener)
#undef IS_PREF_MUTED
#undef MUTE_DEAF
#undef MUTE_PREF
#undef MUTE_RANGE
/// Track datums, used in jukeboxes
/datum/track
/// Readable name, used in the jukebox menu
var/song_name = "generic"
/// Filepath of the song
var/song_path = null
/// How long is the song in deciseconds
var/song_length = 0
/// How long is a beat of the song in decisconds
/// Used to determine time between effects when played
var/song_beat = 0
// Default track supplied for testing and also because it's a banger
/datum/track/default
song_path = 'sound/ambience/title3.ogg'
song_name = "Tintin on the Moon"
song_length = 3 MINUTES + 52 SECONDS
song_beat = 1 SECONDS
+133 -209
View File
@@ -1,121 +1,60 @@
/// Helper macro to check if the passed mob has jukebox sound preference enabled
#define HAS_JUKEBOX_PREF(mob) (!QDELETED(mob) && !isnull(mob.client) && mob.client.prefs.read_preference(/datum/preference/toggle/sound_jukebox))
/obj/machinery/jukebox
name = "jukebox"
desc = "A classic music player."
icon = 'icons/obj/machines/music.dmi'
icon_state = "jukebox"
base_icon_state = "jukebox"
verb_say = "states"
density = TRUE
req_access = list(ACCESS_BAR)
/// Whether we're actively playing music
var/active = FALSE
/// List of weakrefs to mobs listening to the current song
var/list/datum/weakref/rangers = list()
/// World.time when the current song will stop playing, but also a cooldown between activations
var/stop = 0
/// List of /datum/tracks we can play
/// Inited from config every time a jukebox is instantiated
var/list/songs = list()
/// Current song selected
var/datum/track/selection = null
/// Volume of the songs played
var/volume = 50
processing_flags = START_PROCESSING_MANUALLY
/// Cooldown between "Error" sound effects being played
COOLDOWN_DECLARE(jukebox_error_cd)
/obj/machinery/jukebox/disco
name = "radiant dance machine mark IV"
desc = "The first three prototypes were discontinued after mass casualty incidents."
icon_state = "disco"
req_access = list(ACCESS_ENGINEERING)
anchored = FALSE
var/list/spotlights = list()
var/list/sparkles = list()
/obj/machinery/jukebox/disco/indestructible
name = "radiant dance machine mark V"
desc = "Now redesigned with data gathered from the extensive disco and plasma research."
req_access = null
anchored = TRUE
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
obj_flags = parent_type::obj_flags | NO_DECONSTRUCTION
/datum/track
var/song_name = "generic"
var/song_path = null
var/song_length = 0
var/song_beat = 0
/datum/track/default
song_path = 'sound/ambience/title3.ogg'
song_name = "Tintin on the Moon"
song_length = 3 MINUTES + 52 SECONDS
song_beat = 1 SECONDS
/// Cooldown between being allowed to play another song
COOLDOWN_DECLARE(jukebox_song_cd)
/// TimerID to when the current song ends
var/song_timerid
/// The actual music player datum that handles the music
var/datum/jukebox/music_player
/obj/machinery/jukebox/Initialize(mapload)
. = ..()
songs = load_songs_from_config()
if(length(songs))
selection = pick(songs)
/// Loads the config sounds once, and returns a copy of them.
/obj/machinery/jukebox/proc/load_songs_from_config()
var/static/list/config_songs
if(isnull(config_songs))
config_songs = list()
var/list/tracks = flist("[global.config.directory]/jukebox_music/sounds/")
for(var/track_file in tracks)
var/datum/track/new_track = new()
new_track.song_path = file("[global.config.directory]/jukebox_music/sounds/[track_file]")
var/list/track_data = splittext(track_file, "+")
if(length(track_data) != 3)
continue
new_track.song_name = track_data[1]
new_track.song_length = text2num(track_data[2])
new_track.song_beat = text2num(track_data[3])
config_songs += new_track
if(!length(config_songs))
// Includes title3 as a default for testing / "no config" support, also because it's a banger
config_songs += new /datum/track/default()
// returns a copy so it can mutate if desired.
return config_songs.Copy()
music_player = new(src)
/obj/machinery/jukebox/Destroy()
dance_over()
selection = null
songs.Cut()
stop_music()
QDEL_NULL(music_player)
return ..()
/obj/machinery/jukebox/attackby(obj/item/O, mob/user, params)
if(!active && !(obj_flags & NO_DECONSTRUCTION))
if(O.tool_behaviour == TOOL_WRENCH)
if(!anchored && !isinspace())
to_chat(user,span_notice("You secure [src] to the floor."))
set_anchored(TRUE)
else if(anchored)
to_chat(user,span_notice("You unsecure and disconnect [src]."))
set_anchored(FALSE)
playsound(src, 'sound/items/deconstruct.ogg', 50, TRUE)
return
return ..()
/obj/machinery/jukebox/no_access
req_access = null
/obj/machinery/jukebox/wrench_act(mob/living/user, obj/item/tool)
if(!isnull(music_player.active_song_sound))
return NONE
if(obj_flags & NO_DECONSTRUCTION)
return NONE
if(default_unfasten_wrench(user, tool) == SUCCESSFUL_UNFASTEN)
return ITEM_INTERACT_SUCCESS
return ITEM_INTERACT_BLOCKING
/obj/machinery/jukebox/update_icon_state()
icon_state = "[initial(icon_state)][active ? "-active" : null]"
icon_state = "[base_icon_state][music_player.active_song_sound ? "-active" : null]"
return ..()
/obj/machinery/jukebox/ui_status(mob/user)
if(isobserver(user))
return ..()
if(!anchored)
to_chat(user,span_warning("This device must be anchored by a wrench!"))
return UI_CLOSE
if(!allowed(user) && !isobserver(user))
if(!allowed(user))
to_chat(user,span_warning("Error: Access Denied."))
user.playsound_local(src, 'sound/misc/compiler-failure.ogg', 25, TRUE)
return UI_CLOSE
if(!songs.len && !isobserver(user))
if(!length(music_player.songs))
to_chat(user,span_warning("Error: No music tracks have been authorized for your station. Petition Central Command to resolve this issue."))
user.playsound_local(src, 'sound/misc/compiler-failure.ogg', 25, TRUE)
return UI_CLOSE
@@ -128,23 +67,7 @@
ui.open()
/obj/machinery/jukebox/ui_data(mob/user)
var/list/data = list()
data["active"] = active
data["songs"] = list()
for(var/datum/track/S in songs)
var/list/track_data = list(
name = S.song_name
)
data["songs"] += list(track_data)
data["track_selected"] = null
data["track_length"] = null
data["track_beat"] = null
if(selection)
data["track_selected"] = selection.song_name
data["track_length"] = DisplayTimeText(selection.song_length)
data["track_beat"] = selection.song_beat
data["volume"] = volume
return data
return music_player.get_ui_data()
/obj/machinery/jukebox/ui_act(action, list/params)
. = ..()
@@ -153,60 +76,120 @@
switch(action)
if("toggle")
if(QDELETED(src))
return
if(!active)
if(stop > world.time)
to_chat(usr, span_warning("Error: The device is still resetting from the last activation, it will be ready again in [DisplayTimeText(stop-world.time)]."))
if(!COOLDOWN_FINISHED(src, jukebox_error_cd))
return
playsound(src, 'sound/misc/compiler-failure.ogg', 50, TRUE)
COOLDOWN_START(src, jukebox_error_cd, 15 SECONDS)
return
if(isnull(music_player.active_song_sound))
if(!COOLDOWN_FINISHED(src, jukebox_song_cd))
to_chat(usr, span_warning("Error: The device is still resetting from the last activation, \
it will be ready again in [DisplayTimeText(COOLDOWN_TIMELEFT(src, jukebox_song_cd))]."))
if(COOLDOWN_FINISHED(src, jukebox_error_cd))
playsound(src, 'sound/misc/compiler-failure.ogg', 33, TRUE)
COOLDOWN_START(src, jukebox_error_cd, 15 SECONDS)
return TRUE
activate_music()
START_PROCESSING(SSobj, src)
return TRUE
else
stop = 0
return TRUE
if("select_track")
if(active)
to_chat(usr, span_warning("Error: You cannot change the song until the current one is over."))
return
var/list/available = list()
for(var/datum/track/S in songs)
available[S.song_name] = S
var/selected = params["track"]
if(QDELETED(src) || !selected || !istype(available[selected], /datum/track))
return
selection = available[selected]
stop_music()
return TRUE
if("select_track")
if(!isnull(music_player.active_song_sound))
to_chat(usr, span_warning("Error: You cannot change the song until the current one is over."))
return TRUE
var/datum/track/new_song = music_player.songs[params["track"]]
if(QDELETED(src) || !istype(new_song, /datum/track))
return TRUE
music_player.selection = new_song
return TRUE
if("set_volume")
var/new_volume = params["volume"]
if(new_volume == "reset")
volume = initial(volume)
return TRUE
if(new_volume == "reset" || new_volume == "max")
music_player.set_volume_to_max()
else if(new_volume == "min")
volume = 0
return TRUE
else if(new_volume == "max")
volume = initial(volume)
return TRUE
else if(text2num(new_volume) != null)
volume = text2num(new_volume)
return TRUE
music_player.set_new_volume(0)
else if(isnum(text2num(new_volume)))
music_player.set_new_volume(text2num(new_volume))
return TRUE
if("loop")
music_player.sound_loops = !!params["looping"]
return TRUE
/obj/machinery/jukebox/proc/activate_music()
active = TRUE
if(!isnull(music_player.active_song_sound))
return FALSE
music_player.start_music()
update_use_power(ACTIVE_POWER_USE)
update_appearance(UPDATE_ICON_STATE)
START_PROCESSING(SSobj, src)
stop = world.time + selection.song_length
if(!music_player.sound_loops)
song_timerid = addtimer(CALLBACK(src, PROC_REF(stop_music)), music_player.selection.song_length, TIMER_UNIQUE|TIMER_STOPPABLE|TIMER_DELETE_ME)
return TRUE
/obj/machinery/jukebox/proc/stop_music()
if(!isnull(song_timerid))
deltimer(song_timerid)
music_player.unlisten_all()
if(!QDELING(src))
COOLDOWN_START(src, jukebox_song_cd, 10 SECONDS)
playsound(src,'sound/machines/terminal_off.ogg',50,TRUE)
update_use_power(IDLE_POWER_USE)
update_appearance(UPDATE_ICON_STATE)
return TRUE
/obj/machinery/jukebox/on_set_is_operational(old_value)
if(!is_operational)
stop_music()
/obj/machinery/jukebox/disco
name = "radiant dance machine mark IV"
desc = "The first three prototypes were discontinued after mass casualty incidents."
icon_state = "disco"
base_icon_state = "disco"
req_access = list(ACCESS_ENGINEERING)
anchored = FALSE
/// Spotlight effects being played
VAR_PRIVATE/list/obj/item/flashlight/spotlight/spotlights = list()
/// Sparkle effects being played
VAR_PRIVATE/list/obj/effect/overlay/sparkles/sparkles = list()
/obj/machinery/jukebox/disco/indestructible
name = "radiant dance machine mark V"
desc = "Now redesigned with data gathered from the extensive disco and plasma research."
req_access = null
anchored = TRUE
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
obj_flags = parent_type::obj_flags | NO_DECONSTRUCTION
/obj/machinery/jukebox/disco/activate_music()
..()
. = ..()
if(!.)
return
dance_setup()
lights_spin()
begin_processing()
/obj/machinery/jukebox/disco/stop_music()
. = ..()
if(!.)
return
QDEL_LIST(spotlights)
QDEL_LIST(sparkles)
end_processing()
/obj/machinery/jukebox/disco/process()
var/dance_num = rand(1, 4) //all will do the same dance
for(var/mob/living/dancer in music_player.get_active_listeners())
if(!(dancer.mobility_flags & MOBILITY_MOVE))
continue
if(HAS_TRAIT(dancer, TRAIT_DISCO_DANCER))
continue
dance(dancer, dance_num)
/obj/machinery/jukebox/disco/proc/dance_setup()
var/turf/cen = get_turf(src)
@@ -247,7 +230,7 @@
/obj/machinery/jukebox/disco/proc/lights_spin()
for(var/i in 1 to 25)
if(QDELETED(src) || !active)
if(QDELETED(src) || isnull(music_player.active_song_sound))
return
var/obj/effect/overlay/sparkles/S = new /obj/effect/overlay/sparkles(src)
S.alpha = 0
@@ -266,7 +249,7 @@
for(var/s in sparkles)
var/obj/effect/overlay/sparkles/reveal = s
reveal.alpha = 255
while(active)
while(!isnull(music_player.active_song_sound))
for(var/g in spotlights) // The multiples reflects custom adjustments to each colors after dozens of tests
var/obj/item/flashlight/spotlight/glow = g
if(QDELETED(glow))
@@ -334,7 +317,7 @@
glow.even_cycle = !glow.even_cycle
if(prob(2)) // Unique effects for the dance floor that show up randomly to mix things up
INVOKE_ASYNC(src, PROC_REF(hierofunk))
sleep(selection.song_beat)
sleep(music_player.selection.song_beat)
if(QDELETED(src))
return
@@ -403,62 +386,3 @@
/obj/machinery/jukebox/disco/proc/dance4_revert(mob/living/dancer, matrix/starting_matrix)
animate(dancer, transform = starting_matrix, time = 5, loop = 0)
REMOVE_TRAIT(dancer, TRAIT_DISCO_DANCER, REF(src))
/obj/machinery/jukebox/proc/dance_over()
for(var/datum/weakref/weak_to_hide_from as anything in rangers)
var/mob/to_hide_from = weak_to_hide_from?.resolve()
to_hide_from?.stop_sound_channel(CHANNEL_JUKEBOX)
rangers.Cut()
/obj/machinery/jukebox/disco/dance_over()
..()
QDEL_LIST(spotlights)
QDEL_LIST(sparkles)
/obj/machinery/jukebox/process()
if(world.time < stop && active)
var/sound/song_played = sound(selection.song_path)
// Goes through existing mobs in rangers to determine if they should not be played to
for(var/datum/weakref/weak_to_hide_from as anything in rangers)
var/mob/to_hide_from = weak_to_hide_from?.resolve()
if(!HAS_JUKEBOX_PREF(to_hide_from) || get_dist(src, get_turf(to_hide_from)) > 10)
rangers -= weak_to_hide_from
to_hide_from?.stop_sound_channel(CHANNEL_JUKEBOX)
// Collect mobs to play the song to, stores weakrefs of them in rangers
for(var/mob/to_play_to in range(world.view, src))
if(!HAS_JUKEBOX_PREF(to_play_to))
continue
var/datum/weakref/weak_playing_to = WEAKREF(to_play_to)
if(rangers[weak_playing_to])
continue
rangers[weak_playing_to] = TRUE
// This plays the sound directly underneath the mob because otherwise it'd get stuck in their left ear or whatever
// Would be neat if it sourced from the box itself though
to_play_to.playsound_local(get_turf(to_play_to), null, volume, channel = CHANNEL_JUKEBOX, sound_to_use = song_played, use_reverb = FALSE)
else if(active)
active = FALSE
update_use_power(IDLE_POWER_USE)
STOP_PROCESSING(SSobj, src)
dance_over()
playsound(src,'sound/machines/terminal_off.ogg',50,TRUE)
update_appearance(UPDATE_ICON_STATE)
stop = world.time + 100
/obj/machinery/jukebox/disco/process()
. = ..()
if(!active)
return
var/dance_num = rand(1,4) //all will do the same dance
for(var/datum/weakref/weak_dancer as anything in rangers)
var/mob/living/to_dance = weak_dancer.resolve()
if(!istype(to_dance) || !(to_dance.mobility_flags & MOBILITY_MOVE))
continue
if(!HAS_TRAIT(to_dance, TRAIT_DISCO_DANCER))
dance(to_dance, dance_num)
#undef HAS_JUKEBOX_PREF
+27 -34
View File
@@ -67,10 +67,6 @@
var/datum/client_colour/rave_screen
/// The current element in the rainbow_order list we are on.
var/rave_number = 1
/// The track we selected to play.
var/datum/track/selection
/// A list of all the songs we can play.
var/list/songs = list()
/// A list of the colors the module can take.
var/static/list/rainbow_order = list(
list(1,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0),
@@ -80,23 +76,18 @@
list(0,0,0,0, 0,0.5,0,0, 0,0,1,0, 0,0,0,1, 0,0,0,0),
list(1,0,0,0, 0,0,0,0, 0,0,1,0, 0,0,0,1, 0,0,0,0),
)
/// What actually plays music to us
var/datum/jukebox/single_mob/music_player
/obj/item/mod/module/visor/rave/Initialize(mapload)
. = ..()
var/list/tracks = flist("[global.config.directory]/jukebox_music/sounds/")
for(var/sound in tracks)
var/datum/track/track = new()
track.song_path = file("[global.config.directory]/jukebox_music/sounds/[sound]")
var/list/sound_params = splittext(sound,"+")
if(length(sound_params) != 3)
continue
track.song_name = sound_params[1]
track.song_length = text2num(sound_params[2])
track.song_beat = text2num(sound_params[3])
songs[track.song_name] = track
if(length(songs))
var/song_name = pick(songs)
selection = songs[song_name]
music_player = new(src)
music_player.sound_loops = TRUE
/obj/item/mod/module/visor/rave/Destroy()
QDEL_NULL(music_player)
QDEL_NULL(rave_screen)
return ..()
/obj/item/mod/module/visor/rave/on_activation()
. = ..()
@@ -104,24 +95,26 @@
return
rave_screen = mod.wearer.add_client_colour(/datum/client_colour/rave)
rave_screen.update_colour(rainbow_order[rave_number])
if(selection)
mod.wearer.playsound_local(get_turf(src), null, 50, channel = CHANNEL_JUKEBOX, sound_to_use = sound(selection.song_path), use_reverb = FALSE)
music_player.start_music(mod.wearer)
/obj/item/mod/module/visor/rave/on_deactivation(display_message = TRUE, deleting = FALSE)
. = ..()
if(!.)
return
QDEL_NULL(rave_screen)
if(selection)
mod.wearer.stop_sound_channel(CHANNEL_JUKEBOX)
if(deleting)
return
SEND_SOUND(mod.wearer, sound('sound/machines/terminal_off.ogg', volume = 50, channel = CHANNEL_JUKEBOX))
if(isnull(music_player.active_song_sound))
return
music_player.unlisten_all()
QDEL_NULL(music_player)
if(deleting)
return
SEND_SOUND(mod.wearer, sound('sound/machines/terminal_off.ogg', volume = 50, channel = CHANNEL_JUKEBOX))
/obj/item/mod/module/visor/rave/generate_worn_overlay(mutable_appearance/standing)
. = ..()
for(var/mutable_appearance/appearance as anything in .)
appearance.color = active ? rainbow_order[rave_number] : null
appearance.color = isnull(music_player.active_song_sound) ? null : rainbow_order[rave_number]
/obj/item/mod/module/visor/rave/on_active_process(seconds_per_tick)
rave_number++
@@ -132,20 +125,20 @@
/obj/item/mod/module/visor/rave/get_configuration()
. = ..()
if(length(songs))
.["selection"] = add_ui_configuration("Song", "list", selection.song_name, clean_songs())
if(length(music_player.songs))
.["selection"] = add_ui_configuration("Song", "list", music_player.selection.song_name, music_player.songs)
/obj/item/mod/module/visor/rave/configure_edit(key, value)
switch(key)
if("selection")
if(active)
if(!isnull(music_player.active_song_sound))
return
selection = songs[value]
/obj/item/mod/module/visor/rave/proc/clean_songs()
. = list()
for(var/track in songs)
. += track
var/datum/track/new_song = music_player.songs[value]
if(QDELETED(src) || !istype(new_song, /datum/track))
return
music_player.selection = new_song
///Tanner - Tans you with spraytan.
/obj/item/mod/module/tanner
+1
View File
@@ -1097,6 +1097,7 @@
#include "code\datums\components\jetpack.dm"
#include "code\datums\components\joint_damage.dm"
#include "code\datums\components\jousting.dm"
#include "code\datums\components\jukebox.dm"
#include "code\datums\components\keep_me_secure.dm"
#include "code\datums\components\knockoff.dm"
#include "code\datums\components\label.dm"
@@ -1,6 +1,7 @@
import { sortBy } from 'common/collections';
import { flow } from 'common/fp';
import { BooleanLike } from '../../common/react';
import { useBackend } from '../backend';
import {
Box,
@@ -13,31 +14,59 @@ import {
} from '../components';
import { Window } from '../layouts';
export const Jukebox = (props) => {
const { act, data } = useBackend();
const { active, track_selected, track_length, track_beat, volume } = data;
const songs = flow([sortBy((song) => song.name)])(data.songs || []);
type Song = {
name: string;
length: number;
beat: number;
};
type Data = {
active: BooleanLike;
looping: BooleanLike;
volume: number;
track_selected: string | null;
songs: Song[];
};
export const Jukebox = () => {
const { act, data } = useBackend<Data>();
const { active, looping, track_selected, volume, songs } = data;
const songs_sorted: Song[] = flow([sortBy((song: Song) => song.name)])(songs);
const song_selected: Song | undefined = songs.find(
(song) => song.name === track_selected,
);
return (
<Window width={370} height={313}>
<Window.Content>
<Section
title="Song Player"
buttons={
<Button
icon={active ? 'pause' : 'play'}
content={active ? 'Stop' : 'Play'}
selected={active}
onClick={() => act('toggle')}
/>
<>
<Button
icon={active ? 'pause' : 'play'}
content={active ? 'Stop' : 'Play'}
selected={active}
onClick={() => act('toggle')}
/>
<Button.Checkbox
icon={'arrow-rotate-left'}
content="Repeat"
disabled={active}
checked={looping}
onClick={() => act('loop', { looping: !looping })}
/>
</>
}
>
<LabeledList>
<LabeledList.Item label="Track Selected">
<Dropdown
width="240px"
options={songs.map((song) => song.name)}
disabled={active}
selected={track_selected || 'Select a Track'}
options={songs_sorted.map((song) => song.name)}
disabled={!!active}
selected={song_selected?.name || 'Select a Track'}
onSelected={(value) =>
act('select_track', {
track: value,
@@ -46,11 +75,11 @@ export const Jukebox = (props) => {
/>
</LabeledList.Item>
<LabeledList.Item label="Track Length">
{track_selected ? track_length : 'No Track Selected'}
{song_selected?.length || 'No Track Selected'}
</LabeledList.Item>
<LabeledList.Item label="Track Beat">
{track_selected ? track_beat : 'No Track Selected'}
{track_beat === 1 ? ' beat' : ' beats'}
{song_selected?.beat || 'No Track Selected'}
{song_selected?.beat === 1 ? ' beat' : ' beats'}
</LabeledList.Item>
</LabeledList>
</Section>