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. A subfolder called [code]characters[/code] holds the quiz's own characters ## instead of songs, see [member characters_path]. 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 subfolder with the quiz's own characters. Any case, e.g. "Characters", works. const CHARACTERS_FOLDER: String = "characters" ## 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 ## The folder's [constant CHARACTERS_FOLDER] subfolder when it has pictures, so a themed ## quiz can bring its own characters. Empty when there is none. var characters_path: String = "" # 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 = [] for subfolder: String in _visible(DirAccess.get_directories_at(folder.path)): if subfolder.to_lower() == CHARACTERS_FOLDER: var characters: String = folder.path.path_join(subfolder) if !FolderCharacters.picture_names(characters).is_empty(): folder.characters_path = characters else: subfolders.append(subfolder) 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 ## How far a prepared quiz has come, to continue it after a restart with ## [method restore_progress]. func progress() -> Dictionary: return {"play_order": play_order, "queue": Array(_quiz_queue), "next": _quiz_next} ## Continues a prepared quiz from [param saved], as given by [method progress]. Songs ## that are no longer in the folder are left out. Returns false, changing nothing, when ## [param saved] does not fit this folder. func restore_progress(saved: Dictionary) -> bool: if kind != Kind.PREPARED_QUIZ || !(saved.get("queue") is Array) \ || !(saved.get("next") is float || saved.get("next") is int): return false var queue: PackedStringArray = [] var next: int = int(saved.next) var known: Dictionary = {} for song_path: String in _quiz_songs: known[song_path] = true for i: int in saved.queue.size(): if known.has(str(saved.queue[i])): queue.append(str(saved.queue[i])) elif i < int(saved.next): next -= 1 play_order = int(saved.get("play_order", play_order)) as PlayOrder _quiz_queue = queue _quiz_next = clampi(next, 0, queue.size()) return true ## 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