diff --git a/_maps/RandomRuins/SpaceRuins/allamericandiner.dmm b/_maps/RandomRuins/SpaceRuins/allamericandiner.dmm index 3f45ad37837..39119905871 100644 --- a/_maps/RandomRuins/SpaceRuins/allamericandiner.dmm +++ b/_maps/RandomRuins/SpaceRuins/allamericandiner.dmm @@ -150,7 +150,6 @@ }, /obj/effect/turf_decal/siding/brown, /obj/machinery/jukebox{ - active = 1; req_access = null }, /turf/open/floor/iron, diff --git a/code/datums/components/jukebox.dm b/code/datums/components/jukebox.dm new file mode 100644 index 00000000000..545b9daab0b --- /dev/null +++ b/code/datums/components/jukebox.dm @@ -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 diff --git a/code/game/machinery/dance_machine.dm b/code/game/machinery/dance_machine.dm index bd86ee6d08f..098c887f2a2 100644 --- a/code/game/machinery/dance_machine.dm +++ b/code/game/machinery/dance_machine.dm @@ -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 diff --git a/code/modules/mod/modules/modules_maint.dm b/code/modules/mod/modules/modules_maint.dm index 30889e4f982..5d0d374523f 100644 --- a/code/modules/mod/modules/modules_maint.dm +++ b/code/modules/mod/modules/modules_maint.dm @@ -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 diff --git a/tgstation.dme b/tgstation.dme index cc526cfe433..cb42b02c5fa 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -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" diff --git a/tgui/packages/tgui/interfaces/Jukebox.jsx b/tgui/packages/tgui/interfaces/Jukebox.tsx similarity index 68% rename from tgui/packages/tgui/interfaces/Jukebox.jsx rename to tgui/packages/tgui/interfaces/Jukebox.tsx index 301650fe775..49cebcfa9da 100644 --- a/tgui/packages/tgui/interfaces/Jukebox.jsx +++ b/tgui/packages/tgui/interfaces/Jukebox.tsx @@ -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(); + 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 (
act('toggle')} - /> + <> +