Files
MusicPlayer/data/quiz_save.gd
Sansan e4352a9f34
Build / test (push) Successful in 1m25s
Build / build (push) Successful in 1m12s
Save the quiz and continue it after a restart; load characters from a folder
The quiz is saved to current_quiz.json in the config folder after every
change, and the game goes on where it was when it starts again: the
players with their points and characters, every song with who got a point
for it and whether it was played, answered or is playing, and the songs
downloaded ahead with their files. The song that was playing is ready to
play again. Songs whose files are gone are downloaded again.

On a server, the saved songs are checked against the server's playlist. If
it changed, e.g. after a sync, or another server is used, the quiz starts
over with the same players. A quiz from a music folder continues offline,
including how far a prepared quiz has come. A new game is saved like any
other change, and a file that can't be read starts a new quiz.

Characters can be read from a folder picked in Settings instead of the
server, for this session. A music folder with a characters folder brings
its own characters, e.g. for a themed quiz; that folder is not counted as
a soundtrack. When the characters come from somewhere else, players pick
their characters again.

Closes #11, closes #44
2026-10-09 18:08:50 +02:00

175 lines
6.8 KiB
GDScript

class_name QuizSave
extends RefCounted
## The quiz going on, saved to a file after every change, so the game goes on where it
## was when it is started again: the players with their points and characters, and every
## song with who got a point for it and whether it was played, answered or is playing.
##
## Songs downloaded ahead keep their files, so they are not downloaded again. A new game
## (Reset points, a sync or another server) is saved like any other change, so the file
## always holds the quiz as it is now.
##
## Kept in [code]current_quiz.json[/code] next to the settings, or
## [code]current_quiz_debug.json[/code] in debug mode, like the saved settings.
const FILE_NAME: String = "current_quiz.json"
const DEBUG_FILE_NAME: String = "current_quiz_debug.json"
## Files with another version are from a game that saved differently, and are ignored.
const VERSION: int = 1
var players: Array[PlayerObject] = []
var songs: Array[SongObject] = []
var currently_playing_song: int = -1
var latest_played_song: int = 0
var game_start_song: int = 0
var first_song_played: bool = false
## Whether statistics were turned off because a player won, see [code]Quiz[/code].
var stats_paused_by_win: bool = false
## The server the songs came from. Empty when they came from a music folder.
var server: String = ""
## The music folder the songs came from, or empty when they came from the server.
var music_folder_path: String = ""
## How far a prepared quiz in [member music_folder_path] has come, see
## [method MusicFolder.progress].
var music_folder_progress: Dictionary = {}
## Where the characters came from, see [method Settings.characters_dir].
var characters_dir: String = ""
## Where the quiz is saved.
static func path() -> String:
return Settings.config_dir().path_join(DEBUG_FILE_NAME if Settings.is_debug else FILE_NAME)
## The quiz as it is now, in [code]Settings[/code] and [code]Playlist[/code].
static func capture(paused_by_win: bool) -> QuizSave:
var save: QuizSave = QuizSave.new()
save.players = Settings.player_array.duplicate()
save.songs = Playlist.song_object_array.duplicate()
save.currently_playing_song = Playlist.currently_playing_song
save.latest_played_song = Playlist.latest_played_song
save.game_start_song = Playlist.game_start_song
save.first_song_played = Playlist.first_song_played
save.stats_paused_by_win = paused_by_win
if Settings.is_offline():
save.music_folder_path = Settings.music_folder.path
save.music_folder_progress = Settings.music_folder.progress()
else:
save.server = Settings.default_path
save.characters_dir = Settings.characters_dir()
return save
## Reads the saved quiz, or returns null when there is none or it can't be read.
static func read() -> QuizSave:
if !FileAccess.file_exists(path()):
return null
var save: QuizSave = from_dict(JSON.parse_string(FileAccess.get_file_as_string(path())))
if save == null:
GameLog.warning("Could not read the saved quiz in ", path(), ", starting a new one")
return save
## Deletes the saved quiz.
static func delete() -> void:
if FileAccess.file_exists(path()):
DirAccess.remove_absolute(path())
## Saves the quiz. It is written to another file first and then moved, so a crash while
## saving never leaves half a file. Returns false when it could not be saved.
func write() -> bool:
var target: String = path()
var temporary: String = target + ".tmp"
DirAccess.make_dir_recursive_absolute(target.get_base_dir())
var file: FileAccess = FileAccess.open(temporary, FileAccess.WRITE)
if file == null:
GameLog.warning("Could not save the quiz to ", temporary, ": ",
error_string(FileAccess.get_open_error()))
return false
file.store_string(JSON.stringify(to_dict(), "\t"))
file.close()
var error: Error = DirAccess.rename_absolute(temporary, target)
if error != OK:
GameLog.warning("Could not save the quiz to ", target, ": ", error_string(error))
return false
return true
## The song files the saved songs use, which must not be deleted.
func song_files() -> PackedStringArray:
var files: PackedStringArray = []
for song_object: SongObject in songs:
if !song_object.song_path.is_empty():
files.append(song_object.song_path)
return files
func to_dict() -> Dictionary:
var player_list: Array[Dictionary] = []
for player: PlayerObject in players:
player_list.append(player.to_dict())
var song_list: Array[Dictionary] = []
for song_object: SongObject in songs:
song_list.append(song_object.to_dict())
return {
"version": VERSION,
"players": player_list,
"songs": song_list,
"currently_playing_song": currently_playing_song,
"latest_played_song": latest_played_song,
"game_start_song": game_start_song,
"first_song_played": first_song_played,
"stats_paused_by_win": stats_paused_by_win,
"server": server,
"music_folder": {"path": music_folder_path, "progress": music_folder_progress},
"characters_dir": characters_dir,
}
## A save made from [param data], as given by [method to_dict], or null when
## [param data] is not a saved quiz of this [constant VERSION]. Players and songs that
## can't be read are left out.
static func from_dict(data: Variant) -> QuizSave:
if !(data is Dictionary) || int(data.get("version", 0)) != VERSION \
|| !(data.get("players") is Array) || !(data.get("songs") is Array):
return null
var save: QuizSave = QuizSave.new()
for player_data: Variant in data.players:
var player: PlayerObject = PlayerObject.from_dict(player_data)
if player != null && save.players.size() < Settings.MAX_NUMBER_OF_PLAYERS:
save.players.append(player)
for song_data: Variant in data.songs:
var song_object: SongObject = SongObject.from_dict(song_data)
if song_object == null:
# The songs' places in the list are their numbers, so none can be left out
save.songs.clear()
break
save.songs.append(song_object)
save.currently_playing_song = clampi(int(data.get("currently_playing_song", -1)), -1,
save.songs.size() - 1)
save.latest_played_song = clampi(int(data.get("latest_played_song", 0)), 0,
maxi(save.songs.size() - 1, 0))
save.game_start_song = clampi(int(data.get("game_start_song", 0)), 0, save.songs.size())
save.first_song_played = bool(data.get("first_song_played", false))
save.stats_paused_by_win = bool(data.get("stats_paused_by_win", false))
save.server = str(data.get("server", ""))
var folder: Variant = data.get("music_folder", {})
if folder is Dictionary:
save.music_folder_path = str(folder.get("path", ""))
if folder.get("progress") is Dictionary:
save.music_folder_progress = folder.progress
save.characters_dir = str(data.get("characters_dir", ""))
return save
## Forgets the songs, e.g. when the server's playlist no longer has them. The players are
## kept, without points.
func drop_songs() -> void:
songs.clear()
currently_playing_song = -1
latest_played_song = 0
game_start_song = 0
first_song_played = false
for player: PlayerObject in players:
player.reset_points()