extends Node const SAVE_FILE_PATH = "user://savegame.save" const SAVE_FORMAT_VERSION = 1 const MAX_GRID_SIZE = 15 const MAX_TILE_TYPES = 10 const MAX_SCORE = 999999999 const MAX_GAMES_PLAYED = 100000 var 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 } } func _ready(): load_game() func save_game(): # Create backup before saving _create_backup() # Add version and validation data var save_data = game_data.duplicate(true) save_data["_version"] = SAVE_FORMAT_VERSION save_data["_checksum"] = _calculate_checksum(save_data) var save_file = 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 = JSON.stringify(save_data) # Validate JSON was created successfully 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(): if not FileAccess.file_exists(SAVE_FILE_PATH): DebugManager.log_info("No save file found, using defaults", "SaveManager") return var save_file = 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 # Check file size to prevent memory exhaustion var file_size = save_file.get_length() if file_size > 1048576: # 1MB limit DebugManager.log_error("Save file too large: %d bytes (max 1MB)" % file_size, "SaveManager") save_file.close() return var json_string = save_file.get_var() save_file.close() if not json_string is String: DebugManager.log_error("Save file contains invalid data type", "SaveManager") return var json = JSON.new() var parse_result = json.parse(json_string) if parse_result != OK: DebugManager.log_error("Failed to parse save file JSON: %s" % json.error_string, "SaveManager") _restore_backup_if_exists() return var loaded_data = json.data if not loaded_data is Dictionary: DebugManager.log_error("Save file root is not a dictionary", "SaveManager") _restore_backup_if_exists() return # Validate and sanitize loaded data if not _validate_save_data(loaded_data): DebugManager.log_error("Save file failed validation, using defaults", "SaveManager") _restore_backup_if_exists() return # Safely merge validated data _merge_validated_data(loaded_data) 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): # 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(): game_data.current_score = 0 game_data.games_played += 1 # Clear any saved grid state for fresh start 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): # 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 total_score overflow var new_total = 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): # Comprehensive 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 few rows of saved layout for y in range(min(3, grid_layout.size())): var row_str = "" 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(): DebugManager.log_info("Clearing saved grid state", "SaveManager") game_data.grid_state.grid_layout = [] save_game() func reset_all_progress(): """Reset all game progress and delete save files completely""" DebugManager.log_info("Starting complete progress reset", "SaveManager") # Reset all game data to initial values 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 = 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 = SAVE_FILE_PATH + ".backup" if FileAccess.file_exists(backup_path): var 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") return true # Security and validation helper functions func _validate_save_data(data: Dictionary) -> bool: # Check required fields exist and have correct types var required_fields = ["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 # Validate numeric fields if not _is_valid_score(data.get("high_score", 0)): return false if not _is_valid_score(data.get("current_score", 0)): return false if not _is_valid_score(data.get("total_score", 0)): return false var games_played = data.get("games_played", 0) # Accept both int and float for games_played, convert to int for validation 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 var games_played_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 # Validate grid state var grid_state = 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_grid_state(grid_state: Dictionary) -> bool: # Check grid size 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 false var size = grid_state.grid_size if not size.has("x") or not size.has("y"): return false var width = size.x var height = size.y if not width is int or not height is int: return false 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 false # Check tile types var tile_types = 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 false # Validate grid layout if present var layout = grid_state.get("grid_layout", []) if layout.size() > 0: return _validate_grid_layout(layout, width, height, tile_types) 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 = 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 = 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) -> 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 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): # 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): var value = loaded_data[key] # Convert float scores to integers game_data[key] = int(value) if (value is float or value is int) else 0 # Games played should always be an integer if loaded_data.has("games_played"): var games_played = loaded_data["games_played"] game_data["games_played"] = int(games_played) if (games_played is float or games_played is int) else 0 # Merge grid state carefully var loaded_grid = loaded_data.get("grid_state", {}) 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: # Simple checksum for save file integrity var json_string = JSON.stringify(data) return str(json_string.hash()) func _create_backup(): # Create backup of current save file if FileAccess.file_exists(SAVE_FILE_PATH): var backup_path = SAVE_FILE_PATH + ".backup" var original = FileAccess.open(SAVE_FILE_PATH, FileAccess.READ) var backup = 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(): var backup_path = SAVE_FILE_PATH + ".backup" if FileAccess.file_exists(backup_path): DebugManager.log_info("Attempting to restore from backup", "SaveManager") var backup = FileAccess.open(backup_path, FileAccess.READ) var original = FileAccess.open(SAVE_FILE_PATH, FileAccess.WRITE) if backup and original: original.store_var(backup.get_var()) original.close() DebugManager.log_info("Backup restored successfully", "SaveManager") load_game() # Try to load the restored backup if backup: backup.close()