Files
MusicPlayer/data/settings.gd
T
Sansan 222d231b1b
Build / test (push) Successful in 1m2s
Build / build (push) Successful in 1m12s
Add offline mode: play from a music folder instead of the server
Settings has a Music folder row. A folder with subfolders is played as
soundtracks: Next picks a random subfolder and a random song in it, the
subfolder is the answer's soundtrack and the file name without its type the
song. The inspiration list and Search show the subfolders.

A folder with songs and no subfolders is a prepared quiz, played once in order
(natural sort) or at random, showing only the song title. It has no winning
score: after the last song Next becomes Show winner, and the players with the
most points win, ties included.

While a folder is used nothing but the characters is asked from the server:
Sync, QR, Statistics, the update check and play counting are off. Characters
fall back to the cached pictures when the server can't be reached. mp3, ogg and
wav play; other files and broken ones are skipped.

The old local playback code, the hidden Open button and the hidden Local toggle
are removed. Next now waits for the song to start before preparing the next one.
2026-10-07 23:07:47 +02:00

240 lines
9.6 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 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
func delete_children(node: Node) -> void:
for n: Node in node.get_children():
node.remove_child(n)
n.queue_free()