mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-13 11:12:14 +00:00
## About The Pull Request - Fixes jukebox volume reverting to 100% instead of preference setting when adjusted at the jukebox. - Fixes jukebox volume preference not applying to active jukebox music track - Splits jukebox and instrument into two different preferences. ## Why It's Good For The Game The jukebox volume correctly updates and remains at what the player sets. ## Changelog 🆑 LT3 fix: Jukebox volume preference applies to currently playing music fix: Jukebox volume no longer resets to 100% when adjusted from the object /🆑
410 lines
13 KiB
Plaintext
410 lines
13 KiB
Plaintext
// 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
|
|
|
|
RegisterSignals(new_listener, list(COMSIG_MOVABLE_MOVED, COMSIG_MOB_JUKEBOX_PREFERENCE_APPLIED), PROC_REF(listener_moved))
|
|
RegisterSignals(new_listener, list(SIGNAL_ADDTRAIT(TRAIT_DEAF), SIGNAL_REMOVETRAIT(TRAIT_DEAF)), PROC_REF(listener_deaf))
|
|
var/pref_volume = new_listener.client?.prefs.read_preference(/datum/preference/numeric/volume/sound_jukebox)
|
|
if(HAS_TRAIT(new_listener, TRAIT_DEAF) || !pref_volume)
|
|
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 * (pref_volume/100)
|
|
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
|
|
var/pref_volume = listener.client?.prefs.read_preference(/datum/preference/numeric/volume/sound_jukebox)
|
|
if((reason & MUTE_PREF) && !pref_volume)
|
|
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,
|
|
COMSIG_MOB_JUKEBOX_PREFERENCE_APPLIED,
|
|
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
|
|
|
|
var/pref_volume = listener.client?.prefs.read_preference(/datum/preference/numeric/volume/sound_jukebox)
|
|
if(!pref_volume)
|
|
listeners[listener] |= SOUND_MUTE
|
|
else
|
|
unmute_listener(listener, MUTE_PREF)
|
|
active_song_sound.volume = volume * (pref_volume/100)
|
|
|
|
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 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/music/lobby_music/title3.ogg'
|
|
song_name = "Tintin on the Moon"
|
|
song_length = 3 MINUTES + 52 SECONDS
|
|
song_beat = 1 SECONDS
|