mirror of
https://github.com/VOREStation/VOREStation.git
synced 2026-08-26 21:49:11 +01:00
Media tracks subsystem, jukebox UI genres
This commit is contained in:
@@ -54,6 +54,7 @@ var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_G
|
||||
// The numbers just define the ordering, they are meaningless otherwise.
|
||||
#define INIT_ORDER_WEBHOOKS 50
|
||||
#define INIT_ORDER_SQLITE 40
|
||||
#define INIT_ORDER_MEDIA_TRACKS 38 // Gotta get that lobby music up, yo
|
||||
#define INIT_ORDER_CHEMISTRY 35
|
||||
#define INIT_ORDER_SKYBOX 30
|
||||
#define INIT_ORDER_MAPPING 25
|
||||
|
||||
@@ -84,3 +84,7 @@
|
||||
|
||||
/proc/cmp_text_asc(a,b)
|
||||
return sorttext(b,a)
|
||||
|
||||
/proc/cmp_media_track_asc(datum/track/A, datum/track/B)
|
||||
var/genre_sort = sorttext(B.genre || "Uncategorized", A.genre || "Uncategorized")
|
||||
return genre_sort || sorttext(B.title, A.title)
|
||||
|
||||
@@ -294,6 +294,8 @@ var/list/gamemode_cache = list()
|
||||
|
||||
var/disable_webhook_embeds = FALSE
|
||||
|
||||
var/static/list/jukebox_track_files
|
||||
|
||||
/datum/configuration/New()
|
||||
var/list/L = typesof(/datum/game_mode) - /datum/game_mode
|
||||
for (var/T in L)
|
||||
@@ -939,6 +941,9 @@ var/list/gamemode_cache = list()
|
||||
|
||||
if("enable_night_shifts")
|
||||
config.enable_night_shifts = TRUE
|
||||
|
||||
if("jukebox_track_files")
|
||||
config.jukebox_track_files = splittext(value, ";")
|
||||
|
||||
// VOREStation Edit Start - Can't be in _vr file because it is loaded too late.
|
||||
if("vgs_access_identifier")
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
SUBSYSTEM_DEF(media_tracks)
|
||||
name = "Media Tracks"
|
||||
flags = SS_NO_FIRE
|
||||
init_order = INIT_ORDER_MEDIA_TRACKS
|
||||
|
||||
/// Every track, including secret
|
||||
var/list/all_tracks = list()
|
||||
/// Non-secret jukebox tracks
|
||||
var/list/jukebox_tracks = list()
|
||||
/// Lobby music tracks
|
||||
var/list/lobby_tracks = list()
|
||||
|
||||
/datum/controller/subsystem/media_tracks/Initialize()
|
||||
load_tracks()
|
||||
sort_tracks()
|
||||
|
||||
/datum/controller/subsystem/media_tracks/proc/load_tracks()
|
||||
for(var/filename in config.jukebox_track_files)
|
||||
report_progress("Loading jukebox track: [filename]")
|
||||
|
||||
if(!fexists(filename))
|
||||
error("File not found: [filename]")
|
||||
continue
|
||||
|
||||
var/list/jsonData = json_decode(file2text(filename))
|
||||
|
||||
if(!istype(jsonData))
|
||||
error("Failed to read tracks from [filename], json_decode failed.")
|
||||
continue
|
||||
|
||||
for(var/entry in jsonData)
|
||||
|
||||
// Critical problems that will prevent the track from working
|
||||
if(!istext(entry["url"]))
|
||||
error("Jukebox entry in [filename]: bad or missing 'url'. Tracks must have a URL.")
|
||||
continue
|
||||
if(!istext(entry["title"]))
|
||||
error("Jukebox entry in [filename]: bad or missing 'title'. Tracks must have a title.")
|
||||
continue
|
||||
if(!isnum(entry["duration"]))
|
||||
error("Jukebox entry in [filename]: bad or missing 'duration'. Tracks must have a duration (in deciseconds).")
|
||||
continue
|
||||
|
||||
// Noncritical problems, we can keep going anyway, but warn so it can be fixed
|
||||
if(!istext(entry["artist"]))
|
||||
warning("Jukebox entry in [filename], [entry["title"]]: bad or missing 'artist'. Please consider crediting the artist.")
|
||||
if(!istext(entry["genre"]))
|
||||
warning("Jukebox entry in [filename], [entry["title"]]: bad or missing 'genre'. Please consider adding a genre.")
|
||||
|
||||
var/datum/track/T = new(entry["url"], entry["title"], entry["duration"], entry["artist"], entry["genre"])
|
||||
|
||||
T.secret = entry["secret"] ? 1 : 0
|
||||
T.lobby = entry["lobby"] ? 1 : 0
|
||||
|
||||
all_tracks += T
|
||||
|
||||
/datum/controller/subsystem/media_tracks/proc/sort_tracks()
|
||||
report_progress("Sorting media tracks...")
|
||||
sortTim(all_tracks, /proc/cmp_media_track_asc)
|
||||
|
||||
jukebox_tracks.Cut()
|
||||
|
||||
for(var/datum/track/T in all_tracks)
|
||||
if(!T.secret)
|
||||
jukebox_tracks += T
|
||||
if(T.lobby)
|
||||
lobby_tracks += T
|
||||
@@ -33,26 +33,6 @@
|
||||
var/list/queue = list()
|
||||
//VOREStation Add End
|
||||
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'),
|
||||
new/datum/track("Stellar Transit", 'sound/ambience/space/space_serithi.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')
|
||||
)
|
||||
|
||||
/obj/machinery/media/jukebox/Initialize()
|
||||
. = ..()
|
||||
@@ -68,16 +48,13 @@
|
||||
// On initialization, copy our tracks from the global list
|
||||
/obj/machinery/media/jukebox/Initialize()
|
||||
. = ..()
|
||||
if(LAZYLEN(all_jukebox_tracks)) //Global list has tracks
|
||||
tracks.Cut()
|
||||
secret_tracks.Cut()
|
||||
for(var/datum/track/T in all_jukebox_tracks) //Load them
|
||||
if(T.secret)
|
||||
secret_tracks |= T
|
||||
else
|
||||
tracks |= T
|
||||
else if(!LAZYLEN(tracks)) //We don't even have default tracks
|
||||
stat |= BROKEN // No tracks configured this round!
|
||||
if(!LAZYLEN(getTracksList()))
|
||||
stat |= BROKEN
|
||||
|
||||
/obj/machinery/media/jukebox/proc/getTracksList()
|
||||
PRIVATE_PROC(TRUE)
|
||||
|
||||
return hacked ? SSmedia_tracks.all_tracks : SSmedia_tracks.jukebox_tracks
|
||||
|
||||
/obj/machinery/media/jukebox/process()
|
||||
if(!playing)
|
||||
@@ -95,6 +72,7 @@
|
||||
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
|
||||
var/list/tracks = getTracksList()
|
||||
switch(loop_mode)
|
||||
if(JUKEMODE_NEXT)
|
||||
var/curTrackIndex = max(1, tracks.Find(current_track))
|
||||
@@ -119,19 +97,16 @@
|
||||
if(current_track && playing)
|
||||
media_url = current_track.url
|
||||
media_start_time = world.time
|
||||
visible_message("<span class='notice'>\The [src] begins to play [current_track.display()].</span>")
|
||||
audible_message("<span class='notice'>\The [src] begins to play [current_track.display()].</span>", runemessage = "[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
|
||||
if(hacked == newhacked)
|
||||
return
|
||||
hacked = newhacked
|
||||
if (hacked)
|
||||
tracks.Add(secret_tracks)
|
||||
else
|
||||
tracks.Remove(secret_tracks)
|
||||
updateDialog()
|
||||
|
||||
/obj/machinery/media/jukebox/attackby(obj/item/W as obj, mob/user as mob)
|
||||
@@ -217,13 +192,15 @@
|
||||
data["volume"] = volume
|
||||
data["current_track_ref"] = null
|
||||
data["current_track"] = null
|
||||
data["current_genre"] = null
|
||||
if(current_track)
|
||||
data["current_track_ref"] = "\ref[current_track]" // Convenient shortcut
|
||||
data["current_track"] = current_track.toTguiList()
|
||||
data["current_genre"] = current_track.genre
|
||||
data["percent"] = playing ? min(100, round(world.time - media_start_time) / current_track.duration) : 0;
|
||||
|
||||
var/list/tgui_tracks = list()
|
||||
for(var/datum/track/T in tracks)
|
||||
for(var/datum/track/T in getTracksList())
|
||||
tgui_tracks.Add(list(T.toTguiList()))
|
||||
data["tracks"] = tgui_tracks
|
||||
|
||||
@@ -235,7 +212,7 @@
|
||||
|
||||
switch(action)
|
||||
if("change_track")
|
||||
var/datum/track/T = locate(params["change_track"]) in tracks
|
||||
var/datum/track/T = locate(params["change_track"]) in getTracksList()
|
||||
if(istype(T))
|
||||
current_track = T
|
||||
StartPlaying()
|
||||
@@ -336,6 +313,7 @@
|
||||
|
||||
// Advance to the next track - Don't start playing it unless we were already playing
|
||||
/obj/machinery/media/jukebox/proc/NextTrack()
|
||||
var/list/tracks = getTracksList()
|
||||
if(!tracks.len) return
|
||||
var/curTrackIndex = max(1, tracks.Find(current_track))
|
||||
var/newTrackIndex = (curTrackIndex % tracks.len) + 1 // Loop back around if past end
|
||||
@@ -346,6 +324,7 @@
|
||||
|
||||
// Advance to the next track - Don't start playing it unless we were already playing
|
||||
/obj/machinery/media/jukebox/proc/PrevTrack()
|
||||
var/list/tracks = getTracksList()
|
||||
if(!tracks.len) return
|
||||
var/curTrackIndex = max(1, tracks.Find(current_track))
|
||||
var/newTrackIndex = curTrackIndex == 1 ? tracks.len : curTrackIndex - 1
|
||||
|
||||
+2
-2
@@ -122,9 +122,9 @@
|
||||
return rand(32000, 55000) //Frequency stuff only works with 45kbps oggs.
|
||||
|
||||
/client/proc/playtitlemusic()
|
||||
if(!ticker || !all_lobby_tracks.len || !media) return
|
||||
if(!ticker || !SSmedia_tracks.lobby_tracks.len || !media) return
|
||||
if(is_preference_enabled(/datum/client_preference/play_lobby_music))
|
||||
var/datum/track/T = pick(all_lobby_tracks)
|
||||
var/datum/track/T = pick(SSmedia_tracks.lobby_tracks)
|
||||
media.push_music(T.url, world.time, 0.85)
|
||||
to_chat(src,"<span class='notice'>Lobby music: <b>[T.title]</b> by <b>[T.artist]</b>.</span>")
|
||||
|
||||
|
||||
@@ -6,15 +6,17 @@
|
||||
/datum/track
|
||||
var/url // URL to load song from
|
||||
var/title // Song title
|
||||
var/artist // Song's creator
|
||||
var/duration // Song length in deciseconds
|
||||
var/artist // Song's creator
|
||||
var/genre // Musical genre
|
||||
var/secret // Show up in regular playlist or secret playlist?
|
||||
var/lobby // Be one of the choices for lobby music?
|
||||
|
||||
/datum/track/New(var/url, var/title, var/duration, var/artist = "", var/secret = 0, var/lobby = 0)
|
||||
/datum/track/New(var/url, var/title, var/duration, var/artist = "", var/genre = "", var/secret = 0, var/lobby = 0)
|
||||
src.url = url
|
||||
src.title = title
|
||||
src.artist = artist
|
||||
src.genre = genre
|
||||
src.duration = duration
|
||||
src.secret = secret
|
||||
src.lobby = lobby
|
||||
@@ -26,53 +28,4 @@
|
||||
return str
|
||||
|
||||
/datum/track/proc/toTguiList()
|
||||
return list("ref" = "\ref[src]", "title" = title, "artist" = artist, "duration" = duration)
|
||||
|
||||
|
||||
// Global list holding all configured jukebox tracks
|
||||
var/global/list/all_jukebox_tracks = list()
|
||||
var/global/list/all_lobby_tracks = list()
|
||||
|
||||
// Read the jukebox configuration file on system startup.
|
||||
/hook/startup/proc/load_jukebox_tracks()
|
||||
var/jukebox_track_file = "config/jukebox.json"
|
||||
var/jukebox_track_file_private = "config/jukebox_private.json" // Uncommitted
|
||||
|
||||
if(!fexists(jukebox_track_file))
|
||||
warning("File not found: [jukebox_track_file]")
|
||||
return 1
|
||||
|
||||
var/list/jsonData = json_decode(file2text(jukebox_track_file))
|
||||
|
||||
if(!istype(jsonData))
|
||||
warning("Failed to read tracks from [jukebox_track_file], json_decode failed.")
|
||||
return 1
|
||||
|
||||
// Optional
|
||||
if(fexists(jukebox_track_file_private))
|
||||
var/list/jsonData_private = json_decode(file2text(jukebox_track_file_private))
|
||||
if(istype(jsonData_private))
|
||||
jsonData += jsonData_private // Pack more tracks in
|
||||
else
|
||||
warning("Failed to read tracks from [jukebox_track_file_private], json_decode failed.")
|
||||
//return 1 // Not worth failing over, as this file is optional
|
||||
|
||||
for(var/entry in jsonData)
|
||||
if(!istext(entry["url"]))
|
||||
warning("Jukebox entry [entry]: bad or missing 'url'")
|
||||
continue
|
||||
if(!istext(entry["title"]))
|
||||
warning("Jukebox entry [entry]: bad or missing 'title'")
|
||||
continue
|
||||
if(!isnum(entry["duration"]))
|
||||
warning("Jukebox entry [entry]: bad or missing 'duration'")
|
||||
continue
|
||||
var/datum/track/T = new(entry["url"], entry["title"], entry["duration"])
|
||||
if(istext(entry["artist"]))
|
||||
T.artist = entry["artist"]
|
||||
T.secret = entry["secret"] ? 1 : 0
|
||||
T.lobby = entry["lobby"] ? 1 : 0
|
||||
all_jukebox_tracks += T
|
||||
if(T.lobby)
|
||||
all_lobby_tracks += T
|
||||
return 1
|
||||
return list("ref" = "\ref[src]", "title" = title, "artist" = artist, "genre" = genre, "duration" = duration)
|
||||
|
||||
@@ -579,3 +579,10 @@ LOADOUT_WHITELIST 1
|
||||
|
||||
# Allowed time from death to defib in minutes
|
||||
DEFIB_TIMER 60
|
||||
|
||||
# Jukebox track files to load, JSON format. Keys include:
|
||||
# REQUIRED: url (str), title (str), duration (num, in DS)
|
||||
# SUGGESTED: artist (str), genre (str)
|
||||
# OPTIONAL: secret (bool), lobby (bool)
|
||||
#JUKEBOX_TRACK_FILES config/jukebox.json;config/jukebox_private.json
|
||||
JUKEBOX_TRACK_FILES config/jukebox.json
|
||||
@@ -17,6 +17,7 @@ export class Collapsible extends Component {
|
||||
const {
|
||||
children,
|
||||
color = 'default',
|
||||
child_mt = 1,
|
||||
title,
|
||||
buttons,
|
||||
...rest
|
||||
@@ -41,7 +42,7 @@ export class Collapsible extends Component {
|
||||
)}
|
||||
</div>
|
||||
{open && (
|
||||
<Box mt={1}>
|
||||
<Box mt={child_mt}>
|
||||
{children}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { round } from 'common/math';
|
||||
import { sortBy } from 'common/collections';
|
||||
import { useBackend } from "../backend";
|
||||
import { Box, Button, LabeledList, ProgressBar, Section, Slider } from "../components";
|
||||
import { Box, Button, Collapsible, LabeledList, ProgressBar, Section, Slider } from "../components";
|
||||
import { Window } from "../layouts";
|
||||
|
||||
export const Jukebox = (props, context) => {
|
||||
@@ -13,10 +12,22 @@ export const Jukebox = (props, context) => {
|
||||
volume,
|
||||
current_track_ref,
|
||||
current_track,
|
||||
current_genre,
|
||||
percent,
|
||||
tracks,
|
||||
} = data;
|
||||
|
||||
let genre_songs = tracks.length && tracks.reduce((acc, obj) => {
|
||||
let key = obj.genre || "Uncategorized";
|
||||
if (!acc[key]) {
|
||||
acc[key] = [];
|
||||
}
|
||||
acc[key].push(obj);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
let true_genre = playing && (current_genre || "Uncategorized");
|
||||
|
||||
return (
|
||||
<Window width={450} height={600} resizable>
|
||||
<Window.Content scrollable>
|
||||
@@ -96,15 +107,25 @@ export const Jukebox = (props, context) => {
|
||||
</LabeledList>
|
||||
</Section>
|
||||
<Section title="Available Tracks">
|
||||
{tracks.length && sortBy(a => a.title)(tracks).map(track => (
|
||||
<Button
|
||||
fluid
|
||||
icon="play"
|
||||
key={track.ref}
|
||||
selected={current_track_ref === track.ref}
|
||||
onClick={() => act("change_track", { change_track: track.ref })}>
|
||||
{track.title}
|
||||
</Button>
|
||||
{tracks.length && Object.keys(genre_songs).sort().map(genre => (
|
||||
<Collapsible
|
||||
title={genre}
|
||||
key={genre}
|
||||
color={true_genre === genre ? "green" : "default"}
|
||||
child_mt={0}>
|
||||
<div style={{ "margin-left": "1em" }}>
|
||||
{genre_songs[genre].map(track => (
|
||||
<Button
|
||||
fluid
|
||||
icon="play"
|
||||
key={track.ref}
|
||||
selected={current_track_ref === track.ref}
|
||||
onClick={() => act("change_track", { change_track: track.ref })}>
|
||||
{track.title}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Collapsible>
|
||||
)) || (
|
||||
<Box color="bad">
|
||||
Error: No songs loaded.
|
||||
@@ -114,4 +135,4 @@ export const Jukebox = (props, context) => {
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -272,6 +272,7 @@
|
||||
#include "code\controllers\subsystems\lighting.dm"
|
||||
#include "code\controllers\subsystems\machines.dm"
|
||||
#include "code\controllers\subsystems\mapping.dm"
|
||||
#include "code\controllers\subsystems\media_tracks.dm"
|
||||
#include "code\controllers\subsystems\mobs.dm"
|
||||
#include "code\controllers\subsystems\nightshift.dm"
|
||||
#include "code\controllers\subsystems\open_space.dm"
|
||||
|
||||
Reference in New Issue
Block a user