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
253 lines
10 KiB
GDScript
253 lines
10 KiB
GDScript
extends Node
|
|
## The game settings and the players, shared by the whole game.
|
|
##
|
|
## Registered as the [code]Settings[/code] autoload.
|
|
|
|
const MAX_NUMBER_OF_PLAYERS: int = 7
|
|
## How many letters a search needs before it also finds names with the letters in order
|
|
## but apart, e.g. "lttp" for "A Link to the Past". Shorter searches only find names
|
|
## that contain the text, since a letter or two in order matches most of the library.
|
|
const LETTERS_FOR_LOOSE_SEARCH: int = 3
|
|
## The MusicServers that can always be picked in the settings.
|
|
const SERVERS: Array[String] = [
|
|
"https://music.sanplex.xyz", "https://tmusic.sanplex.xyz", "https://rmusic.sanplex.xyz",
|
|
"https://old-music.sanplex.xyz", "http://localhost:8080",
|
|
]
|
|
## The server debug mode uses.
|
|
const DEBUG_SERVER: String = "http://localhost:8080"
|
|
## The winning score debug mode starts with, so a game is quick to win while testing.
|
|
const DEBUG_WINNING_SCORE: int = 5
|
|
## The settings the game saves by itself, by section, in [method saved_settings_path].
|
|
## Count plays in statistics and Prefer less played soundtracks are left out on purpose.
|
|
const SAVED_SETTINGS: Dictionary[String, Array] = {
|
|
"quiz": [
|
|
&"auto_repeat_song", &"stop_after_current", &"hide_next_track", &"hide_beginning",
|
|
&"hide_for_seconds", &"hide_length", &"hide_ticks", &"winning_score",
|
|
&"inspiration_list_speed",
|
|
],
|
|
"window": [&"fullscreen"],
|
|
"app": [&"last_seen_version"],
|
|
}
|
|
## When this environment variable is set, the settings files are kept in that folder
|
|
## instead of the user's config folder. The tests set it so they never touch the real files.
|
|
const CONFIG_DIR_ENV: String = "MUSICPLAYER_CONFIG_DIR"
|
|
## The file the user edits by hand. The game writes it only when it is missing, with every
|
|
## setting commented out.
|
|
const CONFIG_FILE_NAME: String = "config.ini"
|
|
const CONFIG_TEMPLATE: String = """# MusicPlayer config. The game never changes this file, so edit it as you like.
|
|
# Lines starting with # or ; are comments. Remove the # in front of a setting to use it,
|
|
# and put it back to go back to the default.
|
|
|
|
[server]
|
|
# The MusicServer to use when the game starts. Another one can still be picked in
|
|
# Settings for as long as the game runs. Debug mode uses http://localhost:8080.
|
|
#url = https://music.sanplex.xyz
|
|
|
|
# Password for getting an API token from the server.
|
|
#password =
|
|
|
|
[game]
|
|
# true or false. Debug mode uses the local server, starts with a winning score of 5 and
|
|
# shows DEBUG in the top bar. Without this setting, the game is in debug mode when run
|
|
# from the Godot editor and not when exported.
|
|
#debug = true
|
|
"""
|
|
|
|
var default_path: String = "https://music.sanplex.xyz"
|
|
## Password for getting an API token from the server. Any password works when the
|
|
## server has none set.
|
|
var api_password: String = ""
|
|
## The servers that can be picked in Settings: [constant SERVERS], plus the server from
|
|
## the config file when it is not one of them.
|
|
var servers: Array[String] = SERVERS.duplicate()
|
|
var selected_server: int = 0
|
|
var is_debug: bool = false
|
|
var stop_after_current: bool = true
|
|
var auto_repeat_song: bool = true
|
|
var hide_next_track: bool = true
|
|
var add_to_stats: bool = true
|
|
var use_low_played_mode: bool = false
|
|
var winning_score: int = 20
|
|
var fullscreen: bool = false
|
|
var inspiration_list_speed: int = 1
|
|
var hide_for_seconds: float = 5.0
|
|
var hide_beginning: bool = false
|
|
var hide_length: bool = false
|
|
var hide_ticks: bool = false
|
|
var player_array: Array[PlayerObject]
|
|
var edit_players: bool = false
|
|
var currently_syncing: bool = false
|
|
var character_select_open: bool = false
|
|
## The ID of the game's play queue on each server, by server URL, set by
|
|
## [code]MusicServerClient[/code]. Saved in the [code][queues][/code] section of
|
|
## [method saved_settings_path], so the game goes on with the same queue after a restart.
|
|
var queue_ids: Dictionary[String, String] = {}
|
|
## The version the game last showed the About popup for. When it is not [member version],
|
|
## this version is new and the main window opens the About popup to show what is new.
|
|
var last_seen_version: String = ""
|
|
## The folder songs are played from instead of the server, picked in the settings, or null
|
|
## to use the server. Kept for this session only.
|
|
var music_folder: MusicFolder = null
|
|
## The folder characters are read from instead of the server, picked in the settings, or
|
|
## empty. Kept for this session only. See [method characters_dir].
|
|
var character_folder: String = ""
|
|
|
|
## The game version, set in Project Settings > Application > Config > Version.
|
|
## Releases are made from it, see RELEASING.md.
|
|
var version: String = ProjectSettings.get_setting("application/config/version", "")
|
|
|
|
|
|
# Autoloads are ready before any scene, so the server and debug mode from the config file
|
|
# are used from the very first request. The saved settings come after, so the debug
|
|
# winning score is only used until another one is saved.
|
|
func _ready() -> void:
|
|
apply_config(load_config())
|
|
load_saved_settings()
|
|
if fullscreen:
|
|
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
|
|
|
|
|
|
## Uses the server, password and debug mode from the hand-edited [param config]. Settings
|
|
## left out of it get their defaults.
|
|
func apply_config(config: IniFile) -> void:
|
|
is_debug = config.get_bool("game", "debug", OS.has_feature("debug"))
|
|
default_path = DEBUG_SERVER if is_debug else SERVERS[0]
|
|
if is_debug:
|
|
winning_score = DEBUG_WINNING_SCORE
|
|
var url: String = config.get_string("server", "url")
|
|
if !url.is_empty():
|
|
default_path = url.trim_suffix("/")
|
|
if !servers.has(default_path):
|
|
servers.append(default_path)
|
|
selected_server = servers.find(default_path)
|
|
api_password = config.get_string("server", "password")
|
|
|
|
|
|
## The folder with the settings files: a MusicPlayer folder in the user's config folder,
|
|
## e.g. [code]~/.config[/code] on Linux and [code]%APPDATA%[/code] on Windows.
|
|
func config_dir() -> String:
|
|
var dir: String = OS.get_environment(CONFIG_DIR_ENV)
|
|
if dir.is_empty():
|
|
dir = OS.get_config_dir().path_join("MusicPlayer")
|
|
return dir
|
|
|
|
|
|
## Where the hand-edited config file is.
|
|
func config_path() -> String:
|
|
return config_dir().path_join(CONFIG_FILE_NAME)
|
|
|
|
|
|
## Where the game saves its settings. Debug mode uses a file of its own, so testing never
|
|
## changes the real settings.
|
|
func saved_settings_path() -> String:
|
|
var file_name: String = "settings_debug.cfg" if is_debug else "settings.cfg"
|
|
return config_dir().path_join(file_name)
|
|
|
|
|
|
## Reads the hand-edited config file. When it is missing, writes one with every setting
|
|
## commented out, so there is a file to edit.
|
|
func load_config() -> IniFile:
|
|
var path: String = config_path()
|
|
if !FileAccess.file_exists(path):
|
|
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
|
|
var template: FileAccess = FileAccess.open(path, FileAccess.WRITE)
|
|
if template != null:
|
|
template.store_string(CONFIG_TEMPLATE)
|
|
return IniFile.load_file(path)
|
|
|
|
|
|
## Loads the settings the game saved. Settings missing from the file, or saved with the
|
|
## wrong type, keep their current value.
|
|
func load_saved_settings() -> void:
|
|
var file: ConfigFile = ConfigFile.new()
|
|
if file.load(saved_settings_path()) != OK:
|
|
return
|
|
for section: String in SAVED_SETTINGS:
|
|
for setting: StringName in SAVED_SETTINGS[section]:
|
|
set(setting, _saved_value(file, section, setting, get(setting)))
|
|
if file.has_section("queues"):
|
|
queue_ids.clear()
|
|
for server: String in file.get_section_keys("queues"):
|
|
var id: Variant = file.get_value("queues", server)
|
|
if id is String:
|
|
queue_ids[server] = id
|
|
|
|
|
|
## Saves the settings in [constant SAVED_SETTINGS], so they are the same the next time
|
|
## the game starts.
|
|
func save_settings() -> void:
|
|
var path: String = saved_settings_path()
|
|
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
|
|
var file: ConfigFile = ConfigFile.new()
|
|
for section: String in SAVED_SETTINGS:
|
|
for setting: StringName in SAVED_SETTINGS[section]:
|
|
file.set_value(section, setting, get(setting))
|
|
for server: String in queue_ids:
|
|
file.set_value("queues", server, queue_ids[server])
|
|
var error: Error = file.save(path)
|
|
if error != OK:
|
|
GameLog.warning("Could not save the settings to %s: %s" % [path, error_string(error)])
|
|
|
|
|
|
## Sets the play queue ID for [param server] and saves it. Only the ID is changed in
|
|
## the file; the other settings in it stay as they were saved.
|
|
func set_queue_id(server: String, id: String) -> void:
|
|
queue_ids[server] = id
|
|
var path: String = saved_settings_path()
|
|
var file: ConfigFile = ConfigFile.new()
|
|
# A missing file is fine: it is written with only the queue ID
|
|
file.load(path)
|
|
file.set_value("queues", server, id)
|
|
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
|
|
var error: Error = file.save(path)
|
|
if error != OK:
|
|
GameLog.warning("Could not save the queue ID to %s: %s" % [path, error_string(error)])
|
|
|
|
|
|
## Turns fullscreen on or off and saves it.
|
|
func set_fullscreen(on: bool) -> void:
|
|
fullscreen = on
|
|
if on:
|
|
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
|
|
else:
|
|
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
|
|
save_settings()
|
|
|
|
|
|
# The saved value of [param key], or [param default] when it is missing or of another type
|
|
func _saved_value(file: ConfigFile, section: String, key: String, default: Variant) -> Variant:
|
|
var value: Variant = file.get_value(section, key, default)
|
|
if typeof(value) != typeof(default):
|
|
push_warning("%s in [%s] should be a %s, not %s" % [key, section,
|
|
type_string(typeof(default)), var_to_str(value)])
|
|
return default
|
|
return value
|
|
|
|
|
|
## Whether songs come from a music folder instead of the server. Nothing but the
|
|
## characters is asked from the server then.
|
|
func is_offline() -> bool:
|
|
return music_folder != null
|
|
|
|
|
|
## Whether a prepared quiz is played from a music folder. It has no winning score: the
|
|
## game ends when every song has played.
|
|
func is_prepared_quiz() -> bool:
|
|
return music_folder != null && music_folder.kind == MusicFolder.Kind.PREPARED_QUIZ
|
|
|
|
|
|
## The folder the characters come from, or empty for the server's: the folder picked in
|
|
## the settings, otherwise the [code]characters[/code] folder of the music folder played.
|
|
func characters_dir() -> String:
|
|
if !character_folder.is_empty():
|
|
return character_folder
|
|
if music_folder != null:
|
|
return music_folder.characters_path
|
|
return ""
|
|
|
|
|
|
func delete_children(node: Node) -> void:
|
|
for n: Node in node.get_children():
|
|
node.remove_child(n)
|
|
n.queue_free()
|