Cover menu in the soundtrack popup ("Change cover"):
- Choose picture uploads a JPEG, PNG or WebP picture (up to 10 MB).
- Pick by ID uses a game's cover on SteamGridDB or an anime's on AniList.
- Remove cover deletes it, so the server searches for a new one.
- The server's reason is shown when it refuses a change.
After a change the new cover time is fetched, and the popup, the
statistics card and the quiz show the new cover.
Quiz: once the answer is shown, the soundtrack's cover shows to the left of
the titles. Cover and titles are centered together, left and right and up
and down, and each title keeps to two lines. Cover times come from the
soundtrack list, and songs keep their soundtrack's UUID, also in saves.
Requests can now send any body with its content type, and every 2xx answer
counts as success. Cover frames are 2:3 like the server's covers. Small
copies of real covers are used by the tests and screenshots.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
193 lines
6.0 KiB
GDScript
193 lines
6.0 KiB
GDScript
extends Node
|
|
## The songs of the current quiz, in the order they were added, and which of them are
|
|
## played, answered and playing.
|
|
##
|
|
## Registered as the [code]Playlist[/code] autoload.
|
|
##
|
|
## Downloaded songs are kept on disk in the [code]songs[/code] folder next to the
|
|
## settings, not in memory. The folder only holds the current playlist: when the game
|
|
## starts, songs the saved quiz does not use are deleted (see [QuizSave]), and the folder
|
|
## is emptied when the playlist is reset.
|
|
|
|
const SONGS_FOLDER_NAME: String = "songs"
|
|
|
|
var song_object_array: Array[SongObject] = []
|
|
var latest_played_song: int = 0
|
|
var currently_playing_song: int = -1
|
|
var first_song_downloaded: bool = false
|
|
var first_song_played: bool = false
|
|
var number_of_tracks_to_preload: int = 1
|
|
var is_song_downloading: bool = false
|
|
## The first song of the current game. Songs before it belong to earlier games, which
|
|
## ended with Reset points.
|
|
var game_start_song: int = 0
|
|
|
|
# How many song files have been written, so each gets a name of its own
|
|
var _song_files_written: int = 0
|
|
|
|
|
|
## The folder downloaded songs are kept in.
|
|
static func songs_folder() -> String:
|
|
return Settings.config_dir().path_join(SONGS_FOLDER_NAME)
|
|
|
|
|
|
func get_next_song() -> SongObject:
|
|
return song_object_array[latest_played_song]
|
|
|
|
|
|
func set_latest_played_song() -> void:
|
|
latest_played_song += 1
|
|
if latest_played_song > song_object_array.size() - 1:
|
|
latest_played_song = song_object_array.size() - 1
|
|
|
|
|
|
func update_last_played_song() -> void:
|
|
latest_played_song = song_object_array.size()
|
|
|
|
|
|
func get_preloaded_songs() -> int:
|
|
var number_of_tracks_preloaded: int = 0
|
|
for song: SongObject in song_object_array:
|
|
if !song.has_played:
|
|
number_of_tracks_preloaded += 1
|
|
return number_of_tracks_preloaded
|
|
|
|
|
|
func should_preload_more_songs() -> bool:
|
|
return number_of_tracks_to_preload > get_preloaded_songs()
|
|
|
|
|
|
func song_is_answered(song_no: int = currently_playing_song) -> void:
|
|
# Nothing is playing yet (-1); a negative index would answer the last, unplayed song
|
|
if song_no < 0 || song_no >= song_object_array.size():
|
|
return
|
|
GameLog.debug("show_answer_pressed song_object: ", song_object_array[song_no]._to_string())
|
|
song_object_array[song_no].is_answered = true
|
|
if Settings.add_to_stats && !Settings.is_offline():
|
|
GameLog.debug("add to stats: ", song_no)
|
|
MusicServerClient.mark_song_played(song_no)
|
|
|
|
|
|
func add_to_playlist(new_song_object: SongObject) -> void:
|
|
song_object_array.append(new_song_object)
|
|
|
|
|
|
func get_playlist() -> Array[SongObject]:
|
|
if Settings.is_debug:
|
|
return song_object_array
|
|
else:
|
|
var temp_list: Array[SongObject] = []
|
|
for s: SongObject in song_object_array:
|
|
if s.has_played:
|
|
temp_list.append(s)
|
|
return temp_list
|
|
|
|
|
|
func reset_playlist() -> void:
|
|
currently_playing_song = -1
|
|
latest_played_song = 0
|
|
game_start_song = 0
|
|
clear_song_list()
|
|
clear_song_files()
|
|
|
|
|
|
func clear_song_list() -> void:
|
|
song_object_array.clear()
|
|
|
|
|
|
func unset_is_playing() -> void:
|
|
for song_object: SongObject in song_object_array:
|
|
song_object.is_playing = false
|
|
|
|
|
|
func set_currently_playing_song(value: int) -> void:
|
|
currently_playing_song = value
|
|
|
|
|
|
func get_current_soundtrack_title() -> String:
|
|
return song_object_array[currently_playing_song].get_soundtrack_title()
|
|
|
|
|
|
func get_current_soundtrack_uuid() -> String:
|
|
return song_object_array[currently_playing_song].get_soundtrack_uuid()
|
|
|
|
|
|
func get_current_song_title() -> String:
|
|
return song_object_array[currently_playing_song].get_song_title()
|
|
|
|
|
|
func add_point(player_id: String) -> void:
|
|
song_object_array[currently_playing_song].add_point(player_id)
|
|
|
|
|
|
func remove_point(player_id: String) -> void:
|
|
song_object_array[currently_playing_song].remove_point(player_id)
|
|
|
|
|
|
## Removes every player's points from every song, for a new game.
|
|
func clear_points() -> void:
|
|
for song_object: SongObject in song_object_array:
|
|
song_object.players_given_point.clear()
|
|
|
|
|
|
## Removes a player's points from every song.
|
|
func remove_player_from_list(player_id: String) -> void:
|
|
for song_object: SongObject in song_object_array:
|
|
song_object.remove_point(player_id)
|
|
|
|
|
|
## The song of [param song_no], read from its file. Returns null if the file is gone, so
|
|
## the song can be downloaded again.
|
|
func get_song(song_no: int) -> AudioStream:
|
|
var song_object: SongObject = song_object_array[song_no]
|
|
if song_object.song_path.is_empty():
|
|
return song_object.song
|
|
return AudioFile.load(song_object.song_path)
|
|
|
|
|
|
func add_song_to_song_object(song_no: int, mp3: PackedByteArray) -> void:
|
|
store_song(song_object_array[song_no], mp3)
|
|
|
|
|
|
## Keeps a downloaded song in a file in [method songs_folder]. If the file can't be
|
|
## written, the song is kept in memory instead.
|
|
func store_song(song_object: SongObject, mp3: PackedByteArray) -> void:
|
|
DirAccess.make_dir_recursive_absolute(songs_folder())
|
|
# Songs kept from a saved quiz already have files, which must not be written over
|
|
var path: String = songs_folder().path_join("song_%d.mp3" % _song_files_written)
|
|
while FileAccess.file_exists(path):
|
|
_song_files_written += 1
|
|
path = songs_folder().path_join("song_%d.mp3" % _song_files_written)
|
|
_song_files_written += 1
|
|
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
|
|
if file == null:
|
|
GameLog.warning("Could not save a song to ", path, ", keeping it in memory: ",
|
|
error_string(FileAccess.get_open_error()))
|
|
var sound: AudioStreamMP3 = AudioStreamMP3.new()
|
|
sound.data = mp3
|
|
song_object.song = sound
|
|
song_object.song_path = ""
|
|
return
|
|
file.store_buffer(mp3)
|
|
file.close()
|
|
song_object.song = null
|
|
song_object.song_path = path
|
|
|
|
|
|
## Deletes the files of the songs downloaded so far, except those in [param keep].
|
|
func clear_song_files(keep: PackedStringArray = []) -> void:
|
|
if !DirAccess.dir_exists_absolute(songs_folder()):
|
|
return
|
|
for file_name: String in DirAccess.get_files_at(songs_folder()):
|
|
var path: String = songs_folder().path_join(file_name)
|
|
if !keep.has(path):
|
|
DirAccess.remove_absolute(path)
|
|
|
|
|
|
func song_has_played(song_no: int = latest_played_song) -> void:
|
|
song_object_array[song_no].has_played = true
|
|
|
|
|
|
func song_is_playing(song_no: int = latest_played_song) -> void:
|
|
song_object_array[song_no].is_playing = true
|