## Save Manager - Secure Game Data Persistence System ## ## Provides secure save/load functionality with tamper detection, race condition protection, ## and permissive validation. Features backup/restore, version migration, and data integrity. ## Implements multi-layered security: checksums, size limits, type validation, and bounds checking. extends Node const SAVE_FILE_PATH: String = "user://savegame.save" const SAVE_FORMAT_VERSION: int = 1 const MAX_GRID_SIZE: int = 15 const MAX_TILE_TYPES: int = 10 const MAX_SCORE: int = 999999999 const MAX_GAMES_PLAYED: int = 100000 const MAX_FILE_SIZE: int = 1048576 # 1MB limit var game_data: Dictionary = { "high_score": 0, "current_score": 0, "games_played": 0, "total_score": 0, "grid_state": { "grid_size": {"x": 8, "y": 8}, "tile_types_count": 5, "active_gem_types": [0, 1, 2, 3, 4], "grid_layout": [] # 2D array of tile types } } # Save operation protection - prevents race conditions var _save_in_progress: bool = false var _restore_in_progress: bool = false func _ready() -> void: """Initialize SaveManager and load existing save data on startup""" load_game() func save_game() -> bool: """Save current game data with race condition protection and error handling""" # Prevent concurrent saves if _save_in_progress: DebugManager.log_warn("Save already in progress, skipping", "SaveManager") return false _save_in_progress = true var result: bool = _perform_save() _save_in_progress = false return result func _perform_save() -> bool: # Create backup before saving _create_backup() # Add version and checksum var save_data: Dictionary = game_data.duplicate(true) save_data["_version"] = SAVE_FORMAT_VERSION # Calculate checksum excluding _checksum field save_data["_checksum"] = _calculate_checksum(save_data) var save_file: FileAccess = FileAccess.open(SAVE_FILE_PATH, FileAccess.WRITE) if save_file == null: DebugManager.log_error( "Failed to open save file for writing: %s" % SAVE_FILE_PATH, "SaveManager" ) return false var json_string: String = JSON.stringify(save_data) # Validate JSON creation if json_string.is_empty(): DebugManager.log_error("Failed to serialize save data to JSON", "SaveManager") save_file.close() return false save_file.store_var(json_string) save_file.close() DebugManager.log_info( ( "Game saved successfully. High score: %d, Current score: %d" % [game_data.high_score, game_data.current_score] ), "SaveManager" ) return true func load_game() -> void: """Load game data from disk with comprehensive validation and error recovery""" if not FileAccess.file_exists(SAVE_FILE_PATH): DebugManager.log_info("No save file found, using defaults", "SaveManager") return # Reset restore flag _restore_in_progress = false var loaded_data = _load_and_parse_save_file() if loaded_data == null: return # Process the loaded data _process_loaded_data(loaded_data) func _load_and_parse_save_file() -> Variant: """Load and parse the save file, returning null on failure""" var save_file: FileAccess = FileAccess.open(SAVE_FILE_PATH, FileAccess.READ) if save_file == null: DebugManager.log_error( "Failed to open save file for reading: %s" % SAVE_FILE_PATH, "SaveManager" ) return null # Check file size var file_size: int = save_file.get_length() if file_size > MAX_FILE_SIZE: DebugManager.log_error( "Save file too large: %d bytes (max %d)" % [file_size, MAX_FILE_SIZE], "SaveManager" ) save_file.close() return null var json_string: Variant = save_file.get_var() save_file.close() if not json_string is String: DebugManager.log_error("Save file contains invalid data type", "SaveManager") return null var json: JSON = JSON.new() var parse_result: Error = json.parse(json_string) if parse_result != OK: DebugManager.log_error( "Failed to parse save file JSON: %s" % json.error_string, "SaveManager" ) _handle_load_failure("JSON parse failed") return null var loaded_data: Variant = json.data if not loaded_data is Dictionary: DebugManager.log_error("Save file root is not a dictionary", "SaveManager") _handle_load_failure("Invalid data format") return null return loaded_data func _process_loaded_data(loaded_data: Variant) -> void: """Process and validate the loaded data""" # Validate checksum first if not _validate_checksum(loaded_data): DebugManager.log_error( "Save file checksum validation failed - possible tampering", "SaveManager" ) _handle_load_failure("Checksum validation failed") return # Handle version migration var migrated_data: Variant = _handle_version_migration(loaded_data) if migrated_data == null: DebugManager.log_error("Save file version migration failed", "SaveManager") _handle_load_failure("Migration failed") return # Validate and fix loaded data if not _validate_and_fix_save_data(migrated_data): DebugManager.log_error( "Save file failed validation after migration, using defaults", "SaveManager" ) _handle_load_failure("Validation failed") return # Safely merge validated data _merge_validated_data(migrated_data) func _handle_load_failure(reason: String) -> void: """Handle load failure with backup restoration attempt""" if not _restore_in_progress: var backup_restored = _restore_backup_if_exists() if not backup_restored: DebugManager.log_warn( "%s and backup restore failed, using defaults" % reason, "SaveManager" ) DebugManager.log_info( ( "Game loaded successfully. High score: %d, Games played: %d" % [game_data.high_score, game_data.games_played] ), "SaveManager" ) func update_current_score(score: int) -> void: # Input validation if score < 0: DebugManager.log_warn("Negative score rejected: %d" % score, "SaveManager") return if score > MAX_SCORE: DebugManager.log_warn( "Score too high, capping at maximum: %d -> %d" % [score, MAX_SCORE], "SaveManager" ) score = MAX_SCORE game_data.current_score = score if score > game_data.high_score: game_data.high_score = score DebugManager.log_info("New high score: %d" % score, "SaveManager") func start_new_game() -> void: game_data.current_score = 0 game_data.games_played += 1 # Clear saved grid state game_data.grid_state.grid_layout = [] DebugManager.log_info( "Started new game #%d (cleared grid state)" % game_data.games_played, "SaveManager" ) func finish_game(final_score: int) -> void: # Input validation if final_score < 0: DebugManager.log_warn("Negative final score rejected: %d" % final_score, "SaveManager") return if final_score > MAX_SCORE: DebugManager.log_warn( "Final score too high, capping: %d -> %d" % [final_score, MAX_SCORE], "SaveManager" ) final_score = MAX_SCORE DebugManager.log_info( "Finishing game with score: %d (previous: %d)" % [final_score, game_data.current_score], "SaveManager" ) game_data.current_score = final_score # Prevent overflow var new_total: int = game_data.total_score + final_score if new_total < game_data.total_score: # Overflow check DebugManager.log_warn("Total score overflow prevented", "SaveManager") game_data.total_score = MAX_SCORE else: game_data.total_score = new_total if final_score > game_data.high_score: game_data.high_score = final_score DebugManager.log_info("New high score achieved: %d" % final_score, "SaveManager") save_game() func get_high_score() -> int: return game_data.high_score func get_current_score() -> int: return game_data.current_score func get_games_played() -> int: return game_data.games_played func get_total_score() -> int: return game_data.total_score func save_grid_state( grid_size: Vector2i, tile_types_count: int, active_gem_types: Array, grid_layout: Array ) -> void: # Input validation if not _validate_grid_parameters(grid_size, tile_types_count, active_gem_types, grid_layout): DebugManager.log_error("Grid state validation failed, not saving", "SaveManager") return DebugManager.log_info( ( "Saving grid state: size(%d,%d), types=%d, layout_rows=%d" % [grid_size.x, grid_size.y, tile_types_count, grid_layout.size()] ), "SaveManager" ) game_data.grid_state.grid_size = {"x": grid_size.x, "y": grid_size.y} game_data.grid_state.tile_types_count = tile_types_count game_data.grid_state.active_gem_types = active_gem_types.duplicate() game_data.grid_state.grid_layout = grid_layout.duplicate(true) # Deep copy # Debug: Print first rows for y in range(min(3, grid_layout.size())): var row_str: String = "" for x in range(min(8, grid_layout[y].size())): row_str += str(grid_layout[y][x]) + " " DebugManager.log_debug("Saved row %d: %s" % [y, row_str], "SaveManager") save_game() func get_saved_grid_state() -> Dictionary: return game_data.grid_state func has_saved_grid() -> bool: return game_data.grid_state.grid_layout.size() > 0 func clear_grid_state() -> void: DebugManager.log_info("Clearing saved grid state", "SaveManager") game_data.grid_state.grid_layout = [] save_game() func reset_all_progress() -> bool: """Reset all progress and delete save files""" DebugManager.log_info("Starting complete progress reset", "SaveManager") # Reset game data to defaults game_data = { "high_score": 0, "current_score": 0, "games_played": 0, "total_score": 0, "grid_state": { "grid_size": {"x": 8, "y": 8}, "tile_types_count": 5, "active_gem_types": [0, 1, 2, 3, 4], "grid_layout": [] # 2D array of tile types } } # Delete main save file if FileAccess.file_exists(SAVE_FILE_PATH): var error: Error = DirAccess.remove_absolute(SAVE_FILE_PATH) if error == OK: DebugManager.log_info("Main save file deleted successfully", "SaveManager") else: DebugManager.log_error( "Failed to delete main save file: error %d" % error, "SaveManager" ) # Delete backup file var backup_path: String = SAVE_FILE_PATH + ".backup" if FileAccess.file_exists(backup_path): var error: Error = DirAccess.remove_absolute(backup_path) if error == OK: DebugManager.log_info("Backup save file deleted successfully", "SaveManager") else: DebugManager.log_error( "Failed to delete backup save file: error %d" % error, "SaveManager" ) DebugManager.log_info( "Progress reset completed - all scores and save data cleared", "SaveManager" ) # Clear restore flag _restore_in_progress = false # Create fresh save file with default data DebugManager.log_info("Creating fresh save file with default data", "SaveManager") save_game() return true # Security and validation helper functions func _validate_save_data(data: Dictionary) -> bool: # Check required fields exist and have correct types if not _validate_required_fields(data): return false # Validate numeric fields if not _validate_score_fields(data): return false # Validate games_played field if not _validate_games_played_field(data): return false # Validate grid state var grid_state: Variant = data.get("grid_state", {}) if not grid_state is Dictionary: DebugManager.log_error("Grid state is not a dictionary", "SaveManager") return false return _validate_grid_state(grid_state) func _validate_required_fields(data: Dictionary) -> bool: """Validate that all required fields exist""" var required_fields: Array[String] = [ "high_score", "current_score", "games_played", "total_score", "grid_state" ] for field in required_fields: if not data.has(field): DebugManager.log_error("Missing required field: %s" % field, "SaveManager") return false return true func _validate_score_fields(data: Dictionary) -> bool: """Validate all score-related fields""" var score_fields = ["high_score", "current_score", "total_score"] for field in score_fields: if not _is_valid_score(data.get(field, 0)): DebugManager.log_error("Invalid %s validation failed" % field, "SaveManager") return false return true func _validate_games_played_field(data: Dictionary) -> bool: """Validate the games_played field""" var games_played: Variant = data.get("games_played", 0) if not (games_played is int or games_played is float): DebugManager.log_error( "Invalid games_played type: %s (type: %s)" % [str(games_played), typeof(games_played)], "SaveManager" ) return false # Check for NaN/Infinity in games_played if it's a float if games_played is float and (is_nan(games_played) or is_inf(games_played)): DebugManager.log_error( "Invalid games_played float value: %s" % str(games_played), "SaveManager" ) return false var games_played_int: int = int(games_played) if games_played_int < 0 or games_played_int > MAX_GAMES_PLAYED: DebugManager.log_error( "Invalid games_played value: %d (range: 0-%d)" % [games_played_int, MAX_GAMES_PLAYED], "SaveManager" ) return false return true func _validate_and_fix_save_data(data: Dictionary) -> bool: """ Permissive validation that fixes issues instead of rejecting data entirely. Used during migration to preserve as much user data as possible. """ DebugManager.log_info("Running permissive validation with auto-fix", "SaveManager") # Ensure all required fields exist, create defaults if missing var required_fields: Array[String] = [ "high_score", "current_score", "games_played", "total_score", "grid_state" ] for field in required_fields: if not data.has(field): DebugManager.log_warn( "Missing required field '%s', adding default value" % field, "SaveManager" ) match field: "high_score", "current_score", "total_score": data[field] = 0 "games_played": data[field] = 0 "grid_state": data[field] = { "grid_size": {"x": 8, "y": 8}, "tile_types_count": 5, "active_gem_types": [0, 1, 2, 3, 4], "grid_layout": [] } # Fix numeric fields - clamp to valid ranges instead of rejecting for field in ["high_score", "current_score", "total_score"]: var value: Variant = data.get(field, 0) if not (value is int or value is float): DebugManager.log_warn("Invalid type for %s, converting to 0" % field, "SaveManager") data[field] = 0 else: var numeric_value: int = int(value) if numeric_value < 0: DebugManager.log_warn("Negative %s fixed to 0" % field, "SaveManager") data[field] = 0 elif numeric_value > MAX_SCORE: DebugManager.log_warn("%s too high, clamped to maximum" % field, "SaveManager") data[field] = MAX_SCORE else: data[field] = numeric_value # Fix games_played var games_played: Variant = data.get("games_played", 0) if not (games_played is int or games_played is float): DebugManager.log_warn("Invalid games_played type, converting to 0", "SaveManager") data["games_played"] = 0 else: var games_played_int: int = int(games_played) if games_played_int < 0: data["games_played"] = 0 elif games_played_int > MAX_GAMES_PLAYED: data["games_played"] = MAX_GAMES_PLAYED else: data["games_played"] = games_played_int # Fix grid_state - ensure it exists and has basic structure var grid_state: Variant = data.get("grid_state", {}) if not grid_state is Dictionary: DebugManager.log_warn("Invalid grid_state, creating default", "SaveManager") data["grid_state"] = { "grid_size": {"x": 8, "y": 8}, "tile_types_count": 5, "active_gem_types": [0, 1, 2, 3, 4], "grid_layout": [] } else: # Fix grid_state fields if they're missing or invalid if not grid_state.has("grid_size") or not grid_state.grid_size is Dictionary: DebugManager.log_warn("Invalid grid_size, using default", "SaveManager") grid_state["grid_size"] = {"x": 8, "y": 8} if not grid_state.has("tile_types_count") or not grid_state.tile_types_count is int: DebugManager.log_warn("Invalid tile_types_count, using default", "SaveManager") grid_state["tile_types_count"] = 5 if not grid_state.has("active_gem_types") or not grid_state.active_gem_types is Array: DebugManager.log_warn("Invalid active_gem_types, using default", "SaveManager") grid_state["active_gem_types"] = [0, 1, 2, 3, 4] if not grid_state.has("grid_layout") or not grid_state.grid_layout is Array: DebugManager.log_warn("Invalid grid_layout, clearing saved grid", "SaveManager") grid_state["grid_layout"] = [] DebugManager.log_info( "Permissive validation completed - data has been fixed and will be loaded", "SaveManager" ) return true func _validate_grid_state(grid_state: Dictionary) -> bool: # Validate grid size var grid_size_validation = _validate_grid_size(grid_state) if not grid_size_validation.valid: return false var width = grid_size_validation.width var height = grid_size_validation.height # Validate tile types var tile_types = _validate_tile_types(grid_state) if tile_types == -1: return false # Validate active gem types if not _validate_active_gem_types(grid_state, tile_types): return false # Validate grid layout if present var layout: Variant = grid_state.get("grid_layout", []) if not layout is Array: DebugManager.log_error("grid_layout is not an array", "SaveManager") return false if layout.size() > 0: return _validate_grid_layout(layout, width, height, tile_types) return true func _validate_grid_size(grid_state: Dictionary) -> Dictionary: """Validate grid size and return validation result with dimensions""" var result = {"valid": false, "width": 0, "height": 0} if not grid_state.has("grid_size") or not grid_state.grid_size is Dictionary: DebugManager.log_error("Invalid grid_size in save data", "SaveManager") return result var size: Variant = grid_state.grid_size if not size.has("x") or not size.has("y"): return result var width: Variant = size.x var height: Variant = size.y if not width is int or not height is int: return result if width < 3 or height < 3 or width > MAX_GRID_SIZE or height > MAX_GRID_SIZE: DebugManager.log_error("Grid size out of bounds: %dx%d" % [width, height], "SaveManager") return result result.valid = true result.width = width result.height = height return result func _validate_tile_types(grid_state: Dictionary) -> int: """Validate tile types count and return it, or -1 if invalid""" var tile_types: Variant = grid_state.get("tile_types_count", 0) if not tile_types is int or tile_types < 3 or tile_types > MAX_TILE_TYPES: DebugManager.log_error("Invalid tile_types_count: %s" % str(tile_types), "SaveManager") return -1 return tile_types func _validate_active_gem_types(grid_state: Dictionary, tile_types: int) -> bool: """Validate active gem types array""" var active_gems: Variant = grid_state.get("active_gem_types", []) if not active_gems is Array: DebugManager.log_error("active_gem_types is not an array", "SaveManager") return false # If active_gem_types exists, validate its contents if active_gems.size() > 0: for i in range(active_gems.size()): var gem_type: Variant = active_gems[i] if not gem_type is int: DebugManager.log_error( "active_gem_types[%d] is not an integer: %s" % [i, str(gem_type)], "SaveManager" ) return false if gem_type < 0 or gem_type >= tile_types: DebugManager.log_error( "active_gem_types[%d] out of range: %d" % [i, gem_type], "SaveManager" ) return false return true func _validate_grid_layout( layout: Array, expected_width: int, expected_height: int, max_tile_type: int ) -> bool: if layout.size() != expected_height: DebugManager.log_error( "Grid layout height mismatch: %d vs %d" % [layout.size(), expected_height], "SaveManager" ) return false for y in range(layout.size()): var row: Variant = layout[y] if not row is Array: DebugManager.log_error("Grid layout row %d is not an array" % y, "SaveManager") return false if row.size() != expected_width: DebugManager.log_error( "Grid layout row %d width mismatch: %d vs %d" % [y, row.size(), expected_width], "SaveManager" ) return false for x in range(row.size()): var tile_type: Variant = row[x] if not tile_type is int: DebugManager.log_error( "Grid tile [%d][%d] is not an integer: %s" % [y, x, str(tile_type)], "SaveManager" ) return false if tile_type < -1 or tile_type >= max_tile_type: DebugManager.log_error( "Grid tile [%d][%d] type out of range: %d" % [y, x, tile_type], "SaveManager" ) return false return true func _validate_grid_parameters( grid_size: Vector2i, tile_types_count: int, active_gem_types: Array, grid_layout: Array ) -> bool: # Validate grid size if ( grid_size.x < 3 or grid_size.y < 3 or grid_size.x > MAX_GRID_SIZE or grid_size.y > MAX_GRID_SIZE ): DebugManager.log_error( ( "Invalid grid size: %dx%d (min 3x3, max %dx%d)" % [grid_size.x, grid_size.y, MAX_GRID_SIZE, MAX_GRID_SIZE] ), "SaveManager" ) return false # Validate tile types count if tile_types_count < 3 or tile_types_count > MAX_TILE_TYPES: DebugManager.log_error( "Invalid tile types count: %d (min 3, max %d)" % [tile_types_count, MAX_TILE_TYPES], "SaveManager" ) return false # Validate active gem types if active_gem_types.size() != tile_types_count: DebugManager.log_error( ( "Active gem types size mismatch: %d vs %d" % [active_gem_types.size(), tile_types_count] ), "SaveManager" ) return false # Validate grid layout return _validate_grid_layout(grid_layout, grid_size.x, grid_size.y, tile_types_count) func _is_valid_score(score: Variant) -> bool: # Accept both int and float, but convert to int for validation if not (score is int or score is float): DebugManager.log_error( "Score is not a number: %s (type: %s)" % [str(score), typeof(score)], "SaveManager" ) return false # Check for NaN and infinity values if score is float: if is_nan(score) or is_inf(score): DebugManager.log_error( "Score contains invalid float value (NaN/Inf): %s" % str(score), "SaveManager" ) return false var score_int = int(score) if score_int < 0 or score_int > MAX_SCORE: DebugManager.log_error("Score out of bounds: %d" % score_int, "SaveManager") return false return true func _merge_validated_data(loaded_data: Dictionary) -> void: # Safely merge only validated fields, converting floats to ints for scores for key in ["high_score", "current_score", "total_score"]: if loaded_data.has(key): # Use safe numeric conversion game_data[key] = _safe_get_numeric_value(loaded_data, key, 0) # Games played should always be an integer if loaded_data.has("games_played"): game_data["games_played"] = _safe_get_numeric_value(loaded_data, "games_played", 0) # Merge grid state carefully var loaded_grid: Variant = loaded_data.get("grid_state", {}) if loaded_grid is Dictionary: for grid_key in ["grid_size", "tile_types_count", "active_gem_types", "grid_layout"]: if loaded_grid.has(grid_key): game_data.grid_state[grid_key] = loaded_grid[grid_key] func _calculate_checksum(data: Dictionary) -> String: # Calculate deterministic checksum EXCLUDING the checksum field itself var data_copy: Dictionary = data.duplicate(true) data_copy.erase("_checksum") # Remove checksum before calculation # Create deterministic checksum using sorted keys to ensure consistency var checksum_string: String = _create_deterministic_string(data_copy) return str(checksum_string.hash()) func _create_deterministic_string(data: Dictionary) -> String: # Create a deterministic string representation by processing keys in sorted order var keys: Array = data.keys() keys.sort() # Ensure consistent ordering var parts: Array[String] = [] for key in keys: var value: Variant = data[key] var key_str: String = str(key) var value_str: String = "" if value is Dictionary: value_str = _create_deterministic_string(value) elif value is Array: value_str = _create_deterministic_array_string(value) else: # CRITICAL FIX: Normalize numeric values to prevent JSON serialization type issues value_str = _normalize_value_for_checksum(value) parts.append(key_str + ":" + value_str) return "{" + ",".join(parts) + "}" func _create_deterministic_array_string(arr: Array) -> String: # Create deterministic string representation of arrays var parts: Array[String] = [] for item in arr: if item is Dictionary: parts.append(_create_deterministic_string(item)) elif item is Array: parts.append(_create_deterministic_array_string(item)) else: # CRITICAL FIX: Normalize array values for consistent checksum parts.append(_normalize_value_for_checksum(item)) return "[" + ",".join(parts) + "]" func _normalize_value_for_checksum(value: Variant) -> String: """ CRITICAL FIX: Normalize values for consistent checksum calculation This prevents JSON serialization type conversion from breaking checksums """ if value == null: return "null" if value is bool: return str(value) if value is String: return value if value is int: # Convert to int string format to match JSON deserialized floats return str(int(value)) if value is float: return _normalize_float_for_checksum(value) return str(value) func _normalize_float_for_checksum(value: float) -> String: """Normalize float values for checksum calculation""" # Convert float to int if it's a whole number (handles JSON conversion) if value == int(value): return str(int(value)) # For actual floats, use consistent precision return "%.10f" % value func _validate_checksum(data: Dictionary) -> bool: # Validate checksum to detect tampering if not data.has("_checksum"): DebugManager.log_warn("No checksum found in save data", "SaveManager") return true # Allow saves without checksum for backward compatibility var stored_checksum: Variant = data["_checksum"] var calculated_checksum: String = _calculate_checksum(data) var is_valid: bool = stored_checksum == calculated_checksum if not is_valid: # MIGRATION COMPATIBILITY: If this is a version 1 save file, it might have the old checksum bug # Try to be more lenient with existing saves to prevent data loss var data_version: Variant = data.get("_version", 0) if data_version <= 1: DebugManager.log_warn( ( "Checksum mismatch in v%d save file - may be due to JSON serialization issue " + ( "(stored: %s, calculated: %s)" % [data_version, stored_checksum, calculated_checksum] ) ), "SaveManager" ) ( DebugManager . log_info( "Allowing load for backward compatibility - checksum will be recalculated on next save", "SaveManager" ) ) # Mark for checksum regeneration by removing the invalid one data.erase("_checksum") return true DebugManager.log_error( ( "Checksum mismatch - stored: %s, calculated: %s" % [stored_checksum, calculated_checksum] ), "SaveManager" ) return false return is_valid func _safe_get_numeric_value(data: Dictionary, key: String, default_value: float) -> int: """Safely extract and convert numeric values with comprehensive validation""" var value: Variant = data.get(key, default_value) # Type validation if not (value is float or value is int): DebugManager.log_warn( ( "Non-numeric value for %s: %s, using default %s" % [key, str(value), str(default_value)] ), "SaveManager" ) return int(default_value) # NaN/Infinity validation for floats if value is float: if is_nan(value) or is_inf(value): DebugManager.log_warn( ( "Invalid float value for %s: %s, using default %s" % [key, str(value), str(default_value)] ), "SaveManager" ) return int(default_value) # Convert to integer and validate bounds var int_value: int = int(value) # Apply bounds checking based on field type if key in ["high_score", "current_score", "total_score"]: if int_value < 0 or int_value > MAX_SCORE: DebugManager.log_warn( "Score %s out of bounds: %d, using default" % [key, int_value], "SaveManager" ) return int(default_value) elif key == "games_played": if int_value < 0 or int_value > MAX_GAMES_PLAYED: DebugManager.log_warn( "Games played out of bounds: %d, using default" % int_value, "SaveManager" ) return int(default_value) return int_value func _handle_version_migration(data: Dictionary) -> Variant: """Handle save data version migration and compatibility""" var data_version: Variant = data.get("_version", 0) # Default to version 0 for old saves if data_version == SAVE_FORMAT_VERSION: # Current version, no migration needed DebugManager.log_info( "Save file is current version (%d)" % SAVE_FORMAT_VERSION, "SaveManager" ) return data if data_version > SAVE_FORMAT_VERSION: # Future version - cannot handle DebugManager.log_error( ( "Save file version (%d) is newer than supported (%d)" % [data_version, SAVE_FORMAT_VERSION] ), "SaveManager" ) return null # Older version - migrate DebugManager.log_info( "Migrating save data from version %d to %d" % [data_version, SAVE_FORMAT_VERSION], "SaveManager" ) return _migrate_save_data(data, data_version) func _migrate_save_data(data: Dictionary, from_version: int) -> Dictionary: """Migrate save data from older versions to current format""" var migrated_data: Dictionary = data.duplicate(true) # Migration from version 0 (no version field) to version 1 if from_version < 1: # Add new fields that didn't exist in version 0 if not migrated_data.has("total_score"): migrated_data["total_score"] = 0 DebugManager.log_info("Added total_score field during migration", "SaveManager") if not migrated_data.has("grid_state"): migrated_data["grid_state"] = { "grid_size": {"x": 8, "y": 8}, "tile_types_count": 5, "active_gem_types": [0, 1, 2, 3, 4], "grid_layout": [] } DebugManager.log_info("Added grid_state structure during migration", "SaveManager") # Ensure all numeric values are within bounds after migration for score_key in ["high_score", "current_score", "total_score"]: if migrated_data.has(score_key): var score_value: Variant = migrated_data[score_key] if score_value is float or score_value is int: var int_score: int = int(score_value) if int_score < 0 or int_score > MAX_SCORE: DebugManager.log_warn( ( "Clamping %s during migration: %d -> %d" % [score_key, int_score, clamp(int_score, 0, MAX_SCORE)] ), "SaveManager" ) migrated_data[score_key] = clamp(int_score, 0, MAX_SCORE) # Future migrations would go here # if from_version < 2: # # Migration logic for version 2 # Update version number migrated_data["_version"] = SAVE_FORMAT_VERSION # Recalculate checksum after migration migrated_data["_checksum"] = _calculate_checksum(migrated_data) DebugManager.log_info("Save data migration completed successfully", "SaveManager") return migrated_data func _create_backup() -> void: # Create backup of current save file if FileAccess.file_exists(SAVE_FILE_PATH): var backup_path: String = SAVE_FILE_PATH + ".backup" var original: FileAccess = FileAccess.open(SAVE_FILE_PATH, FileAccess.READ) var backup: FileAccess = FileAccess.open(backup_path, FileAccess.WRITE) if original and backup: backup.store_var(original.get_var()) backup.close() if original: original.close() func _restore_backup_if_exists() -> bool: var backup_path: String = SAVE_FILE_PATH + ".backup" if not FileAccess.file_exists(backup_path): DebugManager.log_warn("No backup file found for recovery", "SaveManager") return false DebugManager.log_info("Attempting to restore from backup", "SaveManager") # Validate backup file size before attempting restore var backup_file: FileAccess = FileAccess.open(backup_path, FileAccess.READ) if backup_file == null: DebugManager.log_error("Failed to open backup file for reading", "SaveManager") return false var backup_size: int = backup_file.get_length() if backup_size > MAX_FILE_SIZE: DebugManager.log_error("Backup file too large: %d bytes" % backup_size, "SaveManager") backup_file.close() return false # Attempt to restore backup var backup_data: Variant = backup_file.get_var() backup_file.close() if backup_data == null: DebugManager.log_error("Backup file contains no data", "SaveManager") return false # Create new save file from backup var original: FileAccess = FileAccess.open(SAVE_FILE_PATH, FileAccess.WRITE) if original == null: DebugManager.log_error("Failed to create new save file from backup", "SaveManager") return false original.store_var(backup_data) original.close() DebugManager.log_info("Backup restored successfully to main save file", "SaveManager") # Note: The restored file will be loaded on the next game restart # We don't recursively load here to prevent infinite loops return true