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