mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-23 13:10:02 +01:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user