Christmas red window and panels with snow white buttons and red text, a thick white "fur" trim on popups, cards, the lists and the player bar, the tree's green for the chosen tab and the play button, and gold for the playing song and highlights. Icons on buttons take the text's colour, and drop-down lists stay dark red so their arrow shows. It uses Nord's round shapes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
289 lines
12 KiB
GDScript
289 lines
12 KiB
GDScript
extends Node
|
|
## The game settings and the players, shared by the whole game.
|
|
##
|
|
## Registered as the [code]Settings[/code] autoload.
|
|
|
|
## Emitted when another theme is used, for code that reads theme colours itself.
|
|
signal theme_changed
|
|
|
|
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", &"theme_id"],
|
|
}
|
|
## The looks the game can have, picked in Settings: an id that is saved, the name shown
|
|
## and the theme file. The first is the default. See themes/README.md.
|
|
const THEMES: Array[Dictionary] = [
|
|
{"id": "classic", "name": "Classic", "path": "res://themes/main_theme.tres"},
|
|
{"id": "original_web", "name": "Original web", "path": "res://themes/original_web_theme.tres"},
|
|
{"id": "nord", "name": "Nord", "path": "res://themes/nord_theme.tres"},
|
|
{"id": "christmas", "name": "Christmas", "path": "res://themes/christmas_theme.tres"},
|
|
]
|
|
## 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 id of the theme in use, one of [constant THEMES].
|
|
var theme_id: String = THEMES[0].id
|
|
## 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()
|
|
_use_theme(theme_id)
|
|
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 ""
|
|
|
|
|
|
## Switches the whole game to the theme [param id] from [constant THEMES] and saves the
|
|
## choice. Controls without a theme of their own take it from the root window.
|
|
func apply_theme(id: String) -> void:
|
|
_use_theme(id)
|
|
save_settings()
|
|
|
|
|
|
## The theme in [constant THEMES] with [param id], or the default one if there is none.
|
|
func theme_entry(id: String) -> Dictionary:
|
|
for entry: Dictionary in THEMES:
|
|
if entry.id == id:
|
|
return entry
|
|
return THEMES[0]
|
|
|
|
|
|
func _use_theme(id: String) -> void:
|
|
var entry: Dictionary = theme_entry(id)
|
|
theme_id = entry.id
|
|
get_tree().root.theme = load(entry.path)
|
|
theme_changed.emit()
|
|
|
|
|
|
func delete_children(node: Node) -> void:
|
|
for n: Node in node.get_children():
|
|
node.remove_child(n)
|
|
n.queue_free()
|