Settings has a Music folder row. A folder with subfolders is played as soundtracks: Next picks a random subfolder and a random song in it, the subfolder is the answer's soundtrack and the file name without its type the song. The inspiration list and Search show the subfolders. A folder with songs and no subfolders is a prepared quiz, played once in order (natural sort) or at random, showing only the song title. It has no winning score: after the last song Next becomes Show winner, and the players with the most points win, ties included. While a folder is used nothing but the characters is asked from the server: Sync, QR, Statistics, the update check and play counting are off. Characters fall back to the cached pictures when the server can't be reached. mp3, ogg and wav play; other files and broken ones are skipped. The old local playback code, the hidden Open button and the hidden Local toggle are removed. Next now waits for the song to start before preparing the next one.
142 lines
4.8 KiB
GDScript
142 lines
4.8 KiB
GDScript
class_name MusicFolder
|
|
extends RefCounted
|
|
## A folder of songs to play instead of the MusicServer's, picked in the settings.
|
|
##
|
|
## A folder with subfolders is a soundtrack folder: each subfolder is a soundtrack, and
|
|
## songs are picked at random from a random soundtrack. Songs at the top are left out.
|
|
## A folder with songs and no subfolders is a prepared quiz: every song plays once, in
|
|
## order or at random, and only the song's name is the answer.
|
|
##
|
|
## Hidden files and folders, and files the game can't play (see [AudioFile]), are left
|
|
## out.
|
|
|
|
enum Kind { EMPTY, SOUNDTRACKS, PREPARED_QUIZ }
|
|
enum PlayOrder { IN_ORDER, RANDOM }
|
|
|
|
## How many files are tried before giving up when files can't be read.
|
|
const MAX_TRIES: int = 20
|
|
|
|
## The folder's path.
|
|
var path: String = ""
|
|
var kind: Kind = Kind.EMPTY
|
|
## How a prepared quiz is played. Change it with [method set_play_order].
|
|
var play_order: PlayOrder = PlayOrder.IN_ORDER
|
|
|
|
# Soundtrack folder: soundtrack name -> paths of its songs
|
|
var _soundtracks: Dictionary[String, PackedStringArray] = {}
|
|
# Prepared quiz: the songs in order, the order they are played in and how many are played
|
|
var _quiz_songs: PackedStringArray = []
|
|
var _quiz_queue: PackedStringArray = []
|
|
var _quiz_next: int = 0
|
|
|
|
|
|
## Reads the folder at [param folder_path]. Its [member kind] is [constant Kind.EMPTY]
|
|
## when there is nothing to play in it.
|
|
static func scan(folder_path: String) -> MusicFolder:
|
|
var folder: MusicFolder = MusicFolder.new()
|
|
folder.path = folder_path.trim_suffix("/")
|
|
var subfolders: PackedStringArray = _visible(DirAccess.get_directories_at(folder.path))
|
|
if !subfolders.is_empty():
|
|
for subfolder: String in subfolders:
|
|
var songs: PackedStringArray = _songs_in(folder.path.path_join(subfolder), true)
|
|
if !songs.is_empty():
|
|
folder._soundtracks[subfolder] = songs
|
|
if !folder._soundtracks.is_empty():
|
|
folder.kind = Kind.SOUNDTRACKS
|
|
else:
|
|
folder._quiz_songs = _songs_in(folder.path, false)
|
|
if !folder._quiz_songs.is_empty():
|
|
folder.kind = Kind.PREPARED_QUIZ
|
|
folder.restart()
|
|
return folder
|
|
|
|
|
|
## The folder's name, to show to the user.
|
|
func display_name() -> String:
|
|
return path.get_file()
|
|
|
|
|
|
## The names of the soundtracks, sorted. Empty for a prepared quiz.
|
|
func soundtrack_names() -> Array[String]:
|
|
var names: Array[String] = []
|
|
names.assign(_soundtracks.keys())
|
|
names.sort_custom(func(a: String, b: String) -> bool: return a.naturalnocasecmp_to(b) < 0)
|
|
return names
|
|
|
|
|
|
## How many songs a prepared quiz has.
|
|
func quiz_size() -> int:
|
|
return _quiz_songs.size()
|
|
|
|
|
|
## Plays a prepared quiz in [param order] from its first song.
|
|
func set_play_order(order: PlayOrder) -> void:
|
|
play_order = order
|
|
restart()
|
|
|
|
|
|
## Starts a prepared quiz over from its first song, shuffled again if it is played at
|
|
## random.
|
|
func restart() -> void:
|
|
_quiz_queue = _quiz_songs.duplicate()
|
|
if play_order == PlayOrder.RANDOM:
|
|
var shuffled: Array = Array(_quiz_queue)
|
|
shuffled.shuffle()
|
|
_quiz_queue = PackedStringArray(shuffled)
|
|
_quiz_next = 0
|
|
|
|
|
|
## The next song to play, or null when a prepared quiz has played every song or no file
|
|
## can be read.
|
|
func next_song() -> SongObject:
|
|
for i: int in MAX_TRIES:
|
|
var song_path: String = _pick()
|
|
if song_path.is_empty():
|
|
return null
|
|
if AudioFile.load(song_path) != null:
|
|
return _song_object(song_path)
|
|
GameLog.warning("Skipping a song that can't be played: ", song_path)
|
|
return null
|
|
|
|
|
|
func _pick() -> String:
|
|
if kind == Kind.SOUNDTRACKS:
|
|
var songs: PackedStringArray = _soundtracks[_soundtracks.keys().pick_random()]
|
|
return songs[randi() % songs.size()]
|
|
if _quiz_next >= _quiz_queue.size():
|
|
return ""
|
|
_quiz_next += 1
|
|
return _quiz_queue[_quiz_next - 1]
|
|
|
|
|
|
func _song_object(song_path: String) -> SongObject:
|
|
var song_object: SongObject = SongObject.new()
|
|
song_object.song_path = song_path
|
|
song_object.song_title = song_path.get_file().get_basename()
|
|
if kind == Kind.SOUNDTRACKS:
|
|
# The soundtrack is the subfolder right under the music folder
|
|
song_object.soundtrack_title = song_path.trim_prefix(path + "/").get_slice("/", 0)
|
|
return song_object
|
|
|
|
|
|
# The playable songs in [param dir], in natural order, also in subfolders if [param deep]
|
|
static func _songs_in(dir: String, deep: bool) -> PackedStringArray:
|
|
var songs: Array[String] = []
|
|
for file_name: String in _visible(DirAccess.get_files_at(dir)):
|
|
if AudioFile.is_playable(file_name):
|
|
songs.append(dir.path_join(file_name))
|
|
songs.sort_custom(func(a: String, b: String) -> bool:
|
|
return a.get_file().naturalnocasecmp_to(b.get_file()) < 0)
|
|
if deep:
|
|
for subfolder: String in _visible(DirAccess.get_directories_at(dir)):
|
|
songs.append_array(_songs_in(dir.path_join(subfolder), true))
|
|
return PackedStringArray(songs)
|
|
|
|
|
|
static func _visible(names: PackedStringArray) -> PackedStringArray:
|
|
var shown: PackedStringArray = []
|
|
for entry: String in names:
|
|
if !entry.begins_with("."):
|
|
shown.append(entry)
|
|
return shown
|