Files
2026-02-23 11:37:27 +01:00

322 lines
12 KiB
GDScript

extends Node
#region Helper enums and data classes
class SettingCategory:
var name: String
var display_text: String
var sub_categories: Array[SettingCategory] = []
var settings: Array[Setting] = []
func _init(p_name: String, p_display_text: String = ""):
name = p_name
display_text = p_display_text if p_display_text != "" else p_name
func add_setting(p_setting: Setting) -> void:
if OS.is_debug_build() and settings.filter(func(s): return s.name == p_setting.name).size() > 0:
push_warning("Setting " + p_setting.name + " already exists in category " + name)
return
settings.append(p_setting)
func add_sub_category(p_category: SettingCategory) -> void:
if OS.is_debug_build() and sub_categories.filter(func(c): return c.name == p_category.name).size() > 0:
push_warning("Sub-category " + p_category.name + " already exists in category " + name)
return
sub_categories.append(p_category)
func get_setting(p_name: String) -> Setting:
for setting in settings:
if setting.name == p_name:
return setting
push_warning("Setting " + p_name + " not found in category " + name)
return null
func get_sub_category(p_name: String) -> SettingCategory:
for category in sub_categories:
if category.name == p_name:
return category
push_warning("Sub-category " + p_name + " not found in category " + name)
return null
@abstract
class Setting:
signal value_changed(value: Variant)
var name: String
var display_text: String
var value: Variant:
set = set_value
var default_value: Variant
var requires_restart: bool
var setter_callable: Callable
func _init(p_name: String, p_display_text: String, p_default_value: Variant, p_setter_callable: Callable = Callable(), p_requires_restart: bool = false):
self.name = p_name
self.display_text = p_display_text
self.value = p_default_value
self.default_value = p_default_value
self.setter_callable = p_setter_callable
self.requires_restart = p_requires_restart
func set_value(p_value: Variant):
if not is_same(p_value, value):
value = p_value
value_changed.emit(value)
if setter_callable.is_valid():
setter_callable.call(value)
class OptionSetting extends Setting:
var options: Array[String]
func _init(p_name: String, p_label_text: String, p_default_value: String, p_options: Array[String], p_setter_callable: Callable = Callable(), p_requires_restart: bool = false):
assert(p_options.has(p_default_value))
self.options = p_options
super(p_name, p_label_text, p_default_value, p_setter_callable, p_requires_restart)
class FloatSetting extends Setting:
var min_value: float
var max_value: float
var step: float
func _init(p_name: String, p_label_text: String, p_default_value: float, p_min: float, p_max: float, p_step: float, p_setter_callable: Callable = Callable(), p_requires_restart: bool = false):
assert(p_default_value >= p_min and p_default_value <= p_max)
self.min_value = p_min
self.max_value = p_max
self.step = p_step
super(p_name, p_label_text, p_default_value, p_setter_callable, p_requires_restart)
class BoolSetting extends Setting:
func _init(p_name: String, p_label_text: String, p_default_value: bool, p_setter_callable: Callable = Callable(), p_requires_restart: bool = false):
super(p_name, p_label_text, p_default_value, p_setter_callable, p_requires_restart)
class InputRemappingSetting extends Setting:
var actions: Array[InputEvent]
func _init(p_name: String, p_label_text: String, p_default_value: Array[InputEvent], p_setter_callable: Callable = Callable(), p_requires_restart: bool = false):
super(p_name, p_label_text, p_default_value, p_setter_callable, p_requires_restart)
self.actions = p_default_value.duplicate()
#endregion
#region Settings interface
## Stores all settings in a hierarchical structure
var _root_categories: Array[SettingCategory] = []
## Adds a new root category which can be populated with settings and sub-_root_categories.
func add_root_category(p_setting_category: SettingCategory) -> void:
if _root_categories.filter(func(c): return c.name == p_setting_category.name).size() > 0:
push_warning("Category " + p_setting_category.name + " does already exist")
return
_root_categories.append(p_setting_category)
func get_root_categories() -> Array[SettingCategory]:
return _root_categories
func get_root_category(p_name: String) -> SettingCategory:
for category in _root_categories:
if category.name == p_name:
return category
push_warning("Category " + p_name + " not found")
return null
## Gets the value of a setting by path (e.g., "Category/SubCategory/SettingName")
func get_value(p_path: String) -> Variant:
var setting = _get_setting_by_path(p_path)
if setting:
return setting.value
push_warning("Setting " + p_path + " not found")
return null
## Sets the value of a setting by path (e.g., "Category/SubCategory/SettingName")
func set_value(p_path: String, p_value: Variant) -> void:
var setting = _get_setting_by_path(p_path)
if setting:
if typeof(p_value) != typeof(setting.default_value):
push_warning("Variant type mismatch for setting " + p_path + " (" + type_string(typeof(p_value)) + " should be " + type_string(typeof(setting.default_value)) + ")")
return
setting.value = p_value
else:
push_warning("Setting " + p_path + " not found")
## Sets the default value of a setting by path (e.g., "Category/SubCategory/SettingName")
func set_default_value(p_path: String, p_default_value: Variant) -> void:
var setting = _get_setting_by_path(p_path)
if setting:
if typeof(p_default_value) != typeof(setting.default_value):
push_warning("Variant type mismatch for setting " + p_path + " (" + type_string(typeof(p_default_value)) + " should be " + type_string(typeof(setting.default_value)) + ")")
return
setting.default_value = p_default_value
else:
push_warning("Setting " + p_path + " not found")
## Resets a setting to its default value by path (e.g., "Category/SubCategory/SettingName")
func reset_value(p_path: String):
var setting = _get_setting_by_path(p_path)
if setting:
setting.value = setting.default_value
if setting.setter_callable.is_valid():
setting.setter_callable.call(setting.default_value)
else:
push_warning("Setting " + p_path + " not found")
## Resets a specific input binding at the given index to its default value
func reset_input_binding(p_path: String, index: int):
var setting = _get_setting_by_path(p_path)
if not setting:
push_warning("Setting " + p_path + " not found")
return
if not setting is InputRemappingSetting:
push_warning("Setting " + p_path + " is not an InputRemappingSetting")
return
var events = setting.value.duplicate() as Array[InputEvent]
if index >= events.size():
push_warning("Index " + str(index) + " out of bounds for input_binding setting " + p_path)
return
var default_event = setting.default_value[index] if index < setting.default_value.size() else null
events[index] = default_event
setting.value = events
if setting.setter_callable.is_valid():
setting.setter_callable.call(events)
## Checks if a setting's value equals its default value
func is_value_default(p_path: String) -> bool:
var setting = _get_setting_by_path(p_path)
if setting:
return is_equal(setting.value, setting.default_value)
push_warning("Setting " + p_path + " not found")
return false
## Checks if a specific input binding at the given index equals its default value
func is_input_binding_default(p_path: String, index: int) -> bool:
var setting = _get_setting_by_path(p_path)
if not setting:
push_warning("Setting " + p_path + " not found")
return false
if not setting is InputRemappingSetting:
push_warning("Setting " + p_path + " is not an InputRemappingSetting")
return false
var event = setting.value[index] if index < setting.value.size() else null
var default_event = setting.default_value[index] if index < setting.default_value.size() else null
return is_equal(event, default_event)
## Helper function to find a setting by path (e.g., "Category/SubCategory/SettingName")
func _get_setting_by_path(p_path: String) -> Setting:
var path_parts = p_path.split("/")
if path_parts.size() < 2:
push_warning("Invalid setting path: " + p_path + ". Expected format: 'Category/SettingName' or 'Category/SubCategory/.../SettingName'")
return null
# Find the root category
var category = get_root_category(path_parts[0])
if not category:
return null
# Traverse subcategories
for i in range(1, path_parts.size() - 1):
category = category.get_sub_category(path_parts[i])
if not category:
return null
# Get the setting from the final category
var setting_name = path_parts[path_parts.size() - 1]
return category.get_setting(setting_name)
func save_config():
var config = ConfigFile.new()
for category in _root_categories:
_save_category_recursive(config, category, category.name)
var error = config.save("user://config.cfg")
if error != OK:
push_error("Failed to save config file: " + str(error))
## Recursively saves all settings in a category and its subcategories
func _save_category_recursive(config: ConfigFile, category: SettingCategory, section_path: String) -> void:
# Save all settings in this category
for setting in category.settings:
config.set_value(section_path, setting.name, setting.value)
# Recursively save all subcategories
for sub_category in category.sub_categories:
var sub_section_path = section_path + "/" + sub_category.name
_save_category_recursive(config, sub_category, sub_section_path)
func load_config():
var config = ConfigFile.new()
var error = config.load("user://config.cfg")
if error != OK:
push_warning("Failed to load config file: " + str(error) + ". Using default values.")
return
for category in _root_categories:
_load_category_recursive(config, category, category.name)
## Recursively loads all settings in a category and its subcategories
func _load_category_recursive(config: ConfigFile, category: SettingCategory, section_path: String) -> void:
# Load all settings in this category
for setting in category.settings:
if not config.has_section_key(section_path, setting.name):
push_warning("Setting " + setting.name + " not found in section " + section_path + ", using default value")
continue
var value = config.get_value(section_path, setting.name, setting.default_value)
if typeof(value) != typeof(setting.default_value):
push_warning("Variant type mismatch for setting " + setting.name + " (" + type_string(typeof(value)) + " should be " + type_string(typeof(setting.default_value)) + "), using default value")
continue
# TODO: Do something about this?
# Set the value (this will trigger the setter and emit signals)
setting.value = value
# Recursively load all subcategories
for sub_category in category.sub_categories:
var sub_section_path = section_path + "/" + sub_category.name
_load_category_recursive(config, sub_category, sub_section_path)
## Be cautious! Clears the config file, resetting all settings to their default values.
## This can be useful for troubleshooting.
func clear_config():
var config = ConfigFile.new()
config.save("user://config.cfg")
#endregion
## Deep property based equality check for two variants
func is_equal(a: Variant, b: Variant) -> bool:
if typeof(a) != typeof(b):
return false
if typeof(a) != TYPE_DICTIONARY and typeof(a) != TYPE_OBJECT and typeof(a) != TYPE_ARRAY:
return a == b
if typeof(a) == TYPE_ARRAY:
if a.size() != b.size():
return false
for i in range(a.size()):
if not is_equal(a[i], b[i]):
return false
return true
if typeof(a) == TYPE_DICTIONARY:
# Compare all keys of the two dictionaries
for key in a.keys():
if not b.has(key):
return false
if not is_equal(a[key], b[key]):
return false
for key in b.keys():
if not a.has(key):
return false
return true
# Both are objects, compare all properties
for prop in a.get_property_list():
if not is_equal(a.get(prop.name), b.get(prop.name)):
return false
return true