add unit tests
saveload fixes
This commit is contained in:
@@ -6,6 +6,11 @@ const MAX_GRID_SIZE = 15
|
||||
const MAX_TILE_TYPES = 10
|
||||
const MAX_SCORE = 999999999
|
||||
const MAX_GAMES_PLAYED = 100000
|
||||
const MAX_FILE_SIZE = 1048576 # 1MB limit
|
||||
|
||||
# Save operation protection
|
||||
var _save_in_progress: bool = false
|
||||
var _restore_in_progress: bool = false
|
||||
|
||||
var game_data = {
|
||||
"high_score": 0,
|
||||
@@ -24,12 +29,24 @@ func _ready():
|
||||
load_game()
|
||||
|
||||
func save_game():
|
||||
# Prevent concurrent saves
|
||||
if _save_in_progress:
|
||||
DebugManager.log_warn("Save already in progress, skipping", "SaveManager")
|
||||
return false
|
||||
|
||||
_save_in_progress = true
|
||||
var result = _perform_save()
|
||||
_save_in_progress = false
|
||||
return result
|
||||
|
||||
func _perform_save():
|
||||
# Create backup before saving
|
||||
_create_backup()
|
||||
|
||||
# Add version and validation data
|
||||
# Add version and checksum
|
||||
var save_data = 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.open(SAVE_FILE_PATH, FileAccess.WRITE)
|
||||
@@ -39,7 +56,7 @@ func save_game():
|
||||
|
||||
var json_string = JSON.stringify(save_data)
|
||||
|
||||
# Validate JSON was created successfully
|
||||
# Validate JSON creation
|
||||
if json_string.is_empty():
|
||||
DebugManager.log_error("Failed to serialize save data to JSON", "SaveManager")
|
||||
save_file.close()
|
||||
@@ -56,15 +73,18 @@ func load_game():
|
||||
DebugManager.log_info("No save file found, using defaults", "SaveManager")
|
||||
return
|
||||
|
||||
# Reset restore flag
|
||||
_restore_in_progress = false
|
||||
|
||||
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
|
||||
# Check file size
|
||||
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")
|
||||
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
|
||||
|
||||
@@ -79,21 +99,52 @@ func load_game():
|
||||
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()
|
||||
if not _restore_in_progress:
|
||||
var backup_restored = _restore_backup_if_exists()
|
||||
if not backup_restored:
|
||||
DebugManager.log_warn("JSON parse failed and backup restore failed, using defaults", "SaveManager")
|
||||
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()
|
||||
if not _restore_in_progress:
|
||||
var backup_restored = _restore_backup_if_exists()
|
||||
if not backup_restored:
|
||||
DebugManager.log_warn("Invalid data format and backup restore failed, using defaults", "SaveManager")
|
||||
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()
|
||||
# Validate checksum first
|
||||
if not _validate_checksum(loaded_data):
|
||||
DebugManager.log_error("Save file checksum validation failed - possible tampering", "SaveManager")
|
||||
if not _restore_in_progress:
|
||||
var backup_restored = _restore_backup_if_exists()
|
||||
if not backup_restored:
|
||||
DebugManager.log_warn("Backup restore failed, using default game data", "SaveManager")
|
||||
return
|
||||
|
||||
# Handle version migration
|
||||
var migrated_data = _handle_version_migration(loaded_data)
|
||||
if migrated_data == null:
|
||||
DebugManager.log_error("Save file version migration failed", "SaveManager")
|
||||
if not _restore_in_progress:
|
||||
var backup_restored = _restore_backup_if_exists()
|
||||
if not backup_restored:
|
||||
DebugManager.log_warn("Migration failed and backup restore failed, using defaults", "SaveManager")
|
||||
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")
|
||||
if not _restore_in_progress:
|
||||
var backup_restored = _restore_backup_if_exists()
|
||||
if not backup_restored:
|
||||
DebugManager.log_warn("Validation failed and backup restore failed, using defaults", "SaveManager")
|
||||
return
|
||||
|
||||
# Use migrated data
|
||||
loaded_data = migrated_data
|
||||
|
||||
# Safely merge validated data
|
||||
_merge_validated_data(loaded_data)
|
||||
|
||||
@@ -116,7 +167,7 @@ func update_current_score(score: int):
|
||||
func start_new_game():
|
||||
game_data.current_score = 0
|
||||
game_data.games_played += 1
|
||||
# Clear any saved grid state for fresh start
|
||||
# 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")
|
||||
|
||||
@@ -132,7 +183,7 @@ func finish_game(final_score: int):
|
||||
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
|
||||
# Prevent 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")
|
||||
@@ -158,7 +209,7 @@ 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
|
||||
# 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
|
||||
@@ -170,7 +221,7 @@ func save_grid_state(grid_size: Vector2i, tile_types_count: int, active_gem_type
|
||||
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
|
||||
# Debug: Print first rows
|
||||
for y in range(min(3, grid_layout.size())):
|
||||
var row_str = ""
|
||||
for x in range(min(8, grid_layout[y].size())):
|
||||
@@ -191,10 +242,10 @@ func clear_grid_state():
|
||||
save_game()
|
||||
|
||||
func reset_all_progress():
|
||||
"""Reset all game progress and delete save files completely"""
|
||||
"""Reset all progress and delete save files"""
|
||||
DebugManager.log_info("Starting complete progress reset", "SaveManager")
|
||||
|
||||
# Reset all game data to initial values
|
||||
# Reset game data to defaults
|
||||
game_data = {
|
||||
"high_score": 0,
|
||||
"current_score": 0,
|
||||
@@ -226,6 +277,14 @@ func reset_all_progress():
|
||||
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
|
||||
@@ -239,18 +298,26 @@ func _validate_save_data(data: Dictionary) -> bool:
|
||||
|
||||
# Validate numeric fields
|
||||
if not _is_valid_score(data.get("high_score", 0)):
|
||||
DebugManager.log_error("Invalid high_score validation failed", "SaveManager")
|
||||
return false
|
||||
if not _is_valid_score(data.get("current_score", 0)):
|
||||
DebugManager.log_error("Invalid current_score validation failed", "SaveManager")
|
||||
return false
|
||||
if not _is_valid_score(data.get("total_score", 0)):
|
||||
DebugManager.log_error("Invalid total_score validation failed", "SaveManager")
|
||||
return false
|
||||
|
||||
# Use safe getter for games_played validation
|
||||
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
|
||||
|
||||
# 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(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")
|
||||
@@ -264,6 +331,93 @@ func _validate_save_data(data: Dictionary) -> bool:
|
||||
|
||||
return _validate_grid_state(grid_state)
|
||||
|
||||
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 = ["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 = 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(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 = 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(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 = 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:
|
||||
# Check grid size
|
||||
if not grid_state.has("grid_size") or not grid_state.grid_size is Dictionary:
|
||||
@@ -288,8 +442,29 @@ func _validate_grid_state(grid_state: Dictionary) -> bool:
|
||||
DebugManager.log_error("Invalid tile_types_count: %s" % str(tile_types), "SaveManager")
|
||||
return false
|
||||
|
||||
# Validate active_gem_types if present
|
||||
var active_gems = 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 = 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
|
||||
|
||||
# Validate grid layout if present
|
||||
var layout = 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)
|
||||
|
||||
@@ -345,6 +520,12 @@ func _is_valid_score(score) -> bool:
|
||||
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")
|
||||
@@ -355,25 +536,205 @@ 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
|
||||
# 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"):
|
||||
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
|
||||
game_data["games_played"] = _safe_get_numeric_value(loaded_data, "games_played", 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]
|
||||
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:
|
||||
# Simple checksum for save file integrity
|
||||
var json_string = JSON.stringify(data)
|
||||
return str(json_string.hash())
|
||||
# Calculate deterministic checksum EXCLUDING the checksum field itself
|
||||
var data_copy = data.duplicate(true)
|
||||
data_copy.erase("_checksum") # Remove checksum before calculation
|
||||
|
||||
# Create deterministic checksum using sorted keys to ensure consistency
|
||||
var checksum_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 = data.keys()
|
||||
keys.sort() # Ensure consistent ordering
|
||||
|
||||
var parts = []
|
||||
for key in keys:
|
||||
var value = data[key]
|
||||
var key_str = str(key)
|
||||
var value_str = ""
|
||||
|
||||
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 = []
|
||||
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) -> String:
|
||||
"""
|
||||
CRITICAL FIX: Normalize values for consistent checksum calculation
|
||||
This prevents JSON serialization type conversion from breaking checksums
|
||||
"""
|
||||
if value == null:
|
||||
return "null"
|
||||
elif value is bool:
|
||||
return str(value)
|
||||
elif value is int:
|
||||
# Convert to int string format to match JSON deserialized floats
|
||||
return str(int(value))
|
||||
elif value is float:
|
||||
# Convert float to int if it's a whole number (handles JSON conversion)
|
||||
if value == int(value):
|
||||
return str(int(value))
|
||||
else:
|
||||
# For actual floats, use consistent precision
|
||||
return "%.10f" % value
|
||||
elif value is String:
|
||||
return value
|
||||
else:
|
||||
return str(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 = data["_checksum"]
|
||||
var calculated_checksum = _calculate_checksum(data)
|
||||
var is_valid = 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 = 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
|
||||
else:
|
||||
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 = 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(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):
|
||||
"""Handle save data version migration and compatibility"""
|
||||
var data_version = 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
|
||||
elif 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
|
||||
else:
|
||||
# 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 = 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 = migrated_data[score_key]
|
||||
if score_value is float or score_value is int:
|
||||
var int_score = 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():
|
||||
# Create backup of current save file
|
||||
@@ -389,14 +750,42 @@ func _create_backup():
|
||||
|
||||
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()
|
||||
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.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 = 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 = 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.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
|
||||
|
||||
Reference in New Issue
Block a user