From 878881e7e9c4b4026ca14e1af2f6490d08b94778 Mon Sep 17 00:00:00 2001 From: Spartan322 Date: Tue, 23 Jun 2026 20:55:42 -0400 Subject: [PATCH] Overhaul game settings Implement startup music disable option Replace GameSettings with Kenyoni AppSettings plugin Remove the C++ GameSettings resource and registration Add Godot-side Settings Autoload Globals: - GameSettings - ModSettings - Vic2Settings Add mods load list to setting GUI Add Victoria 2 base defines path to setting GUI Refactor callers (GameStart, MusicManager, MapView, OptionsMenu, MainMenu) to use the new API Add SettingsContainer UI to generate options dynamically Update project.godot to register autoloads and enable the plugin Update COPYRIGHT to attribute the new plugin files --- COPYRIGHT | 5 + .../core/io/GameSettings.cpp | 226 ---------- .../core/io/GameSettings.hpp | 50 --- .../core/register_core_types.cpp | 2 - game/addons/kenyoni/app_settings/LICENSE.md | 7 + .../kenyoni/app_settings/app_settings.gd | 142 ++++++ .../kenyoni/app_settings/app_settings.gd.uid | 1 + game/addons/kenyoni/app_settings/icon.svg | 93 ++++ .../kenyoni/app_settings/icon.svg.import | 44 ++ game/addons/kenyoni/app_settings/plugin.cfg | 19 + game/addons/kenyoni/app_settings/plugin.gd | 3 + .../addons/kenyoni/app_settings/plugin.gd.uid | 1 + game/addons/kenyoni/app_settings/registry.gd | 216 ++++++++++ .../kenyoni/app_settings/registry.gd.uid | 1 + game/addons/kenyoni/app_settings/setting.gd | 234 ++++++++++ .../kenyoni/app_settings/setting.gd.uid | 1 + .../localisation/locales/en_GB/menus.csv | 9 +- game/project.godot | 7 +- game/src/Autoload/GuiScale.gd | 62 --- game/src/Autoload/GuiScale.gd.uid | 1 - .../src/Autoload/MusicManager/MusicManager.gd | 14 +- game/src/Autoload/Resolution.gd | 158 ------- game/src/Autoload/Resolution.gd.uid | 1 - game/src/Autoload/Settings/GameSettings.gd | 403 ++++++++++++++++++ .../src/Autoload/Settings/GameSettings.gd.uid | 1 + game/src/Autoload/Settings/ModSettings.gd | 41 ++ game/src/Autoload/Settings/ModSettings.gd.uid | 1 + game/src/Autoload/Settings/Vic2Settings.gd | 126 ++++++ .../src/Autoload/Settings/Vic2Settings.gd.uid | 1 + game/src/Systems/Session/Map/MapView.gd | 8 +- game/src/Systems/Startup/GameStart.gd | 88 +--- game/src/Systems/Startup/GameStart.tscn | 15 +- game/src/UI/GameMenu/GameMenu/LocaleButton.gd | 82 ---- .../UI/GameMenu/GameMenu/LocaleButton.gd.uid | 1 - .../UI/GameMenu/GameMenu/LocaleButton.tscn | 13 - game/src/UI/GameMenu/MainMenu/MainMenu.gd | 17 + game/src/UI/GameMenu/MainMenu/MainMenu.tscn | 11 +- .../OptionMenu/AutosaveIntervalSelector.gd | 2 - .../AutosaveIntervalSelector.gd.uid | 1 - .../UI/GameMenu/OptionMenu/ControlsTab.tscn | 15 +- game/src/UI/GameMenu/OptionMenu/GeneralTab.gd | 9 - .../UI/GameMenu/OptionMenu/GeneralTab.gd.uid | 1 - .../UI/GameMenu/OptionMenu/GeneralTab.tscn | 81 ---- .../GameMenu/OptionMenu/GuiScaleSelector.gd | 64 --- .../OptionMenu/GuiScaleSelector.gd.uid | 1 - .../OptionMenu/MonitorDisplaySelector.gd | 29 -- .../OptionMenu/MonitorDisplaySelector.gd.uid | 1 - .../src/UI/GameMenu/OptionMenu/OptionsMenu.gd | 98 +++-- .../UI/GameMenu/OptionMenu/OptionsMenu.tscn | 32 +- game/src/UI/GameMenu/OptionMenu/OtherTab.tscn | 18 - .../OptionMenu/QualityPresetSelector.gd | 4 - .../OptionMenu/QualityPresetSelector.gd.uid | 1 - .../OptionMenu/RefreshRateSelector.gd | 5 - .../OptionMenu/RefreshRateSelector.gd.uid | 1 - .../GameMenu/OptionMenu/ResolutionSelector.gd | 90 ---- .../OptionMenu/ResolutionSelector.gd.uid | 1 - .../GameMenu/OptionMenu/ScreenModeSelector.gd | 44 -- .../OptionMenu/ScreenModeSelector.gd.uid | 1 - .../SettingNodes/SettingCheckBox.gd | 50 --- .../SettingNodes/SettingCheckBox.gd.uid | 1 - .../OptionMenu/SettingNodes/SettingHSlider.gd | 42 -- .../SettingNodes/SettingHSlider.gd.uid | 1 - .../SettingNodes/SettingOptionButton.gd | 80 ---- .../SettingNodes/SettingOptionButton.gd.uid | 1 - .../SettingNodes/SettingRevertButton.gd | 27 -- .../SettingNodes/SettingRevertButton.gd.uid | 1 - .../OptionMenu/SettingRevertDialog.gd | 37 -- .../OptionMenu/SettingRevertDialog.gd.uid | 1 - .../GameMenu/OptionMenu/SettingsContainer.gd | 347 +++++++++++++++ .../OptionMenu/SettingsContainer.gd.uid | 1 + .../OptionMenu/SettingsContainer.tscn | 40 ++ game/src/UI/GameMenu/OptionMenu/SoundTab.tscn | 34 -- game/src/UI/GameMenu/OptionMenu/VideoTab.gd | 9 - .../UI/GameMenu/OptionMenu/VideoTab.gd.uid | 1 - game/src/UI/GameMenu/OptionMenu/VideoTab.tscn | 183 -------- game/src/UI/GameMenu/OptionMenu/VolumeGrid.gd | 54 --- .../UI/GameMenu/OptionMenu/VolumeGrid.gd.uid | 1 - .../UI/GameMenu/OptionMenu/VolumeGrid.tscn | 8 - 78 files changed, 1856 insertions(+), 1667 deletions(-) delete mode 100644 extension/src/openvic-extension/core/io/GameSettings.cpp delete mode 100644 extension/src/openvic-extension/core/io/GameSettings.hpp create mode 100644 game/addons/kenyoni/app_settings/LICENSE.md create mode 100644 game/addons/kenyoni/app_settings/app_settings.gd create mode 100644 game/addons/kenyoni/app_settings/app_settings.gd.uid create mode 100644 game/addons/kenyoni/app_settings/icon.svg create mode 100644 game/addons/kenyoni/app_settings/icon.svg.import create mode 100644 game/addons/kenyoni/app_settings/plugin.cfg create mode 100644 game/addons/kenyoni/app_settings/plugin.gd create mode 100644 game/addons/kenyoni/app_settings/plugin.gd.uid create mode 100644 game/addons/kenyoni/app_settings/registry.gd create mode 100644 game/addons/kenyoni/app_settings/registry.gd.uid create mode 100644 game/addons/kenyoni/app_settings/setting.gd create mode 100644 game/addons/kenyoni/app_settings/setting.gd.uid delete mode 100644 game/src/Autoload/GuiScale.gd delete mode 100644 game/src/Autoload/GuiScale.gd.uid delete mode 100644 game/src/Autoload/Resolution.gd delete mode 100644 game/src/Autoload/Resolution.gd.uid create mode 100644 game/src/Autoload/Settings/GameSettings.gd create mode 100644 game/src/Autoload/Settings/GameSettings.gd.uid create mode 100644 game/src/Autoload/Settings/ModSettings.gd create mode 100644 game/src/Autoload/Settings/ModSettings.gd.uid create mode 100644 game/src/Autoload/Settings/Vic2Settings.gd create mode 100644 game/src/Autoload/Settings/Vic2Settings.gd.uid delete mode 100644 game/src/UI/GameMenu/GameMenu/LocaleButton.gd delete mode 100644 game/src/UI/GameMenu/GameMenu/LocaleButton.gd.uid delete mode 100644 game/src/UI/GameMenu/GameMenu/LocaleButton.tscn delete mode 100644 game/src/UI/GameMenu/OptionMenu/AutosaveIntervalSelector.gd delete mode 100644 game/src/UI/GameMenu/OptionMenu/AutosaveIntervalSelector.gd.uid delete mode 100644 game/src/UI/GameMenu/OptionMenu/GeneralTab.gd delete mode 100644 game/src/UI/GameMenu/OptionMenu/GeneralTab.gd.uid delete mode 100644 game/src/UI/GameMenu/OptionMenu/GeneralTab.tscn delete mode 100644 game/src/UI/GameMenu/OptionMenu/GuiScaleSelector.gd delete mode 100644 game/src/UI/GameMenu/OptionMenu/GuiScaleSelector.gd.uid delete mode 100644 game/src/UI/GameMenu/OptionMenu/MonitorDisplaySelector.gd delete mode 100644 game/src/UI/GameMenu/OptionMenu/MonitorDisplaySelector.gd.uid delete mode 100644 game/src/UI/GameMenu/OptionMenu/OtherTab.tscn delete mode 100644 game/src/UI/GameMenu/OptionMenu/QualityPresetSelector.gd delete mode 100644 game/src/UI/GameMenu/OptionMenu/QualityPresetSelector.gd.uid delete mode 100644 game/src/UI/GameMenu/OptionMenu/RefreshRateSelector.gd delete mode 100644 game/src/UI/GameMenu/OptionMenu/RefreshRateSelector.gd.uid delete mode 100644 game/src/UI/GameMenu/OptionMenu/ResolutionSelector.gd delete mode 100644 game/src/UI/GameMenu/OptionMenu/ResolutionSelector.gd.uid delete mode 100644 game/src/UI/GameMenu/OptionMenu/ScreenModeSelector.gd delete mode 100644 game/src/UI/GameMenu/OptionMenu/ScreenModeSelector.gd.uid delete mode 100644 game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingCheckBox.gd delete mode 100644 game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingCheckBox.gd.uid delete mode 100644 game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingHSlider.gd delete mode 100644 game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingHSlider.gd.uid delete mode 100644 game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingOptionButton.gd delete mode 100644 game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingOptionButton.gd.uid delete mode 100644 game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingRevertButton.gd delete mode 100644 game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingRevertButton.gd.uid delete mode 100644 game/src/UI/GameMenu/OptionMenu/SettingRevertDialog.gd delete mode 100644 game/src/UI/GameMenu/OptionMenu/SettingRevertDialog.gd.uid create mode 100644 game/src/UI/GameMenu/OptionMenu/SettingsContainer.gd create mode 100644 game/src/UI/GameMenu/OptionMenu/SettingsContainer.gd.uid create mode 100644 game/src/UI/GameMenu/OptionMenu/SettingsContainer.tscn delete mode 100644 game/src/UI/GameMenu/OptionMenu/SoundTab.tscn delete mode 100644 game/src/UI/GameMenu/OptionMenu/VideoTab.gd delete mode 100644 game/src/UI/GameMenu/OptionMenu/VideoTab.gd.uid delete mode 100644 game/src/UI/GameMenu/OptionMenu/VideoTab.tscn delete mode 100644 game/src/UI/GameMenu/OptionMenu/VolumeGrid.gd delete mode 100644 game/src/UI/GameMenu/OptionMenu/VolumeGrid.gd.uid delete mode 100644 game/src/UI/GameMenu/OptionMenu/VolumeGrid.tscn diff --git a/COPYRIGHT b/COPYRIGHT index 70dfcf90..77850288 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -40,6 +40,11 @@ Comment: Kenney's UI Audio Pack Copyright: 2020 Kenney License: CC0-1.0 +Files: game/addons/kenyoni/app_settings/* +Comment: AppSettings Godot Plugin +Copyright: 2022-present Iceflower S (iceflower@gmx.de) +License: Expat + Files: game/addons/MusicMetadata/* Comment: MusicMetadata Godot Plugin Copyright: 2024 Aiden Olsen, James Wilcock, NovaDC diff --git a/extension/src/openvic-extension/core/io/GameSettings.cpp b/extension/src/openvic-extension/core/io/GameSettings.cpp deleted file mode 100644 index 31affd41..00000000 --- a/extension/src/openvic-extension/core/io/GameSettings.cpp +++ /dev/null @@ -1,226 +0,0 @@ -#include "GameSettings.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "openvic-extension/core/Bind.hpp" - -using namespace OpenVic; -using namespace godot; - -GameSettings::GameSettings() { - config.instantiate(); - defaults.instantiate(); -} - -void GameSettings::reset_settings() { - config = defaults; - emit_changed(); -} - -Error GameSettings::save(String path) { - if (path.is_empty()) { - path = get_path(); - } - for (String const& section : defaults->get_sections()) { - for (String const& key : defaults->get_section_keys(section)) { - if (!has_section_key(section, key)) { - config->set_value(section, key, defaults->get_value(section, key)); - } - } - } - Error err = config->save(path); - return err; -} - -Error GameSettings::load(String path) { - if (path.is_empty()) { - path = get_path(); - } else { - set_path(path); - } - - Error err = config->load(path); - emit_changed(); - return err; -} - -Ref GameSettings::load_from_file(String path) { - Ref settings; - if (ResourceLoader::get_singleton()->has_cached(path)) { - settings = ResourceLoader::get_singleton()->get_cached_ref(path); - if (settings.is_valid()) { - return settings; - } - } - - settings.instantiate(); - if (!FileAccess::file_exists(path)) { - if (String dir = path.get_base_dir(); !DirAccess::dir_exists_absolute(dir)) { - DirAccess::make_dir_recursive_absolute(dir); - } - - ERR_FAIL_COND_V(settings->save(path) != OK, Ref()); - } else { - ERR_FAIL_COND_V(settings->load(path) != OK, Ref()); - } - return settings; -} - -void GameSettings::set_value(String section, String key, Variant value) { - if (value == Variant()) { - config->set_value(section, key, defaults->get_value(section, key)); - return; - } - - config->set_value(section, key, value); -} - -Variant GameSettings::get_value(String section, String key, Variant default_value) { - if (default_value != Variant()) { - if (defaults->has_section_key(section, key)) { - ERR_FAIL_COND_V_MSG( - defaults->get_value(section, key) != default_value, nullptr, - vformat("Setting '%s.%s' default value does match previously set default value.", section, key) - ); - } else { - defaults->set_value(section, key, default_value); - } - } else if (defaults->has_section_key(section, key)) { - default_value = defaults->get_value(section, key); - } - - if (!has_section_key(section, key)) { - set_value(section, key, default_value); - return default_value; - } - - return config->get_value(section, key); -} - -bool GameSettings::has_section(String section) const { - return config->has_section(section); -} - -bool GameSettings::has_section_key(String section, String key) const { - return config->has_section_key(section, key); -} - -void GameSettings::reset_section(String section) { - set_block_signals(true); - for (String const& key : config->get_section_keys(section)) { - reset_section_key(section, key); - } - set_block_signals(false); - emit_changed(); -} - -void GameSettings::reset_section_key(String section, String key) { - config->set_value(section, key, defaults->get_value(section, key)); - emit_changed(); -} - -PackedStringArray GameSettings::get_sections() const { - return config->get_sections(); -} - -PackedStringArray GameSettings::get_section_keys(String section) const { - return config->get_section_keys(section); -} - -Error GameSettings::load_deprecated_file(String path, Dictionary section_keys) { - Ref config; - config.instantiate(); - if (Error err = config->load(path); err != OK) { - return err; - } - - Array sections = section_keys.keys(); - for (size_t i = 0; i < sections.size(); i++) { - Variant section = sections[i]; - const Variant::Type section_type = section.get_type(); - ERR_FAIL_COND_V(section_type != Variant::STRING && section_type != Variant::STRING_NAME, ERR_INVALID_PARAMETER); - - Variant key = section_keys[section]; - switch (key.get_type()) { - using enum Variant::Type; - case NIL: - for (String const& key : config->get_section_keys(section)) { - set_value(section, key, config->get_value(section, key)); - } - break; - case STRING: - case STRING_NAME: - if (config->has_section_key(section, key) && !has_section_key(section, key)) { - set_value(section, key, config->get_value(section, key)); - } - break; - - case PACKED_STRING_ARRAY: { - PackedStringArray array = key; - for (String const& k : array) { - if (config->has_section_key(section, k) && !has_section_key(section, k)) { - set_value(section, k, config->get_value(section, k)); - } - } - break; - } - - case ARRAY: { - Array array = key; - for (size_t arr_i = 0; arr_i < array.size(); arr_i++) { - Variant const& inner_key = array[arr_i]; - Variant::Type inner_key_type = inner_key.get_type(); - ERR_FAIL_COND_V( - inner_key_type != Variant::STRING && inner_key_type != Variant::STRING_NAME, ERR_INVALID_PARAMETER - ); - - String const& k = inner_key; - if (config->has_section_key(section, k) && !has_section_key(section, k)) { - set_value(section, k, config->get_value(section, k)); - } - } - break; - } - - default: // - ERR_FAIL_V(ERR_INVALID_PARAMETER); - } - } - emit_changed(); - - return OK; -} - -void GameSettings::_bind_methods() { - OV_BIND_METHOD(GameSettings::reset_settings); - - OV_BIND_METHOD(GameSettings::save, { "path" }, DEFVAL(String())); - OV_BIND_METHOD(GameSettings::load, { "path" }, DEFVAL(String())); - - OV_BIND_SMETHOD(GameSettings::load_from_file, { "path" }); - - OV_BIND_METHOD(GameSettings::set_value, { "section", "key", "value" }); - OV_BIND_METHOD(GameSettings::get_value, { "section", "key", "default" }, DEFVAL(nullptr)); - - OV_BIND_METHOD(GameSettings::has_section, { "section" }); - OV_BIND_METHOD(GameSettings::has_section_key, { "section", "key" }); - - OV_BIND_METHOD(GameSettings::reset_section, { "section" }); - OV_BIND_METHOD(GameSettings::reset_section_key, { "section", "key" }); - - OV_BIND_METHOD(GameSettings::get_sections); - OV_BIND_METHOD(GameSettings::get_section_keys, { "section" }); - - OV_BIND_METHOD(GameSettings::load_deprecated_file, { "path", "section_keys" }); -} diff --git a/extension/src/openvic-extension/core/io/GameSettings.hpp b/extension/src/openvic-extension/core/io/GameSettings.hpp deleted file mode 100644 index c6abf442..00000000 --- a/extension/src/openvic-extension/core/io/GameSettings.hpp +++ /dev/null @@ -1,50 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace OpenVic { - class GameSettings : public godot::Resource { - GDCLASS(GameSettings, godot::Resource); - - godot::Ref config; - godot::Ref defaults; - - protected: - static void _bind_methods(); - - public: - void reset_settings(); - - godot::Error save(godot::String path = godot::String()); - godot::Error load(godot::String path = godot::String()); - - static godot::Ref load_from_file(godot::String path); - - void set_value(godot::String section, godot::String key, godot::Variant value); - godot::Variant get_value(godot::String section, godot::String key, godot::Variant default_value = nullptr); - - bool has_section(godot::String section) const; - bool has_section_key(godot::String section, godot::String key) const; - - void reset_section(godot::String section); - void reset_section_key(godot::String section, godot::String key); - - godot::PackedStringArray get_sections() const; - godot::PackedStringArray get_section_keys(godot::String section) const; - - godot::Error load_deprecated_file(godot::String path, godot::Dictionary section_keys); - - GameSettings(); - }; -} diff --git a/extension/src/openvic-extension/core/register_core_types.cpp b/extension/src/openvic-extension/core/register_core_types.cpp index f055ee8d..9a3a1ea7 100644 --- a/extension/src/openvic-extension/core/register_core_types.cpp +++ b/extension/src/openvic-extension/core/register_core_types.cpp @@ -4,7 +4,6 @@ #include #include "openvic-extension/core/ArgumentParser.hpp" -#include "openvic-extension/core/io/GameSettings.hpp" using namespace OpenVic; using namespace godot; @@ -14,7 +13,6 @@ static ArgumentParser* _argument_parser = nullptr; void OpenVic::register_core_types() { GDREGISTER_CLASS(ArgumentParser); GDREGISTER_CLASS(ArgumentOption); - GDREGISTER_CLASS(GameSettings); _argument_parser = memnew(ArgumentParser); Engine::get_singleton()->register_singleton("ArgumentParser", ArgumentParser::get_singleton()); diff --git a/game/addons/kenyoni/app_settings/LICENSE.md b/game/addons/kenyoni/app_settings/LICENSE.md new file mode 100644 index 00000000..4abfd1fb --- /dev/null +++ b/game/addons/kenyoni/app_settings/LICENSE.md @@ -0,0 +1,7 @@ +Copyright 2022-present Iceflower S (iceflower@gmx.de) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/game/addons/kenyoni/app_settings/app_settings.gd b/game/addons/kenyoni/app_settings/app_settings.gd new file mode 100644 index 00000000..491f49b5 --- /dev/null +++ b/game/addons/kenyoni/app_settings/app_settings.gd @@ -0,0 +1,142 @@ +extends Node +## Node that wraps a `Registry` and adds per-frame batched signals. + +const Registry := preload("res://addons/kenyoni/app_settings/registry.gd") +const Setting := preload("res://addons/kenyoni/app_settings/setting.gd") + +## Emitted once per frame when one or more settings were applied during that frame. Inspect `get_changed_applied_settings()` for the affected keys. +signal settings_applied() +## Emitted once per frame when one or more effective setting values changed during that frame. Inspect `get_changed_settings()` for the affected keys. +signal settings_changed() +## Emitted once per frame when one or more staged values were set or cleared during that frame. Inspect `get_changed_staged_settings()` for the affected keys. +signal settings_staged_changed() + +## Emitted immediately after a setting is applied. `key` is the key of the applied setting. +signal applied(key: StringName) +## Emitted immediately when a setting's effective value changes. `key` is the key of the changed setting. +signal changed(key: StringName) +## Emitted immediately when a staged value is set or cleared. `key` is the key of the affected setting. +signal staged_changed(key: StringName) + +var _registry: Registry = Registry.new() + +## Keys of settings that were applied at least once this frame. +var _settings_applied: PackedStringArray = [] +## Keys of settings whose effective value changed at least once this frame. +var _settings_changed: PackedStringArray = [] +## Keys of settings whose staged value changed or was cleared at least once this frame. +var _settings_staged_changed: PackedStringArray = [] + +func _ready() -> void: + self._registry.applied.connect(self._on_applied) + self._registry.changed.connect(self._on_changed) + self._registry.staged_changed.connect(self._on_staged_changed) + +func _process(_delta: float) -> void: + if !self._settings_staged_changed.is_empty(): + self.settings_staged_changed.emit() + self._settings_staged_changed = [] + if !self._settings_changed.is_empty(): + self.settings_changed.emit() + self._settings_changed = [] + if !self._settings_applied.is_empty(): + self.settings_applied.emit() + self._settings_applied = [] + +## Add a new setting. The setting's key must be unique. +func add_setting(setting: Setting) -> void: + self._registry.add_setting(setting) + +## Return `true` if a setting with `key` exists. +func has_setting(key: StringName) -> bool: + return self._registry.has_setting(key) + +## Return the `Setting` for `key`, or `null` if it does not exist. +func get_setting(key: StringName) -> Setting: + return self._registry.get_setting(key) + +## Remove the setting identified by `key`. Does nothing if the key does not exist. +func remove_setting(key: StringName) -> void: + self._registry.remove_setting(key) + +## Set the value of the setting identified by `key`. See `Setting.set_value()` for details on how values are assigned and validated. +func set_value(key: StringName, value: Variant) -> void: + self._registry.set_value(key, value) + +## Return the effective value of the setting identified by `key`. See `Setting.value()` for details on how values are determined. +func get_value(key: StringName) -> Variant: + return self._registry.get_value(key) + +## Return all `Setting` objects whose keys begin with `section`. +## `depth` limits the number of `/`-separated levels below `section` that are included; `-1` means unlimited. +## `filter` is a `Callable` with signature `func(setting: Setting) -> bool`. Defaults to `Registry._exclude_internal`. +## See `Registry.get_section()` for full details. +func get_section(section: String, depth: int = -1, filter: Callable = Registry._exclude_internal) -> Array[Setting]: + return self._registry.get_section(section, depth, filter) + +## Return the keys of all settings whose keys begin with `section`. +## `depth` limits the number of `/`-separated levels below `section` that are included; `-1` means unlimited. +## `filter` is a `Callable` with signature `func(setting: Setting) -> bool`. Defaults to `Registry._exclude_internal`. +## See `Registry.get_section_keys()` for full details. +func get_section_keys(section: String, depth: int = -1, filter: Callable = Registry._exclude_internal) -> PackedStringArray: + return self._registry.get_section_keys(section, depth, filter) + +## Return the names of the immediate child sections under `parent_section`. +## Pass an empty string for `parent_section` to get the top-level section names. +## `filter` is a `Callable` with signature `func(setting: Setting) -> bool`. Defaults to `Registry._exclude_internal`. +## See `Registry.get_sub_sections()` for full details. +func get_sub_sections(parent_section: String = "", filter: Callable = Registry._exclude_internal) -> PackedStringArray: + return self._registry.get_sub_sections(parent_section, filter) + +## Call `apply()` on every registered setting unconditionally. +func apply_all() -> void: + self._registry.apply_all() + +## Call `apply()` only on settings that have a pending staged value. +func apply_staged_values() -> void: + self._registry.apply_staged_values() + +## Discard all pending staged values across every registered setting. +func discard_staged_values() -> void: + self._registry.discard_staged_values() + +## Return `true` if at least one registered setting has a pending staged value. +func has_staged_values() -> bool: + return self._registry.has_staged_values() + +## Return the keys of settings that were applied at least once since the previous `_process()` call. +## The array is cleared after each frame, so it only contains settings applied during the current frame. +func get_changed_applied_settings() -> PackedStringArray: + return self._settings_applied + +## Return the keys of settings whose effective values changed at least once since the previous `_process()` call. +## The array is cleared after each frame, so it only contains settings changed during the current frame. +func get_changed_settings() -> PackedStringArray: + return self._settings_changed + +## Return the keys of settings whose staged values were set or cleared at least once since the previous `_process()` call. +## The array is cleared after each frame, so it only contains settings changed during the current frame. +func get_changed_staged_settings() -> PackedStringArray: + return self._settings_staged_changed + +## Load values from `config` into matching settings. See `Registry.from_config()` for full details. +func from_config(config: ConfigFile) -> void: + self._registry.from_config(config) + +## Serialize settings to a `ConfigFile` and returns it. +## `filter` controls which settings are included; defaults to exported settings only. +## See `Registry.to_config()` for full details. +func to_config(filter: Callable = Registry._include_exported) -> ConfigFile: + return self._registry.to_config(filter) + +func _on_applied(key: StringName) -> void: + self._settings_applied.append(key) + self.applied.emit(key) + +func _on_changed(key: StringName) -> void: + self._settings_changed.append(key) + self.changed.emit(key) + +func _on_staged_changed(key: StringName) -> void: + self._settings_staged_changed.append(key) + self.staged_changed.emit(key) diff --git a/game/addons/kenyoni/app_settings/app_settings.gd.uid b/game/addons/kenyoni/app_settings/app_settings.gd.uid new file mode 100644 index 00000000..9d28c4fc --- /dev/null +++ b/game/addons/kenyoni/app_settings/app_settings.gd.uid @@ -0,0 +1 @@ +uid://s0mbaify7qp4 diff --git a/game/addons/kenyoni/app_settings/icon.svg b/game/addons/kenyoni/app_settings/icon.svg new file mode 100644 index 00000000..6ccd6b18 --- /dev/null +++ b/game/addons/kenyoni/app_settings/icon.svg @@ -0,0 +1,93 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/game/addons/kenyoni/app_settings/icon.svg.import b/game/addons/kenyoni/app_settings/icon.svg.import new file mode 100644 index 00000000..dbb3502f --- /dev/null +++ b/game/addons/kenyoni/app_settings/icon.svg.import @@ -0,0 +1,44 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://na0dkspiigrh" +path="res://.godot/imported/icon.svg-b11363c5846b6bc3fcd532dfda71ceda.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/kenyoni/app_settings/icon.svg" +dest_files=["res://.godot/imported/icon.svg-b11363c5846b6bc3fcd532dfda71ceda.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=0.016 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/game/addons/kenyoni/app_settings/plugin.cfg b/game/addons/kenyoni/app_settings/plugin.cfg new file mode 100644 index 00000000..940b1551 --- /dev/null +++ b/game/addons/kenyoni/app_settings/plugin.cfg @@ -0,0 +1,19 @@ +[plugin] + +name="AppSettings" +description="A flexible and lightweight settings library that handles validation, applying, saving, and resetting of app/game settings with minimal boilerplate." +author="Kenyoni Software" +version="1.0.1" +script="plugin.gd" +license="MIT" +repository="https://github.com/kenyoni-software/godot-addons" +keywords=[ + "node" +] +classifiers=[ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: MIT License" +] + +[plugin.dependencies] +godot=">=4.4" diff --git a/game/addons/kenyoni/app_settings/plugin.gd b/game/addons/kenyoni/app_settings/plugin.gd new file mode 100644 index 00000000..a6974b6c --- /dev/null +++ b/game/addons/kenyoni/app_settings/plugin.gd @@ -0,0 +1,3 @@ + +@tool +extends EditorPlugin diff --git a/game/addons/kenyoni/app_settings/plugin.gd.uid b/game/addons/kenyoni/app_settings/plugin.gd.uid new file mode 100644 index 00000000..ba1024bd --- /dev/null +++ b/game/addons/kenyoni/app_settings/plugin.gd.uid @@ -0,0 +1 @@ +uid://s7v2ollg02gy diff --git a/game/addons/kenyoni/app_settings/registry.gd b/game/addons/kenyoni/app_settings/registry.gd new file mode 100644 index 00000000..fa5b02da --- /dev/null +++ b/game/addons/kenyoni/app_settings/registry.gd @@ -0,0 +1,216 @@ +extends RefCounted +## Container that owns and manages a collection of `Setting` objects. + +const Setting := preload("res://addons/kenyoni/app_settings/setting.gd") + +## Emitted immediately after a setting's `apply()` completes. `key` is the key of the applied setting. +signal applied(key: StringName) +## Emitted when a setting's effective value changes, either by direct assignment or when a staged value is committed. `key` is the key of the changed setting. +signal changed(key: StringName) +## Emitted when a setting's staged value is set or cleared. `key` is the key of the affected setting. +signal staged_changed(key: StringName) + +var _settings: Dictionary[StringName, Setting] = {} + +## Register `setting` with this registry and sets its internal registry reference to `self`. +## +## Fails with a push_error if: +## - The key ends with `/` or contains `//`. +## - The setting is already registered with a different registry. +## - A setting with the same key already exists in this registry. +func add_setting(setting: Setting) -> void: + if setting.key().ends_with("/") || setting.key().contains("//"): + push_error("Setting keys should not end with a slash or contain an empty section.") + return + if setting._registry != null && setting._registry != self: + push_error("Setting with key '%s' already belongs to another registry.".format(setting.key())) + return + if self.has_setting(setting.key()): + push_error("Setting with key '%s' already exists.".format(setting.key())) + return + setting._registry = self + self._settings[setting.key()] = setting + +## Return `true` if a setting with `key` exists in this registry. +func has_setting(key: StringName) -> bool: + return self._settings.has(key) + +## Return the `Setting` for `key`, or `null` if no such setting exists. +func get_setting(key: StringName) -> Setting: + return self._settings.get(key, null) + +## Remove the setting identified by `key`. Does nothing if the key does not exist. +func remove_setting(key: StringName) -> void: + var setting: Setting = self.get_setting(key) + if setting != null: + self._settings.erase(key) + +## Return all `Setting` objects whose keys begin with `section`. +## +## `section` is a key prefix; pass an empty string to match all settings. +## `depth` limits how many additional `/`-separated levels below `section` are included; `-1` means unlimited. +## `filter` is a `Callable` with signature `func(setting: Setting) -> bool`; only settings for which it returns `true` are included. Defaults to `_exclude_internal`. +func get_section(section: String, depth: int = -1, filter: Callable = _exclude_internal) -> Array[Setting]: + assert(!section.ends_with("/"), "key should not end with a slash") + + var max_level: int = -1 + if depth != -1: + max_level = section.count("/") + depth + if section != "": + section += "/" + + var settings: Array[Setting] = [] + for key: StringName in self._settings: + if max_level != -1 && key.count("/") > max_level: + continue + + if !key.begins_with(section): + continue + + var setting: Setting = self._settings[key] + if filter.is_valid() && !filter.call(setting): + continue + + settings.append(setting) + + return settings + +## Return the keys of all settings whose keys begin with `section`. +## +## `section` is a key prefix; pass an empty string to match all settings. +## `depth` limits how many additional `/`-separated levels below `section` are included; `-1` means unlimited. +## `filter` is a `Callable` with signature `func(setting: Setting) -> bool`; only matching settings contribute keys. Defaults to `_exclude_internal`. +func get_section_keys(section: String, depth: int = -1, filter: Callable = _exclude_internal) -> PackedStringArray: + assert(!section.ends_with("/"), "key should not end with a slash") + + var max_level: int = -1 + if depth != -1: + max_level = section.count("/") + depth + if section != "": + section += "/" + + var keys: PackedStringArray = [] + for key: StringName in self._settings: + if max_level != -1 && key.count("/") > max_level: + continue + + if !key.begins_with(section): + continue + + if filter.is_valid() && !filter.call(self._settings[key]): + continue + + keys.append(key) + + return keys + +## Return the names of the immediate child sections under `parent_section`. The names order is not guaranteed to be stable. +## +## A child section name is the single path component that follows `parent_section` in a matching key. +## For example, given a key `"graphics/display/vsync"` and `parent_section = "graphics"`, this returns `["display"]`. +## Pass an empty string for `parent_section` to get the top-level section names. +## `filter` is a `Callable` with signature `func(setting: Setting) -> bool`. +func get_sub_sections(parent_section: String = "", filter: Callable = _exclude_internal) -> PackedStringArray: + assert(!parent_section.ends_with("/"), "key should not end with a slash") + var sub_section_level: int = parent_section.count("/") + if parent_section != "": + sub_section_level += 1 + + if parent_section != "": + parent_section += "/" + + var seen: Dictionary[String, bool] = {} + for key: StringName in self._settings: + if key.count("/") <= sub_section_level || !key.begins_with(parent_section): + continue + + var setting: Setting = self._settings[key] + if filter.is_valid() && !filter.call(setting): + continue + + seen[key.get_slice("/", sub_section_level)] = true + + return PackedStringArray(seen.keys()) + +## Set the value of the setting identified by `key`. +## Emits a warning if `key` does not exist. Staged mode, readonly mode, and validation all apply. +func set_value(key: StringName, value: Variant) -> void: + var setting: Setting = self.get_setting(key) + if setting == null: + push_warning("Setting with key '%s' not found."%key) + return + setting.set_value(value) + +## Return the current effective value of the setting identified by `key`. +## Emits a warning and returns `null` if `key` does not exist. +func get_value(key: StringName) -> Variant: + var setting: Setting = self.get_setting(key) + if setting == null: + push_warning("Setting with key '%s' not found."%key) + return null + return setting.value() + +## Call `apply()` on every registered setting unconditionally. +func apply_all() -> void: + for setting: Setting in self._settings.values(): + setting.apply() + +## Call `apply()` only on settings that have a pending staged value. +func apply_staged_values() -> void: + for setting: Setting in self._settings.values(): + if setting._has_staged_value: + setting.apply() + +## Discard all pending staged values. `staged_changed` is emitted for each setting that had a staged value. +func discard_staged_values() -> void: + for setting: Setting in self._settings.values(): + if setting.has_staged_value(): + setting.discard_staged_value() + +## Return `true` if at least one registered setting has a pending staged value. +func has_staged_values() -> bool: + for setting: Setting in self._settings.values(): + if setting.has_staged_value(): + return true + return false + +## Load values from `config` into matching settings. +## +## For each `section`/`key` pair in the `ConfigFile`, the corresponding setting key is constructed as `"section/key"` and looked up in the registry. +## Only settings that exist, are exported, and are not readonly are updated. +## Unknown keys emit a warning. +func from_config(config: ConfigFile) -> void: + for section: String in config.get_sections(): + for key: String in config.get_section_keys(section): + var setting: Setting = self.get_setting(section.path_join(key)) + if setting == null: + push_warning("Setting '%s/%s' not found.".format(section, key)) + continue + # checking readonly is duplicated here, but it might be possible that set_value might push a warning when trying to set a readonly setting + if setting.is_exported() && !setting.is_readonly(): + setting.set_value(config.get_value(section, key)) + +## Serialize settings to a `ConfigFile`. +## +## Each setting's key is split on the last `/`: the left part becomes the `ConfigFile` section and the right part becomes the config key. Settings without a `/` in their key are placed in the empty-string section. +## `filter` is a `Callable` with signature `func(setting: Setting) -> bool` that controls which settings are included. Defaults to `_include_exported`. +func to_config(filter: Callable = _include_exported) -> ConfigFile: + var config: ConfigFile = ConfigFile.new() + for key: StringName in self._settings: + var setting: Setting = self.get_setting(key) + if filter.is_valid() && !filter.call(setting): + continue + if key.count("/") > 0: + var keys: PackedStringArray = key.rsplit("/", true, 1) + config.set_value(keys[0], keys[1], setting.value()) + else: + config.set_value("", key, setting.value()) + return config + +## Return `true` for non-internal settings. +static func _exclude_internal(setting: Setting) -> bool: + return !setting.is_internal() + +## Return `true` for exported settings. +static func _include_exported(setting: Setting) -> bool: + return setting.is_exported() diff --git a/game/addons/kenyoni/app_settings/registry.gd.uid b/game/addons/kenyoni/app_settings/registry.gd.uid new file mode 100644 index 00000000..d184b9d9 --- /dev/null +++ b/game/addons/kenyoni/app_settings/registry.gd.uid @@ -0,0 +1 @@ +uid://c5m00sjogbel0 diff --git a/game/addons/kenyoni/app_settings/setting.gd b/game/addons/kenyoni/app_settings/setting.gd new file mode 100644 index 00000000..6dcf363f --- /dev/null +++ b/game/addons/kenyoni/app_settings/setting.gd @@ -0,0 +1,234 @@ +extends RefCounted +## A single configurable value identified by a hierarchical key. +## +## Each setting holds a current effective value, a default value, optional validation and apply callables, and metadata. +## Settings are owned by a `Registry`, which must be assigned before `apply()` is called. +## +## **Staged mode** — when enabled, `set_value()` stores a pending value rather than applying it immediately. +## The pending value becomes the effective value only when `apply()` is called. +## +## **Readonly mode** — when enabled, all writes via `set_value()` and `reset()` are silently ignored. +class_name _KenyoniAppSettingSetting + +const Registry := preload("res://addons/kenyoni/app_settings/registry.gd") + +## The `Registry` this setting belongs to. Assigned by `Registry.add_setting()`. +var _registry: Registry = null +## Current effective value. +var _value: Variant +## Called every time `apply()` executes. Signature: `func(setting: Setting) -> void`. +var _apply_fn: Callable = Callable() +## Called to validate a candidate value before assignment. Must return `true` to accept the value. Signature: `func(setting: Setting, value: Variant) -> bool`. +var _validate_fn: Callable = Callable() +## Unique hierarchical key, e.g. `"graphics/display/fullscreen"`. +var _key: StringName +## Value restored by `reset()`. Set at construction and never changed afterwards. +var _default_value: Variant = null + +## Whether staged mode is active. +var _is_staged: bool = false +## The pending staged value. Only meaningful when `_has_staged_value` is `true`. +var _staged_value: Variant = null +## `true` when a staged value is pending. +var _has_staged_value: bool = false +## When `true`, all write operations are silently ignored. +var _is_readonly: bool = false +## When `true`, the setting is excluded from auto-generated UIs. +var _is_internal: bool = false + +## Create a setting with the given `key_` and `default_value_`. +## The default value is assigned as the initial effective value; `apply()` is not called. +func _init(key_: StringName, default_value_: Variant) -> void: + self._key = key_ + self._default_value = default_value_ + self._value = default_value_ + +## Assign `new_value` after passing it through the validator. +## +## - If the setting is readonly, the call is ignored. +## - If `new_value` equals the current effective value and no staged value is pending, nothing happens. +## - If `new_value` equals the current effective value but a staged value is pending, the staged value is cleared. +## - In staged mode, `new_value` is stored as a pending staged value and `staged_changed` is emitted on the registry. +## - In normal mode, `new_value` is set immediately, `changed` is emitted, and `apply()` is called. +func set_value(new_value: Variant) -> void: + if !self.validate(new_value): + return + self._set_value_no_validation(new_value) + +## Return the current effective value. Does not reflect any pending staged value. +func value() -> Variant: + return self._value + +## Return the hierarchical key that uniquely identifies this setting. +func key() -> StringName: + return self._key + +## Return the default value supplied at construction time. +func default_value() -> Variant: + return self._default_value + +## Return true if the value is valid according to the validate callable or if no validate callable is set. +func validate(value_: Variant) -> bool: + if self._validate_fn.is_valid(): + return self._validate_fn.call(self, value_) + return true + +## Commit the current state and triggers side-effects. +## +## If a staged value is pending: +## - It replaces the effective value. +## - `staged_changed` is emitted on the registry. +## - `changed` is emitted on the registry only if the staged value differed from the previous effective value. +## +## Regardless of staged state, the apply callable is invoked (if set) and `applied` is emitted on the registry. +func apply() -> void: + if self._has_staged_value: + var is_different: bool = self._staged_value != self._value + self._value = self._staged_value + self._staged_value = null + self._has_staged_value = false + if self._registry != null: + self._registry.staged_changed.emit(self._key) + if is_different: + self._registry.changed.emit(self._key) + if self._apply_fn.is_valid(): + self._apply_fn.call(self) + self._registry.applied.emit(self._key) + +## Reset the setting to its default value. Has no effect when readonly. +func reset() -> void: + self._set_value_no_validation(self._default_value) + +## Enable or disable staged mode. +## When `staged` is `false`, any pending staged value is discarded. +## Return `self` to allow method chaining. +func set_staged(staged: bool = true) -> _KenyoniAppSettingSetting: + self._is_staged = staged + if !staged: + self.discard_staged_value() + return self + +## Set the validate callable. Signature: `func(setting: Setting, value: Variant) -> bool`. +## Return `self` to allow method chaining. +func set_validate_fn(fn: Callable) -> _KenyoniAppSettingSetting: + self._validate_fn = fn + return self + +## Set the apply callable. Signature: `func(setting: Setting) -> void`. +## Return `self` to allow method chaining. +func set_apply_fn(fn: Callable) -> _KenyoniAppSettingSetting: + self._apply_fn = fn + return self + +## Return `true` when staged mode is active. +func is_staged_mode() -> bool: + return self._is_staged + +## Return `true` when a staged value is pending and has not yet been applied. +func has_staged_value() -> bool: + return self._has_staged_value + +## Discard any pending staged value. +## Emit `staged_changed` on the registry if a value was actually cleared. +func discard_staged_value() -> void: + if !self._has_staged_value: + return + self._staged_value = null + self._has_staged_value = false + if self._registry != null: + self._registry.staged_changed.emit(self._key) + +## Return the pending staged value, or `null` if none exists. +## Use `has_staged_value()` to distinguish a stored `null` from the absence of a staged value. +func staged_value() -> Variant: + return self._staged_value + +## Return the staged value if one is pending, otherwise return the current effective value. +func staged_or_value() -> Variant: + if self._has_staged_value: + return self._staged_value + return self._value + +## Enable or disables readonly mode. While readonly, `set_value()` and `reset()` are silently ignored. +## Returns `self` to allow method chaining. +func set_readonly(readonly: bool = true) -> _KenyoniAppSettingSetting: + self._is_readonly = readonly + return self + +## Return `true` when the setting is readonly. +func is_readonly() -> bool: + return self._is_readonly + +## Mark or unmarks the setting as internal. Internal settings should be excluded from auto-generated UIs. +## Returns `self` to allow method chaining. +func set_internal(internal: bool = true) -> _KenyoniAppSettingSetting: + self._is_internal = internal + return self + +## Return `true` when the setting is marked as internal. +func is_internal() -> bool: + return self._is_internal + +# Metadata +# +# Metadata follows Godot's `property_info` conventions where applicable. +# +# Built-in keys: +# - `exported` (`bool`): whether the setting is included by `Registry.to_config()`. Defaults to `true`. +# - `description` (`String`): human-readable label for display in a settings UI. + +## Set whether the setting should be exported when generating configuration files. +## Returns `self` to allow method chaining. +func set_exported(exported: bool = true) -> _KenyoniAppSettingSetting: + self.set_meta(&"exported", exported) + return self + +## Return `true` when the setting is exported to config files. `true` by default. +func is_exported() -> bool: + return self.get_meta(&"exported", true) + +## Set a human-readable description string. +## Returns `self` to allow method chaining. +func set_description(text: String) -> _KenyoniAppSettingSetting: + self.set_meta(&"description", text) + return self + +## Return the description string, or an empty string if none is set. +func description() -> String: + return self.get_meta(&"description", "") + +## Store an arbitrary metadata value under `meta_key`. Fluent wrapper around `Object.set_meta()`. +## Returns `self` to allow method chaining. +func add_meta(meta_key: StringName, val: Variant) -> _KenyoniAppSettingSetting: + self.set_meta(meta_key, val) + return self + +## Internal setter that skips the validate callable. +## +## - If the setting is readonly, the call is ignored. +## - If `new_value` equals the current effective value and no staged value is pending, nothing happens. +## - If `new_value` equals the current effective value but a staged value is pending, the staged value is cleared. +## - In staged mode, `new_value` is stored as a pending staged value and `staged_changed` is emitted. +## - In normal mode, `new_value` is set immediately, `changed` is emitted, and `apply()` is called. +func _set_value_no_validation(new_value: Variant) -> void: + if self._is_readonly: + return + if self._value == new_value: + if self._has_staged_value: + self.discard_staged_value() + return + + if self._is_staged: + if self._has_staged_value && self._staged_value == new_value: + return + self._staged_value = new_value + self._has_staged_value = true + if self._registry != null: + self._registry.staged_changed.emit(self._key) + return + else: + self._value = new_value + + if self._registry != null: + self._registry.changed.emit(self._key) + self.apply() diff --git a/game/addons/kenyoni/app_settings/setting.gd.uid b/game/addons/kenyoni/app_settings/setting.gd.uid new file mode 100644 index 00000000..59e41aea --- /dev/null +++ b/game/addons/kenyoni/app_settings/setting.gd.uid @@ -0,0 +1 @@ +uid://dmjyp52dm0c6o diff --git a/game/assets/localisation/locales/en_GB/menus.csv b/game/assets/localisation/locales/en_GB/menus.csv index 12225bba..0509774a 100644 --- a/game/assets/localisation/locales/en_GB/menus.csv +++ b/game/assets/localisation/locales/en_GB/menus.csv @@ -79,7 +79,14 @@ SFX_BUS;SFX Volume OPTIONS_SOUND_EXPLODE_EARS;Explode Eardrums on Startup? OPTIONS_CONTROLS;Controls -OPTIONS_OTHER;Other + +;; Mods Tab +OPTIONS_MODS;Mods +OPTIONS_MODS_LOAD_LIST;Load List + +;; Victoria 2 Tab +OPTIONS_VICTORIA_2;Victoria 2 +OPTIONS_VICTORIA_2_BASE_DEFINES_PATH;Base Defines Path ;; Multiplayer Menu MP_BACK;X diff --git a/game/project.godot b/game/project.godot index dbf23256..a64ff6aa 100644 --- a/game/project.godot +++ b/game/project.godot @@ -29,9 +29,10 @@ buses/default_bus_layout="res://assets/audio/default_bus_layout.tres" [autoload] -Resolution="*res://src/Autoload/Resolution.gd" +GameSettings="*uid://ca2dw5ms1hgr3" +Vic2Settings="*uid://bx1bmxelv4qvw" +ModSettings="*uid://bb5mucsux10sj" WindowOverride="*res://src/Autoload/WindowOverride.gd" -GuiScale="*res://src/Autoload/GuiScale.gd" Events="*res://src/Autoload/Events/Events.gd" GameLoader="*res://src/Autoload/GameLoader.gd" SoundManager="*res://src/Autoload/SoundManager.gd" @@ -59,7 +60,7 @@ window/size/window_height_override.editor=0 [editor_plugins] -enabled=PackedStringArray("res://addons/MusicMetadata/plugin.cfg", "res://addons/keychain/plugin.cfg") +enabled=PackedStringArray("res://addons/MusicMetadata/plugin.cfg", "res://addons/kenyoni/app_settings/plugin.cfg", "res://addons/keychain/plugin.cfg") [filesystem] diff --git a/game/src/Autoload/GuiScale.gd b/game/src/Autoload/GuiScale.gd deleted file mode 100644 index b8af59c8..00000000 --- a/game/src/Autoload/GuiScale.gd +++ /dev/null @@ -1,62 +0,0 @@ -extends Node - -const error_guiscale : float = -1.0 - -@export -var minimum_guiscale : float = 0.1 - -const _starting_guiscales : Dictionary = { - float(0.5) : &"0.5x", - float(0.75): &"0.75x", - float(1) : &"1x", - float(1.5) : &"1.5x", - float(2) : &"2x", -} - -var _guiscales: Dictionary - -#Similar to Resolution.gd, but we don't bother checking for strings from files -#and we have floats instead of vector2 integers - -func _ready() -> void: - assert(minimum_guiscale > 0, "Minimum gui scale must be positive") - for guiscale_value : float in _starting_guiscales: - add_guiscale(guiscale_value, _starting_guiscales[guiscale_value]) - assert(not _guiscales.is_empty(), "No valid starting gui scales!") - -func has_guiscale(guiscale_value : float) -> bool: - return guiscale_value in _guiscales - -func add_guiscale(guiscale_value: float, guiscale_name: StringName=&"") -> bool: - if has_guiscale(guiscale_value): return true - var scale_dict := { value = guiscale_value } - if not guiscale_name.is_empty(): - scale_dict.display_name = guiscale_name - else: - scale_dict.display_name = StringName("%sx" % guiscale_value) - if guiscale_value < minimum_guiscale: - push_error("GUI scale %s is smaller than the minimum %s" % [scale_dict.display_name, minimum_guiscale]) - return false - _guiscales[guiscale_value] = scale_dict - return true - -#returns floats -func get_guiscale_value_list() -> Array: - var list := _guiscales.keys() - list.sort_custom(func(a : float, b : float) -> bool: return a > b) - return list - -func get_guiscale_display_name(guiscale_value : float) -> StringName: - return _guiscales.get(guiscale_value, {display_name = &"unknown gui scale"}).display_name - -func get_current_guiscale() -> float: - return get_tree().root.content_scale_factor - -func set_guiscale(guiscale:float) -> void: - print("New GUI scale: %f" % guiscale) - if not has_guiscale(guiscale): - push_warning("Setting GUI Scale to non-standard value %sx" % [guiscale]) - get_tree().root.content_scale_factor = guiscale - -func reset_guiscale() -> void: - set_guiscale(get_current_guiscale()) diff --git a/game/src/Autoload/GuiScale.gd.uid b/game/src/Autoload/GuiScale.gd.uid deleted file mode 100644 index 1b4d45a0..00000000 --- a/game/src/Autoload/GuiScale.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dv3llyqmfa8gg diff --git a/game/src/Autoload/MusicManager/MusicManager.gd b/game/src/Autoload/MusicManager/MusicManager.gd index 281f704d..c5373735 100644 --- a/game/src/Autoload/MusicManager/MusicManager.gd +++ b/game/src/Autoload/MusicManager/MusicManager.gd @@ -165,18 +165,8 @@ func _ready() -> void: add_ootb_music() #don't start the current song for compat mode, do that from #GameStart so we can wait until the music is loaded - var settings := GameSettings.load_from_file("user://settings.cfg") - if not settings.has_section("audio"): return - set_paused(not settings.get_value("audio", "startup_music", true)) - for key : String in settings.get_section_keys("audio"): - if not key.ends_with("_BUS"): continue - var bus_name := key - if key == "MASTER_BUS": bus_name = "Master" - var bus_index := AudioServer.get_bus_index(bus_name) - if bus_index == -1: - push_error("Could not find bus '%s'.", bus_name) - continue - AudioServer.set_bus_volume_db(bus_index, linear_to_db(settings.get_value("audio", key, 100.0) / 100)) + var auto_start_music := GameSettings.get_setting(GameSettings.AUDIO_MUSIC_START_PLAY) + set_startup_music(auto_start_music.value()) func set_startup_music(play : bool) -> void: set_paused(not play) diff --git a/game/src/Autoload/Resolution.gd b/game/src/Autoload/Resolution.gd deleted file mode 100644 index 20522dfe..00000000 --- a/game/src/Autoload/Resolution.gd +++ /dev/null @@ -1,158 +0,0 @@ -extends Node - -signal resolution_added(value : Vector2i) - -const error_resolution : Vector2i = Vector2i(-1,-1) - -var default_resolution : Vector2i = error_resolution - -@export -var minimum_resolution : Vector2i = Vector2i(1,1) - -# REQUIREMENTS: -# * SS-130, SS-131, SS-132, SS-133 -const _starting_resolutions : Array[Vector2i] = [ - Vector2i(3840,2160), - Vector2i(2560,1080), - Vector2i(1920,1080), - Vector2i(1366,768), - Vector2i(1536,864), - Vector2i(1280,720), - Vector2i(1440,900), - Vector2i(1600,900), - Vector2i(1024,600), - Vector2i(800,600) -] - -var _resolutions : Array[Vector2i] - -const _regex_pattern : String = "(\\d+)\\s*[xX,]\\s*(\\d+)" -var _regex : RegEx - -func _ready() -> void: - assert(minimum_resolution.x > 0 and minimum_resolution.y > 0, "Minimum resolution must be positive!") - for resolution_value : Vector2i in _starting_resolutions: - add_resolution(resolution_value) - - default_resolution = Vector2i( - ProjectSettings.get_setting("display/window/size/viewport_width"), - ProjectSettings.get_setting("display/window/size/viewport_height") - ) - if default_resolution > minimum_resolution: - add_resolution(default_resolution) - - assert(not _resolutions.is_empty(), "No valid starting resolutions!") - - _regex = RegEx.new() - var err := _regex.compile(_regex_pattern) - assert(err == OK, "Resolution RegEx failed to compile!") - -func has_resolution(resolution_value : Vector2i) -> bool: - return resolution_value in _resolutions - -func add_resolution(resolution_value : Vector2i) -> bool: - if has_resolution(resolution_value): return true - if resolution_value.x < minimum_resolution.x or resolution_value.y < minimum_resolution.y: - push_error("Resolution %dx%d is smaller than minimum (%dx%d)" % [resolution_value.x, resolution_value.y, minimum_resolution.x, minimum_resolution.y]) - return false - _resolutions.append(resolution_value) - resolution_added.emit(resolution_value) - return true - -func get_resolution_value_list() -> Array[Vector2i]: - var list : Array[Vector2i] = [] - # Return a sorted copy instead of a reference to the private array - list.append_array(_resolutions) - list.sort_custom(func(a : Vector2i, b : Vector2i) -> bool: return a > b) - return list - -func get_resolution_value_from_string(resolution_string : String) -> Vector2i: - if not resolution_string.is_empty(): - var result := _regex.search(resolution_string) - if result: return Vector2i(result.get_string(1).to_int(), result.get_string(2).to_int()) - return error_resolution - -func get_current_resolution() -> Vector2i: - var viewport := get_viewport() - if viewport != null: - var window := viewport.get_window() - if window != null: - match window.mode: - Window.MODE_EXCLUSIVE_FULLSCREEN, Window.MODE_FULLSCREEN: - return window.content_scale_size - _: - return window.size - push_error("Trying to get resolution before window exists!") - return error_resolution - -func set_resolution(resolution : Vector2i) -> void: - if not has_resolution(resolution): - push_warning("Setting resolution to non-standard value %sx%s" % [resolution.x, resolution.y]) - var viewport := get_viewport() - if viewport != null: - var window := viewport.get_window() - if window != null: - if Engine.is_embedded_in_editor(): return - match window.mode: - Window.MODE_EXCLUSIVE_FULLSCREEN, Window.MODE_FULLSCREEN: - window.content_scale_size = resolution - _: - window.size = resolution - window.content_scale_size = Vector2i(0,0) - return - push_error("Trying to set resolution before window exists!") - -func set_resolution_from(load_value : Variant) -> Vector2i: - var target_resolution := Resolution.error_resolution - match typeof(load_value): - TYPE_VECTOR2I: - target_resolution = load_value - return target_resolution - TYPE_STRING, TYPE_STRING_NAME: - target_resolution = Resolution.get_resolution_value_from_string(load_value) - return target_resolution - if Resolution.add_resolution(target_resolution): - Resolution.set_resolution(target_resolution) - return target_resolution - -func get_current_window_mode() -> Window.Mode: - var viewport := get_viewport() - if viewport != null: - var window := viewport.get_window() - if window != null: - return window.mode - push_error("Trying to get window mode before it exists!") - return Window.MODE_WINDOWED - -func set_window_mode(mode : Window.Mode) -> void: - var viewport := get_viewport() - if viewport != null: - var window := viewport.get_window() - if window != null: - if Engine.is_embedded_in_editor(): return - var current_resolution := get_current_resolution() - var current_monitor := window.current_screen - window.mode = mode - window.current_screen = current_monitor - set_resolution(current_resolution) - return - push_error("Trying to set window mode before it exists!") - -func get_current_monitor() -> int: - var viewport := get_viewport() - if viewport != null: - var window := viewport.get_window() - if window != null: - return window.current_screen - push_error("Trying to get monitor index before window exists!") - return 0 - -func set_monitor(index : int) -> void: - var viewport := get_viewport() - if viewport != null: - var window := viewport.get_window() - if window != null: - if Engine.is_embedded_in_editor(): return - window.current_screen = index - return - push_error("Trying to set monitor index before window exists!") diff --git a/game/src/Autoload/Resolution.gd.uid b/game/src/Autoload/Resolution.gd.uid deleted file mode 100644 index aae0e078..00000000 --- a/game/src/Autoload/Resolution.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dwn88q0823oee diff --git a/game/src/Autoload/Settings/GameSettings.gd b/game/src/Autoload/Settings/GameSettings.gd new file mode 100644 index 00000000..b1d2e291 --- /dev/null +++ b/game/src/Autoload/Settings/GameSettings.gd @@ -0,0 +1,403 @@ +extends "res://addons/kenyoni/app_settings/app_settings.gd" + +enum SaveGameFormat { + BINARY, + TEXT +} +const SAVE_GAME_FORMAT_DISPLAY_NAMES: PackedStringArray = [ + "OPTIONS_GENERAL_BINARY", + "OPTIONS_GENERAL_TEXT" +] + +enum AutoSaveInterval { + MONTHLY, + BIMONTHLY, + YEARLY, + BIYEARLY, + NEVER +} +const AUTO_SAVE_INTERVAL_DISPLAY_NAMES: PackedStringArray = [ + "OPTIONS_GENERAL_AUTOSAVE_MONTHLY", + "OPTIONS_GENERAL_AUTOSAVE_BIMONTHLY", + "OPTIONS_GENERAL_AUTOSAVE_YEARLY", + "OPTIONS_GENERAL_AUTOSAVE_BIYEARLY", + "OPTIONS_GENERAL_AUTOSAVE_NEVER" +] + +enum RefreshRate { + VSYNC, + VSYNC_ADAPTIVE, + VSYNC_MAILBOX, + _30HZ, + _60HZ, + _90HZ, + _120HZ, + _144HZ, + _365HZ, + UNLIMITED +} +const REFRESH_RATE_DISPLAY_NAMES: PackedStringArray = [ + "VSync", + "Adaptive VSync", + "Mailbox VSynx", + "30hz", + "60hz", + "90hz", + "120hz", + "144hz", + "365hz", + "Unlimited" +] + +enum GraphicsDetail { + LOW, + MEDIUM, + HIGH, + ULTRA, + CUSTOM +} +const GRAPHICS_DETAIL_DISPLAY_NAMES: PackedStringArray = [ + "OPTIONS_VIDEO_QUALITY_LOW", + "OPTIONS_VIDEO_QUALITY_MEDIUM", + "OPTIONS_VIDEO_QUALITY_HIGH", + "OPTIONS_VIDEO_QUALITY_ULTRA", + "OPTIONS_VIDEO_QUALITY_CUSTOM" +] + +const RESOLUTIONS: Array[Vector2i] = [ + Vector2i(3840, 2160), + Vector2i(2560, 1080), + Vector2i(1920, 1080), + Vector2i(1366, 768), + Vector2i(1536, 864), + Vector2i(1280, 720), + Vector2i(1440, 900), + Vector2i(1600, 900), + Vector2i(1024, 600), + Vector2i(800, 600) +] +const RESOLUTION_DISPLAY_NAMES: PackedStringArray = [ + "3840x2160", + "2560x1080", + "1920x1080", + "1366x768", + "1536x864", + "1280x720", + "1440x900", + "1600x900", + "1024x600", + "800x600" +] + +const SCREEN_MODES: PackedInt32Array = [ + DisplayServer.WINDOW_MODE_FULLSCREEN, + DisplayServer.WINDOW_MODE_EXCLUSIVE_FULLSCREEN, + DisplayServer.WINDOW_MODE_WINDOWED +] +const SCREEN_MODES_DISPLAY_NAMES: PackedStringArray = [ + "OPTIONS_VIDEO_FULLSCREEN", + "OPTIONS_VIDEO_BORDERLESS", + "OPTIONS_VIDEO_WINDOWED" +] + +const SETTINGS_FILE := "user://settings.cfg" + +const GENERAL_SAVE_GAME_FORMAT := &"general/save_game_format" +const GENERAL_AUTO_SAVE_INTERVAL := &"general/auto_save_interval" +const GENERAL_LANGUAGE := &"general/language" + +const VIDEO_RESOLUTION := &"video/resolution" +const VIDEO_GUI_SCALING_FACTOR := &"video/gui_scaling_factor" +const VIDEO_SCREEN_MODE := &"video/screen_mode" +const VIDEO_MONITOR_SELECTION := &"video/monitor_selection" +const VIDEO_REFRESH_RATE := &"video/refresh_rate" +const VIDEO_QUALITY_PRESET := &"video/quality_preset" + +const AUDIO_MASTER_VOLUME := &"audio/master_volume" +const AUDIO_MUSIC_VOLUME := &"audio/music_volume" +const AUDIO_SFX_VOLUME := &"audio/sfx_volume" +const AUDIO_MUSIC_START_PLAY := &"audio/music_start_play" + +const INTERNAL_WINDOW_WIDTH = &"display/window/size/viewport_width" +const INTERNAL_WINDOW_HEIGHT = &"display/window/size/viewport_height" + +var video_revert_group := RevertGroup.new("OPTIONS_VIDEO_REVERT_DIALOG_TITLE", "OPTIONS_VIDEO_REVERT_DIALOG_TEXT") + +func _init() -> void: + Localisation.initialize() + # General Settings + self.add_setting(Setting.new(GENERAL_SAVE_GAME_FORMAT, SaveGameFormat.BINARY) + .set_description("The type of format to save.") + .set_staged() + .set_validate_fn(_enum_validate) + .add_meta(&"display_name", "OPTIONS_GENERAL_SAVEFORMAT") + .add_meta(&"type", TYPE_INT) + .add_meta(&"hint", PROPERTY_HINT_ENUM) + .add_meta(&"values", SaveGameFormat.values()) + .add_meta(&"display_values", SAVE_GAME_FORMAT_DISPLAY_NAMES)) + self.add_setting(Setting.new(GENERAL_AUTO_SAVE_INTERVAL, AutoSaveInterval.MONTHLY) + .set_description("The ingame interval to auto-save by.") + .set_staged() + .set_validate_fn(_enum_validate) + .add_meta(&"display_name", "OPTIONS_GENERAL_AUTOSAVE") + .add_meta(&"type", TYPE_INT) + .add_meta(&"hint", PROPERTY_HINT_ENUM) + .add_meta(&"values", AutoSaveInterval.values()) + .add_meta(&"display_values", AUTO_SAVE_INTERVAL_DISPLAY_NAMES)) + self.add_setting(Setting.new(GENERAL_LANGUAGE, Localisation.get_default_locale()) + .set_description("The language of the game.") + .set_validate_fn( + func(_stg: Setting, val: Variant) -> bool: + return TranslationServer.has_translation_for_locale(val, false) + ) + .set_apply_fn(func(stg: Setting) -> void: TranslationServer.set_locale(stg.value() as String)) + .add_meta(&"type", TYPE_STRING) + .add_meta(&"hint", PROPERTY_HINT_ENUM) + .add_meta(&"values", _get_loaded_locales()) + .add_meta(&"display_values", _get_loaded_locale_names())) + + # Video Settings + self.add_setting(Setting.new(VIDEO_RESOLUTION, Vector2i( + ProjectSettings.get_setting("display/window/size/viewport_width"), + ProjectSettings.get_setting("display/window/size/viewport_height") + )) + .set_description("The video resolution for the game.") + .set_apply_fn(_resolution_apply) + .add_meta(&"type", TYPE_VECTOR2I) + .add_meta(&"hint", PROPERTY_HINT_ENUM) + .add_meta(&"values", RESOLUTIONS) + .add_meta(&"display_values", RESOLUTION_DISPLAY_NAMES) + .add_meta(&"translate_value_function", _resolution_translate_value) + .add_meta(&"no_default", true) + .add_meta(&"revert_group", video_revert_group)) + self.add_setting(Setting.new(VIDEO_GUI_SCALING_FACTOR, 1) + .set_description("The scaling factor for the game's GUI.") + .set_staged() + .set_apply_fn(_gui_scaling_factor_apply) + .add_meta(&"display_name", "OPTIONS_VIDEO_GUI_SCALE") + .add_meta(&"type", TYPE_FLOAT) + .add_meta(&"hint", PROPERTY_HINT_RANGE) + .add_meta(&"max", 2) + .add_meta(&"step", 0.1)) + self.add_setting(Setting.new(VIDEO_SCREEN_MODE, DisplayServer.WINDOW_MODE_FULLSCREEN) + .set_description("The windowing mode of the game.") + .set_validate_fn(_screen_mode_validate) + .set_apply_fn(_screen_mode_apply) + .add_meta(&"type", TYPE_INT) + .add_meta(&"hint", PROPERTY_HINT_ENUM) + .add_meta(&"values", SCREEN_MODES) + .add_meta(&"display_values", SCREEN_MODES_DISPLAY_NAMES) + .add_meta(&"revert_group", video_revert_group)) + self.add_setting(Setting.new(VIDEO_MONITOR_SELECTION, 0) + .set_description("The monitor to display the game to.") + .set_validate_fn(_enum_validate) + .set_apply_fn(_refresh_rate_apply) + .add_meta(&"type", TYPE_INT) + .add_meta(&"hint", PROPERTY_HINT_ENUM) + .add_meta(&"values", range(DisplayServer.get_screen_count())) + .add_meta(&"display_values", _get_monitor_display_names()) + .add_meta(&"revert_group", video_revert_group)) + self.add_setting(Setting.new(VIDEO_REFRESH_RATE, RefreshRate.VSYNC) + .set_description("The refresh for the game.") + .set_staged() + .set_validate_fn(_enum_validate) + .set_apply_fn(_refresh_rate_apply) + .add_meta(&"type", TYPE_INT) + .add_meta(&"hint", PROPERTY_HINT_ENUM) + .add_meta(&"values", RefreshRate.values()) + .add_meta(&"display_values", REFRESH_RATE_DISPLAY_NAMES)) + self.add_setting(Setting.new(VIDEO_QUALITY_PRESET, GraphicsDetail.MEDIUM) + .set_description("Graphical detail level of the game.") + .set_staged() + .set_validate_fn(_enum_validate) + .add_meta(&"display_name", "OPTIONS_VIDEO_QUALITY") + .add_meta(&"type", TYPE_INT) + .add_meta(&"hint", PROPERTY_HINT_ENUM) + .add_meta(&"values", GraphicsDetail.values()) + .add_meta(&"display_values", GRAPHICS_DETAIL_DISPLAY_NAMES)) + + # Audio Settings + self.add_setting(Setting.new(AUDIO_MASTER_VOLUME, 100) + .set_description("Game's Master volume.") + .set_apply_fn(_volume_apply.bind(AudioServer.get_bus_index(&"Master"))) + .set_validate_fn(_volume_validate) + .add_meta(&"display_name", "MASTER_BUS") + .add_meta(&"type", TYPE_INT) + .add_meta(&"hint", PROPERTY_HINT_RANGE)) + self.add_setting(Setting.new(AUDIO_MUSIC_VOLUME, 100) + .set_description("Game's Music volume.") + .set_apply_fn(_volume_apply.bind(AudioServer.get_bus_index(&"MUSIC_BUS"))) + .set_validate_fn(_volume_validate) + .add_meta(&"display_name", "MUSIC_BUS") + .add_meta(&"type", TYPE_INT) + .add_meta(&"hint", PROPERTY_HINT_RANGE)) + self.add_setting(Setting.new(AUDIO_SFX_VOLUME, 100) + .set_description("Game's Sound Effects volume.") + .set_apply_fn(_volume_apply.bind(AudioServer.get_bus_index(&"SFX_BUS"))) + .set_validate_fn(_volume_validate) + .add_meta(&"display_name", "SFX_BUS") + .add_meta(&"type", TYPE_INT) + .add_meta(&"hint", PROPERTY_HINT_RANGE)) + self.add_setting(Setting.new(AUDIO_MUSIC_START_PLAY, true) + .set_description("Whether to start the game with music already playing.") + .add_meta(&"display_name", "OPTIONS_SOUND_EXPLODE_EARS") + .add_meta(&"type", TYPE_BOOL)) + + self.add_setting(Setting.new(INTERNAL_WINDOW_WIDTH, ProjectSettings.get_setting(INTERNAL_WINDOW_WIDTH)) + .set_internal()) + self.add_setting(Setting.new(INTERNAL_WINDOW_HEIGHT, ProjectSettings.get_setting(INTERNAL_WINDOW_HEIGHT)) + .set_internal()) + + self.load() + self.apply_all() + + _set_window_override(get_setting(VIDEO_RESOLUTION).value() as Vector2i) + + await tree_entered + # Preserves GUI scaling factor on scene change + get_tree().scene_changed.connect(_gui_scaling_factor_apply.bind(self.get_setting(VIDEO_GUI_SCALING_FACTOR))) + +func save() -> Error: + return self.to_config().save(SETTINGS_FILE) + +func load() -> Error: + var cfg: ConfigFile = ConfigFile.new() + var err: Error = cfg.load(SETTINGS_FILE) + if err == Error.ERR_FILE_NOT_FOUND: + return Error.OK + if err != Error.OK: + return err + self.from_config(cfg) + + return Error.OK + +func get_game_resolution() -> Vector2i: + var window := get_window() + assert(window != null) + match window.mode: + Window.MODE_EXCLUSIVE_FULLSCREEN, Window.MODE_FULLSCREEN: + return window.content_scale_size + _: + return window.size + +func _enum_validate(stg: Setting, val: Variant) -> bool: + return val in stg.get_meta(&"values") + +func _get_loaded_locales() -> PackedStringArray: + var result := TranslationServer.get_loaded_locales() + var default_locale := Localisation.get_default_locale() + if default_locale not in result: + result.push_back(default_locale) + return result + +func _get_loaded_locale_names() -> PackedStringArray: + var locales_country_rename : Dictionary = ProjectSettings.get_setting("internationalization/locale/country_short_name", {}) + + var result: PackedStringArray = [] + for locale: String in _get_loaded_locales(): + var locale_name := TranslationServer.get_locale_name(locale) + var comma_idx := locale_name.find(", ") + if comma_idx != -1: + var locale_country_name := locale_name.substr(comma_idx + 2) + locale_country_name = locales_country_rename.get(locale_country_name, "") + if not locale_country_name.is_empty(): + locale_name = locale_name.left(comma_idx + 2) + locale_country_name + result.append(locale_name) + return result + +func _resolution_apply(stg: Setting) -> void: + if Engine.is_embedded_in_editor(): + _push_embedded_warning(str(stg.value())) + return + var window := get_window() + if window == null: return + match window.mode: + Window.MODE_EXCLUSIVE_FULLSCREEN, Window.MODE_FULLSCREEN: + window.content_scale_size = stg.value() as Vector2i + _: + window.size = stg.value() as Vector2i + _set_window_override(window.size) + window.content_scale_size = Vector2i(0,0) + +func _resolution_translate_value(stg: Setting, value: Variant, _display_value: String) -> String: + var resolution := value as Vector2i + var format_dict := { + "width": resolution.x, + "height": resolution.y + } + format_dict["name"] = tr("OPTIONS_VIDEO_RESOLUTION_{width}x{height}".format(format_dict)) + if format_dict["name"].begins_with("OPTIONS"): format_dict["name"] = "" + var result := "OPTIONS_VIDEO_RESOLUTION_DIMS" + if format_dict["name"]: result += "_NAMED" + if resolution == stg.default_value(): result += "_DEFAULT" + format_dict["width"] = Localisation.tr_number(resolution.x) + format_dict["height"] = Localisation.tr_number(resolution.y) + return tr(result).format(format_dict) + +func _gui_scaling_factor_apply(stg: Setting) -> void: + if not is_inside_tree(): return + get_tree().root.content_scale_factor = stg.value() + +func _screen_mode_validate(_stg: Setting, val: Variant) -> bool: + return _get_enum_values(&"DisplayServer", &"WindowMode").find_key(val) != null + +func _screen_mode_apply(stg: Setting) -> void: + if Engine.is_embedded_in_editor(): + var window_mode_name: String = _get_enum_values(&"DisplayServer", &"WindowMode").find_key(stg.value()) + _push_embedded_warning("DisplayServer." + window_mode_name) + return + var window := get_window() + if window == null: return + window.mode = stg.value() + _set_window_override(window.size) + +func _refresh_rate_apply(stg: Setting) -> void: + var refresh_rate := stg.value() as RefreshRate + match refresh_rate: + RefreshRate.VSYNC: DisplayServer.window_set_vsync_mode(DisplayServer.VSyncMode.VSYNC_ENABLED) + RefreshRate.VSYNC_ADAPTIVE: DisplayServer.window_set_vsync_mode(DisplayServer.VSyncMode.VSYNC_ADAPTIVE) + RefreshRate.VSYNC_MAILBOX: DisplayServer.window_set_vsync_mode(DisplayServer.VSyncMode.VSYNC_MAILBOX) + _: + DisplayServer.window_set_vsync_mode(DisplayServer.VSyncMode.VSYNC_DISABLED) + match refresh_rate: + RefreshRate._30HZ: Engine.max_fps = 30 + RefreshRate._60HZ: Engine.max_fps = 60 + RefreshRate._90HZ: Engine.max_fps = 90 + RefreshRate._120HZ: Engine.max_fps = 120 + RefreshRate._144HZ: Engine.max_fps = 144 + RefreshRate._365HZ: Engine.max_fps = 365 + RefreshRate.UNLIMITED: Engine.max_fps = 0 + +func _get_monitor_display_names() -> PackedStringArray: + var result: PackedStringArray = [] + for index: int in range(DisplayServer.get_screen_count()): + result.append("Display " + str(index + 1)) + return result + +func _volume_apply(stg: Setting, bus_index: int) -> void: + const RATIO_FOR_LINEAR : float = 100 + AudioServer.set_bus_volume_db(bus_index, linear_to_db(stg.value() / RATIO_FOR_LINEAR)) + +func _volume_validate(stg: Setting, val: Variant) -> bool: + return val >= stg.get_meta(&"min", 0) && val <= stg.get_meta(&"max", 120) + +func _set_window_override(size : Vector2i) -> void: + get_setting(INTERNAL_WINDOW_WIDTH).set_value(size.x) + get_setting(INTERNAL_WINDOW_HEIGHT).set_value(size.y) + +func _get_enum_values(clazz: StringName, enum_name: StringName) -> Dictionary[StringName, int]: + var result: Dictionary[StringName, int] = {} + for value_name: String in ClassDB.class_get_enum_constants(clazz, enum_name): + result[value_name] = ClassDB.class_get_integer_constant(clazz, value_name) + return result + +func _push_embedded_warning(value: String) -> void: + push_warning("Setting screen mode to ", value, " for an editor embedded window, this will not be reflected in the embedded window.") + +class RevertGroup: + var title : String + var text : String + + func _init(ti: String = "Please Confirm...", tex: String = "< reverting in {time} seconds >") -> void: + title = ti + text = tex diff --git a/game/src/Autoload/Settings/GameSettings.gd.uid b/game/src/Autoload/Settings/GameSettings.gd.uid new file mode 100644 index 00000000..499a1d06 --- /dev/null +++ b/game/src/Autoload/Settings/GameSettings.gd.uid @@ -0,0 +1 @@ +uid://ca2dw5ms1hgr3 diff --git a/game/src/Autoload/Settings/ModSettings.gd b/game/src/Autoload/Settings/ModSettings.gd new file mode 100644 index 00000000..24ba7ca5 --- /dev/null +++ b/game/src/Autoload/Settings/ModSettings.gd @@ -0,0 +1,41 @@ +extends "res://addons/kenyoni/app_settings/app_settings.gd" + +const SETTINGS_FILE: String = "user://mods.cfg" + +const MODS_LOAD_LIST := &"mods/load_list" + +func _init() -> void: + self.add_setting(Setting.new(MODS_LOAD_LIST, PackedStringArray()) + .set_validate_fn(_load_list_validate) + .add_meta(&"type", TYPE_PACKED_STRING_ARRAY)) + + self.load() + self.apply_all() + +func _notification(what: int) -> void: + if what != NOTIFICATION_WM_CLOSE_REQUEST: return + save() + +func save() -> Error: + return self.to_config().save(SETTINGS_FILE) + +func load() -> Error: + var cfg: ConfigFile = ConfigFile.new() + var err: Error = cfg.load(SETTINGS_FILE) + if err == Error.ERR_FILE_NOT_FOUND: + return Error.OK + if err != Error.OK: + return err + self.from_config(cfg) + + return Error.OK + +func get_load_list() -> PackedStringArray: + return get_setting(MODS_LOAD_LIST).value() as PackedStringArray + +func _load_list_validate(_stg: Setting, val: Variant) -> bool: + var result := true + var array := val as PackedStringArray + for path: String in array: + result = result and (path.is_relative_path() or path.is_absolute_path()) + return result diff --git a/game/src/Autoload/Settings/ModSettings.gd.uid b/game/src/Autoload/Settings/ModSettings.gd.uid new file mode 100644 index 00000000..c4b4f69e --- /dev/null +++ b/game/src/Autoload/Settings/ModSettings.gd.uid @@ -0,0 +1 @@ +uid://bb5mucsux10sj diff --git a/game/src/Autoload/Settings/Vic2Settings.gd b/game/src/Autoload/Settings/Vic2Settings.gd new file mode 100644 index 00000000..35441006 --- /dev/null +++ b/game/src/Autoload/Settings/Vic2Settings.gd @@ -0,0 +1,126 @@ +extends "res://addons/kenyoni/app_settings/app_settings.gd" + +const LEGACY_SETTINGS_FILES : PackedStringArray = [] + +const BASE_DEFINES_LEGACY_PATHS : PackedStringArray = [ + "general/base_defines_path" +] + +const SETTINGS_FILE: String = "user://vic2.cfg" + +const GENERAL_BASE_DEFINES_PATH := &"victoria_2/base_defines_path" + +var _base_path_find_dialog := FileDialog.new() + +func _init() -> void: + self.add_setting(Setting.new(GENERAL_BASE_DEFINES_PATH, "") + .set_validate_fn(_base_defines_path_validate) + .add_meta(&"type", TYPE_STRING) + .add_meta(&"hint", PROPERTY_HINT_DIR) + .add_meta(&"legacy_paths", BASE_DEFINES_LEGACY_PATHS)) + + self.load() + self.apply_all() + + for setting: Setting in get_section("", -1, func(_stg: Setting) -> bool: return true): + if not setting.validate(setting.value()): + setting._set_value_no_validation(setting.default_value()) + + _base_path_find_dialog.mode_overrides_title = false + _base_path_find_dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR + _base_path_find_dialog.access = FileDialog.ACCESS_FILESYSTEM + _base_path_find_dialog.show_hidden_files = true + _base_path_find_dialog.title = "VIC2_DIR_DIALOG_TITLE" + _base_path_find_dialog.cancel_button_text = "VIC2_DIR_DIALOG_CANCEL" + _base_path_find_dialog.ok_button_text = "VIC2_DIR_DIALOG_SELECT" + _base_path_find_dialog.size = Vector2i(935, 175) + _base_path_find_dialog.disable_3d = true + _base_path_find_dialog.process_mode = Node.PROCESS_MODE_WHEN_PAUSED + add_child(_base_path_find_dialog) + +func _notification(what: int) -> void: + if what != NOTIFICATION_WM_CLOSE_REQUEST: return + save() + +func save() -> Error: + return self.to_config().save(SETTINGS_FILE) + +func load() -> Error: + var cfg := ConfigFile.new() + var err : Error + + var settings_with_legacy: Array[Setting] = get_section("", -1, func(stg: Setting) -> bool: return stg.has_meta(&"legacy_paths")) + + for path: String in LEGACY_SETTINGS_FILES: + err = cfg.load(path) + if err != OK: continue + for setting: Setting in settings_with_legacy: + _load_legacy_value(cfg, setting) + + cfg.clear() + err = cfg.load(SETTINGS_FILE) + if err == Error.ERR_FILE_NOT_FOUND: return Error.OK + if err != Error.OK: return err + + for setting: Setting in settings_with_legacy: + _load_legacy_value(cfg, setting, true) + + self.from_config(cfg) + + return Error.OK + +func get_base_defines_path() -> String: + return get_setting(GENERAL_BASE_DEFINES_PATH).value() + +func find_base_path(search_path: String) -> String: + var result := GameSingleton.search_for_game_path(search_path) + if not result: + push_warning("Failed to find base path using ", search_path) + return result + +func show_base_path_find_dialog() -> void: + var setting := get_setting(GENERAL_BASE_DEFINES_PATH) + + get_tree().paused = true + var ok_button := _base_path_find_dialog.get_ok_button() + ok_button.auto_translate = true + var cancel_button := _base_path_find_dialog.get_cancel_button() + cancel_button.auto_translate = true + _base_path_find_dialog.canceled.connect(_on_base_path_find_dialog_failed, CONNECT_ONE_SHOT) + _base_path_find_dialog.dir_selected.connect(_on_base_path_find_dialog_dir_selected) + while not setting.value(): + _show_alert() + _base_path_find_dialog.popup_centered_ratio() + await _base_path_find_dialog.dir_selected + _base_path_find_dialog.canceled.disconnect(_on_base_path_find_dialog_failed) + get_tree().paused = false + +func _base_defines_path_validate(_stg: Setting, val: Variant) -> bool: + return (val as String).is_absolute_path() and DirAccess.dir_exists_absolute(val) + +func _show_alert() -> void: + OS.alert(tr("ERROR_ASSET_PATH_NOT_FOUND_MESSAGE"), tr("ERROR_ASSET_PATH_NOT_FOUND")) + +func _on_base_path_find_dialog_failed() -> void: + get_window().mode = Window.MODE_WINDOWED + _show_alert() + get_tree().root.propagate_notification(NOTIFICATION_WM_CLOSE_REQUEST) + get_tree().quit() + +func _on_base_path_find_dialog_dir_selected(dir : String) -> void: + var setting := get_setting(GENERAL_BASE_DEFINES_PATH) + setting.set_value(GameSingleton.search_for_game_path(dir)) + +func _load_legacy_value(config: ConfigFile, setting: Setting, skip_non_legacy: bool = false) -> void: + var split: PackedStringArray + + var legacy_paths: PackedStringArray = setting.get_meta(&"legacy_paths") + for legacy_setting_path in legacy_paths: + split = legacy_setting_path.rsplit("/", true, 1) + if not config.has_section_key(split[0], split[1]): continue + setting.set_value(config.get_value(split[0], split[1])) + if skip_non_legacy: return + + split = setting.key().rsplit("/", true, 1) + if not config.has_section_key(split[0], split[1]): return + setting.set_value(config.get_value(split[0], split[1])) diff --git a/game/src/Autoload/Settings/Vic2Settings.gd.uid b/game/src/Autoload/Settings/Vic2Settings.gd.uid new file mode 100644 index 00000000..bc496e83 --- /dev/null +++ b/game/src/Autoload/Settings/Vic2Settings.gd.uid @@ -0,0 +1 @@ +uid://bx1bmxelv4qvw diff --git a/game/src/Systems/Session/Map/MapView.gd b/game/src/Systems/Session/Map/MapView.gd index 681cdc91..360add17 100644 --- a/game/src/Systems/Session/Map/MapView.gd +++ b/game/src/Systems/Session/Map/MapView.gd @@ -159,14 +159,14 @@ func _viewport_to_map_coords(pos_viewport : Vector2) -> Vector2: return _world_to_map_coords(_viewport_to_world_coords(pos_viewport)) func look_at_map_position(pos : Vector2) -> void: - var viewport_centre : Vector2 = Vector2(0.5, 0.5) * _viewport_dims / GuiScale.get_current_guiscale() + var viewport_centre : Vector2 = Vector2(0.5, 0.5) * _viewport_dims / get_tree().root.content_scale_factor var pos_delta : Vector3 = _map_to_world_coords(pos) - _viewport_to_world_coords(viewport_centre) _camera.position.x += pos_delta.x _camera.position.z += pos_delta.z func zoom_in() -> void: _zoom_target -= _zoom_target_step - _zoom_position = (Vector2(0.5, 0.5) - _mouse_pos_viewport * GuiScale.get_current_guiscale() / _viewport_dims) * _zoom_position_multiplier + _zoom_position = (Vector2(0.5, 0.5) - _mouse_pos_viewport * get_tree().root.content_scale_factor / _viewport_dims) * _zoom_position_multiplier func zoom_out() -> void: _zoom_target += _zoom_target_step @@ -282,7 +282,7 @@ func _process(delta : float) -> void: _mouse_over_viewport = false province_unhovered.emit() - _viewport_dims = Vector2(Resolution.get_current_resolution()) + _viewport_dims = Vector2(GameSettings.get_game_resolution()) # Process movement _movement_process(delta) # Keep within map bounds @@ -317,7 +317,7 @@ func _movement_process(delta : float) -> void: func _edge_scrolling_vector() -> Vector2: if not _mouse_over_viewport: return Vector2() - var mouse_vector := _mouse_pos_viewport * GuiScale.get_current_guiscale() / _viewport_dims - Vector2(0.5, 0.5) + var mouse_vector := _mouse_pos_viewport * get_tree().root.content_scale_factor / _viewport_dims - Vector2(0.5, 0.5) # Only scroll if outside the move threshold. if abs(mouse_vector.x) < 0.5 - _edge_move_threshold and abs(mouse_vector.y) < 0.5 - _edge_move_threshold: return Vector2() diff --git a/game/src/Systems/Startup/GameStart.gd b/game/src/Systems/Startup/GameStart.gd index 6579d78e..1d93ab95 100644 --- a/game/src/Systems/Startup/GameStart.gd +++ b/game/src/Systems/Startup/GameStart.gd @@ -1,20 +1,10 @@ extends Control const LoadingScreen := preload("res://src/Systems/Startup/LoadingScreen.gd") -const SoundTabScene := preload("res://src/UI/GameMenu/OptionMenu/SoundTab.tscn") const GameMenuScene := preload("res://src/UI/GameMenu/GameMenu/GameMenu.tscn") @export_subgroup("Nodes") @export var loading_screen : LoadingScreen -@export var vic2_dir_dialog : FileDialog - -@export_subgroup("") -@export var section_name : String = "general" -@export var setting_name : String = "base_defines_path" - -var _settings_base_path : String = "" -var _mod_list : PackedStringArray = [] -var _vic2_settings : GameSettings func _enter_tree() -> void: Keychain.keep_binding_check = func(action_name : StringName) -> bool: @@ -47,7 +37,6 @@ func _ready() -> void: "Hotkeys": Keychain.InputGroup.new("UI") } - Localisation.initialize() if ArgumentParser.get_option_value(&"help"): # For some reason this doesn't get freed properly # Godot will always quit before it frees the active StreamPlayback resource @@ -56,87 +45,50 @@ func _ready() -> void: get_tree().quit() return - _vic2_settings = GameSettings.load_from_file("user://vic2.cfg") - _vic2_settings.changed.connect(_vic2_settings.save) - _vic2_settings.load_deprecated_file("user://settings.cfg", { "general": "base_defines_path" }) - _settings_base_path = _vic2_settings.get_value(section_name, setting_name, "") - await _setup_compatibility_mode_paths() await loading_screen.start_loading_screen(_initialize_game) -func _on_vic2_dir_dialog_failed() -> void: - get_window().mode = Window.MODE_WINDOWED - OS.alert(tr("ERROR_ASSET_PATH_NOT_FOUND_MESSAGE"), tr("ERROR_ASSET_PATH_NOT_FOUND")) - get_tree().root.propagate_notification(NOTIFICATION_WM_CLOSE_REQUEST) - get_tree().quit() - -var _selected_base_path : String -func _on_vic2_dir_dialog_dir_selected(dir : String) -> void: - _selected_base_path = GameSingleton.search_for_game_path(dir) - if not _selected_base_path: - OS.alert(tr("ERROR_ASSET_PATH_NOT_FOUND_MESSAGE"), tr("ERROR_ASSET_PATH_NOT_FOUND")) - func _setup_compatibility_mode_paths() -> void: # To test mods, set your base path to Victoria II and then pass mods in reverse order with --mod="mod" for each mod. var arg_base_path : String = ArgumentParser.get_option_value(&"base-path") var arg_search_path : String = ArgumentParser.get_option_value(&"search-path") - var actual_base_path : String = "" + var setting := Vic2Settings.get_setting(Vic2Settings.GENERAL_BASE_DEFINES_PATH) if arg_base_path: if arg_search_path: push_warning("Exact base path and search base path arguments both used:\nBase: ", arg_base_path, "\nSearch: ", arg_search_path) - actual_base_path = arg_base_path - elif arg_search_path: + setting.set_value(arg_base_path) + elif Vic2Settings.get_base_defines_path().is_empty() and arg_search_path: # This will also search for a Steam install if the hint doesn't help - actual_base_path = GameSingleton.search_for_game_path(arg_search_path) - if not actual_base_path: - push_warning("Failed to find assets using search hint: ", arg_search_path) - - if not actual_base_path: - if _settings_base_path: - actual_base_path = _settings_base_path + setting.set_value(Vic2Settings.find_base_path(arg_search_path)) + + if Vic2Settings.get_base_defines_path().is_empty(): + # Check if the program is being run from inside the install directory, + # and if not also search for a Steam install + var root_base_path := Vic2Settings.find_base_path(".") + if root_base_path.is_empty(): + await Vic2Settings.show_base_path_find_dialog() else: - # Check if the program is being run from inside the install directory, - # and if not also search for a Steam install - actual_base_path = GameSingleton.search_for_game_path(".") - if not actual_base_path: - get_tree().paused = true - var ok_button := vic2_dir_dialog.get_ok_button() - ok_button.auto_translate = true - # WHY WON'T CANCEL AUTO-TRANSLATE WORK NOW?!?!?!? - var cancel_button := vic2_dir_dialog.get_cancel_button() - cancel_button.auto_translate = true - vic2_dir_dialog.canceled.connect(_on_vic2_dir_dialog_failed, CONNECT_ONE_SHOT) - vic2_dir_dialog.dir_selected.connect(_on_vic2_dir_dialog_dir_selected) - while not _selected_base_path: - vic2_dir_dialog.popup_centered_ratio() - await vic2_dir_dialog.dir_selected - actual_base_path = _selected_base_path - get_tree().paused = false - - if not _settings_base_path: - _settings_base_path = actual_base_path - _vic2_settings.set_value(section_name, setting_name, _settings_base_path) - _vic2_settings.emit_changed() + setting.set_value(root_base_path) # Add mod paths - var mod_status_file := ConfigFile.new() - mod_status_file.load("user://mods.cfg") - _mod_list = mod_status_file.get_value("mods", "load_list", []) + var load_list_setting := ModSettings.get_setting(ModSettings.MODS_LOAD_LIST) + var load_list := ModSettings.get_load_list() for mod in ArgumentParser.get_option_value(&"mod"): - if mod not in _mod_list and mod != "": - _mod_list.push_back(mod) + if mod not in load_list and mod != "": + load_list.append(mod) + load_list_setting.set_value(load_list) func _load_compatibility_mode() -> void: - if GameSingleton.set_compatibility_mode_roots(_settings_base_path) != OK: + if GameSingleton.set_compatibility_mode_roots(Vic2Settings.get_base_defines_path()) != OK: push_error("Errors setting game roots!") CursorManager.initial_cursor_setup() setup_title_theme() - if GameSingleton.load_defines_compatibility_mode(_mod_list) != OK: + if GameSingleton.load_defines_compatibility_mode(ModSettings.get_load_list()) != OK: push_error("Errors loading game defines!") SoundSingleton.load_sounds() @@ -160,7 +112,7 @@ func setup_title_theme() -> void: # REQUIREMENTS # * FS-333, FS-334, FS-335, FS-341 func _initialize_game() -> void: - if not _settings_base_path: + if Vic2Settings.get_base_defines_path().is_empty(): return var start := Time.get_ticks_usec() diff --git a/game/src/Systems/Startup/GameStart.tscn b/game/src/Systems/Startup/GameStart.tscn index 77cb5852..94c27832 100644 --- a/game/src/Systems/Startup/GameStart.tscn +++ b/game/src/Systems/Startup/GameStart.tscn @@ -7,7 +7,7 @@ [ext_resource type="Texture2D" uid="uid://cgdnixsyh7bja" path="res://assets/graphics/splash_image.png" id="4_5b6yq"] [ext_resource type="VideoStream" uid="uid://bj077egtjnlfr" path="res://assets/graphics/splash_startup.ogv" id="5_8euyy"] -[node name="GameStartup" type="Control" unique_id=561399280 node_paths=PackedStringArray("loading_screen", "vic2_dir_dialog")] +[node name="GameStartup" type="Control" unique_id=561399280 node_paths=PackedStringArray("loading_screen")] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 @@ -16,7 +16,6 @@ grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_e0cos") loading_screen = NodePath("LoadingScreen") -vic2_dir_dialog = NodePath("Vic2DirDialog") [node name="LoadingScreen" parent="." unique_id=337902464 instance=ExtResource("2_h0oiw")] visible = false @@ -52,17 +51,5 @@ stream = ExtResource("5_8euyy") autoplay = true expand = true -[node name="Vic2DirDialog" type="FileDialog" parent="." unique_id=1644780236] -process_mode = 2 -disable_3d = true -title = "VIC2_DIR_DIALOG_TITLE" -size = Vector2i(935, 175) -ok_button_text = "VIC2_DIR_DIALOG_SELECT" -cancel_button_text = "VIC2_DIR_DIALOG_CANCEL" -mode_overrides_title = false -file_mode = 2 -access = 2 -show_hidden_files = true - [connection signal="splash_end" from="SplashContainer" to="." method="_on_splash_container_splash_end"] [connection signal="finished" from="SplashContainer/SplashVideo" to="SplashContainer" method="_on_splash_startup_finished"] diff --git a/game/src/UI/GameMenu/GameMenu/LocaleButton.gd b/game/src/UI/GameMenu/GameMenu/LocaleButton.gd deleted file mode 100644 index 21bc83f0..00000000 --- a/game/src/UI/GameMenu/GameMenu/LocaleButton.gd +++ /dev/null @@ -1,82 +0,0 @@ -extends OptionButton - -const section_name : String = "localisation" -const setting_name : String = "locale" - -var settings_path := "user://settings.cfg" - -var _default_locale_index : int - -func _enter_tree() -> void: - var settings := GameSettings.load_from_file(settings_path) - settings.changed.connect(load_setting.bind(settings)) - item_selected.connect(set_setting.bind(settings)) - -func _ready() -> void: - var locales_country_rename : Dictionary = ProjectSettings.get_setting("internationalization/locale/country_short_name", {}) - - var locales_list := TranslationServer.get_loaded_locales() - var default_locale := Localisation.get_default_locale() - if default_locale not in locales_list: - locales_list.push_back(default_locale) - - for locale : String in locales_list: - # locale_name consists of a compulsory language name and optional script - # and country names, in the format: "[ (script)][, country]" - var locale_name := TranslationServer.get_locale_name(locale) - var comma_idx := locale_name.find(", ") - if comma_idx != -1: - var locale_country_name := locale_name.substr(comma_idx + 2) - locale_country_name = locales_country_rename.get(locale_country_name, "") - if not locale_country_name.is_empty(): - locale_name = locale_name.left(comma_idx + 2) + locale_country_name - - add_item(locale_name) - set_item_metadata(item_count - 1, locale) - - if locale == default_locale: - _default_locale_index = item_count - 1 - -func _notification(what : int) -> void: - match what: - NOTIFICATION_TRANSLATION_CHANGED: - _select_locale_by_string(TranslationServer.get_locale()) - -func _valid_index(index : int) -> bool: - return 0 <= index and index < item_count - -func load_setting(menu : GameSettings) -> void: - var load_value : Variant = menu.get_value(section_name, setting_name, Localisation.get_default_locale()) - match typeof(load_value): - TYPE_STRING, TYPE_STRING_NAME: - if _select_locale_by_string(load_value as String): - item_selected.emit(selected) - return - push_error("Setting value '%s' invalid for setting [%s] %s" % [load_value, section_name, setting_name]) - reset_setting() - -func _select_locale_by_string(locale : String) -> bool: - for idx : int in item_count: - if get_item_metadata(idx) == locale: - selected = idx - return true - selected = _default_locale_index - return false - -# REQUIREMENTS: -# * UIFUN-74 -func set_setting(index : int, menu : GameSettings) -> void: - menu.set_value(section_name, setting_name, get_item_metadata(index)) - -func reset_setting() -> void: - _select_locale_by_string(TranslationServer.get_locale()) - -# REQUIREMENTS: -# * SS-58 -# * UIFUN-323 -func _on_item_selected(index : int) -> void: - if _valid_index(index): - TranslationServer.set_locale(get_item_metadata(index)) - else: - push_error("Invalid LocaleButton index: %d" % index) - reset_setting() diff --git a/game/src/UI/GameMenu/GameMenu/LocaleButton.gd.uid b/game/src/UI/GameMenu/GameMenu/LocaleButton.gd.uid deleted file mode 100644 index c877b7c0..00000000 --- a/game/src/UI/GameMenu/GameMenu/LocaleButton.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dtmc4h78k8685 diff --git a/game/src/UI/GameMenu/GameMenu/LocaleButton.tscn b/game/src/UI/GameMenu/GameMenu/LocaleButton.tscn deleted file mode 100644 index 98bdfe0b..00000000 --- a/game/src/UI/GameMenu/GameMenu/LocaleButton.tscn +++ /dev/null @@ -1,13 +0,0 @@ -[gd_scene format=3 uid="uid://b7oncobnacxmt"] - -[ext_resource type="Script" uid="uid://dtmc4h78k8685" path="res://src/UI/GameMenu/GameMenu/LocaleButton.gd" id="1_ganev"] - -[node name="LocaleButton" type="OptionButton" unique_id=1593449628] -editor_description = "UI-600, UI-895, UI-896, UI-898, UI-899, UIFUN-322" -custom_minimum_size = Vector2(150, 0) -alignment = 2 -text_overrun_behavior = 2 -fit_to_longest_item = false -script = ExtResource("1_ganev") - -[connection signal="item_selected" from="." to="." method="_on_item_selected"] diff --git a/game/src/UI/GameMenu/MainMenu/MainMenu.gd b/game/src/UI/GameMenu/MainMenu/MainMenu.gd index f4599955..5c87f7ba 100644 --- a/game/src/UI/GameMenu/MainMenu/MainMenu.gd +++ b/game/src/UI/GameMenu/MainMenu/MainMenu.gd @@ -9,11 +9,28 @@ signal continue_button_pressed @export var _new_game_button : BaseButton +@export +var _bottom_margin : MarginContainer + +var _language_button : SettingsContainer.EnumOptionButton + # REQUIREMENTS: # * SS-3 func _ready() -> void: _on_new_game_button_visibility_changed() + var language_setting := GameSettings.get_setting(GameSettings.GENERAL_LANGUAGE) + _language_button = SettingsContainer.EnumOptionButton.new(language_setting) + _language_button.size_flags_horizontal = Control.SIZE_SHRINK_END + _bottom_margin.add_child(_language_button) + + GameSettings.changed.connect(self._on_setting_updated) + GameSettings.staged_changed.connect(self._on_setting_updated) + +func _on_setting_updated(key: StringName) -> void: + if key != GameSettings.GENERAL_LANGUAGE: return + _language_button.update() + # REQUIREMENTS: # * SS-14 # * UIFUN-32 diff --git a/game/src/UI/GameMenu/MainMenu/MainMenu.tscn b/game/src/UI/GameMenu/MainMenu/MainMenu.tscn index 0373bd18..4ba3e144 100644 --- a/game/src/UI/GameMenu/MainMenu/MainMenu.tscn +++ b/game/src/UI/GameMenu/MainMenu/MainMenu.tscn @@ -3,10 +3,9 @@ [ext_resource type="Theme" uid="uid://qoi3oec48jp0" path="res://assets/graphics/theme/main_menu.tres" id="1_1yri4"] [ext_resource type="Script" uid="uid://d0t3iw73mpivr" path="res://src/UI/GameMenu/MainMenu/MainMenu.gd" id="2_nm1fq"] [ext_resource type="Texture2D" uid="uid://dxys0wg0f0ic5" path="res://assets/graphics/OpenVicFINALREALTRANS.png" id="3_58ess"] -[ext_resource type="PackedScene" uid="uid://b7oncobnacxmt" path="res://src/UI/GameMenu/GameMenu/LocaleButton.tscn" id="3_amonp"] [ext_resource type="PackedScene" uid="uid://cen7wkmn6og66" path="res://src/UI/GameMenu/ReleaseInfoBox/ReleaseInfoBox.tscn" id="3_km0er"] -[node name="MainMenu" type="Control" unique_id=318440097 node_paths=PackedStringArray("_new_game_button")] +[node name="MainMenu" type="Control" unique_id=318440097 node_paths=PackedStringArray("_new_game_button", "_bottom_margin")] editor_description = "UI-13" layout_mode = 3 anchors_preset = 15 @@ -17,6 +16,7 @@ grow_vertical = 2 theme = ExtResource("1_1yri4") script = ExtResource("2_nm1fq") _new_game_button = NodePath("MenuPanel/MenuList/ButtonListMargin/ButtonList/NewGameButton") +_bottom_margin = NodePath("MenuPanel/MenuList/BottomMargin") [node name="MenuPanel" type="PanelContainer" parent="." unique_id=1123691100] layout_mode = 1 @@ -135,13 +135,6 @@ theme_type_variation = &"BottomMargin" [node name="ReleaseInfoBox" parent="MenuPanel/MenuList/BottomMargin" unique_id=1710064563 instance=ExtResource("3_km0er")] layout_mode = 2 -[node name="LocaleButton" parent="MenuPanel/MenuList/BottomMargin" unique_id=1657101050 instance=ExtResource("3_amonp")] -editor_description = "SS-87" -layout_mode = 2 -size_flags_horizontal = 8 -alignment = 0 -text_overrun_behavior = 4 - [connection signal="pressed" from="MenuPanel/MenuList/ButtonListMargin/ButtonList/NewGameButton" to="." method="_on_new_game_button_pressed"] [connection signal="visibility_changed" from="MenuPanel/MenuList/ButtonListMargin/ButtonList/NewGameButton" to="." method="_on_new_game_button_visibility_changed"] [connection signal="pressed" from="MenuPanel/MenuList/ButtonListMargin/ButtonList/ContinueButton" to="." method="_on_continue_button_pressed"] diff --git a/game/src/UI/GameMenu/OptionMenu/AutosaveIntervalSelector.gd b/game/src/UI/GameMenu/OptionMenu/AutosaveIntervalSelector.gd deleted file mode 100644 index 2c558621..00000000 --- a/game/src/UI/GameMenu/OptionMenu/AutosaveIntervalSelector.gd +++ /dev/null @@ -1,2 +0,0 @@ -extends SettingOptionButton - diff --git a/game/src/UI/GameMenu/OptionMenu/AutosaveIntervalSelector.gd.uid b/game/src/UI/GameMenu/OptionMenu/AutosaveIntervalSelector.gd.uid deleted file mode 100644 index 3d728672..00000000 --- a/game/src/UI/GameMenu/OptionMenu/AutosaveIntervalSelector.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://si04yyqrxja4 diff --git a/game/src/UI/GameMenu/OptionMenu/ControlsTab.tscn b/game/src/UI/GameMenu/OptionMenu/ControlsTab.tscn index 4d0c336d..efb76eb3 100644 --- a/game/src/UI/GameMenu/OptionMenu/ControlsTab.tscn +++ b/game/src/UI/GameMenu/OptionMenu/ControlsTab.tscn @@ -1,14 +1,19 @@ [gd_scene format=3 uid="uid://cdwymd51i4b2f"] -[ext_resource type="PackedScene" uid="uid://bq7ibhm0txl5p" path="res://addons/keychain/ShortcutEdit.tscn" id="1_fv8sh"] +[ext_resource type="PackedScene" uid="uid://cka0tjqrek13" path="res://src/UI/GameMenu/OptionMenu/KeychainShortcutEdit.tscn" id="1_5cbaf"] -[node name="Controls" type="Control" unique_id=1618691119] -layout_mode = 3 +[node name="Controls" type="MarginContainer" unique_id=742864330] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 +theme_override_constants/margin_left = 8 +theme_override_constants/margin_top = 8 +theme_override_constants/margin_right = 8 +theme_override_constants/margin_bottom = 8 -[node name="ShortcutEdit" parent="." unique_id=270667577 instance=ExtResource("1_fv8sh")] -layout_mode = 1 +[node name="Controls" parent="." unique_id=1297136943 instance=ExtResource("1_5cbaf")] +editor_description = "SS-27, UI-49, UIFUN-46" +layout_mode = 2 +metadata/_tab_index = 0 diff --git a/game/src/UI/GameMenu/OptionMenu/GeneralTab.gd b/game/src/UI/GameMenu/OptionMenu/GeneralTab.gd deleted file mode 100644 index 3d98678e..00000000 --- a/game/src/UI/GameMenu/OptionMenu/GeneralTab.gd +++ /dev/null @@ -1,9 +0,0 @@ -extends HBoxContainer - -@export var initial_focus: Control - -func _notification(what : int) -> void: - match(what): - NOTIFICATION_VISIBILITY_CHANGED: - if visible and is_inside_tree(): - initial_focus.grab_focus() diff --git a/game/src/UI/GameMenu/OptionMenu/GeneralTab.gd.uid b/game/src/UI/GameMenu/OptionMenu/GeneralTab.gd.uid deleted file mode 100644 index de72b7f2..00000000 --- a/game/src/UI/GameMenu/OptionMenu/GeneralTab.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dg8y1dvid41mm diff --git a/game/src/UI/GameMenu/OptionMenu/GeneralTab.tscn b/game/src/UI/GameMenu/OptionMenu/GeneralTab.tscn deleted file mode 100644 index 8e6b4c3b..00000000 --- a/game/src/UI/GameMenu/OptionMenu/GeneralTab.tscn +++ /dev/null @@ -1,81 +0,0 @@ -[gd_scene format=3 uid="uid://duwjal7sd7p6w"] - -[ext_resource type="Script" uid="uid://dg8y1dvid41mm" path="res://src/UI/GameMenu/OptionMenu/GeneralTab.gd" id="1_gbutn"] -[ext_resource type="PackedScene" uid="uid://b7oncobnacxmt" path="res://src/UI/GameMenu/GameMenu/LocaleButton.tscn" id="2_5cfd7"] -[ext_resource type="Script" uid="uid://b06h8rw7shprx" path="res://src/UI/GameMenu/OptionMenu/SettingNodes/SettingOptionButton.gd" id="2_msx2u"] -[ext_resource type="Script" uid="uid://si04yyqrxja4" path="res://src/UI/GameMenu/OptionMenu/AutosaveIntervalSelector.gd" id="2_t06tb"] - -[node name="GeneralTab" type="HBoxContainer" unique_id=1892269527 node_paths=PackedStringArray("initial_focus")] -editor_description = "UI-48, UIFUN-45" -alignment = 1 -script = ExtResource("1_gbutn") -initial_focus = NodePath("VBoxContainer/GridContainer/SavegameFormatSelector") - -[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=1333184019] -layout_mode = 2 - -[node name="Control" type="Control" parent="VBoxContainer" unique_id=1211064786] -layout_mode = 2 -size_flags_vertical = 3 -size_flags_stretch_ratio = 0.1 - -[node name="GridContainer" type="GridContainer" parent="VBoxContainer" unique_id=930258876] -layout_mode = 2 -size_flags_vertical = 3 -columns = 2 - -[node name="SavegameFormatLabel" type="Label" parent="VBoxContainer/GridContainer" unique_id=886608465] -layout_mode = 2 -text = "OPTIONS_GENERAL_SAVEFORMAT" - -[node name="SavegameFormatSelector" type="OptionButton" parent="VBoxContainer/GridContainer" unique_id=807681088] -editor_description = "UI-50" -layout_mode = 2 -focus_neighbor_bottom = NodePath("../AutosaveIntervalSelector") -selected = 0 -item_count = 2 -popup/item_0/text = "OPTIONS_GENERAL_BINARY" -popup/item_0/id = 0 -popup/item_1/text = "OPTIONS_GENERAL_TEXT" -popup/item_1/id = 1 -script = ExtResource("2_msx2u") -section_name = "general" -setting_name = "savegame_format" -default_selected = 0 - -[node name="AutosaveIntervalLabel" type="Label" parent="VBoxContainer/GridContainer" unique_id=407389934] -layout_mode = 2 -text = "OPTIONS_GENERAL_AUTOSAVE" - -[node name="AutosaveIntervalSelector" type="OptionButton" parent="VBoxContainer/GridContainer" unique_id=1634806442] -editor_description = "UI-15, UIFUN-19" -layout_mode = 2 -focus_neighbor_top = NodePath("../SavegameFormatSelector") -focus_neighbor_bottom = NodePath("../LocaleButton") -selected = 0 -item_count = 5 -popup/item_0/text = "OPTIONS_GENERAL_AUTOSAVE_MONTHLY" -popup/item_0/id = 0 -popup/item_1/text = "OPTIONS_GENERAL_AUTOSAVE_BIMONTHLY" -popup/item_1/id = 1 -popup/item_2/text = "OPTIONS_GENERAL_AUTOSAVE_YEARLY" -popup/item_2/id = 2 -popup/item_3/text = "OPTIONS_GENERAL_AUTOSAVE_BIYEARLY" -popup/item_3/id = 3 -popup/item_4/text = "OPTIONS_GENERAL_AUTOSAVE_NEVER" -popup/item_4/id = 4 -script = ExtResource("2_t06tb") -section_name = "general" -setting_name = "autosave_interval" -default_selected = 0 - -[node name="LocaleLabel" type="Label" parent="VBoxContainer/GridContainer" unique_id=218400055] -layout_mode = 2 -text = "OPTIONS_GENERAL_LANGUAGE" - -[node name="LocaleButton" parent="VBoxContainer/GridContainer" unique_id=36180585 instance=ExtResource("2_5cfd7")] -editor_description = "UI-79" -layout_mode = 2 -focus_neighbor_top = NodePath("../AutosaveIntervalSelector") -alignment = 0 -text_overrun_behavior = 4 diff --git a/game/src/UI/GameMenu/OptionMenu/GuiScaleSelector.gd b/game/src/UI/GameMenu/OptionMenu/GuiScaleSelector.gd deleted file mode 100644 index f3c7b22f..00000000 --- a/game/src/UI/GameMenu/OptionMenu/GuiScaleSelector.gd +++ /dev/null @@ -1,64 +0,0 @@ -extends SettingOptionButton - -# REQUIREMENTS -# * UIFUN-24 -# * UIFUN-31 - -@export -var default_value : float = GuiScale.error_guiscale - -func _find_guiscale_index_by_value(value : float) -> int: - for item_index : int in item_count: - if get_item_metadata(item_index) == value: - return item_index - return -1 - -func _sync_guiscales(to_select : float = GuiScale.get_current_guiscale()) -> void: - clear() - default_selected = -1 - selected = -1 - for guiscale_value : float in GuiScale.get_guiscale_value_list(): - add_item(GuiScale.get_guiscale_display_name(guiscale_value)) - set_item_metadata(item_count - 1, guiscale_value) - - if guiscale_value == default_value: - default_selected = item_count - 1 - - if guiscale_value == to_select: - selected = item_count - 1 - - if default_selected == -1: - default_selected = item_count - 1 - - if selected == -1: - selected = default_selected - -func _setup_button() -> void: - if default_value <= 0: - default_value = ProjectSettings.get_setting("display/window/stretch/scale") - GuiScale.add_guiscale(default_value, &"default") - _sync_guiscales() - -func _get_value_for_file(select_value : int): - if _valid_index(select_value): - return get_item_metadata(select_value) - else: - return null - -func _set_value_from_file(load_value : Variant) -> void: - if typeof(load_value) == TYPE_FLOAT: - var target_guiscale : float = load_value - selected = _find_guiscale_index_by_value(target_guiscale) - if selected != -1: return - if GuiScale.add_guiscale(target_guiscale): - _sync_guiscales(target_guiscale) - return - push_error("Setting value '%s' invalid for setting [%s] %s" % [load_value, section_name, setting_name]) - selected = default_selected - -func _on_option_selected(index : int, by_user : bool) -> void: - if _valid_index(index): - GuiScale.set_guiscale(get_item_metadata(index)) - else: - push_error("Invalid GuiScaleSelector index: %d" % index) - reset_setting(not by_user) diff --git a/game/src/UI/GameMenu/OptionMenu/GuiScaleSelector.gd.uid b/game/src/UI/GameMenu/OptionMenu/GuiScaleSelector.gd.uid deleted file mode 100644 index f4bee4de..00000000 --- a/game/src/UI/GameMenu/OptionMenu/GuiScaleSelector.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bt8wuvopl1kqv diff --git a/game/src/UI/GameMenu/OptionMenu/MonitorDisplaySelector.gd b/game/src/UI/GameMenu/OptionMenu/MonitorDisplaySelector.gd deleted file mode 100644 index 44f89f06..00000000 --- a/game/src/UI/GameMenu/OptionMenu/MonitorDisplaySelector.gd +++ /dev/null @@ -1,29 +0,0 @@ -extends SettingRevertButton - -func _setup_button() -> void: - clear() - for screen_index : int in DisplayServer.get_screen_count(): - # Placeholder option text awaiting _update_monitor_options_text() - add_item(str(screen_index + 1)) - _update_monitor_options_text() - default_selected = Resolution.get_current_monitor() - -func _notification(what : int) -> void: - match what: - NOTIFICATION_TRANSLATION_CHANGED: - _update_monitor_options_text() - -func _update_monitor_options_text() -> void: - for index : int in get_item_count(): - set_item_text(index, tr("OPTIONS_VIDEO_MONITOR").format({ "index": Localisation.tr_number(index + 1) })) - -func _on_option_selected(index : int, by_user : bool) -> void: - if _valid_index(index): - if by_user: - print("Start Revert Countdown!") - revert_dialog.show_dialog.call_deferred(self) - previous_index = Resolution.get_current_monitor() - Resolution.set_monitor(index) - else: - push_error("Invalid MonitorDisplaySelector index: %d" % index) - reset_setting(not by_user) diff --git a/game/src/UI/GameMenu/OptionMenu/MonitorDisplaySelector.gd.uid b/game/src/UI/GameMenu/OptionMenu/MonitorDisplaySelector.gd.uid deleted file mode 100644 index 1de56f65..00000000 --- a/game/src/UI/GameMenu/OptionMenu/MonitorDisplaySelector.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b1bxjetbdem3j diff --git a/game/src/UI/GameMenu/OptionMenu/OptionsMenu.gd b/game/src/UI/GameMenu/OptionMenu/OptionsMenu.gd index d5cd1ba7..a96fbdc8 100644 --- a/game/src/UI/GameMenu/OptionMenu/OptionsMenu.gd +++ b/game/src/UI/GameMenu/OptionMenu/OptionsMenu.gd @@ -1,21 +1,17 @@ extends Control +const AppSettings := preload("res://addons/kenyoni/app_settings/app_settings.gd") + # REQUIREMENTS # * SS-13 signal back_button_pressed @export var _tab_container : TabContainer - -@onready -var _settings := GameSettings.load_from_file("user://settings.cfg") +@export var _settings_container: PackedScene func _ready() -> void: - _tab_container.set_tab_title(0, "OPTIONS_GENERAL") - _tab_container.set_tab_title(1, "OPTIONS_VIDEO") - _tab_container.set_tab_title(2, "OPTIONS_SOUND") - _tab_container.set_tab_title(3, "OPTIONS_CONTROLS") - _tab_container.set_tab_title(4, "OPTIONS_OTHER") + GameSettings.settings_applied.connect(_on_settings_applied) # Prepare options menu before loading user settings var tab_bar : TabBar = _tab_container.get_tab_bar() @@ -27,14 +23,6 @@ func _ready() -> void: button_list.alignment = BoxContainer.ALIGNMENT_END tab_bar.add_child(button_list) - # REQUIREMENTS - # * UI-12 - # * UIFUN-14 - var reset_button := Button.new() - reset_button.text = "OPTIONS_RESET" - reset_button.pressed.connect(_settings.reset_settings) - button_list.add_child(reset_button) - # REQUIREMENTS # * UI-11 # * UIFUN-17 @@ -42,42 +30,68 @@ func _ready() -> void: back_button.text = "OPTIONS_BACK" back_button.pressed.connect(_on_back_button_pressed) button_list.add_child(back_button) - _save_overrides.call_deferred() - _settings.changed.emit() + _setup_settings() + +func _setup_settings() -> void: + _iterate_settings_sections(GameSettings) + _iterate_settings_sections(ModSettings) + _iterate_settings_sections(Vic2Settings) + + _tab_container.move_child(_tab_container.get_child(0), 3) + _tab_container.set_tab_title(2, "OPTIONS_SOUND") + _tab_container.set_tab_title(3, "OPTIONS_CONTROLS") + _tab_container.current_tab = 0 + +func _iterate_settings_sections(app_setting: AppSettings) -> void: + var tab_count := _tab_container.get_tab_count() - 1 + + var sections: PackedStringArray = app_setting.get_sub_sections() + for idx: int in range(sections.size()): + var section: String = sections[idx] + _setup_settings_section(app_setting, tab_count + idx, section) + +func _setup_settings_section(app_setting: AppSettings, section_index: int, section: String) -> void: + var all_internal := true + for setting: AppSettings.Setting in app_setting.get_section(section): + if setting.is_internal(): continue + all_internal = false + break + if all_internal: return + + var container: SettingsContainer = _settings_container.instantiate() + container.name = section.capitalize().validate_node_name() + container.section_key = section + _tab_container.add_child(container) + _tab_container.set_tab_title(section_index + 1, "OPTIONS_" + section.to_upper()) + + # all settings in section without a sub section + for setting: AppSettings.Setting in app_setting.get_section(section, 1): + container.add_setting(setting) + # add all sub sections and their settings + for sub_section: String in app_setting.get_sub_sections(section): + container.add_section(sub_section.capitalize()) + for setting: AppSettings.Setting in app_setting.get_section(section.path_join(sub_section)): + container.add_setting(setting) func _notification(what : int) -> void: match what: + NOTIFICATION_VISIBILITY_CHANGED: + set_process_input(is_visible_in_tree()) NOTIFICATION_CRASH, NOTIFICATION_WM_CLOSE_REQUEST: _on_window_close_requested() func _input(event : InputEvent) -> void: - if self.is_visible_in_tree(): - if event.is_action_pressed("ui_cancel"): - _on_back_button_pressed() + if event.is_action_pressed("ui_cancel"): + _on_back_button_pressed() + accept_event() func _on_back_button_pressed() -> void: back_button_pressed.emit() - _settings.save() - _save_overrides() + GameSettings.apply_staged_values() func _on_window_close_requested() -> void: - _settings.save() - _save_overrides() - -func _save_overrides() -> void: - var override_path : String = ProjectSettings.get_setting_with_override("application/config/project_settings_override") - if override_path.is_empty(): - return - var override_settings := GameSettings.load_from_file(override_path) - override_settings.set_block_signals(true) - if FileAccess.file_exists(override_path): - if override_settings.load(override_path) != OK: - push_error("Failed to load overrides from %s" % override_path) - override_settings.set_value("display", "window/size/mode", Resolution.get_current_window_mode()) - var resolution : Vector2i = Resolution.get_current_resolution() - override_settings.set_value("display", "window/size/viewport_width", resolution.x) - override_settings.set_value("display", "window/size/viewport_height", resolution.y) - if override_settings.save(override_path) != OK: - push_error("Failed to save overrides to %s" % override_path) - override_settings.set_block_signals(false) + GameSettings.apply_staged_values() + +func _on_settings_applied() -> void: + GameSettings.save() diff --git a/game/src/UI/GameMenu/OptionMenu/OptionsMenu.tscn b/game/src/UI/GameMenu/OptionMenu/OptionsMenu.tscn index 2b3599d3..01016059 100644 --- a/game/src/UI/GameMenu/OptionMenu/OptionsMenu.tscn +++ b/game/src/UI/GameMenu/OptionMenu/OptionsMenu.tscn @@ -2,11 +2,8 @@ [ext_resource type="Theme" uid="uid://fbxssqcg1s0m" path="res://assets/graphics/theme/options_menu.tres" id="1_0up1d"] [ext_resource type="Script" uid="uid://i4sj0j5sss2a" path="res://src/UI/GameMenu/OptionMenu/OptionsMenu.gd" id="1_tlein"] -[ext_resource type="PackedScene" uid="uid://bq3awxxjn1tuw" path="res://src/UI/GameMenu/OptionMenu/VideoTab.tscn" id="2_ji8xr"] -[ext_resource type="PackedScene" uid="uid://cbtgwpx2wxi33" path="res://src/UI/GameMenu/OptionMenu/SoundTab.tscn" id="3_4w35t"] -[ext_resource type="PackedScene" uid="uid://duwjal7sd7p6w" path="res://src/UI/GameMenu/OptionMenu/GeneralTab.tscn" id="3_6gvf6"] -[ext_resource type="PackedScene" uid="uid://dp2grvybtecqu" path="res://src/UI/GameMenu/OptionMenu/OtherTab.tscn" id="5_ahefp"] -[ext_resource type="PackedScene" uid="uid://cka0tjqrek13" path="res://src/UI/GameMenu/OptionMenu/KeychainShortcutEdit.tscn" id="6_f7qfn"] +[ext_resource type="PackedScene" uid="uid://bmfn87k0801ih" path="res://src/UI/GameMenu/OptionMenu/SettingsContainer.tscn" id="3_l1bnp"] +[ext_resource type="PackedScene" uid="uid://cdwymd51i4b2f" path="res://src/UI/GameMenu/OptionMenu/ControlsTab.tscn" id="4_v2f4w"] [node name="OptionsMenu" type="PanelContainer" unique_id=2001550106 node_paths=PackedStringArray("_tab_container")] editor_description = "UI-25" @@ -19,6 +16,7 @@ theme = ExtResource("1_0up1d") theme_type_variation = &"BackgroundPanel" script = ExtResource("1_tlein") _tab_container = NodePath("Margin/Tab") +_settings_container = ExtResource("3_l1bnp") [node name="Margin" type="MarginContainer" parent="." unique_id=1213664740] layout_mode = 2 @@ -31,28 +29,6 @@ tab_alignment = 1 current_tab = 0 use_hidden_tabs_for_min_size = true -[node name="General" parent="Margin/Tab" unique_id=498602338 instance=ExtResource("3_6gvf6")] +[node name="Controls" parent="Margin/Tab" unique_id=742864330 instance=ExtResource("4_v2f4w")] layout_mode = 2 metadata/_tab_index = 0 - -[node name="Video" parent="Margin/Tab" unique_id=683372802 instance=ExtResource("2_ji8xr")] -editor_description = "UI-46, UIFUN-43" -visible = false -layout_mode = 2 -metadata/_tab_index = 1 - -[node name="Sound" parent="Margin/Tab" unique_id=588606448 instance=ExtResource("3_4w35t")] -editor_description = "UI-47, UIFUN-44" -visible = false -layout_mode = 2 -metadata/_tab_index = 2 - -[node name="Controls" parent="Margin/Tab" unique_id=1771990114 instance=ExtResource("6_f7qfn")] -editor_description = "SS-27, UI-49, UIFUN-46" -visible = false -layout_mode = 2 -metadata/_tab_index = 3 - -[node name="Other" parent="Margin/Tab" unique_id=1256534261 instance=ExtResource("5_ahefp")] -layout_mode = 2 -metadata/_tab_index = 4 diff --git a/game/src/UI/GameMenu/OptionMenu/OtherTab.tscn b/game/src/UI/GameMenu/OptionMenu/OtherTab.tscn deleted file mode 100644 index 1c16f5ab..00000000 --- a/game/src/UI/GameMenu/OptionMenu/OtherTab.tscn +++ /dev/null @@ -1,18 +0,0 @@ -[gd_scene format=3 uid="uid://dp2grvybtecqu"] - -[node name="Other" type="Control" unique_id=901785660] -visible = false -layout_mode = 3 -anchors_preset = 0 - -[node name="HBoxContainer" type="HBoxContainer" parent="." unique_id=2103943180] -layout_mode = 0 -offset_right = 40.0 -offset_bottom = 40.0 - -[node name="Label" type="Label" parent="HBoxContainer" unique_id=1044986875] -layout_mode = 2 -text = "Spinbox Example :)" - -[node name="SpinBox" type="SpinBox" parent="HBoxContainer" unique_id=278098148] -layout_mode = 2 diff --git a/game/src/UI/GameMenu/OptionMenu/QualityPresetSelector.gd b/game/src/UI/GameMenu/OptionMenu/QualityPresetSelector.gd deleted file mode 100644 index 4fb02a63..00000000 --- a/game/src/UI/GameMenu/OptionMenu/QualityPresetSelector.gd +++ /dev/null @@ -1,4 +0,0 @@ -extends SettingOptionButton - -func _setup_button() -> void: - pass diff --git a/game/src/UI/GameMenu/OptionMenu/QualityPresetSelector.gd.uid b/game/src/UI/GameMenu/OptionMenu/QualityPresetSelector.gd.uid deleted file mode 100644 index 083af518..00000000 --- a/game/src/UI/GameMenu/OptionMenu/QualityPresetSelector.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://fggqr3vjg8rd diff --git a/game/src/UI/GameMenu/OptionMenu/RefreshRateSelector.gd b/game/src/UI/GameMenu/OptionMenu/RefreshRateSelector.gd deleted file mode 100644 index d66833e3..00000000 --- a/game/src/UI/GameMenu/OptionMenu/RefreshRateSelector.gd +++ /dev/null @@ -1,5 +0,0 @@ -extends SettingOptionButton - - -func _setup_button() -> void: - pass diff --git a/game/src/UI/GameMenu/OptionMenu/RefreshRateSelector.gd.uid b/game/src/UI/GameMenu/OptionMenu/RefreshRateSelector.gd.uid deleted file mode 100644 index 84b304d4..00000000 --- a/game/src/UI/GameMenu/OptionMenu/RefreshRateSelector.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dhgxcqr7tlicj diff --git a/game/src/UI/GameMenu/OptionMenu/ResolutionSelector.gd b/game/src/UI/GameMenu/OptionMenu/ResolutionSelector.gd deleted file mode 100644 index a502b455..00000000 --- a/game/src/UI/GameMenu/OptionMenu/ResolutionSelector.gd +++ /dev/null @@ -1,90 +0,0 @@ -extends SettingRevertButton - -# REQUIREMENTS -# * UIFUN-21, UIFUN-28, UIFUN-301, UIFUN-302 - -@export var default_value : Vector2i = Resolution.error_resolution - -func _find_resolution_index_by_value(value : Vector2i) -> int: - for item_index : int in item_count: - if get_item_metadata(item_index) == value: - return item_index - return -1 - -func _sync_resolutions() -> void: - clear() - default_selected = -1 - selected = -1 - var current_resolution := Resolution.get_current_resolution() - for resolution_value : Vector2i in Resolution.get_resolution_value_list(): - # Placeholder option text awaiting _update_resolution_options_text() - add_item(str(resolution_value)) - set_item_metadata(item_count - 1, resolution_value) - - if resolution_value == default_value: - default_selected = item_count - 1 - - if resolution_value == current_resolution: - selected = item_count - 1 - - if default_selected == -1: - default_selected = item_count - 1 - - if selected == -1: - selected = default_selected - _update_resolution_options_text() - -func _notification(what : int) -> void: - match what: - NOTIFICATION_TRANSLATION_CHANGED: - if is_node_ready(): - _update_resolution_options_text() - -func _update_resolution_options_text() -> void: - for index : int in get_item_count(): - var resolution_value : Vector2i = get_item_metadata(index) - var format_dict := { "width": resolution_value.x, "height": resolution_value.y } - format_dict["name"] = tr("OPTIONS_VIDEO_RESOLUTION_{width}x{height}".format(format_dict)) - if format_dict["name"].begins_with("OPTIONS"): format_dict["name"] = "" - var display_name := "OPTIONS_VIDEO_RESOLUTION_DIMS" - if format_dict["name"]: - display_name += "_NAMED" - if resolution_value == default_value: - display_name += "_DEFAULT" - format_dict["width"] = Localisation.tr_number(resolution_value.x) - format_dict["height"] = Localisation.tr_number(resolution_value.y) - display_name = tr(display_name).format(format_dict) - set_item_text(index, display_name) - -func _setup_button() -> void: - Resolution.resolution_added.connect(func (_value : Vector2i) -> void: _sync_resolutions()) - default_value = Resolution.default_resolution - _sync_resolutions() - -func _get_value_for_file(select_value : int): - if _valid_index(select_value): - return get_item_metadata(select_value) - else: - return null - -# REQUIREMENTS: -# * SS-25 -func _set_value_from_file(load_value : Variant) -> void: - var target_resolution := Resolution.set_resolution_from(load_value) - if target_resolution != Resolution.error_resolution: - selected = _find_resolution_index_by_value(target_resolution) - if selected != -1: return - push_error("Setting value '%s' invalid for setting [%s] %s" % [load_value, section_name, setting_name]) - selected = default_selected - -func _on_option_selected(index : int, by_user : bool) -> void: - if _valid_index(index): - if by_user: - print("Start Revert Countdown!") - revert_dialog.show_dialog.call_deferred(self) - previous_index = _find_resolution_index_by_value(Resolution.get_current_resolution()) - - Resolution.set_resolution(get_item_metadata(index)) - else: - push_error("Invalid ResolutionSelector index: %d" % index) - reset_setting(not by_user) diff --git a/game/src/UI/GameMenu/OptionMenu/ResolutionSelector.gd.uid b/game/src/UI/GameMenu/OptionMenu/ResolutionSelector.gd.uid deleted file mode 100644 index 4aed4b59..00000000 --- a/game/src/UI/GameMenu/OptionMenu/ResolutionSelector.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://baata5fx7gs5h diff --git a/game/src/UI/GameMenu/OptionMenu/ScreenModeSelector.gd b/game/src/UI/GameMenu/OptionMenu/ScreenModeSelector.gd deleted file mode 100644 index 84ec9c9e..00000000 --- a/game/src/UI/GameMenu/OptionMenu/ScreenModeSelector.gd +++ /dev/null @@ -1,44 +0,0 @@ -extends SettingRevertButton - -# REQUIREMENTS -# * SS-26, SS-127, SS-128 -# * UIFUN-42 - -enum ScreenMode { Unknown = -1, Fullscreen, Borderless, Windowed } - -func get_screen_mode_from_window_mode(window_mode : Window.Mode) -> ScreenMode: - match window_mode: - Window.MODE_EXCLUSIVE_FULLSCREEN: - return ScreenMode.Fullscreen - Window.MODE_FULLSCREEN: - return ScreenMode.Borderless - Window.MODE_WINDOWED, Window.MODE_MINIMIZED: - return ScreenMode.Windowed - _: - return ScreenMode.Unknown - -func get_window_mode_from_screen_mode(screen_mode : ScreenMode) -> Window.Mode: - match screen_mode: - ScreenMode.Fullscreen: - return Window.MODE_EXCLUSIVE_FULLSCREEN - ScreenMode.Borderless: - return Window.MODE_FULLSCREEN - ScreenMode.Windowed: - return Window.MODE_WINDOWED - _: - return Window.MODE_EXCLUSIVE_FULLSCREEN - -func _setup_button() -> void: - default_selected = get_screen_mode_from_window_mode(Resolution.get_current_window_mode()) - selected = default_selected - -func _on_option_selected(index : int, by_user : bool) -> void: - if _valid_index(index): - if by_user: - print("Start Revert Countdown!") - revert_dialog.show_dialog.call_deferred(self) - previous_index = get_screen_mode_from_window_mode(Resolution.get_current_window_mode()) - Resolution.set_window_mode(get_window_mode_from_screen_mode(index)) - else: - push_error("Invalid ScreenModeSelector index: %d" % index) - reset_setting(not by_user) diff --git a/game/src/UI/GameMenu/OptionMenu/ScreenModeSelector.gd.uid b/game/src/UI/GameMenu/OptionMenu/ScreenModeSelector.gd.uid deleted file mode 100644 index da1fa5af..00000000 --- a/game/src/UI/GameMenu/OptionMenu/ScreenModeSelector.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c1sayjwmkvd46 diff --git a/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingCheckBox.gd b/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingCheckBox.gd deleted file mode 100644 index 970adde7..00000000 --- a/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingCheckBox.gd +++ /dev/null @@ -1,50 +0,0 @@ -extends CheckBox -class_name SettingCheckBox - -signal option_selected(pressed : bool, by_user : bool) - -@export -var section_name : String = "setting" - -@export -var setting_name : String = "setting_checkbox" - -@export -var default_pressed : bool = true - -var settings_path := "user://settings.cfg" - -func _setup_button() -> void: - pass - -func _enter_tree() -> void: - var settings := GameSettings.load_from_file(settings_path) - settings.changed.connect(load_setting.bind(settings)) - toggled.connect(set_setting.bind(settings)) - -func _ready() -> void: - toggled.connect(func(p : bool) -> void: option_selected.emit(p, true)) - _setup_button() - -func _set_value_from_file(load_value : Variant) -> void: - match typeof(load_value): - TYPE_BOOL, TYPE_INT: - set_pressed_no_signal(load_value as bool) - return - TYPE_STRING, TYPE_STRING_NAME: - var load_str := (load_value as String).to_lower() - if load_str.is_empty() or load_str.begins_with("f") or load_str.begins_with("n"): - set_pressed_no_signal(false) - return - if load_str.begins_with("t") or load_str.begins_with("y"): - set_pressed_no_signal(true) - return - push_error("Setting value '%s' invalid for setting [%s] \"%s\"" % [load_value, section_name, setting_name]) - set_pressed_no_signal(default_pressed) - -func load_setting(menu : GameSettings) -> void: - _set_value_from_file(menu.get_value(section_name, setting_name, default_pressed)) - option_selected.emit(button_pressed, false) - -func set_setting(is_toggled : bool, menu : GameSettings) -> void: - menu.set_value(section_name, setting_name, is_toggled) diff --git a/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingCheckBox.gd.uid b/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingCheckBox.gd.uid deleted file mode 100644 index 1857d967..00000000 --- a/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingCheckBox.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cuk60t6htfhxu diff --git a/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingHSlider.gd b/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingHSlider.gd deleted file mode 100644 index 64205a19..00000000 --- a/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingHSlider.gd +++ /dev/null @@ -1,42 +0,0 @@ -extends HSlider -class_name SettingHSlider - -@export -var section_name : String = "setting" - -@export -var setting_name : String = "setting_hslider" - -@export -var default_value : float = 0 - -var settings_path := "user://settings.cfg" - -func _enter_tree() -> void: - var settings := GameSettings.load_from_file(settings_path) - settings.changed.connect(load_setting.bind(settings)) - value_changed.connect(set_setting.bind(settings)) - -func load_setting(menu : GameSettings) -> void: - var load_value : Variant = menu.get_value(section_name, setting_name, default_value) - match typeof(load_value): - TYPE_FLOAT, TYPE_INT: - if value == load_value: - value_changed.emit(value) - value = load_value - return - TYPE_STRING, TYPE_STRING_NAME: - var load_string := load_value as String - if load_string.is_valid_float(): - load_value = load_string.to_float() - if value == load_value: value_changed.emit(value) - value = load_value - return - push_error("Setting value '%s' invalid for setting [%s] \"%s\"" % [load_value, section_name, setting_name]) - value = default_value - -func set_setting(val : float, menu : GameSettings) -> void: - menu.set_value(section_name, setting_name, val) - -func reset_setting() -> void: - value = default_value diff --git a/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingHSlider.gd.uid b/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingHSlider.gd.uid deleted file mode 100644 index 48082dc1..00000000 --- a/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingHSlider.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bip8da2fn2e5r diff --git a/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingOptionButton.gd b/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingOptionButton.gd deleted file mode 100644 index f0408d42..00000000 --- a/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingOptionButton.gd +++ /dev/null @@ -1,80 +0,0 @@ -extends OptionButton -class_name SettingOptionButton - -signal option_selected(index : int, by_user : bool) - -@export -var section_name : String = "setting" - -@export -var setting_name : String = "setting_optionbutton" - -@export -var default_selected : int = -1: - get: return default_selected - set(v): - if v < 0 or item_count == 0: - default_selected = -1 - return - default_selected = v % item_count - -var settings_path := "user://settings.cfg" - -func _valid_index(index : int) -> bool: - return 0 <= index and index < item_count - -func _get_value_for_file(select_value : int) -> Variant: - if _valid_index(select_value): - return select_value - else: - return null - -func _set_value_from_file(load_value : Variant) -> void: - match typeof(load_value): - TYPE_INT: - if _valid_index(load_value): - selected = load_value - return - TYPE_STRING, TYPE_STRING_NAME: - var load_string := load_value as String - if load_string.is_valid_int(): - var load_int := load_string.to_int() - if _valid_index(load_int): - selected = load_int - return - for item_index : int in item_count: - if load_string == get_item_text(item_index): - selected = item_index - return - push_error("Setting value '%s' invalid for setting [%s] \"%s\"" % [load_value, section_name, setting_name]) - selected = default_selected - -func _setup_button() -> void: - pass - -func _enter_tree() -> void: - var settings := GameSettings.load_from_file(settings_path) - settings.changed.connect(load_setting.bind(settings)) - item_selected.connect(set_setting.bind(settings)) - -func _ready() -> void: - item_selected.connect(func(index : int) -> void: option_selected.emit(index, true)) - _setup_button() - if not _valid_index(default_selected) or selected == -1: - var msg := "Failed to generate any valid %s %s options." % [setting_name, section_name] - push_error(msg) - OS.alert(msg, "Options Error: %s / %s" % [section_name, setting_name]) - get_tree().root.propagate_notification(NOTIFICATION_WM_CLOSE_REQUEST) - get_tree().quit() - -func set_setting(index : int, menu : GameSettings) -> void: - menu.set_value(section_name, setting_name, _get_value_for_file(index)) - -func load_setting(menu : GameSettings) -> void: - _set_value_from_file(menu.get_value(section_name, setting_name, _get_value_for_file(default_selected))) - option_selected.emit(selected, false) - -func reset_setting(no_emit : bool = false) -> void: - selected = default_selected - if not no_emit: - option_selected.emit(selected, false) diff --git a/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingOptionButton.gd.uid b/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingOptionButton.gd.uid deleted file mode 100644 index b966de64..00000000 --- a/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingOptionButton.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b06h8rw7shprx diff --git a/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingRevertButton.gd b/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingRevertButton.gd deleted file mode 100644 index 6785f73c..00000000 --- a/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingRevertButton.gd +++ /dev/null @@ -1,27 +0,0 @@ -extends SettingOptionButton -class_name SettingRevertButton - -@export_group("Nodes") -@export var revert_dialog : SettingRevertDialog - -var previous_index : int = -1 - -func _ready() -> void: - super() - if revert_dialog != null: - revert_dialog.visibility_changed.connect(_on_revert_dialog_visibility_changed) - revert_dialog.dialog_accepted.connect(_on_accepted) - revert_dialog.dialog_reverted.connect(_on_reverted) - -func _on_revert_dialog_visibility_changed() -> void: - disabled = revert_dialog.visible - if not revert_dialog.visible: - previous_index = -1 - -func _on_reverted(button : SettingRevertButton) -> void: - if button != self: return - selected = previous_index - option_selected.emit(selected, false) - -func _on_accepted(button : SettingRevertButton) -> void: - if button != self: return diff --git a/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingRevertButton.gd.uid b/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingRevertButton.gd.uid deleted file mode 100644 index 1660dcd5..00000000 --- a/game/src/UI/GameMenu/OptionMenu/SettingNodes/SettingRevertButton.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bm7oqgf7xfgxu diff --git a/game/src/UI/GameMenu/OptionMenu/SettingRevertDialog.gd b/game/src/UI/GameMenu/OptionMenu/SettingRevertDialog.gd deleted file mode 100644 index 7928d152..00000000 --- a/game/src/UI/GameMenu/OptionMenu/SettingRevertDialog.gd +++ /dev/null @@ -1,37 +0,0 @@ -extends ConfirmationDialog -class_name SettingRevertDialog - -signal dialog_accepted(button : SettingRevertButton) -signal dialog_reverted(button : SettingRevertButton) - -@export var dialog_text_key : String = "< reverting in {time} seconds >" - -@export_group("Nodes") -@export var timer : Timer - -var _revert_node : SettingRevertButton = null - -func show_dialog(button : SettingRevertButton, time : float = 0) -> void: - timer.start(time) - popup_centered(Vector2(1,1)) - _revert_node = button - -func _notification(what : int) -> void: - if what == NOTIFICATION_VISIBILITY_CHANGED: - set_process(visible) - if not visible: _revert_node = null - -func _process(_delta : float) -> void: - dialog_text = tr(dialog_text_key).format({ "time": Localisation.tr_number(int(timer.time_left)) }) - -func _on_canceled_or_close_requested() -> void: - timer.stop() - dialog_reverted.emit(_revert_node) - -func _on_confirmed() -> void: - timer.stop() - dialog_accepted.emit(_revert_node) - -func _on_resolution_revert_timer_timeout() -> void: - dialog_reverted.emit(_revert_node) - hide() diff --git a/game/src/UI/GameMenu/OptionMenu/SettingRevertDialog.gd.uid b/game/src/UI/GameMenu/OptionMenu/SettingRevertDialog.gd.uid deleted file mode 100644 index 1ab88e1b..00000000 --- a/game/src/UI/GameMenu/OptionMenu/SettingRevertDialog.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cmym81ujx2s8a diff --git a/game/src/UI/GameMenu/OptionMenu/SettingsContainer.gd b/game/src/UI/GameMenu/OptionMenu/SettingsContainer.gd new file mode 100644 index 00000000..bc230228 --- /dev/null +++ b/game/src/UI/GameMenu/OptionMenu/SettingsContainer.gd @@ -0,0 +1,347 @@ +class_name SettingsContainer +extends ScrollContainer + +@export var _container: GridContainer +var section_key: StringName + +var _revert_group_to_dialog : Dictionary[GameSettings.RevertGroup, RevertDialog] = {} + +func _ready() -> void: + GameSettings.changed.connect(_try_update) + GameSettings.staged_changed.connect(_try_update) + GameSettings.applied.connect(_on_setting_applied) + +func add_section(section: String) -> void: + if _container.get_child_count() > 0: + var spacer: Control = Control.new() + spacer.custom_minimum_size = Vector2(0, 32) + _container.add_child(spacer) + _container.add_child(Control.new()) + _container.add_child(Control.new()) + + var label: Label = Label.new() + _container.add_child(label) + label.text = section + label.theme_type_variation = &"HeaderLarge" + _container.add_child(Control.new()) + _container.add_child(Control.new()) + +func add_setting(setting: GameSettings.Setting) -> void: + _add_label(setting) + _add_edit(setting) + _add_reset_button(setting) + +func _add_label(setting: GameSettings.Setting) -> void: + var label := SettingLabel.new(setting) + _container.add_child(label) + +func _add_edit(setting: GameSettings.Setting) -> void: + var typ: Variant.Type = setting.get_meta(&"type", TYPE_NIL) + var type_hint: PropertyHint = setting.get_meta(&"hint", PROPERTY_HINT_NONE) + + var control: Control + if typ == TYPE_BOOL and type_hint == PROPERTY_HINT_ENUM: + control = BoolEnumButton.new(setting) + elif typ == TYPE_BOOL: + control = BoolButton.new(setting) + elif typ != TYPE_NIL and type_hint == PROPERTY_HINT_ENUM: + control = EnumOptionButton.create_button(setting) + elif (typ == TYPE_INT or typ == TYPE_FLOAT) and type_hint == PROPERTY_HINT_RANGE: + control = RangeSlider.new(setting) + elif type_hint == PROPERTY_HINT_DIR\ + or type_hint == PROPERTY_HINT_FILE\ + or type_hint == PROPERTY_HINT_FILE_PATH: + control = FileSelectEdit.new(setting) + else: + control = PlaceholderLabel.new(setting) + + if setting.has_meta(&"revert_group"): + _add_revert_dialog(setting) + + control.name = setting.key().validate_node_name() + _container.add_child(control) + +func _add_reset_button(setting: GameSettings.Setting) -> void: + # hide reset button if setting is marked having no default value + if setting.get_meta(&"no_default", false): + _container.add_child(Control.new()) + return + var reset_button: ResetButton = ResetButton.new(setting) + _container.add_child(reset_button) + +func _add_revert_dialog(setting: GameSettings.Setting) -> void: + var revert_group : GameSettings.RevertGroup = setting.get_meta(&"revert_group") + var revert_dialog : RevertDialog = _revert_group_to_dialog.get(revert_group) + if revert_dialog == null: + revert_dialog = RevertDialog.new(revert_group) + _revert_group_to_dialog[revert_group] = revert_dialog + add_child(revert_dialog) + setting.set_meta(&"revert_value", setting.value()) + +func _try_update(key: StringName) -> void: + if not key.begins_with(section_key + "/"): return + var child := _container.get_node(key.validate_node_name()) + var child_key: StringName = child.get_meta(&"setting_key", "") + if child_key != key: return + child.update() + var button := (_container.get_child(child.get_index() + 1) as ResetButton) + if button: button.update() + +func _on_setting_applied(key: StringName) -> void: + if not key.begins_with(section_key + "/"): return + var stg := GameSettings.get_setting(key) + if not stg.has_meta(&"revert_value"): return + + var previous_value : Variant = stg.get_meta(&"revert_value") + var revert_group : GameSettings.RevertGroup = stg.get_meta(&"revert_group") + var revert_dialog : RevertDialog = _revert_group_to_dialog[revert_group] + var callable := _on_revert_dialog_revert.bind(stg, previous_value) + revert_dialog.reverted.connect(callable) + stg.set_meta(&"revert_value", stg.value()) + + revert_dialog.show_dialog() + GameSettings.applied.disconnect(_on_setting_applied) + revert_dialog.visibility_changed.connect(func() -> void: + revert_dialog.reverted.disconnect(callable) + GameSettings.applied.connect(_on_setting_applied), + CONNECT_ONE_SHOT) + +func _on_revert_dialog_revert(stg: GameSettings.Setting, prev_value: Variant) -> void: + stg.set_value(prev_value) + stg.set_meta(&"revert_value", prev_value) + +class SettingLabel extends Label: + var setting: GameSettings.Setting + + func _init(stg: GameSettings.Setting) -> void: + setting = stg + text = setting.get_meta( + &"display_name", + "OPTIONS_" + setting.key().replace("/", "_").to_upper()) + tooltip_text = setting.description() + custom_minimum_size = Vector2(145, 0) + autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + mouse_filter = Control.MOUSE_FILTER_PASS + set_meta(&"setting_key", setting.key()) + +class BoolEnumButton extends OptionButton: + var setting: GameSettings.Setting + + func _init(stg: GameSettings.Setting) -> void: + setting = stg + set_meta(&"setting_key", setting.key()) + size_flags_horizontal = Control.SIZE_EXPAND_FILL + add_item("On") + add_item("Off") + update() + item_selected.connect(func(idx: int) -> void: setting.set_value(idx == 0)) + + func update() -> void: + disabled = setting.is_readonly() + if setting.staged_or_value(): + select(0) + else: + select(1) + +class BoolButton extends CheckButton: + var setting: GameSettings.Setting + + func _init(stg: GameSettings.Setting) -> void: + setting = stg + set_meta(&"setting_key", setting.key()) + size_flags_horizontal = Control.SIZE_EXPAND_FILL + update() + toggled.connect(func(toggled_on: bool) -> void: setting.set_value(toggled_on)) + + func update() -> void: + disabled = setting.is_readonly() + button_pressed = setting.staged_or_value() + +class EnumOptionButton extends OptionButton: + var setting: GameSettings.Setting + + static func create_button(stg: GameSettings.Setting) -> Control: + var values: Array[Variant] = stg.get_meta(&"values", []) + var display_values: Array[String] = [] + display_values.assign(stg.get_meta(&"display_values", [])) + if values.size() != display_values.size(): + push_error("Setting %s has mismatched values and display_values size.".format(stg.key())) + return PlaceholderLabel.new(stg) + return EnumOptionButton.new(stg) + + func _init(stg: GameSettings.Setting) -> void: + setting = stg + var values: Array[Variant] = setting.get_meta(&"values", []) + var display_values: Array[String] = [] + display_values.assign(setting.get_meta(&"display_values", [])) + var translate_function : Callable = setting.get_meta( + &"translate_value_function", + func(_s, _v, display_value: String) -> String: return display_value) + + set_meta(&"setting_key", setting.key()) + size_flags_horizontal = Control.SIZE_EXPAND_FILL + var sel_idx: int = 0 + for idx: int in range(len(values)): + var translated_display : String = translate_function.call(setting, values[idx], display_values[idx]) + add_item(translated_display) + if setting.staged_or_value() == values[idx]: + sel_idx = idx + update(sel_idx) + item_selected.connect(func(idx: int) -> void: + setting.set_value(setting.get_meta(&"values")[idx]) + ) + + func update(index : int = -1) -> void: + disabled = setting.is_readonly() + if index == -1: + var values: Array[Variant] = setting.get_meta(&"values", []) + select(values.find(setting.staged_or_value())) + else: + select(index) + +class RangeSlider extends HBoxContainer: + var setting: GameSettings.Setting + var _slider := HSlider.new() + var _spinbox := SpinBox.new() + + func _init(stg: GameSettings.Setting) -> void: + setting = stg + set_meta(&"setting_key", setting.key()) + size_flags_horizontal = Control.SIZE_EXPAND_FILL + add_child(_slider) + _spinbox.share(_slider) + _slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _slider.size_flags_vertical = Control.SIZE_EXPAND_FILL + _slider.min_value = setting.get_meta(&"min", 0) + _slider.max_value = setting.get_meta(&"max", 120) + _slider.step = setting.get_meta(&"step", 1) + update() + _slider.value_changed.connect(func(val: float) -> void: + setting.set_value(val) + ) + add_child(_spinbox) + + func update() -> void: + _slider.editable = not setting.is_readonly() + _slider.value = setting.staged_or_value() + +class FileSelectEdit extends HBoxContainer: + var setting: GameSettings.Setting + var _line_edit := LineEdit.new() + var _dialog_button := Button.new() + var _dialog := FileDialog.new() + + func _init(stg: GameSettings.Setting) -> void: + setting = stg + set_meta(&"setting_key", setting.key()) + size_flags_horizontal = Control.SIZE_EXPAND_FILL + add_child(_line_edit) + _line_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _line_edit.size_flags_vertical = Control.SIZE_EXPAND_FILL + + add_child(_dialog_button) + _dialog_button.text = "🗂️" + _dialog_button.tooltip_text = "Open file dialog" + + add_child(_dialog) + var type_hint: PropertyHint = setting.get_meta(&"hint") + var use_file_mode := type_hint == PROPERTY_HINT_FILE or type_hint == PROPERTY_HINT_GLOBAL_FILE + _dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE if use_file_mode else FileDialog.FILE_MODE_OPEN_DIR + var is_global := type_hint == PROPERTY_HINT_GLOBAL_FILE or type_hint == PROPERTY_HINT_GLOBAL_DIR + _dialog.access = FileDialog.ACCESS_FILESYSTEM if is_global else FileDialog.ACCESS_USERDATA + _dialog.show_hidden_files = true + _dialog.disable_3d = true + + update() + _line_edit.text_submitted.connect(_on_path_selected) + _dialog_button.pressed.connect(_dialog.popup_file_dialog) + if use_file_mode: + _dialog.file_selected.connect(_on_path_selected) + else: + _dialog.dir_selected.connect(_on_path_selected) + + func update() -> void: + _line_edit.editable = not setting.is_readonly() + _line_edit.placeholder_text = setting.staged_or_value() + + func _on_path_selected(path: String) -> void: + setting.set_value(path) + update() + +class PlaceholderLabel extends Label: + var setting: GameSettings.Setting + + func _init(stg: GameSettings.Setting) -> void: + setting = stg + set_meta(&"setting_key", setting.key()) + size_flags_horizontal = Control.SIZE_EXPAND_FILL + text = str(setting.staged_or_value()) + + func update() -> void: + pass + +class ResetButton extends Button: + var setting: GameSettings.Setting + + func _init(stg: GameSettings.Setting) -> void: + setting = stg + set_meta(&"setting_key", setting.key()) + tooltip_text = "Reset to default" + text = "↩" + update() + pressed.connect(func() -> void: setting.reset()) + + func _ready() -> void: + add_theme_font_size_override(&"font_size", 20) + + func update() -> void: + disabled = setting.staged_or_value() == setting.default_value() + +class RevertDialog extends ConfirmationDialog: + signal accepted() + signal reverted() + + var _timer := Timer.new() + var _revert_group : GameSettings.RevertGroup + + func show_dialog() -> void: + _timer.start() + popup_centered(Vector2(1,1)) + + func _init(revert_group : GameSettings.RevertGroup) -> void: + _revert_group = revert_group + title = _revert_group.title + cancel_button_text = "DIALOG_CANCEL" + ok_button_text = "DIALOG_OK" + disable_3d = true + size = Vector2i(730, 100) + confirmed.connect(_on_confirmed) + canceled.connect(_on_canceled) + close_requested.connect(_on_canceled) + + add_child(_timer) + _timer.wait_time = 5 + _timer.one_shot = true + _timer.timeout.connect(_on_timer_timeout) + + func _process(_delta : float) -> void: + dialog_text = tr(_revert_group.text).format({ "time": Localisation.tr_number(int(_timer.time_left)) }) + + func _notification(what : int) -> void: + match what: + NOTIFICATION_VISIBILITY_CHANGED: + set_process(visible) + NOTIFICATION_WM_CLOSE_REQUEST, NOTIFICATION_CRASH: + _on_canceled() + + func _on_confirmed() -> void: + _timer.stop() + accepted.emit() + + func _on_canceled() -> void: + _timer.stop() + reverted.emit() + + func _on_timer_timeout() -> void: + hide() + reverted.emit() diff --git a/game/src/UI/GameMenu/OptionMenu/SettingsContainer.gd.uid b/game/src/UI/GameMenu/OptionMenu/SettingsContainer.gd.uid new file mode 100644 index 00000000..875d7aba --- /dev/null +++ b/game/src/UI/GameMenu/OptionMenu/SettingsContainer.gd.uid @@ -0,0 +1 @@ +uid://cyfju5iaq3vki diff --git a/game/src/UI/GameMenu/OptionMenu/SettingsContainer.tscn b/game/src/UI/GameMenu/OptionMenu/SettingsContainer.tscn new file mode 100644 index 00000000..a22279a6 --- /dev/null +++ b/game/src/UI/GameMenu/OptionMenu/SettingsContainer.tscn @@ -0,0 +1,40 @@ +[gd_scene format=3 uid="uid://bmfn87k0801ih"] + +[ext_resource type="Script" uid="uid://cyfju5iaq3vki" path="res://src/UI/GameMenu/OptionMenu/SettingsContainer.gd" id="1_iycy5"] + +[node name="SettingsContainer" type="ScrollContainer" unique_id=1873202677 node_paths=PackedStringArray("_container")] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_iycy5") +_container = NodePath("MarginContainer/HBoxContainer/GridContainer") + +[node name="MarginContainer" type="MarginContainer" parent="." unique_id=572273292] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/margin_left = 16 +theme_override_constants/margin_top = 16 +theme_override_constants/margin_right = 16 +theme_override_constants/margin_bottom = 16 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer" unique_id=1222362511] +layout_mode = 2 + +[node name="Control" type="Control" parent="MarginContainer/HBoxContainer" unique_id=307683519] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_stretch_ratio = 0.3 + +[node name="GridContainer" type="GridContainer" parent="MarginContainer/HBoxContainer" unique_id=1975309189] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/h_separation = 32 +columns = 3 + +[node name="Control2" type="Control" parent="MarginContainer/HBoxContainer" unique_id=429296085] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_stretch_ratio = 0.3 diff --git a/game/src/UI/GameMenu/OptionMenu/SoundTab.tscn b/game/src/UI/GameMenu/OptionMenu/SoundTab.tscn deleted file mode 100644 index 4acc28e4..00000000 --- a/game/src/UI/GameMenu/OptionMenu/SoundTab.tscn +++ /dev/null @@ -1,34 +0,0 @@ -[gd_scene format=3 uid="uid://cbtgwpx2wxi33"] - -[ext_resource type="PackedScene" uid="uid://dy4si8comamnv" path="res://src/UI/GameMenu/OptionMenu/VolumeGrid.tscn" id="1_okpft"] -[ext_resource type="Script" uid="uid://cuk60t6htfhxu" path="res://src/UI/GameMenu/OptionMenu/SettingNodes/SettingCheckBox.gd" id="2_f3aj4"] - -[node name="Sound" type="HBoxContainer" unique_id=848250411] -alignment = 1 - -[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=1260106103] -layout_mode = 2 - -[node name="Control" type="Control" parent="VBoxContainer" unique_id=58781728] -layout_mode = 2 -size_flags_vertical = 3 -size_flags_stretch_ratio = 0.1 - -[node name="VolumeGrid" parent="VBoxContainer" unique_id=1166324459 instance=ExtResource("1_okpft")] -layout_mode = 2 - -[node name="ButtonGrid" type="GridContainer" parent="VBoxContainer" unique_id=591022586] -layout_mode = 2 -size_flags_vertical = 2 -columns = 2 - -[node name="Spacer" type="Control" parent="VBoxContainer/ButtonGrid" unique_id=742251692] -layout_mode = 2 -size_flags_horizontal = 3 - -[node name="EarExploder" type="CheckBox" parent="VBoxContainer/ButtonGrid" unique_id=372950547] -layout_mode = 2 -text = "OPTIONS_SOUND_EXPLODE_EARS" -script = ExtResource("2_f3aj4") -section_name = "audio" -setting_name = "startup_music" diff --git a/game/src/UI/GameMenu/OptionMenu/VideoTab.gd b/game/src/UI/GameMenu/OptionMenu/VideoTab.gd deleted file mode 100644 index 3d98678e..00000000 --- a/game/src/UI/GameMenu/OptionMenu/VideoTab.gd +++ /dev/null @@ -1,9 +0,0 @@ -extends HBoxContainer - -@export var initial_focus: Control - -func _notification(what : int) -> void: - match(what): - NOTIFICATION_VISIBILITY_CHANGED: - if visible and is_inside_tree(): - initial_focus.grab_focus() diff --git a/game/src/UI/GameMenu/OptionMenu/VideoTab.gd.uid b/game/src/UI/GameMenu/OptionMenu/VideoTab.gd.uid deleted file mode 100644 index bd042701..00000000 --- a/game/src/UI/GameMenu/OptionMenu/VideoTab.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d2baocaag5ifg diff --git a/game/src/UI/GameMenu/OptionMenu/VideoTab.tscn b/game/src/UI/GameMenu/OptionMenu/VideoTab.tscn deleted file mode 100644 index 4ae752fc..00000000 --- a/game/src/UI/GameMenu/OptionMenu/VideoTab.tscn +++ /dev/null @@ -1,183 +0,0 @@ -[gd_scene format=3 uid="uid://bq3awxxjn1tuw"] - -[ext_resource type="Script" uid="uid://baata5fx7gs5h" path="res://src/UI/GameMenu/OptionMenu/ResolutionSelector.gd" id="1_i8nro"] -[ext_resource type="Script" uid="uid://d2baocaag5ifg" path="res://src/UI/GameMenu/OptionMenu/VideoTab.gd" id="1_jvv62"] -[ext_resource type="Script" uid="uid://c1sayjwmkvd46" path="res://src/UI/GameMenu/OptionMenu/ScreenModeSelector.gd" id="2_wa7vw"] -[ext_resource type="Script" uid="uid://bt8wuvopl1kqv" path="res://src/UI/GameMenu/OptionMenu/GuiScaleSelector.gd" id="3_pgc5d"] -[ext_resource type="Script" uid="uid://b1bxjetbdem3j" path="res://src/UI/GameMenu/OptionMenu/MonitorDisplaySelector.gd" id="3_y6lyb"] -[ext_resource type="Script" uid="uid://dhgxcqr7tlicj" path="res://src/UI/GameMenu/OptionMenu/RefreshRateSelector.gd" id="4_381mg"] -[ext_resource type="Script" uid="uid://fggqr3vjg8rd" path="res://src/UI/GameMenu/OptionMenu/QualityPresetSelector.gd" id="5_srg4v"] -[ext_resource type="Script" uid="uid://cmym81ujx2s8a" path="res://src/UI/GameMenu/OptionMenu/SettingRevertDialog.gd" id="8_ug5mo"] - -[node name="Video" type="HBoxContainer" unique_id=1406273878 node_paths=PackedStringArray("initial_focus")] -editor_description = "UI-46" -alignment = 1 -script = ExtResource("1_jvv62") -initial_focus = NodePath("VideoSettingList/VideoSettingGrid/ResolutionSelector") - -[node name="VideoSettingList" type="VBoxContainer" parent="." unique_id=358688510] -layout_mode = 2 - -[node name="Control" type="Control" parent="VideoSettingList" unique_id=277801617] -layout_mode = 2 -size_flags_vertical = 3 -size_flags_stretch_ratio = 0.1 - -[node name="VideoSettingGrid" type="GridContainer" parent="VideoSettingList" unique_id=1448783179] -layout_mode = 2 -size_flags_vertical = 3 -columns = 2 - -[node name="ResolutionLabel" type="Label" parent="VideoSettingList/VideoSettingGrid" unique_id=902590164] -layout_mode = 2 -text = "OPTIONS_VIDEO_RESOLUTION" - -[node name="ResolutionSelector" type="OptionButton" parent="VideoSettingList/VideoSettingGrid" unique_id=1406125022 node_paths=PackedStringArray("revert_dialog")] -editor_description = "UI-19" -layout_mode = 2 -focus_neighbor_bottom = NodePath("../ScreenModeSelector") -selected = 0 -item_count = 1 -popup/item_0/text = "MISSING" -popup/item_0/id = 0 -script = ExtResource("1_i8nro") -revert_dialog = NodePath("../../../VideoRevertDialog") -section_name = "video" -setting_name = "resolution" - -[node name="GuiScaleLabel" type="Label" parent="VideoSettingList/VideoSettingGrid" unique_id=1004691853] -layout_mode = 2 -text = "OPTIONS_VIDEO_GUI_SCALE" - -[node name="GuiScaleSelector" type="OptionButton" parent="VideoSettingList/VideoSettingGrid" unique_id=1231600790] -editor_description = "UI-23" -layout_mode = 2 -focus_neighbor_bottom = NodePath("../ScreenModeSelector") -selected = 0 -item_count = 1 -popup/item_0/text = "MISSING" -popup/item_0/id = 0 -script = ExtResource("3_pgc5d") -section_name = "video" -setting_name = "gui_scale" - -[node name="ScreenModeLabel" type="Label" parent="VideoSettingList/VideoSettingGrid" unique_id=1576159469] -editor_description = "UI-44" -layout_mode = 2 -text = "OPTIONS_VIDEO_SCREEN_MODE" - -[node name="ScreenModeSelector" type="OptionButton" parent="VideoSettingList/VideoSettingGrid" unique_id=1604517552 node_paths=PackedStringArray("revert_dialog")] -layout_mode = 2 -focus_neighbor_top = NodePath("../ResolutionSelector") -focus_neighbor_bottom = NodePath("../MonitorDisplaySelector") -selected = 0 -item_count = 3 -popup/item_0/text = "OPTIONS_VIDEO_FULLSCREEN" -popup/item_0/id = 0 -popup/item_1/text = "OPTIONS_VIDEO_BORDERLESS" -popup/item_1/id = 1 -popup/item_2/text = "OPTIONS_VIDEO_WINDOWED" -popup/item_2/id = 2 -script = ExtResource("2_wa7vw") -revert_dialog = NodePath("../../../VideoRevertDialog") -section_name = "video" -setting_name = "mode_selected" - -[node name="MonitorSelectionLabel" type="Label" parent="VideoSettingList/VideoSettingGrid" unique_id=1936849369] -layout_mode = 2 -text = "OPTIONS_VIDEO_MONITOR_SELECTION" - -[node name="MonitorDisplaySelector" type="OptionButton" parent="VideoSettingList/VideoSettingGrid" unique_id=535768394 node_paths=PackedStringArray("revert_dialog")] -layout_mode = 2 -focus_neighbor_top = NodePath("../ScreenModeSelector") -focus_neighbor_bottom = NodePath("../RefreshRateSelector") -selected = 0 -item_count = 1 -popup/item_0/text = "MISSING" -popup/item_0/id = 0 -script = ExtResource("3_y6lyb") -revert_dialog = NodePath("../../../VideoRevertDialog") -section_name = "video" -setting_name = "current_screen" - -[node name="RefreshRateLabel" type="Label" parent="VideoSettingList/VideoSettingGrid" unique_id=1383296993] -layout_mode = 2 -text = "OPTIONS_VIDEO_REFRESH_RATE" - -[node name="RefreshRateSelector" type="OptionButton" parent="VideoSettingList/VideoSettingGrid" unique_id=1722236592] -editor_description = "UI-18, UIFUN-20" -layout_mode = 2 -tooltip_text = "OPTIONS_VIDEO_REFRESH_RATE_TOOLTIP" -focus_neighbor_top = NodePath("../MonitorDisplaySelector") -focus_neighbor_bottom = NodePath("../QualityPresetSelector") -selected = 0 -item_count = 8 -popup/item_0/text = "VSYNC" -popup/item_0/id = 0 -popup/item_1/text = "30hz" -popup/item_1/id = 1 -popup/item_2/text = "60hz" -popup/item_2/id = 2 -popup/item_3/text = "90hz" -popup/item_3/id = 3 -popup/item_4/text = "120hz" -popup/item_4/id = 4 -popup/item_5/text = "144hz" -popup/item_5/id = 5 -popup/item_6/text = "365hz" -popup/item_6/id = 6 -popup/item_7/text = "Unlimited" -popup/item_7/id = 7 -script = ExtResource("4_381mg") -section_name = "video" -setting_name = "refresh_rate" -default_selected = 0 - -[node name="QualityPresetLabel" type="Label" parent="VideoSettingList/VideoSettingGrid" unique_id=2146844500] -layout_mode = 2 -text = "OPTIONS_VIDEO_QUALITY" - -[node name="QualityPresetSelector" type="OptionButton" parent="VideoSettingList/VideoSettingGrid" unique_id=251436011] -editor_description = "UI-21, UIFUN-22" -layout_mode = 2 -focus_neighbor_top = NodePath("../RefreshRateSelector") -selected = 1 -item_count = 5 -popup/item_0/text = "OPTIONS_VIDEO_QUALITY_LOW" -popup/item_0/id = 0 -popup/item_1/text = "OPTIONS_VIDEO_QUALITY_MEDIUM" -popup/item_1/id = 1 -popup/item_2/text = "OPTIONS_VIDEO_QUALITY_HIGH" -popup/item_2/id = 2 -popup/item_3/text = "OPTIONS_VIDEO_QUALITY_ULTRA" -popup/item_3/id = 3 -popup/item_4/text = "OPTIONS_VIDEO_QUALITY_CUSTOM" -popup/item_4/id = 4 -script = ExtResource("5_srg4v") -section_name = "video" -setting_name = "quality_preset" -default_selected = 1 - -[node name="VideoRevertDialog" type="ConfirmationDialog" parent="." unique_id=1494559472 node_paths=PackedStringArray("timer")] -editor_description = "UI-873" -disable_3d = true -title = "OPTIONS_VIDEO_REVERT_DIALOG_TITLE" -size = Vector2i(730, 100) -ok_button_text = "DIALOG_OK" -cancel_button_text = "DIALOG_CANCEL" -script = ExtResource("8_ug5mo") -dialog_text_key = "OPTIONS_VIDEO_REVERT_DIALOG_TEXT" -timer = NodePath("VideoRevertTimer") - -[node name="VideoRevertTimer" type="Timer" parent="VideoRevertDialog" unique_id=555804643] -wait_time = 5.0 -one_shot = true - -[connection signal="option_selected" from="VideoSettingList/VideoSettingGrid/ResolutionSelector" to="VideoSettingList/VideoSettingGrid/ResolutionSelector" method="_on_option_selected"] -[connection signal="option_selected" from="VideoSettingList/VideoSettingGrid/GuiScaleSelector" to="VideoSettingList/VideoSettingGrid/GuiScaleSelector" method="_on_option_selected"] -[connection signal="option_selected" from="VideoSettingList/VideoSettingGrid/ScreenModeSelector" to="VideoSettingList/VideoSettingGrid/ScreenModeSelector" method="_on_option_selected"] -[connection signal="option_selected" from="VideoSettingList/VideoSettingGrid/MonitorDisplaySelector" to="VideoSettingList/VideoSettingGrid/MonitorDisplaySelector" method="_on_option_selected"] -[connection signal="canceled" from="VideoRevertDialog" to="VideoRevertDialog" method="_on_canceled_or_close_requested"] -[connection signal="close_requested" from="VideoRevertDialog" to="VideoRevertDialog" method="_on_canceled_or_close_requested"] -[connection signal="confirmed" from="VideoRevertDialog" to="VideoRevertDialog" method="_on_confirmed"] -[connection signal="timeout" from="VideoRevertDialog/VideoRevertTimer" to="VideoRevertDialog" method="_on_resolution_revert_timer_timeout"] diff --git a/game/src/UI/GameMenu/OptionMenu/VolumeGrid.gd b/game/src/UI/GameMenu/OptionMenu/VolumeGrid.gd deleted file mode 100644 index 47ced387..00000000 --- a/game/src/UI/GameMenu/OptionMenu/VolumeGrid.gd +++ /dev/null @@ -1,54 +0,0 @@ -extends GridContainer - -const RATIO_FOR_LINEAR : float = 100 - -var _slider_dictionary : Dictionary - -var initial_focus : Control - -func get_db_as_volume_value(db : float) -> float: - # db_to_linear produces a float between 0 and 1 from a db value - return db_to_linear(db) * RATIO_FOR_LINEAR - -func get_volume_value_as_db(value : float) -> float: - # linear_to_db consumes a float between 0 and 1 to produce the db value - return linear_to_db(value / RATIO_FOR_LINEAR) - -func add_volume_row(bus_name : String, bus_index : int) -> HSlider: - var volume_label := Label.new() - if bus_name == "Master": - volume_label.text = "MASTER_BUS" - else: - volume_label.text = bus_name - add_child(volume_label) - - var volume_slider := SettingHSlider.new() - volume_slider.section_name = "audio" - volume_slider.setting_name = volume_label.text - volume_slider.custom_minimum_size = Vector2(290, 0) - volume_slider.size_flags_vertical = Control.SIZE_FILL - volume_slider.min_value = 0 - volume_slider.default_value = 100 - volume_slider.max_value = 120 # 120 so volume can be boosted somewhat - volume_slider.value_changed.connect(_on_slider_value_changed.bind(bus_index)) - add_child(volume_slider) - - _slider_dictionary[volume_label.text] = volume_slider - if not initial_focus: initial_focus = volume_slider - return volume_slider - -# REQUIREMENTS -# * UI-22 -func _enter_tree() -> void: - for bus_index : int in AudioServer.bus_count: - add_volume_row(AudioServer.get_bus_name(bus_index), bus_index) - -func _notification(what : int) -> void: - match(what): - NOTIFICATION_VISIBILITY_CHANGED: - if visible and is_inside_tree() and initial_focus: initial_focus.grab_focus() - -# REQUIREMENTS -# * UIFUN-30 -func _on_slider_value_changed(value : float, bus_index : int) -> void: - AudioServer.set_bus_volume_db(bus_index, get_volume_value_as_db(value)) diff --git a/game/src/UI/GameMenu/OptionMenu/VolumeGrid.gd.uid b/game/src/UI/GameMenu/OptionMenu/VolumeGrid.gd.uid deleted file mode 100644 index 7db87bdf..00000000 --- a/game/src/UI/GameMenu/OptionMenu/VolumeGrid.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dopkfnjx2idjr diff --git a/game/src/UI/GameMenu/OptionMenu/VolumeGrid.tscn b/game/src/UI/GameMenu/OptionMenu/VolumeGrid.tscn deleted file mode 100644 index 9bf070c9..00000000 --- a/game/src/UI/GameMenu/OptionMenu/VolumeGrid.tscn +++ /dev/null @@ -1,8 +0,0 @@ -[gd_scene format=3 uid="uid://dy4si8comamnv"] - -[ext_resource type="Script" uid="uid://dopkfnjx2idjr" path="res://src/UI/GameMenu/OptionMenu/VolumeGrid.gd" id="1_wb64h"] - -[node name="VolumeGrid" type="GridContainer" unique_id=1663583968] -size_flags_vertical = 0 -columns = 2 -script = ExtResource("1_wb64h")