Some checks failed
Continuous Integration / Code Formatting (pull_request) Successful in 27s
Continuous Integration / Code Quality Check (pull_request) Successful in 29s
Continuous Integration / Test Execution (pull_request) Failing after 33s
Continuous Integration / CI Summary (pull_request) Failing after 5s
396 lines
12 KiB
GDScript
396 lines
12 KiB
GDScript
extends Node
|
|
|
|
const LANGUAGES_JSON_PATH := "res://localization/languages.json"
|
|
const SETTINGS_FILE = "user://settings.cfg"
|
|
const MAX_JSON_FILE_SIZE = 65536 # 64KB limit for languages.json
|
|
const MAX_SETTING_STRING_LENGTH = 10 # Max length for string settings like language code
|
|
# dev `user://`=`%APPDATA%\Godot\app_userdata\Skelly`
|
|
# prod `user://`=`%APPDATA%\Skelly\`
|
|
|
|
var settings: Dictionary = {}
|
|
|
|
var default_settings: Dictionary = {
|
|
"master_volume": 0.50, "music_volume": 0.40, "sfx_volume": 0.50, "language": "en"
|
|
}
|
|
|
|
var languages_data: Dictionary = {}
|
|
|
|
|
|
func _ready() -> void:
|
|
DebugManager.log_info("SettingsManager ready", "SettingsManager")
|
|
load_languages()
|
|
load_settings()
|
|
|
|
|
|
func load_settings() -> void:
|
|
var config = ConfigFile.new()
|
|
var load_result = config.load(SETTINGS_FILE)
|
|
|
|
# Ensure settings dictionary exists
|
|
if settings.is_empty():
|
|
settings = default_settings.duplicate()
|
|
|
|
if load_result == OK:
|
|
for key in default_settings.keys():
|
|
var loaded_value = config.get_value("settings", key, default_settings[key])
|
|
# Validate loaded settings before applying
|
|
if _validate_setting_value(key, loaded_value):
|
|
settings[key] = loaded_value
|
|
else:
|
|
DebugManager.log_warn(
|
|
(
|
|
"Invalid setting value for '%s', using default: %s"
|
|
% [key, str(default_settings[key])]
|
|
),
|
|
"SettingsManager"
|
|
)
|
|
settings[key] = default_settings[key]
|
|
DebugManager.log_info("Settings loaded: " + str(settings), "SettingsManager")
|
|
else:
|
|
DebugManager.log_warn(
|
|
"No settings file found (Error code: %d), using defaults" % load_result,
|
|
"SettingsManager"
|
|
)
|
|
settings = default_settings.duplicate()
|
|
|
|
# Apply settings with error handling
|
|
_apply_all_settings()
|
|
|
|
|
|
func _apply_all_settings():
|
|
DebugManager.log_info("Applying settings: " + str(settings), "SettingsManager")
|
|
|
|
# Apply language setting
|
|
if "language" in settings:
|
|
TranslationServer.set_locale(settings["language"])
|
|
|
|
# Apply audio settings with error checking
|
|
var master_bus = AudioServer.get_bus_index("Master")
|
|
var music_bus = AudioServer.get_bus_index("Music")
|
|
var sfx_bus = AudioServer.get_bus_index("SFX")
|
|
|
|
if master_bus >= 0 and "master_volume" in settings:
|
|
AudioServer.set_bus_volume_db(master_bus, linear_to_db(settings["master_volume"]))
|
|
else:
|
|
DebugManager.log_warn(
|
|
"Master audio bus not found or master_volume setting missing", "SettingsManager"
|
|
)
|
|
|
|
if music_bus >= 0 and "music_volume" in settings:
|
|
AudioServer.set_bus_volume_db(music_bus, linear_to_db(settings["music_volume"]))
|
|
else:
|
|
DebugManager.log_warn(
|
|
"Music audio bus not found or music_volume setting missing", "SettingsManager"
|
|
)
|
|
|
|
if sfx_bus >= 0 and "sfx_volume" in settings:
|
|
AudioServer.set_bus_volume_db(sfx_bus, linear_to_db(settings["sfx_volume"]))
|
|
else:
|
|
DebugManager.log_warn(
|
|
"SFX audio bus not found or sfx_volume setting missing", "SettingsManager"
|
|
)
|
|
|
|
|
|
func save_settings():
|
|
var config = ConfigFile.new()
|
|
for key in settings.keys():
|
|
config.set_value("settings", key, settings[key])
|
|
|
|
var save_result = config.save(SETTINGS_FILE)
|
|
if save_result != OK:
|
|
DebugManager.log_error(
|
|
"Failed to save settings (Error code: %d)" % save_result, "SettingsManager"
|
|
)
|
|
return false
|
|
|
|
DebugManager.log_info("Settings saved: " + str(settings), "SettingsManager")
|
|
return true
|
|
|
|
|
|
func get_setting(key: String):
|
|
return settings.get(key)
|
|
|
|
|
|
func set_setting(key: String, value) -> bool:
|
|
if not key in default_settings:
|
|
DebugManager.log_error("Unknown setting key: " + key, "SettingsManager")
|
|
return false
|
|
|
|
# Validate value type and range based on key
|
|
if not _validate_setting_value(key, value):
|
|
DebugManager.log_error(
|
|
"Invalid value for setting '%s': %s" % [key, str(value)], "SettingsManager"
|
|
)
|
|
return false
|
|
|
|
settings[key] = value
|
|
_apply_setting_side_effect(key, value)
|
|
return true
|
|
|
|
|
|
func _validate_setting_value(key: String, value) -> bool:
|
|
match key:
|
|
"master_volume", "music_volume", "sfx_volume":
|
|
return _validate_volume_setting(key, value)
|
|
"language":
|
|
return _validate_language_setting(value)
|
|
_:
|
|
return _validate_default_setting(key, value)
|
|
|
|
|
|
func _validate_volume_setting(key: String, value) -> bool:
|
|
## Validate volume settings with numeric validation.
|
|
##
|
|
## Validates audio volume values are numbers within range (0.0 to 1.0).
|
|
## Handles edge cases like NaN and infinity values.
|
|
##
|
|
## Args:
|
|
## key: The setting key being validated (for error reporting)
|
|
## value: The volume value to validate
|
|
##
|
|
## Returns:
|
|
## bool: True if the value is a valid volume setting, False otherwise
|
|
if not (value is float or value is int):
|
|
return false
|
|
# Convert to float for validation
|
|
var float_value = float(value)
|
|
# Check for NaN and infinity
|
|
if is_nan(float_value) or is_inf(float_value):
|
|
DebugManager.log_warn(
|
|
"Invalid float value for %s: %s" % [key, str(value)], "SettingsManager"
|
|
)
|
|
return false
|
|
# Range validation
|
|
return float_value >= 0.0 and float_value <= 1.0
|
|
|
|
|
|
func _validate_language_setting(value) -> bool:
|
|
if not value is String:
|
|
return false
|
|
# Prevent extremely long strings
|
|
if value.length() > MAX_SETTING_STRING_LENGTH:
|
|
DebugManager.log_warn(
|
|
"Language code too long: %d characters" % value.length(), "SettingsManager"
|
|
)
|
|
return false
|
|
# Check for valid characters (alphanumeric and common separators only)
|
|
var regex = RegEx.new()
|
|
regex.compile("^[a-zA-Z0-9_-]+$")
|
|
if not regex.search(value):
|
|
DebugManager.log_warn(
|
|
"Language code contains invalid characters: %s" % value, "SettingsManager"
|
|
)
|
|
return false
|
|
# Check if language is supported
|
|
if languages_data.has("languages") and languages_data.languages is Dictionary:
|
|
return value in languages_data.languages
|
|
# Fallback to basic validation if languages not loaded
|
|
return value in ["en", "ru"]
|
|
|
|
|
|
func _validate_default_setting(key: String, value) -> bool:
|
|
# Default validation: accept if type matches default setting type
|
|
var default_value = default_settings.get(key)
|
|
if default_value == null:
|
|
DebugManager.log_warn("Unknown setting key in validation: %s" % key, "SettingsManager")
|
|
return false
|
|
return typeof(value) == typeof(default_value)
|
|
|
|
|
|
func _apply_setting_side_effect(key: String, value) -> void:
|
|
match key:
|
|
"language":
|
|
TranslationServer.set_locale(value)
|
|
"master_volume":
|
|
if AudioServer.get_bus_index("Master") >= 0:
|
|
AudioServer.set_bus_volume_db(
|
|
AudioServer.get_bus_index("Master"), linear_to_db(value)
|
|
)
|
|
"music_volume":
|
|
AudioManager.update_music_volume(value)
|
|
"sfx_volume":
|
|
if AudioServer.get_bus_index("SFX") >= 0:
|
|
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("SFX"), linear_to_db(value))
|
|
|
|
|
|
func load_languages():
|
|
var file_content = _load_languages_file()
|
|
if file_content.is_empty():
|
|
_load_default_languages_with_fallback("File loading failed")
|
|
return
|
|
|
|
var parsed_data = _parse_languages_json(file_content)
|
|
if not parsed_data:
|
|
_load_default_languages_with_fallback("JSON parsing failed")
|
|
return
|
|
|
|
if not _validate_languages_structure(parsed_data):
|
|
_load_default_languages_with_fallback("Structure validation failed")
|
|
return
|
|
|
|
languages_data = parsed_data
|
|
DebugManager.log_info(
|
|
"Languages loaded successfully: " + str(languages_data.languages.keys()), "SettingsManager"
|
|
)
|
|
|
|
|
|
func _load_languages_file() -> String:
|
|
var file = FileAccess.open(LANGUAGES_JSON_PATH, FileAccess.READ)
|
|
if not file:
|
|
var error_code = FileAccess.get_open_error()
|
|
DebugManager.log_error(
|
|
"Could not open languages.json (Error code: %d)" % error_code, "SettingsManager"
|
|
)
|
|
return ""
|
|
|
|
# Check file size to prevent memory exhaustion
|
|
var file_size = file.get_length()
|
|
if file_size > MAX_JSON_FILE_SIZE:
|
|
DebugManager.log_error(
|
|
"Languages.json file too large: %d bytes (max %d)" % [file_size, MAX_JSON_FILE_SIZE],
|
|
"SettingsManager"
|
|
)
|
|
file.close()
|
|
return ""
|
|
|
|
if file_size == 0:
|
|
DebugManager.log_error("Languages.json file is empty", "SettingsManager")
|
|
file.close()
|
|
return ""
|
|
|
|
var json_string = file.get_as_text()
|
|
var file_error = file.get_error()
|
|
file.close()
|
|
|
|
if file_error != OK:
|
|
DebugManager.log_error(
|
|
"Error reading languages.json (Error code: %d)" % file_error, "SettingsManager"
|
|
)
|
|
return ""
|
|
|
|
return json_string
|
|
|
|
|
|
func _parse_languages_json(json_string: String) -> Dictionary:
|
|
# Validate the JSON string is not empty
|
|
if json_string.is_empty():
|
|
DebugManager.log_error("Languages.json contains empty content", "SettingsManager")
|
|
return {}
|
|
|
|
var json = JSON.new()
|
|
var parse_result = json.parse(json_string)
|
|
if parse_result != OK:
|
|
DebugManager.log_error(
|
|
"JSON parsing failed at line %d: %s" % [json.error_line, json.error_string],
|
|
"SettingsManager"
|
|
)
|
|
return {}
|
|
|
|
if not json.data or not json.data is Dictionary:
|
|
DebugManager.log_error("Invalid JSON data structure in languages.json", "SettingsManager")
|
|
return {}
|
|
|
|
return json.data
|
|
|
|
|
|
func _load_default_languages_with_fallback(reason: String):
|
|
DebugManager.log_warn("Loading default languages due to: " + reason, "SettingsManager")
|
|
_load_default_languages()
|
|
|
|
|
|
func _load_default_languages():
|
|
# Fallback language data when JSON file fails to load
|
|
languages_data = {
|
|
"languages":
|
|
{"en": {"name": "English", "flag": "🇺🇸"}, "ru": {"name": "Русский", "flag": "🇷🇺"}}
|
|
}
|
|
DebugManager.log_info("Default languages loaded as fallback", "SettingsManager")
|
|
|
|
|
|
func get_languages_data():
|
|
return languages_data
|
|
|
|
|
|
func reset_settings_to_defaults() -> void:
|
|
DebugManager.log_info("Resetting all settings to defaults", "SettingsManager")
|
|
for key in default_settings.keys():
|
|
settings[key] = default_settings[key]
|
|
_apply_setting_side_effect(key, settings[key])
|
|
var save_success = save_settings()
|
|
if save_success:
|
|
DebugManager.log_info("Settings reset completed successfully", "SettingsManager")
|
|
else:
|
|
DebugManager.log_error("Failed to save reset settings", "SettingsManager")
|
|
|
|
|
|
func _validate_languages_structure(data: Dictionary) -> bool:
|
|
## Validate the structure and content of languages.json data.
|
|
##
|
|
## Validates language data loaded from the languages.json file.
|
|
## Ensures the data structure is valid and contains required fields.
|
|
##
|
|
## Args:
|
|
## data: Dictionary containing the parsed languages.json data
|
|
##
|
|
## Returns:
|
|
## bool: True if data structure is valid, False if validation fails
|
|
if not _validate_languages_root_structure(data):
|
|
return false
|
|
|
|
var languages = data["languages"]
|
|
return _validate_individual_languages(languages)
|
|
|
|
|
|
func _validate_languages_root_structure(data: Dictionary) -> bool:
|
|
"""Validate the root structure of languages data"""
|
|
if not data.has("languages"):
|
|
DebugManager.log_error("Languages.json missing 'languages' key", "SettingsManager")
|
|
return false
|
|
|
|
var languages = data["languages"]
|
|
if not languages is Dictionary:
|
|
DebugManager.log_error("'languages' is not a dictionary", "SettingsManager")
|
|
return false
|
|
|
|
if languages.is_empty():
|
|
DebugManager.log_error("Languages dictionary is empty", "SettingsManager")
|
|
return false
|
|
|
|
return true
|
|
|
|
|
|
func _validate_individual_languages(languages: Dictionary) -> bool:
|
|
"""Validate each individual language entry"""
|
|
for lang_code in languages.keys():
|
|
if not _validate_single_language_entry(lang_code, languages[lang_code]):
|
|
return false
|
|
return true
|
|
|
|
|
|
func _validate_single_language_entry(lang_code: Variant, lang_data: Variant) -> bool:
|
|
"""Validate a single language entry"""
|
|
if not lang_code is String:
|
|
DebugManager.log_error(
|
|
"Language code is not a string: %s" % str(lang_code), "SettingsManager"
|
|
)
|
|
return false
|
|
|
|
if lang_code.length() > MAX_SETTING_STRING_LENGTH:
|
|
DebugManager.log_error("Language code too long: %s" % lang_code, "SettingsManager")
|
|
return false
|
|
|
|
if not lang_data is Dictionary:
|
|
DebugManager.log_error(
|
|
"Language data for '%s' is not a dictionary" % lang_code, "SettingsManager"
|
|
)
|
|
return false
|
|
|
|
# Validate required fields in language data
|
|
if not lang_data.has("name") or not lang_data["name"] is String:
|
|
DebugManager.log_error(
|
|
"Language '%s' missing valid 'name' field" % lang_code, "SettingsManager"
|
|
)
|
|
return false
|
|
|
|
return true
|