feature/saves-and-score (#8)
Reviewed-on: #8 Co-authored-by: Vladimir nett00n Budylnikov <git@nett00n.org> Co-committed-by: Vladimir nett00n Budylnikov <git@nett00n.org>
This commit is contained in:
@@ -7,12 +7,19 @@ var pending_gameplay_mode: String = "match3"
|
||||
var is_changing_scene: bool = false
|
||||
|
||||
func start_new_game() -> void:
|
||||
SaveManager.start_new_game()
|
||||
start_game_with_mode("match3")
|
||||
|
||||
func continue_game() -> void:
|
||||
# Don't reset score - just load the game scene
|
||||
start_game_with_mode("match3")
|
||||
|
||||
func start_match3_game() -> void:
|
||||
SaveManager.start_new_game()
|
||||
start_game_with_mode("match3")
|
||||
|
||||
func start_clickomania_game() -> void:
|
||||
SaveManager.start_new_game()
|
||||
start_game_with_mode("clickomania")
|
||||
|
||||
func start_game_with_mode(gameplay_mode: String) -> void:
|
||||
@@ -65,13 +72,25 @@ func start_game_with_mode(gameplay_mode: String) -> void:
|
||||
if get_tree().current_scene.has_method("set_gameplay_mode"):
|
||||
DebugManager.log_info("Setting gameplay mode to: %s" % pending_gameplay_mode, "GameManager")
|
||||
get_tree().current_scene.set_gameplay_mode(pending_gameplay_mode)
|
||||
|
||||
# Load saved score
|
||||
if get_tree().current_scene.has_method("set_global_score"):
|
||||
var saved_score = SaveManager.get_current_score()
|
||||
DebugManager.log_info("Loading saved score: %d" % saved_score, "GameManager")
|
||||
get_tree().current_scene.set_global_score(saved_score)
|
||||
else:
|
||||
DebugManager.log_error("Game scene does not have set_gameplay_mode method", "GameManager")
|
||||
|
||||
is_changing_scene = false
|
||||
|
||||
func save_game() -> void:
|
||||
DebugManager.log_info("Game saved (mock)", "GameManager")
|
||||
# Get current score from the active game scene
|
||||
var current_score = 0
|
||||
if get_tree().current_scene and get_tree().current_scene.has_method("get_global_score"):
|
||||
current_score = get_tree().current_scene.get_global_score()
|
||||
|
||||
SaveManager.finish_game(current_score)
|
||||
DebugManager.log_info("Game saved with score: %d" % current_score, "GameManager")
|
||||
|
||||
func exit_to_main_menu() -> void:
|
||||
# Prevent concurrent scene changes
|
||||
|
||||
402
src/autoloads/SaveManager.gd
Normal file
402
src/autoloads/SaveManager.gd
Normal file
@@ -0,0 +1,402 @@
|
||||
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()
|
||||
1
src/autoloads/SaveManager.gd.uid
Normal file
1
src/autoloads/SaveManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d1fdah5x4rme0
|
||||
Reference in New Issue
Block a user