[MIRROR] Refactor jukebox, jukebox datum, now jukebox audio is positional. And it fully respects deaf people. (#26301)

* Refactor jukebox, jukebox datum, now jukebox audio is positional. And it fully respects deaf people.

* fix our stuff

---------

Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
Co-authored-by: jjpark-kb <mccorvey.norman@gmail.com>
This commit is contained in:
SkyratBot
2024-02-04 08:01:27 -05:00
committed by GitHub
co-authored by MrMelbert jjpark-kb
parent 972e517cfe
commit 3d28268ea0
7 changed files with 706 additions and 263 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 @@
/* SKYRAT EDIT: See modular_skyrat/modules/jukebox /// 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 SKYRAT EDIT END*/
+135 -211
View File
@@ -1,122 +1,61 @@
/* SKYRAT EDIT REMOVAL BEGIN - JUKEBOX - MOVED TO 'modular_skyrat/modules/jukebox/code/dance_machine.dm'
/// 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))
/* SKYRAT EDIT: See modular_skyrat/modules/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
@@ -129,23 +68,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)
. = ..()
@@ -154,60 +77,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)
@@ -248,7 +231,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
@@ -267,7 +250,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))
@@ -335,7 +318,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
@@ -404,63 +387,4 @@
/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
*/ //SKYRAT EDIT REMOVAL END
SKYRAT EDIT END*/
+28 -35
View File
@@ -55,7 +55,7 @@
mod.wearer.investigate_log("has been killed by [src].", INVESTIGATE_DEATHS)
mod.wearer.death() //just in case, for some reason, they're still alive
flash_color(mod.wearer, flash_color = "#FF0000", flash_time = 10 SECONDS)
/* SKYRAT EDIT: See skyrat_modular/modules/jukebox
///Rave Visor - Gives you a rainbow visor and plays jukebox music to you.
/obj/item/mod/module/visor/rave
name = "MOD rave visor module"
@@ -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,21 +125,21 @@
/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
SKYRAT EDIT END */
///Tanner - Tans you with spraytan.
/obj/item/mod/module/tanner
name = "MOD tanning module"
@@ -0,0 +1,90 @@
///Rave Visor - Gives you a rainbow visor and plays jukebox music to you.
/obj/item/mod/module/visor/rave
name = "MOD rave visor module"
desc = "A Super Cool Awesome Visor (SCAV), intended for modular suits."
icon_state = "rave_visor"
complexity = 1
overlay_state_inactive = "module_rave"
/// The client colors applied to the wearer.
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),
list(1,0,0,0, 0,0.5,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0),
list(1,0,0,0, 0,1,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0),
list(0,0,0,0, 0,1,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0),
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),
)
/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]
/obj/item/mod/module/visor/rave/on_activation()
. = ..()
if(!.)
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)
/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))
/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
/obj/item/mod/module/visor/rave/on_active_process(seconds_per_tick)
rave_number++
if(rave_number > length(rainbow_order))
rave_number = 1
mod.wearer.update_clothing(mod.slot_flags)
rave_screen.update_colour(rainbow_order[rave_number])
/obj/item/mod/module/visor/rave/get_configuration()
. = ..()
if(length(songs))
.["selection"] = add_ui_configuration("Song", "list", selection.song_name, clean_songs())
/obj/item/mod/module/visor/rave/configure_edit(key, value)
switch(key)
if("selection")
if(active)
return
selection = songs[value]
/obj/item/mod/module/visor/rave/proc/clean_songs()
. = list()
for(var/track in songs)
. += track
+2
View File
@@ -1189,6 +1189,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"
@@ -7335,6 +7336,7 @@
#include "modular_skyrat\modules\interaction_menu\code\interaction_datum.dm"
#include "modular_skyrat\modules\jukebox\code\dance_machine.dm"
#include "modular_skyrat\modules\jukebox\code\jukebox_subsystem.dm"
#include "modular_skyrat\modules\jukebox\code\ravemod.dm"
#include "modular_skyrat\modules\jungle\code\flora.dm"
#include "modular_skyrat\modules\knives\knives.dm"
#include "modular_skyrat\modules\layer_shift\code\mob_movement.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>