Short songs (#30): a quiz settings switch and a Seconds per song row (30 by default, 10-120 in steps of 5), saved with the quiz settings. Only part of each quiz song plays, from a random point 10-60% into it, picked once per song so restart and repeat play the same part. The progress bar, time and seeking stay within the part, a hidden beginning counts inside it, and its end counts as the song ending (repeat, stop or go to the next song as set). Random songs (#51): the low-played switch in Settings is now a Normal / Low played / Classic choice, stored as Settings.random_mode and not saved. fetch_random_song(mode) picks /music/rand, /music/rand/low or /music/rand/classic. Closes #30 Closes #51 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
184 lines
6.9 KiB
GDScript
184 lines
6.9 KiB
GDScript
class_name SongLoader
|
|
extends Node
|
|
## Downloads songs from the server into the Playlist ahead of time, so the next song
|
|
## can start at once. With a music folder (see [method Settings.is_offline]) the songs are
|
|
## picked from the folder instead.
|
|
##
|
|
## How many songs it keeps ready is the cache size in the settings
|
|
## ([code]Playlist.number_of_tracks_to_preload[/code]).
|
|
|
|
## Emitted when the songs already in the server playlist have been loaded into the
|
|
## Playlist.
|
|
|
|
signal song_list_loaded
|
|
|
|
## Emitted when a new song has been downloaded and added to the Playlist.
|
|
signal song_added
|
|
|
|
## Emitted when the songs of a saved quiz could not be continued, because the server's
|
|
## playlist no longer has them, e.g. after a sync. The quiz starts over.
|
|
signal saved_songs_dropped
|
|
|
|
## Seconds to wait before retrying a download step that failed.
|
|
var retry_seconds: float = 5.0
|
|
|
|
|
|
## Loads the songs already in the server playlist, then starts preloading new ones.
|
|
## Preloading waits for the list, because loading the list clears the playlist.
|
|
##
|
|
## With [param saved], the songs of a saved quiz are used where they match the server's
|
|
## playlist, so who got points and what was answered or downloaded ahead is kept. When
|
|
## the server's playlist no longer has them, they are dropped and
|
|
## [signal saved_songs_dropped] is emitted.
|
|
func load_song_list(saved: QuizSave = null) -> void:
|
|
GameLog.debug("load_song_list")
|
|
if Settings.is_offline():
|
|
_start_folder_playlist(saved)
|
|
return
|
|
var song_list_fetched: Callable = func song_list_fetched(songs: Array[SongInfo]) -> void:
|
|
if Settings.is_offline():
|
|
return
|
|
Playlist.clear_song_list()
|
|
var continued: int = 0
|
|
if saved != null && !saved.songs.is_empty():
|
|
if saved_songs_match(saved.songs, songs):
|
|
continued = saved.songs.size()
|
|
_continue_songs(saved)
|
|
else:
|
|
GameLog.warning("The server's playlist changed, so the saved quiz starts over")
|
|
saved_songs_dropped.emit()
|
|
for info: SongInfo in songs.slice(continued):
|
|
var new_song_object: SongObject = SongObject.new()
|
|
new_song_object.song_number = info.song_no
|
|
new_song_object.soundtrack_title = info.soundtrack
|
|
new_song_object.song_title = info.song
|
|
new_song_object.is_answered = true
|
|
new_song_object.has_played = true
|
|
Playlist.add_to_playlist(new_song_object)
|
|
if continued == 0:
|
|
Playlist.update_last_played_song()
|
|
# Songs already on the server were played before, so the game starts after them
|
|
Playlist.game_start_song = Playlist.song_object_array.size()
|
|
song_list_loaded.emit()
|
|
# Songs downloaded ahead before a restart already fill some of the cache
|
|
fill_cache()
|
|
MusicServerClient.fetch_song_list(song_list_fetched,
|
|
_retry_later.bind(load_song_list.bind(saved)))
|
|
|
|
|
|
## Whether [param saved_songs] are the first songs of the server's playlist [param songs]:
|
|
## the same numbers, soundtracks and titles in the same order.
|
|
static func saved_songs_match(saved_songs: Array[SongObject], songs: Array[SongInfo]) -> bool:
|
|
if saved_songs.size() > songs.size():
|
|
return false
|
|
for i: int in saved_songs.size():
|
|
if saved_songs[i].song_number != songs[i].song_no \
|
|
|| saved_songs[i].soundtrack_title != songs[i].soundtrack \
|
|
|| saved_songs[i].song_title != songs[i].song:
|
|
return false
|
|
return true
|
|
|
|
|
|
# Puts the saved songs in the playlist, where the quiz was
|
|
func _continue_songs(saved: QuizSave) -> void:
|
|
for song_object: SongObject in saved.songs:
|
|
Playlist.add_to_playlist(song_object)
|
|
Playlist.currently_playing_song = saved.currently_playing_song
|
|
Playlist.latest_played_song = saved.latest_played_song
|
|
Playlist.game_start_song = saved.game_start_song
|
|
Playlist.first_song_played = saved.first_song_played
|
|
|
|
|
|
## Starts downloading one more song, which keeps downloading until the cache is full.
|
|
func fetch_song() -> void:
|
|
GameLog.debug("fetch_song")
|
|
if Settings.is_offline():
|
|
_add_folder_songs()
|
|
return
|
|
Playlist.is_song_downloading = true
|
|
_download_random_song(SongObject.new())
|
|
|
|
|
|
## Starts downloading a song unless one is already downloading.
|
|
func fetch_song_if_idle() -> void:
|
|
if !Playlist.is_song_downloading:
|
|
fetch_song()
|
|
|
|
|
|
## Starts downloading if nothing is downloading and the cache is not full, e.g. after
|
|
## the cache size was raised.
|
|
func fill_cache() -> void:
|
|
if !Playlist.is_song_downloading && Playlist.should_preload_more_songs():
|
|
fetch_song()
|
|
|
|
|
|
# Fetching a song takes three requests. If one fails, only that step is retried, so
|
|
# the server and the game keep the same songs in their playlists.
|
|
func _download_random_song(new_song_object: SongObject) -> void:
|
|
var song_downloaded: Callable = func song_downloaded(body: PackedByteArray) -> void:
|
|
GameLog.debug("song_downloaded")
|
|
# A music folder was picked while the song was downloading
|
|
if Settings.is_offline():
|
|
return
|
|
Playlist.store_song(new_song_object, body)
|
|
_add_song_to_queue(new_song_object)
|
|
MusicServerClient.fetch_random_song(Settings.random_mode, song_downloaded,
|
|
_retry_later.bind(_download_random_song.bind(new_song_object)))
|
|
|
|
|
|
func _add_song_to_queue(new_song_object: SongObject) -> void:
|
|
MusicServerClient.add_to_queue(_fetch_song_info.bind(new_song_object),
|
|
_retry_later.bind(_add_song_to_queue.bind(new_song_object)))
|
|
|
|
|
|
func _fetch_song_info(new_song_object: SongObject) -> void:
|
|
var song_info_fetched: Callable = func song_info_fetched(info: SongInfo) -> void:
|
|
GameLog.debug("song_info_fetched")
|
|
if Settings.is_offline():
|
|
return
|
|
new_song_object.song_number = info.song_no
|
|
new_song_object.soundtrack_title = info.soundtrack
|
|
new_song_object.song_title = info.song
|
|
GameLog.debug("new_song_object: ", new_song_object.to_string())
|
|
Playlist.add_to_playlist(new_song_object)
|
|
if Playlist.should_preload_more_songs():
|
|
fetch_song()
|
|
else:
|
|
Playlist.is_song_downloading = false
|
|
song_added.emit()
|
|
MusicServerClient.fetch_song_info(song_info_fetched,
|
|
_retry_later.bind(_fetch_song_info.bind(new_song_object)))
|
|
|
|
|
|
func _retry_later(step: Callable) -> void:
|
|
GameLog.warning("Song download failed, retrying in %s seconds" % retry_seconds)
|
|
get_tree().create_timer(retry_seconds).timeout.connect(func() -> void:
|
|
if !Settings.is_offline():
|
|
step.call())
|
|
|
|
|
|
# A music folder has no playlist on a server, so the game starts with an empty one, or
|
|
# with the saved quiz's songs
|
|
func _start_folder_playlist(saved: QuizSave = null) -> void:
|
|
Playlist.clear_song_list()
|
|
Playlist.update_last_played_song()
|
|
Playlist.game_start_song = 0
|
|
if saved != null && !saved.songs.is_empty():
|
|
_continue_songs(saved)
|
|
Playlist.is_song_downloading = false
|
|
song_list_loaded.emit()
|
|
fill_cache()
|
|
|
|
|
|
# Picks songs from the music folder until enough are ready. A prepared quiz runs out
|
|
# when every song is in the playlist.
|
|
func _add_folder_songs() -> void:
|
|
Playlist.is_song_downloading = false
|
|
while Playlist.should_preload_more_songs():
|
|
var song_object: SongObject = Settings.music_folder.next_song()
|
|
if song_object == null:
|
|
return
|
|
song_object.song_number = Playlist.song_object_array.size()
|
|
Playlist.add_to_playlist(song_object)
|
|
song_added.emit()
|