From 60285dad52d0decd0de96369526cb8dfbd740298 Mon Sep 17 00:00:00 2001 From: Leshana Date: Wed, 22 Mar 2017 20:43:16 -0400 Subject: [PATCH 1/2] Fix mob.loc = X on spawn * When spawning into the game (both latejoin and normal join) the job controller used loc = X to set location. This meant that Enter() was never called for entering the spawn location, and all other associated problems. * This screws up the new jukebox music system because mob.lastarea is not set correctly. * Fixed signpost teleporting for the same reason (nobody wants beach tunes playing forever) --- code/defines/obj.dm | 3 +-- code/game/jobs/job_controller.dm | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/code/defines/obj.dm b/code/defines/obj.dm index e6d97a8fed..0435ccb83d 100644 --- a/code/defines/obj.dm +++ b/code/defines/obj.dm @@ -11,8 +11,7 @@ switch(alert("Travel back to ss13?",,"Yes","No")) if("Yes") if(user.z != src.z) return - user.loc.loc.Exited(user) - user.loc = pick(latejoin) + user.forceMove(pick(latejoin)) if("No") return diff --git a/code/game/jobs/job_controller.dm b/code/game/jobs/job_controller.dm index 5b9c2ea4ac..6495a56828 100644 --- a/code/game/jobs/job_controller.dm +++ b/code/game/jobs/job_controller.dm @@ -416,13 +416,13 @@ var/global/datum/controller/occupations/job_master if(!S) S = locate("start*[rank]") // use old stype if(istype(S, /obj/effect/landmark/start) && istype(S.loc, /turf)) - H.loc = S.loc + H.forceMove(S.loc) else LateSpawn(H, rank) // Moving wheelchair if they have one if(H.buckled && istype(H.buckled, /obj/structure/bed/chair/wheelchair)) - H.buckled.loc = H.loc + H.buckled.forceMove(H.loc) H.buckled.set_dir(H.dir) // If they're head, give them the account info for their department @@ -627,12 +627,12 @@ var/global/datum/controller/occupations/job_master if(spawnpos && istype(spawnpos)) if(spawnpos.check_job_spawning(rank)) - H.loc = pick(spawnpos.turfs) + H.forceMove(pick(spawnpos.turfs)) . = spawnpos.msg else H << "Your chosen spawnpoint ([spawnpos.display_name]) is unavailable for your chosen job. Spawning you at the Arrivals shuttle instead." - H.loc = pick(latejoin) + H.forceMove(pick(latejoin)) . = "has arrived on the station" else - H.loc = pick(latejoin) + H.forceMove(pick(latejoin)) . = "has arrived on the station" From c63c68c9a801196a3621fed80cc7d4b1f7adf44f Mon Sep 17 00:00:00 2001 From: Leshana Date: Thu, 23 Mar 2017 20:22:50 -0400 Subject: [PATCH 2/2] Implements browser streaming media jukeboxes Ports media code from vgstation, updates it for this codebase and modernizes it. * Changes jukeboxes to load songs using an embedded browser instead of sending over BYOND's sound channels. This means they load out of band without lagging the server. Also songs can be resumed mid-song, so leaving and returning to an area doesn't start the music over. * The old WMP and VLC player modes from /vg are still supported, but adds a new default mode using HTML5 audio to play the music. * WMP - The oldest, still works on IE on windows, but only there, and Microsoft could break it any second. * VLC - Works on all platforms, but requires user to have VLC pre-installed on their computer. Uses a scary plugin. * HTML5 - New default, It is cross platform but doesn't require you to have VLC installed to work. Also caches songs locally even between rounds. * Changed jukebox.txt to be jukebox.json, now can include artist information as well. Must include the duration of songs as well. * For HTML5 audio compatibility, use only MP3 files, its the only format supported on all browsers. * Jukebox itself is also upgraded, instead of just repeating the same song over and over it can actually advance to the next song when one is done playing. Has a few modes including random, next, and single song. * Jukeboxes have a UI improvement, and have a volume control. * Three new settings are added to global settings in character setup * Jukebox music on/off toggles jukebox music independently of normal station ambience. Now you can hear ambience but not music. (or vice versa if you wanted...) * Jukebox music volume. Control the relative volume of jukebox music. Actual volume is player's configured volume * jukebox's configured volume. * Media player type. Choose between WMP, VLC, and HTML5 * Fixes a few bugs in the /vg code. --- code/game/machinery/jukebox.dm | 200 ++++++++++-------- code/game/machinery/jukebox_vr.dm | 45 ---- .../preference_setup/global/06_media.dm | 53 +++++ .../preference_setup/global/setting_datums.dm | 10 + code/modules/media/media_machinery.dm | 74 +++++++ code/modules/media/media_player_html5.dm | 27 +++ code/modules/media/media_player_vlc.dm | 27 +++ code/modules/media/media_player_wmp.dm | 14 ++ code/modules/media/media_tracks.dm | 57 +++++ code/modules/media/mediamanager.dm | 189 +++++++++++++++++ interface/skin.dmf | 26 +++ nano/templates/jukebox.tmpl | 42 +++- vorestation.dme | 8 +- 13 files changed, 631 insertions(+), 141 deletions(-) delete mode 100644 code/game/machinery/jukebox_vr.dm create mode 100644 code/modules/client/preference_setup/global/06_media.dm create mode 100644 code/modules/media/media_machinery.dm create mode 100644 code/modules/media/media_player_html5.dm create mode 100644 code/modules/media/media_player_vlc.dm create mode 100644 code/modules/media/media_player_wmp.dm create mode 100644 code/modules/media/media_tracks.dm create mode 100644 code/modules/media/mediamanager.dm diff --git a/code/game/machinery/jukebox.dm b/code/game/machinery/jukebox.dm index 4eb0e7d408..9ee5a3f70b 100644 --- a/code/game/machinery/jukebox.dm +++ b/code/game/machinery/jukebox.dm @@ -1,10 +1,12 @@ -datum/track - var/title - var/sound +// +// Media Player Jukebox +// Rewritten by Leshana from existing Polaris code, merging in D2K5 and N3X15 work +// -datum/track/New(var/title_name, var/audio) - title = title_name - sound = audio +#define JUKEMODE_NEXT 1 // Advance to next song in the track list +#define JUKEMODE_RANDOM 2 // Not shuffle, randomly picks next each time. +#define JUKEMODE_REPEAT_SONG 3 // Play the same song over and over +#define JUKEMODE_PLAY_ONCE 4 // Play, then stop. /obj/machinery/media/jukebox/ name = "space jukebox" @@ -19,33 +21,17 @@ datum/track/New(var/title_name, var/audio) active_power_usage = 100 circuit = /obj/item/weapon/circuitboard/jukebox - var/playing = 0 - // Vars for hacking var/datum/wires/jukebox/wires = null var/hacked = 0 // Whether to show the hidden songs or not - var/freq = 0 + var/freq = 0 // Currently no effect, will return in phase II of mediamanager. - var/datum/track/current_track - var/list/datum/track/tracks = list( - new/datum/track("Beyond", 'sound/ambience/ambispace.ogg'), - new/datum/track("Clouds of Fire", 'sound/music/clouds.s3m'), - new/datum/track("D`Bert", 'sound/music/title2.ogg'), - new/datum/track("D`Fort", 'sound/ambience/song_game.ogg'), - new/datum/track("Floating", 'sound/music/main.ogg'), - new/datum/track("Endless Space", 'sound/music/space.ogg'), - new/datum/track("Part A", 'sound/misc/TestLoop1.ogg'), - new/datum/track("Scratch", 'sound/music/title1.ogg'), - new/datum/track("Trai`Tor", 'sound/music/traitor.ogg'), - ) - - // Only visible if hacked - var/list/datum/track/secret_tracks = list( - new/datum/track("Clown", 'sound/music/clown.ogg'), - new/datum/track("Space Asshole", 'sound/music/space_asshole.ogg'), - new/datum/track("Thunderdome", 'sound/music/THUNDERDOME.ogg'), - new/datum/track("Russkiy rep Diskoteka", 'sound/music/russianrapdisco.ogg') - ) + var/loop_mode = JUKEMODE_PLAY_ONCE // Behavior when finished playing a song + var/max_queue_len = 3 // How many songs are we allowed to queue up? + var/datum/track/current_track // Currently playing song + var/list/datum/track/queue = list() // Queued songs + var/list/datum/track/tracks = list() // Available tracks + var/list/datum/track/secret_tracks = list() // Only visible if hacked /obj/machinery/media/jukebox/New() ..() @@ -54,11 +40,70 @@ datum/track/New(var/title_name, var/audio) update_icon() /obj/machinery/media/jukebox/Destroy() - StopPlaying() qdel(wires) wires = null ..() +// On initialization, copy our tracks from the global list +/obj/machinery/media/jukebox/initialize() + ..() + if(all_jukebox_tracks.len < 1) + stat |= BROKEN // No tracks configured this round! + return + // Ootherwise load from the global list! + for(var/datum/track/T in all_jukebox_tracks) + if(T.secret) + secret_tracks |= T + else + tracks |= T + return + +/obj/machinery/media/jukebox/process() + if(!playing) + return + if(inoperable()) + disconnect_media_source() + playing = 0 + return + // If the current track isn't finished playing, let it keep going + if(current_track && world.time < media_start_time + current_track.duration) + return + // Otherwise time to pick a new one! + if(queue.len > 0) + current_track = queue[1] + queue.Cut(1, 2) // Remove the item we just took off the list + else + // Oh... nothing in queue? Well then pick next according to our rules + switch(loop_mode) + if(JUKEMODE_NEXT) + var/curTrackIndex = max(1, tracks.Find(current_track)) + var/newTrackIndex = (curTrackIndex % tracks.len) + 1 // Loop back around if past end + current_track = tracks[newTrackIndex] + if(JUKEMODE_RANDOM) + var/previous_track = current_track + do + current_track = pick(tracks) + while(current_track == previous_track && tracks.len > 1) + if(JUKEMODE_REPEAT_SONG) + current_track = current_track + if(JUKEMODE_PLAY_ONCE) + current_track = null + playing = 0 + update_icon() + updateDialog() + start_stop_song() + +// Tells the media manager to start or stop playing based on current settings. +/obj/machinery/media/jukebox/proc/start_stop_song() + if(current_track && playing) + media_url = current_track.url + media_start_time = world.time + visible_message("\The [src] begins to play [current_track.display()].") + else + media_url = "" + media_start_time = 0 + update_music() + /obj/machinery/media/jukebox/proc/set_hacked(var/newhacked) if (hacked == newhacked) return hacked = newhacked @@ -80,13 +125,16 @@ datum/track/New(var/title_name, var/audio) if(istype(W, /obj/item/device/multitool)) return wires.Interact(user) if(istype(W, /obj/item/weapon/wrench)) - if(playing) - StopPlaying() user.visible_message("[user] has [anchored ? "un" : ""]secured \the [src].", "You [anchored ? "un" : ""]secure \the [src].") anchored = !anchored playsound(src.loc, 'sound/items/Ratchet.ogg', 50, 1) power_change() update_icon() + if(!anchored) + playing = 0 + disconnect_media_source() + else + update_media_source() return return ..() @@ -125,16 +173,23 @@ datum/track/New(var/title_name, var/audio) usr << "You must secure \the [src] first." return - if(stat & (NOPOWER|BROKEN)) + if(inoperable()) usr << "\The [src] doesn't appear to function." return if(href_list["change_track"]) - for(var/datum/track/T in tracks) - if(T.title == href_list["title"]) - current_track = T - StartPlaying() - break + var/datum/track/T = locate(href_list["change_track"]) in tracks + if(istype(T)) + current_track = T + StartPlaying() + else if(href_list["loopmode"]) + var/newval = text2num(href_list["loopmode"]) + loop_mode = sanitize_inlist(newval, list(JUKEMODE_NEXT, JUKEMODE_RANDOM, JUKEMODE_REPEAT_SONG, JUKEMODE_PLAY_ONCE), loop_mode) + else if(href_list["volume"]) + var/newval = input("Choose Jukebox volume (0-100%)", "Jukebox volume", round(volume * 100.0)) + newval = sanitize_integer(text2num(newval), min = 0, max = 100, default = volume * 100.0) + volume = newval / 100.0 + update_music() // To broadcast volume change without restarting song else if(href_list["stop"]) StopPlaying() else if(href_list["play"]) @@ -162,36 +217,39 @@ datum/track/New(var/title_name, var/audio) return 1 /obj/machinery/media/jukebox/interact(mob/user) - if(stat & (NOPOWER|BROKEN)) + if(inoperable()) usr << "\The [src] doesn't appear to function." return - ui_interact(user) /obj/machinery/media/jukebox/ui_interact(mob/user, ui_key = "jukebox", var/datum/nanoui/ui = null, var/force_open = 1) var/title = "RetroBox - Space Style" var/data[0] - if(!(stat & (NOPOWER|BROKEN))) - data["current_track"] = current_track != null ? current_track.title : "" + if(operable()) data["playing"] = playing + data["hacked"] = hacked + data["max_queue_len"] = max_queue_len + data["media_start_time"] = media_start_time + data["loop_mode"] = loop_mode + data["volume"] = volume + if(current_track) + data["current_track_ref"] = "\ref[current_track]" // Convenient shortcut + data["current_track"] = current_track.toNanoList() + data["percent"] = playing ? min(100, round(world.time - media_start_time) / current_track.duration) : 0; var/list/nano_tracks = new for(var/datum/track/T in tracks) - nano_tracks[++nano_tracks.len] = list("track" = T.title) - + nano_tracks[++nano_tracks.len] = T.toNanoList() data["tracks"] = nano_tracks // update the ui if it exists, returns null if no ui is passed/found ui = nanomanager.try_update_ui(user, src, ui_key, ui, data, force_open) if (!ui) - // the ui does not exist, so we'll create a new() one - // for a list of parameters and their descriptions see the code docs in \code\modules\nano\nanoui.dm ui = new(user, src, ui_key, "jukebox.tmpl", title, 450, 600) - // when the ui is first opened this is the data it will use ui.set_initial_data(data) - // open the new ui window ui.open() + ui.set_auto_update(playing) /obj/machinery/media/jukebox/attack_ai(mob/user as mob) return src.attack_hand(user) @@ -212,24 +270,6 @@ datum/track/New(var/title_name, var/audio) new /obj/effect/decal/cleanable/blood/oil(src.loc) qdel(src) -/obj/machinery/media/jukebox/attackby(obj/item/W as obj, mob/user as mob) - src.add_fingerprint(user) - - if(default_deconstruction_screwdriver(user, W)) - return - if(default_deconstruction_crowbar(user, W)) - return - if(istype(W, /obj/item/weapon/wrench)) - if(playing) - StopPlaying() - user.visible_message("[user] has [anchored ? "un" : ""]secured \the [src].", "You [anchored ? "un" : ""]secure \the [src].") - anchored = !anchored - playsound(src.loc, 'sound/items/Ratchet.ogg', 50, 1) - power_change() - update_icon() - return - return ..() - /obj/machinery/media/jukebox/emag_act(var/remaining_charges, var/mob/user) if(!emagged) emagged = 1 @@ -239,37 +279,19 @@ datum/track/New(var/title_name, var/audio) return 1 /obj/machinery/media/jukebox/proc/StopPlaying() - var/area/main_area = get_area(src) - // Always kill the current sound - for(var/mob/living/M in mobs_in_area(main_area)) - M << sound(null, channel = 1) - - main_area.forced_ambience = null playing = 0 update_use_power(1) update_icon() - + start_stop_song() /obj/machinery/media/jukebox/proc/StartPlaying() - StopPlaying() if(!current_track) return - - var/area/main_area = get_area(src) - if(freq) - var/sound/new_song = sound(current_track.sound, channel = 1, repeat = 1, volume = 25) - new_song.frequency = freq - main_area.forced_ambience = list(new_song) - else - main_area.forced_ambience = list(current_track.sound) - - for(var/mob/living/M in mobs_in_area(main_area)) - if(M.mind) - main_area.play_ambience(M) - playing = 1 update_use_power(2) update_icon() + start_stop_song() + updateDialog() // Advance to the next track - Don't start playing it unless we were already playing /obj/machinery/media/jukebox/proc/NextTrack() @@ -278,7 +300,7 @@ datum/track/New(var/title_name, var/audio) var/newTrackIndex = (curTrackIndex % tracks.len) + 1 // Loop back around if past end current_track = tracks[newTrackIndex] if(playing) - StartPlaying() + start_stop_song() updateDialog() // Advance to the next track - Don't start playing it unless we were already playing @@ -288,5 +310,5 @@ datum/track/New(var/title_name, var/audio) var/newTrackIndex = curTrackIndex == 1 ? tracks.len : curTrackIndex - 1 current_track = tracks[newTrackIndex] if(playing) - StartPlaying() + start_stop_song() updateDialog() diff --git a/code/game/machinery/jukebox_vr.dm b/code/game/machinery/jukebox_vr.dm deleted file mode 100644 index 290bb9688a..0000000000 --- a/code/game/machinery/jukebox_vr.dm +++ /dev/null @@ -1,45 +0,0 @@ -// -// VOREStation Custom - Configurable Jukebox! -// - -/datum/track - var/secret = 0 // Whether or not this is a SECRET TRACK OOOOOH - -// On initialization, copy our tracks from the global list -/obj/machinery/media/jukebox/initialize() - ..() - if(all_jukebox_tracks.len) - tracks.Cut() - secret_tracks.Cut() - for(var/datum/track/T in all_jukebox_tracks) - if(T.secret) - secret_tracks += T - else - tracks += T - return - -// Global list holding all configured jukebox tracks -var/global/list/all_jukebox_tracks = list() - -// Read the jukebox configuration file on system startup. -/hook/startup/proc/load_jukebox_tracks() - var/jukebox_track_file = "config/jukebox.txt" - if(!fexists(jukebox_track_file)) - warning("File not found: [jukebox_track_file]") - return - // Helpful regex that ignores comments and parses our file format - var/regex/lineSplitter = regex("^(?!#)(.+?)\\|(.+?)\\|(.+)$") - var/list/Lines = file2list(jukebox_track_file) - for(var/t in Lines) - if(!t) continue - if(!lineSplitter.Find(t)) continue - var/file = trim(lineSplitter.group[1]) - var/title = trim(lineSplitter.group[2]) - var/isSecret = text2num(trim(lineSplitter.group[3])) - if(!fexists(file)) - warning("In [jukebox_track_file], sound file file not found: [file]") - continue - var/datum/track/T = new(title, file(file)) - T.secret = isSecret ? 1 : 0 - all_jukebox_tracks += T - return 1 diff --git a/code/modules/client/preference_setup/global/06_media.dm b/code/modules/client/preference_setup/global/06_media.dm new file mode 100644 index 0000000000..cd6a12a6f9 --- /dev/null +++ b/code/modules/client/preference_setup/global/06_media.dm @@ -0,0 +1,53 @@ +/datum/preferences + var/media_volume = 1 + var/media_player = 2 // 0 = VLC, 1 = WMP, 2 = HTML5, 3+ = unassigned + +/datum/category_item/player_setup_item/player_global/media + name = "Media" + sort_order = 6 + +/datum/category_item/player_setup_item/player_global/media/load_preferences(var/savefile/S) + S["media_volume"] >> pref.media_volume + S["media_player"] >> pref.media_player + +/datum/category_item/player_setup_item/player_global/media/save_preferences(var/savefile/S) + S["media_volume"] << pref.media_volume + S["media_player"] << pref.media_player + +/datum/category_item/player_setup_item/player_global/media/sanitize_preferences() + pref.media_volume = isnum(pref.media_volume) ? Clamp(pref.media_volume, 0, 1) : initial(pref.media_volume) + pref.media_player = sanitize_inlist(pref.media_player, list(0, 1, 2), initial(pref.media_player)) + +/datum/category_item/player_setup_item/player_global/media/content(var/mob/user) + . += "Jukebox Volume:" + . += "[round(pref.media_volume * 100)]%
" + . += "Media Player Type: Depending on you operating system, one of these might work better. " + . += "Use HTML5 if it works for you. If neither HTML5 nor WMP work, you'll have to fall back to using VLC, " + . += "but this requires you have the VLC client installed on your comptuer." + . += "Try the others if you want but you'll probably just get no music.
" + . += (pref.media_player == 2) ? "HTML5 " : "HTML5 " + . += (pref.media_player == 1) ? "WMP " : "WMP " + . += (pref.media_player == 0) ? "VLC " : "VLC " + . += "
" + +/datum/category_item/player_setup_item/player_global/media/OnTopic(var/href, var/list/href_list, var/mob/user) + if(href_list["change_media_volume"]) + if(CanUseTopic(user)) + var/value = input("Choose your Jukebox volume (0-100%)", "Jukebox volume", round(pref.media_volume * 100)) + if(isnum(value)) + value = Clamp(value, 0, 100) + pref.media_volume = value/100.0 + if(user.client && user.client.media) + user.client.media.update_volume(pref.media_volume) + return TOPIC_REFRESH + else if(href_list["set_media_player"]) + if(CanUseTopic(user)) + var/newval = sanitize_inlist(text2num(href_list["set_media_player"]), list(0, 1, 2), pref.media_player) + if(newval != pref.media_player) + pref.media_player = newval + if(user.client && user.client.media) + user.client.media.open() + spawn(10) + user.update_music() + return TOPIC_REFRESH + return ..() diff --git a/code/modules/client/preference_setup/global/setting_datums.dm b/code/modules/client/preference_setup/global/setting_datums.dm index 71559fed0e..675cbdf7a7 100644 --- a/code/modules/client/preference_setup/global/setting_datums.dm +++ b/code/modules/client/preference_setup/global/setting_datums.dm @@ -74,6 +74,16 @@ var/list/_client_preferences_by_type preference_mob << sound(null, repeat = 0, wait = 0, volume = 0, channel = 1) preference_mob << sound(null, repeat = 0, wait = 0, volume = 0, channel = 2) +/datum/client_preference/play_jukebox + description ="Play jukebox music" + key = "SOUND_JUKEBOX" + +/datum/client_preference/play_jukebox/toggled(var/mob/preference_mob, var/enabled) + if(!enabled) + preference_mob.stop_all_music() + else + preference_mob.update_music() + /datum/client_preference/ghost_ears description ="Ghost ears" key = "CHAT_GHOSTEARS" diff --git a/code/modules/media/media_machinery.dm b/code/modules/media/media_machinery.dm new file mode 100644 index 0000000000..42816ca51a --- /dev/null +++ b/code/modules/media/media_machinery.dm @@ -0,0 +1,74 @@ +// Machinery serving as a media source. +/obj/machinery/media + var/playing = 0 // Am I playing right now? + var/media_url = "" // URL of media I am playing + var/media_start_time = 0 // world.time when it started playing + var/volume = 1 // 0 - 1 for ease of coding. + + var/area/master_area // My area + + // ~Leshana - Transmitters unimplemented + +// Notify everyone in the area of new music. +// YOU MUST SET MEDIA_URL AND MEDIA_START_TIME YOURSELF! +/obj/machinery/media/proc/update_music() + update_media_source() + // Bail if we lost connection to master. + if(!master_area) + return + // Send update to clients. + for(var/mob/M in mobs_in_area(master_area)) + if(M && M.client) + M.update_music() + +/obj/machinery/media/proc/update_media_source() + var/area/A = get_area_master(src) + if(!A) + return + // Check if there's a media source already. + if(A.media_source && A.media_source != src) // If it does, the new media source replaces it. basically, the last media source arrived gets played on top. + A.media_source.disconnect_media_source() // You can turn a media source off and on for it to come back on top. + A.media_source = src + master_area = A + return + else + A.media_source = src + master_area = A + +/obj/machinery/media/proc/disconnect_media_source() + var/area/A = get_area_master(src) + // Sanity + if(!A) + master_area = null + return + // Check if there's a media source already. + if(A && A.media_source && A.media_source != src) + master_area = null + return + // Update Media Source. + A.media_source = null + // Clients + for(var/mob/M in mobs_in_area(A)) + if(M && M.client) + M.update_music() + master_area = null + +/obj/machinery/media/Move() + ..() + if(anchored) + update_music() + +/obj/machinery/media/forceMove(var/atom/destination) + disconnect_media_source() + ..() + if(anchored) + update_music() + +/obj/machinery/media/initialize() + ..() + update_media_source() + +/obj/machinery/media/Destroy() + disconnect_media_source() + ..() + diff --git a/code/modules/media/media_player_html5.dm b/code/modules/media/media_player_html5.dm new file mode 100644 index 0000000000..b5093db567 --- /dev/null +++ b/code/modules/media/media_player_html5.dm @@ -0,0 +1,27 @@ +// IT IS FINALLY TIME. IT IS HERE. Converted to HTML5