Add gdlint and gdformat scripts
This commit is contained in:
@@ -7,6 +7,7 @@ var music_player: AudioStreamPlayer
|
||||
var ui_click_player: AudioStreamPlayer
|
||||
var click_stream: AudioStream
|
||||
|
||||
|
||||
func _ready():
|
||||
music_player = AudioStreamPlayer.new()
|
||||
add_child(music_player)
|
||||
@@ -32,22 +33,26 @@ func _ready():
|
||||
|
||||
_start_music()
|
||||
|
||||
|
||||
func _load_stream() -> AudioStream:
|
||||
var res = load(MUSIC_PATH)
|
||||
if not res or not res is AudioStream:
|
||||
return null
|
||||
return res
|
||||
|
||||
|
||||
func _configure_stream_loop(stream: AudioStream) -> void:
|
||||
if stream is AudioStreamWAV:
|
||||
stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
||||
elif stream is AudioStreamOggVorbis:
|
||||
stream.loop = true
|
||||
|
||||
|
||||
func _configure_audio_bus() -> void:
|
||||
music_player.bus = "Music"
|
||||
music_player.volume_db = linear_to_db(SettingsManager.get_setting("music_volume"))
|
||||
|
||||
|
||||
func update_music_volume(volume: float) -> void:
|
||||
var volume_db = linear_to_db(volume)
|
||||
music_player.volume_db = volume_db
|
||||
@@ -58,16 +63,19 @@ func update_music_volume(volume: float) -> void:
|
||||
else:
|
||||
_stop_music()
|
||||
|
||||
|
||||
func _start_music() -> void:
|
||||
if music_player.playing:
|
||||
return
|
||||
music_player.play()
|
||||
|
||||
|
||||
func _stop_music() -> void:
|
||||
if not music_player.playing:
|
||||
return
|
||||
music_player.stop()
|
||||
|
||||
|
||||
func play_ui_click() -> void:
|
||||
if not click_stream:
|
||||
return
|
||||
|
||||
@@ -2,54 +2,58 @@ extends Node
|
||||
|
||||
signal debug_toggled(enabled: bool)
|
||||
|
||||
enum LogLevel {
|
||||
TRACE = 0,
|
||||
DEBUG = 1,
|
||||
INFO = 2,
|
||||
WARN = 3,
|
||||
ERROR = 4,
|
||||
FATAL = 5
|
||||
}
|
||||
enum LogLevel { TRACE = 0, DEBUG = 1, INFO = 2, WARN = 3, ERROR = 4, FATAL = 5 }
|
||||
|
||||
var debug_enabled: bool = false
|
||||
var debug_overlay_visible: bool = false
|
||||
var current_log_level: LogLevel = LogLevel.INFO
|
||||
|
||||
|
||||
func _ready():
|
||||
log_info("DebugManager loaded")
|
||||
|
||||
|
||||
func toggle_debug():
|
||||
debug_enabled = !debug_enabled
|
||||
debug_toggled.emit(debug_enabled)
|
||||
log_info("Debug mode: " + ("ON" if debug_enabled else "OFF"))
|
||||
|
||||
|
||||
func set_debug_enabled(enabled: bool):
|
||||
if debug_enabled != enabled:
|
||||
debug_enabled = enabled
|
||||
debug_toggled.emit(debug_enabled)
|
||||
|
||||
|
||||
func is_debug_enabled() -> bool:
|
||||
return debug_enabled
|
||||
|
||||
|
||||
func toggle_overlay():
|
||||
debug_overlay_visible = !debug_overlay_visible
|
||||
|
||||
|
||||
func set_overlay_visible(visible: bool):
|
||||
debug_overlay_visible = visible
|
||||
|
||||
|
||||
func is_overlay_visible() -> bool:
|
||||
return debug_overlay_visible
|
||||
|
||||
|
||||
func set_log_level(level: LogLevel):
|
||||
current_log_level = level
|
||||
log_info("Log level set to: " + _log_level_to_string(level))
|
||||
|
||||
|
||||
func get_log_level() -> LogLevel:
|
||||
return current_log_level
|
||||
|
||||
|
||||
func _should_log(level: LogLevel) -> bool:
|
||||
return level >= current_log_level
|
||||
|
||||
|
||||
func _log_level_to_string(level: LogLevel) -> String:
|
||||
match level:
|
||||
LogLevel.TRACE:
|
||||
@@ -67,41 +71,48 @@ func _log_level_to_string(level: LogLevel) -> String:
|
||||
_:
|
||||
return "UNKNOWN"
|
||||
|
||||
|
||||
func _format_log_message(level: LogLevel, message: String, category: String = "") -> String:
|
||||
var timestamp = Time.get_datetime_string_from_system()
|
||||
var level_str = _log_level_to_string(level)
|
||||
var category_str = (" [" + category + "]") if category != "" else ""
|
||||
return "[%s] %s%s: %s" % [timestamp, level_str, category_str, message]
|
||||
|
||||
|
||||
func log_trace(message: String, category: String = ""):
|
||||
if _should_log(LogLevel.TRACE):
|
||||
var formatted = _format_log_message(LogLevel.TRACE, message, category)
|
||||
if debug_enabled:
|
||||
print(formatted)
|
||||
|
||||
|
||||
func log_debug(message: String, category: String = ""):
|
||||
if _should_log(LogLevel.DEBUG):
|
||||
var formatted = _format_log_message(LogLevel.DEBUG, message, category)
|
||||
if debug_enabled:
|
||||
print(formatted)
|
||||
|
||||
|
||||
func log_info(message: String, category: String = ""):
|
||||
if _should_log(LogLevel.INFO):
|
||||
var formatted = _format_log_message(LogLevel.INFO, message, category)
|
||||
print(formatted)
|
||||
|
||||
|
||||
func log_warn(message: String, category: String = ""):
|
||||
if _should_log(LogLevel.WARN):
|
||||
var formatted = _format_log_message(LogLevel.WARN, message, category)
|
||||
print(formatted)
|
||||
push_warning(formatted)
|
||||
|
||||
|
||||
func log_error(message: String, category: String = ""):
|
||||
if _should_log(LogLevel.ERROR):
|
||||
var formatted = _format_log_message(LogLevel.ERROR, message, category)
|
||||
print(formatted)
|
||||
push_error(formatted)
|
||||
|
||||
|
||||
func log_fatal(message: String, category: String = ""):
|
||||
if _should_log(LogLevel.FATAL):
|
||||
var formatted = _format_log_message(LogLevel.FATAL, message, category)
|
||||
|
||||
@@ -6,22 +6,27 @@ const MAIN_SCENE_PATH := "res://scenes/main/main.tscn"
|
||||
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
|
||||
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:
|
||||
# Input validation
|
||||
if not gameplay_mode or gameplay_mode.is_empty():
|
||||
@@ -29,7 +34,9 @@ func start_game_with_mode(gameplay_mode: String) -> void:
|
||||
return
|
||||
|
||||
if not gameplay_mode is String:
|
||||
DebugManager.log_error("Invalid gameplay mode type: " + str(typeof(gameplay_mode)), "GameManager")
|
||||
DebugManager.log_error(
|
||||
"Invalid gameplay mode type: " + str(typeof(gameplay_mode)), "GameManager"
|
||||
)
|
||||
return
|
||||
|
||||
# Prevent concurrent scene changes
|
||||
@@ -40,7 +47,10 @@ func start_game_with_mode(gameplay_mode: String) -> void:
|
||||
# Validate gameplay mode
|
||||
var valid_modes = ["match3", "clickomania"]
|
||||
if not gameplay_mode in valid_modes:
|
||||
DebugManager.log_error("Invalid gameplay mode: '%s'. Valid modes: %s" % [gameplay_mode, str(valid_modes)], "GameManager")
|
||||
DebugManager.log_error(
|
||||
"Invalid gameplay mode: '%s'. Valid modes: %s" % [gameplay_mode, str(valid_modes)],
|
||||
"GameManager"
|
||||
)
|
||||
return
|
||||
|
||||
is_changing_scene = true
|
||||
@@ -54,7 +64,9 @@ func start_game_with_mode(gameplay_mode: String) -> void:
|
||||
|
||||
var result = get_tree().change_scene_to_packed(packed_scene)
|
||||
if result != OK:
|
||||
DebugManager.log_error("Failed to change to game scene (Error code: %d)" % result, "GameManager")
|
||||
DebugManager.log_error(
|
||||
"Failed to change to game scene (Error code: %d)" % result, "GameManager"
|
||||
)
|
||||
is_changing_scene = false
|
||||
return
|
||||
|
||||
@@ -83,6 +95,7 @@ func start_game_with_mode(gameplay_mode: String) -> void:
|
||||
|
||||
is_changing_scene = false
|
||||
|
||||
|
||||
func save_game() -> void:
|
||||
# Get current score from the active game scene
|
||||
var current_score = 0
|
||||
@@ -92,10 +105,13 @@ func save_game() -> void:
|
||||
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
|
||||
if is_changing_scene:
|
||||
DebugManager.log_warn("Scene change already in progress, ignoring exit to main menu request", "GameManager")
|
||||
DebugManager.log_warn(
|
||||
"Scene change already in progress, ignoring exit to main menu request", "GameManager"
|
||||
)
|
||||
return
|
||||
|
||||
is_changing_scene = true
|
||||
@@ -109,7 +125,9 @@ func exit_to_main_menu() -> void:
|
||||
|
||||
var result = get_tree().change_scene_to_packed(packed_scene)
|
||||
if result != OK:
|
||||
DebugManager.log_error("Failed to change to main scene (Error code: %d)" % result, "GameManager")
|
||||
DebugManager.log_error(
|
||||
"Failed to change to main scene (Error code: %d)" % result, "GameManager"
|
||||
)
|
||||
is_changing_scene = false
|
||||
return
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
extends Node
|
||||
|
||||
|
||||
func _ready():
|
||||
# Set default locale if not already set
|
||||
if TranslationServer.get_locale() == "":
|
||||
TranslationServer.set_locale("en")
|
||||
|
||||
|
||||
func change_language(locale: String):
|
||||
TranslationServer.set_locale(locale)
|
||||
# Signal to update UI elements
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
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
|
||||
const MAX_FILE_SIZE = 1048576 # 1MB limit
|
||||
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
|
||||
|
||||
# Save operation protection
|
||||
var _save_in_progress: bool = false
|
||||
var _restore_in_progress: bool = false
|
||||
|
||||
var game_data = {
|
||||
var game_data: Dictionary = {
|
||||
"high_score": 0,
|
||||
"current_score": 0,
|
||||
"games_played": 0,
|
||||
"total_score": 0,
|
||||
"grid_state": {
|
||||
"grid_state":
|
||||
{
|
||||
"grid_size": {"x": 8, "y": 8},
|
||||
"tile_types_count": 5,
|
||||
"active_gem_types": [0, 1, 2, 3, 4],
|
||||
@@ -25,36 +26,41 @@ var game_data = {
|
||||
}
|
||||
}
|
||||
|
||||
func _ready():
|
||||
|
||||
func _ready() -> void:
|
||||
load_game()
|
||||
|
||||
func save_game():
|
||||
|
||||
func save_game() -> bool:
|
||||
# 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()
|
||||
var result: bool = _perform_save()
|
||||
_save_in_progress = false
|
||||
return result
|
||||
|
||||
func _perform_save():
|
||||
|
||||
func _perform_save() -> bool:
|
||||
# Create backup before saving
|
||||
_create_backup()
|
||||
|
||||
# Add version and checksum
|
||||
var save_data = game_data.duplicate(true)
|
||||
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.open(SAVE_FILE_PATH, FileAccess.WRITE)
|
||||
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")
|
||||
DebugManager.log_error(
|
||||
"Failed to open save file for writing: %s" % SAVE_FILE_PATH, "SaveManager"
|
||||
)
|
||||
return false
|
||||
|
||||
var json_string = JSON.stringify(save_data)
|
||||
var json_string: String = JSON.stringify(save_data)
|
||||
|
||||
# Validate JSON creation
|
||||
if json_string.is_empty():
|
||||
@@ -65,10 +71,17 @@ func _perform_save():
|
||||
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")
|
||||
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():
|
||||
|
||||
func load_game() -> void:
|
||||
if not FileAccess.file_exists(SAVE_FILE_PATH):
|
||||
DebugManager.log_info("No save file found, using defaults", "SaveManager")
|
||||
return
|
||||
@@ -76,70 +89,90 @@ func load_game():
|
||||
# Reset restore flag
|
||||
_restore_in_progress = false
|
||||
|
||||
var save_file = FileAccess.open(SAVE_FILE_PATH, FileAccess.READ)
|
||||
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")
|
||||
DebugManager.log_error(
|
||||
"Failed to open save file for reading: %s" % SAVE_FILE_PATH, "SaveManager"
|
||||
)
|
||||
return
|
||||
|
||||
# Check file size
|
||||
var file_size = save_file.get_length()
|
||||
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")
|
||||
DebugManager.log_error(
|
||||
"Save file too large: %d bytes (max %d)" % [file_size, MAX_FILE_SIZE], "SaveManager"
|
||||
)
|
||||
save_file.close()
|
||||
return
|
||||
|
||||
var json_string = save_file.get_var()
|
||||
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
|
||||
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(json_string)
|
||||
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")
|
||||
DebugManager.log_error(
|
||||
"Failed to parse save file JSON: %s" % json.error_string, "SaveManager"
|
||||
)
|
||||
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")
|
||||
DebugManager.log_warn(
|
||||
"JSON parse failed and backup restore failed, using defaults", "SaveManager"
|
||||
)
|
||||
return
|
||||
|
||||
var loaded_data = json.data
|
||||
var loaded_data: Variant = json.data
|
||||
if not loaded_data is Dictionary:
|
||||
DebugManager.log_error("Save file root is not a dictionary", "SaveManager")
|
||||
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")
|
||||
DebugManager.log_warn(
|
||||
"Invalid data format and backup restore failed, using defaults", "SaveManager"
|
||||
)
|
||||
return
|
||||
|
||||
# Validate checksum first
|
||||
if not _validate_checksum(loaded_data):
|
||||
DebugManager.log_error("Save file checksum validation failed - possible tampering", "SaveManager")
|
||||
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")
|
||||
DebugManager.log_warn(
|
||||
"Backup restore failed, using default game data", "SaveManager"
|
||||
)
|
||||
return
|
||||
|
||||
# Handle version migration
|
||||
var migrated_data = _handle_version_migration(loaded_data)
|
||||
var migrated_data: Variant = _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")
|
||||
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")
|
||||
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")
|
||||
DebugManager.log_warn(
|
||||
"Validation failed and backup restore failed, using defaults", "SaveManager"
|
||||
)
|
||||
return
|
||||
|
||||
# Use migrated data
|
||||
@@ -148,15 +181,24 @@ func load_game():
|
||||
# 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")
|
||||
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):
|
||||
|
||||
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")
|
||||
DebugManager.log_warn(
|
||||
"Score too high, capping at maximum: %d -> %d" % [score, MAX_SCORE], "SaveManager"
|
||||
)
|
||||
score = MAX_SCORE
|
||||
|
||||
game_data.current_score = score
|
||||
@@ -164,27 +206,36 @@ func update_current_score(score: int):
|
||||
game_data.high_score = score
|
||||
DebugManager.log_info("New high score: %d" % score, "SaveManager")
|
||||
|
||||
func start_new_game():
|
||||
|
||||
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")
|
||||
DebugManager.log_info(
|
||||
"Started new game #%d (cleared grid state)" % game_data.games_played, "SaveManager"
|
||||
)
|
||||
|
||||
func finish_game(final_score: int):
|
||||
|
||||
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")
|
||||
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")
|
||||
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 = game_data.total_score + final_score
|
||||
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
|
||||
@@ -196,25 +247,38 @@ func finish_game(final_score: int):
|
||||
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):
|
||||
|
||||
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")
|
||||
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
|
||||
@@ -223,25 +287,29 @@ func save_grid_state(grid_size: Vector2i, tile_types_count: int, active_gem_type
|
||||
|
||||
# Debug: Print first rows
|
||||
for y in range(min(3, grid_layout.size())):
|
||||
var row_str = ""
|
||||
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():
|
||||
|
||||
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():
|
||||
|
||||
func reset_all_progress() -> bool:
|
||||
"""Reset all progress and delete save files"""
|
||||
DebugManager.log_info("Starting complete progress reset", "SaveManager")
|
||||
|
||||
@@ -251,7 +319,8 @@ func reset_all_progress():
|
||||
"current_score": 0,
|
||||
"games_played": 0,
|
||||
"total_score": 0,
|
||||
"grid_state": {
|
||||
"grid_state":
|
||||
{
|
||||
"grid_size": {"x": 8, "y": 8},
|
||||
"tile_types_count": 5,
|
||||
"active_gem_types": [0, 1, 2, 3, 4],
|
||||
@@ -261,22 +330,28 @@ func reset_all_progress():
|
||||
|
||||
# Delete main save file
|
||||
if FileAccess.file_exists(SAVE_FILE_PATH):
|
||||
var error = DirAccess.remove_absolute(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")
|
||||
DebugManager.log_error(
|
||||
"Failed to delete main save file: error %d" % error, "SaveManager"
|
||||
)
|
||||
|
||||
# Delete backup file
|
||||
var backup_path = SAVE_FILE_PATH + ".backup"
|
||||
var backup_path: String = SAVE_FILE_PATH + ".backup"
|
||||
if FileAccess.file_exists(backup_path):
|
||||
var error = DirAccess.remove_absolute(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_error(
|
||||
"Failed to delete backup save file: error %d" % error, "SaveManager"
|
||||
)
|
||||
|
||||
DebugManager.log_info("Progress reset completed - all scores and save data cleared", "SaveManager")
|
||||
DebugManager.log_info(
|
||||
"Progress reset completed - all scores and save data cleared", "SaveManager"
|
||||
)
|
||||
|
||||
# Clear restore flag
|
||||
_restore_in_progress = false
|
||||
@@ -287,10 +362,13 @@ func reset_all_progress():
|
||||
|
||||
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"]
|
||||
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")
|
||||
@@ -308,29 +386,38 @@ func _validate_save_data(data: Dictionary) -> bool:
|
||||
return false
|
||||
|
||||
# Use safe getter for games_played validation
|
||||
var games_played = data.get("games_played", 0)
|
||||
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")
|
||||
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")
|
||||
DebugManager.log_error(
|
||||
"Invalid games_played float value: %s" % str(games_played), "SaveManager"
|
||||
)
|
||||
return false
|
||||
|
||||
var games_played_int = int(games_played)
|
||||
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")
|
||||
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", {})
|
||||
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_and_fix_save_data(data: Dictionary) -> bool:
|
||||
"""
|
||||
Permissive validation that fixes issues instead of rejecting data entirely.
|
||||
@@ -339,10 +426,14 @@ func _validate_and_fix_save_data(data: Dictionary) -> bool:
|
||||
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"]
|
||||
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")
|
||||
DebugManager.log_warn(
|
||||
"Missing required field '%s', adding default value" % field, "SaveManager"
|
||||
)
|
||||
match field:
|
||||
"high_score", "current_score", "total_score":
|
||||
data[field] = 0
|
||||
@@ -358,12 +449,12 @@ func _validate_and_fix_save_data(data: Dictionary) -> bool:
|
||||
|
||||
# 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)
|
||||
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(value)
|
||||
var numeric_value: int = int(value)
|
||||
if numeric_value < 0:
|
||||
DebugManager.log_warn("Negative %s fixed to 0" % field, "SaveManager")
|
||||
data[field] = 0
|
||||
@@ -374,12 +465,12 @@ func _validate_and_fix_save_data(data: Dictionary) -> bool:
|
||||
data[field] = numeric_value
|
||||
|
||||
# Fix games_played
|
||||
var games_played = data.get("games_played", 0)
|
||||
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(games_played)
|
||||
var games_played_int: int = int(games_played)
|
||||
if games_played_int < 0:
|
||||
data["games_played"] = 0
|
||||
elif games_played_int > MAX_GAMES_PLAYED:
|
||||
@@ -388,7 +479,7 @@ func _validate_and_fix_save_data(data: Dictionary) -> bool:
|
||||
data["games_played"] = games_played_int
|
||||
|
||||
# Fix grid_state - ensure it exists and has basic structure
|
||||
var grid_state = data.get("grid_state", {})
|
||||
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"] = {
|
||||
@@ -415,21 +506,24 @@ func _validate_and_fix_save_data(data: Dictionary) -> bool:
|
||||
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")
|
||||
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:
|
||||
DebugManager.log_error("Invalid grid_size in save data", "SaveManager")
|
||||
return false
|
||||
|
||||
var size = grid_state.grid_size
|
||||
var size: Variant = grid_state.grid_size
|
||||
if not size.has("x") or not size.has("y"):
|
||||
return false
|
||||
|
||||
var width = size.x
|
||||
var height = size.y
|
||||
var width: Variant = size.x
|
||||
var height: Variant = 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:
|
||||
@@ -437,13 +531,13 @@ func _validate_grid_state(grid_state: Dictionary) -> bool:
|
||||
return false
|
||||
|
||||
# Check tile types
|
||||
var tile_types = grid_state.get("tile_types_count", 0)
|
||||
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 false
|
||||
|
||||
# Validate active_gem_types if present
|
||||
var active_gems = grid_state.get("active_gem_types", [])
|
||||
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
|
||||
@@ -451,16 +545,20 @@ func _validate_grid_state(grid_state: Dictionary) -> bool:
|
||||
# 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]
|
||||
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")
|
||||
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")
|
||||
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", [])
|
||||
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
|
||||
@@ -470,60 +568,102 @@ func _validate_grid_state(grid_state: Dictionary) -> bool:
|
||||
|
||||
return true
|
||||
|
||||
func _validate_grid_layout(layout: Array, expected_width: int, expected_height: int, max_tile_type: int) -> bool:
|
||||
|
||||
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")
|
||||
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]
|
||||
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")
|
||||
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]
|
||||
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")
|
||||
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")
|
||||
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:
|
||||
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
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:
|
||||
|
||||
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")
|
||||
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")
|
||||
DebugManager.log_error(
|
||||
"Score contains invalid float value (NaN/Inf): %s" % str(score), "SaveManager"
|
||||
)
|
||||
return false
|
||||
|
||||
var score_int = int(score)
|
||||
@@ -532,7 +672,8 @@ func _is_valid_score(score) -> bool:
|
||||
return false
|
||||
return true
|
||||
|
||||
func _merge_validated_data(loaded_data: Dictionary):
|
||||
|
||||
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):
|
||||
@@ -544,31 +685,33 @@ func _merge_validated_data(loaded_data: Dictionary):
|
||||
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", {})
|
||||
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 = data.duplicate(true)
|
||||
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 = _create_deterministic_string(data_copy)
|
||||
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 = data.keys()
|
||||
var keys: Array = data.keys()
|
||||
keys.sort() # Ensure consistent ordering
|
||||
|
||||
var parts = []
|
||||
var parts: Array[String] = []
|
||||
for key in keys:
|
||||
var value = data[key]
|
||||
var key_str = str(key)
|
||||
var value_str = ""
|
||||
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)
|
||||
@@ -582,9 +725,10 @@ func _create_deterministic_string(data: Dictionary) -> String:
|
||||
|
||||
return "{" + ",".join(parts) + "}"
|
||||
|
||||
|
||||
func _create_deterministic_array_string(arr: Array) -> String:
|
||||
# Create deterministic string representation of arrays
|
||||
var parts = []
|
||||
var parts: Array[String] = []
|
||||
for item in arr:
|
||||
if item is Dictionary:
|
||||
parts.append(_create_deterministic_string(item))
|
||||
@@ -596,7 +740,8 @@ func _create_deterministic_array_string(arr: Array) -> String:
|
||||
|
||||
return "[" + ",".join(parts) + "]"
|
||||
|
||||
func _normalize_value_for_checksum(value) -> String:
|
||||
|
||||
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
|
||||
@@ -620,82 +765,134 @@ func _normalize_value_for_checksum(value) -> String:
|
||||
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
|
||||
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 = data.get("_version", 0)
|
||||
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")
|
||||
(
|
||||
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")
|
||||
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)
|
||||
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")
|
||||
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")
|
||||
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)
|
||||
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")
|
||||
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")
|
||||
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):
|
||||
|
||||
func _handle_version_migration(data: Dictionary) -> Variant:
|
||||
"""Handle save data version migration and compatibility"""
|
||||
var data_version = data.get("_version", 0) # Default to version 0 for old saves
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
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)
|
||||
var migrated_data: Dictionary = data.duplicate(true)
|
||||
|
||||
# Migration from version 0 (no version field) to version 1
|
||||
if from_version < 1:
|
||||
@@ -716,11 +913,17 @@ func _migrate_save_data(data: Dictionary, from_version: int) -> Dictionary:
|
||||
# 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]
|
||||
var score_value: Variant = migrated_data[score_key]
|
||||
if score_value is float or score_value is int:
|
||||
var int_score = int(score_value)
|
||||
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")
|
||||
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
|
||||
@@ -736,20 +939,22 @@ func _migrate_save_data(data: Dictionary, from_version: int) -> Dictionary:
|
||||
DebugManager.log_info("Save data migration completed successfully", "SaveManager")
|
||||
return migrated_data
|
||||
|
||||
func _create_backup():
|
||||
|
||||
func _create_backup() -> void:
|
||||
# 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)
|
||||
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():
|
||||
var backup_path = SAVE_FILE_PATH + ".backup"
|
||||
|
||||
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
|
||||
@@ -757,19 +962,19 @@ func _restore_backup_if_exists():
|
||||
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)
|
||||
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 = backup_file.get_length()
|
||||
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 = backup_file.get_var()
|
||||
var backup_data: Variant = backup_file.get_var()
|
||||
backup_file.close()
|
||||
|
||||
if backup_data == null:
|
||||
@@ -777,7 +982,7 @@ func _restore_backup_if_exists():
|
||||
return false
|
||||
|
||||
# Create new save file from backup
|
||||
var original = FileAccess.open(SAVE_FILE_PATH, FileAccess.WRITE)
|
||||
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
|
||||
|
||||
@@ -7,24 +7,22 @@ const MAX_SETTING_STRING_LENGTH = 10 # Max length for string settings like lang
|
||||
# dev `user://`=`%APPDATA%\Godot\app_userdata\Skelly`
|
||||
# prod `user://`=`%APPDATA%\Skelly\`
|
||||
|
||||
var settings = {
|
||||
var settings: Dictionary = {}
|
||||
|
||||
var default_settings: Dictionary = {
|
||||
"master_volume": 0.50, "music_volume": 0.40, "sfx_volume": 0.50, "language": "en"
|
||||
}
|
||||
|
||||
var default_settings = {
|
||||
"master_volume": 0.50,
|
||||
"music_volume": 0.40,
|
||||
"sfx_volume": 0.50,
|
||||
"language": "en"
|
||||
}
|
||||
var languages_data: Dictionary = {}
|
||||
|
||||
var languages_data = {}
|
||||
|
||||
func _ready():
|
||||
func _ready() -> void:
|
||||
DebugManager.log_info("SettingsManager ready", "SettingsManager")
|
||||
load_languages()
|
||||
load_settings()
|
||||
|
||||
func load_settings():
|
||||
|
||||
func load_settings() -> void:
|
||||
var config = ConfigFile.new()
|
||||
var load_result = config.load(SETTINGS_FILE)
|
||||
|
||||
@@ -39,16 +37,26 @@ func load_settings():
|
||||
if _validate_setting_value(key, loaded_value):
|
||||
settings[key] = loaded_value
|
||||
else:
|
||||
DebugManager.log_warn("Invalid setting value for '%s', using default: %s" % [key, str(default_settings[key])], "SettingsManager")
|
||||
DebugManager.log_warn(
|
||||
(
|
||||
"Invalid setting value for '%s', using default: %s"
|
||||
% [key, str(default_settings[key])]
|
||||
),
|
||||
"SettingsManager"
|
||||
)
|
||||
settings[key] = default_settings[key]
|
||||
DebugManager.log_info("Settings loaded: " + str(settings), "SettingsManager")
|
||||
else:
|
||||
DebugManager.log_warn("No settings file found (Error code: %d), using defaults" % load_result, "SettingsManager")
|
||||
DebugManager.log_warn(
|
||||
"No settings file found (Error code: %d), using defaults" % load_result,
|
||||
"SettingsManager"
|
||||
)
|
||||
settings = default_settings.duplicate()
|
||||
|
||||
# Apply settings with error handling
|
||||
_apply_all_settings()
|
||||
|
||||
|
||||
func _apply_all_settings():
|
||||
DebugManager.log_info("Applying settings: " + str(settings), "SettingsManager")
|
||||
|
||||
@@ -64,17 +72,24 @@ func _apply_all_settings():
|
||||
if master_bus >= 0 and "master_volume" in settings:
|
||||
AudioServer.set_bus_volume_db(master_bus, linear_to_db(settings["master_volume"]))
|
||||
else:
|
||||
DebugManager.log_warn("Master audio bus not found or master_volume setting missing", "SettingsManager")
|
||||
DebugManager.log_warn(
|
||||
"Master audio bus not found or master_volume setting missing", "SettingsManager"
|
||||
)
|
||||
|
||||
if music_bus >= 0 and "music_volume" in settings:
|
||||
AudioServer.set_bus_volume_db(music_bus, linear_to_db(settings["music_volume"]))
|
||||
else:
|
||||
DebugManager.log_warn("Music audio bus not found or music_volume setting missing", "SettingsManager")
|
||||
DebugManager.log_warn(
|
||||
"Music audio bus not found or music_volume setting missing", "SettingsManager"
|
||||
)
|
||||
|
||||
if sfx_bus >= 0 and "sfx_volume" in settings:
|
||||
AudioServer.set_bus_volume_db(sfx_bus, linear_to_db(settings["sfx_volume"]))
|
||||
else:
|
||||
DebugManager.log_warn("SFX audio bus not found or sfx_volume setting missing", "SettingsManager")
|
||||
DebugManager.log_warn(
|
||||
"SFX audio bus not found or sfx_volume setting missing", "SettingsManager"
|
||||
)
|
||||
|
||||
|
||||
func save_settings():
|
||||
var config = ConfigFile.new()
|
||||
@@ -83,15 +98,19 @@ func save_settings():
|
||||
|
||||
var save_result = config.save(SETTINGS_FILE)
|
||||
if save_result != OK:
|
||||
DebugManager.log_error("Failed to save settings (Error code: %d)" % save_result, "SettingsManager")
|
||||
DebugManager.log_error(
|
||||
"Failed to save settings (Error code: %d)" % save_result, "SettingsManager"
|
||||
)
|
||||
return false
|
||||
|
||||
DebugManager.log_info("Settings saved: " + str(settings), "SettingsManager")
|
||||
return true
|
||||
|
||||
|
||||
func get_setting(key: String):
|
||||
return settings.get(key)
|
||||
|
||||
|
||||
func set_setting(key: String, value) -> bool:
|
||||
if not key in default_settings:
|
||||
DebugManager.log_error("Unknown setting key: " + key, "SettingsManager")
|
||||
@@ -99,13 +118,16 @@ func set_setting(key: String, value) -> bool:
|
||||
|
||||
# Validate value type and range based on key
|
||||
if not _validate_setting_value(key, value):
|
||||
DebugManager.log_error("Invalid value for setting '%s': %s" % [key, str(value)], "SettingsManager")
|
||||
DebugManager.log_error(
|
||||
"Invalid value for setting '%s': %s" % [key, str(value)], "SettingsManager"
|
||||
)
|
||||
return false
|
||||
|
||||
settings[key] = value
|
||||
_apply_setting_side_effect(key, value)
|
||||
return true
|
||||
|
||||
|
||||
func _validate_setting_value(key: String, value) -> bool:
|
||||
match key:
|
||||
"master_volume", "music_volume", "sfx_volume":
|
||||
@@ -116,7 +138,9 @@ func _validate_setting_value(key: String, value) -> bool:
|
||||
var float_value = float(value)
|
||||
# Check for NaN and infinity
|
||||
if is_nan(float_value) or is_inf(float_value):
|
||||
DebugManager.log_warn("Invalid float value for %s: %s" % [key, str(value)], "SettingsManager")
|
||||
DebugManager.log_warn(
|
||||
"Invalid float value for %s: %s" % [key, str(value)], "SettingsManager"
|
||||
)
|
||||
return false
|
||||
# Range validation
|
||||
return float_value >= 0.0 and float_value <= 1.0
|
||||
@@ -125,13 +149,17 @@ func _validate_setting_value(key: String, value) -> bool:
|
||||
return false
|
||||
# Prevent extremely long strings
|
||||
if value.length() > MAX_SETTING_STRING_LENGTH:
|
||||
DebugManager.log_warn("Language code too long: %d characters" % value.length(), "SettingsManager")
|
||||
DebugManager.log_warn(
|
||||
"Language code too long: %d characters" % value.length(), "SettingsManager"
|
||||
)
|
||||
return false
|
||||
# Check for valid characters (alphanumeric and common separators only)
|
||||
var regex = RegEx.new()
|
||||
regex.compile("^[a-zA-Z0-9_-]+$")
|
||||
if not regex.search(value):
|
||||
DebugManager.log_warn("Language code contains invalid characters: %s" % value, "SettingsManager")
|
||||
DebugManager.log_warn(
|
||||
"Language code contains invalid characters: %s" % value, "SettingsManager"
|
||||
)
|
||||
return false
|
||||
# Check if language is supported
|
||||
if languages_data.has("languages") and languages_data.languages is Dictionary:
|
||||
@@ -147,31 +175,40 @@ func _validate_setting_value(key: String, value) -> bool:
|
||||
return false
|
||||
return typeof(value) == typeof(default_value)
|
||||
|
||||
|
||||
func _apply_setting_side_effect(key: String, value) -> void:
|
||||
match key:
|
||||
"language":
|
||||
TranslationServer.set_locale(value)
|
||||
"master_volume":
|
||||
if AudioServer.get_bus_index("Master") >= 0:
|
||||
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Master"), linear_to_db(value))
|
||||
AudioServer.set_bus_volume_db(
|
||||
AudioServer.get_bus_index("Master"), linear_to_db(value)
|
||||
)
|
||||
"music_volume":
|
||||
AudioManager.update_music_volume(value)
|
||||
"sfx_volume":
|
||||
if AudioServer.get_bus_index("SFX") >= 0:
|
||||
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("SFX"), linear_to_db(value))
|
||||
|
||||
|
||||
func load_languages():
|
||||
var file = FileAccess.open(LANGUAGES_JSON_PATH, FileAccess.READ)
|
||||
if not file:
|
||||
var error_code = FileAccess.get_open_error()
|
||||
DebugManager.log_error("Could not open languages.json (Error code: %d)" % error_code, "SettingsManager")
|
||||
DebugManager.log_error(
|
||||
"Could not open languages.json (Error code: %d)" % error_code, "SettingsManager"
|
||||
)
|
||||
_load_default_languages()
|
||||
return
|
||||
|
||||
# Check file size to prevent memory exhaustion
|
||||
var file_size = file.get_length()
|
||||
if file_size > MAX_JSON_FILE_SIZE:
|
||||
DebugManager.log_error("Languages.json file too large: %d bytes (max %d)" % [file_size, MAX_JSON_FILE_SIZE], "SettingsManager")
|
||||
DebugManager.log_error(
|
||||
"Languages.json file too large: %d bytes (max %d)" % [file_size, MAX_JSON_FILE_SIZE],
|
||||
"SettingsManager"
|
||||
)
|
||||
file.close()
|
||||
_load_default_languages()
|
||||
return
|
||||
@@ -187,7 +224,9 @@ func load_languages():
|
||||
file.close()
|
||||
|
||||
if file_error != OK:
|
||||
DebugManager.log_error("Error reading languages.json (Error code: %d)" % file_error, "SettingsManager")
|
||||
DebugManager.log_error(
|
||||
"Error reading languages.json (Error code: %d)" % file_error, "SettingsManager"
|
||||
)
|
||||
_load_default_languages()
|
||||
return
|
||||
|
||||
@@ -200,7 +239,10 @@ func load_languages():
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(json_string)
|
||||
if parse_result != OK:
|
||||
DebugManager.log_error("JSON parsing failed at line %d: %s" % [json.error_line, json.error_string], "SettingsManager")
|
||||
DebugManager.log_error(
|
||||
"JSON parsing failed at line %d: %s" % [json.error_line, json.error_string],
|
||||
"SettingsManager"
|
||||
)
|
||||
_load_default_languages()
|
||||
return
|
||||
|
||||
@@ -216,21 +258,24 @@ func load_languages():
|
||||
return
|
||||
|
||||
languages_data = json.data
|
||||
DebugManager.log_info("Languages loaded successfully: " + str(languages_data.languages.keys()), "SettingsManager")
|
||||
DebugManager.log_info(
|
||||
"Languages loaded successfully: " + str(languages_data.languages.keys()), "SettingsManager"
|
||||
)
|
||||
|
||||
|
||||
func _load_default_languages():
|
||||
# Fallback language data when JSON file fails to load
|
||||
languages_data = {
|
||||
"languages": {
|
||||
"en": {"name": "English", "flag": "🇺🇸"},
|
||||
"ru": {"name": "Русский", "flag": "🇷🇺"}
|
||||
}
|
||||
"languages":
|
||||
{"en": {"name": "English", "flag": "🇺🇸"}, "ru": {"name": "Русский", "flag": "🇷🇺"}}
|
||||
}
|
||||
DebugManager.log_info("Default languages loaded as fallback", "SettingsManager")
|
||||
|
||||
|
||||
func get_languages_data():
|
||||
return languages_data
|
||||
|
||||
|
||||
func reset_settings_to_defaults() -> void:
|
||||
DebugManager.log_info("Resetting all settings to defaults", "SettingsManager")
|
||||
for key in default_settings.keys():
|
||||
@@ -242,6 +287,7 @@ func reset_settings_to_defaults() -> void:
|
||||
else:
|
||||
DebugManager.log_error("Failed to save reset settings", "SettingsManager")
|
||||
|
||||
|
||||
func _validate_languages_structure(data: Dictionary) -> bool:
|
||||
"""Validate the structure and content of languages.json data"""
|
||||
if not data.has("languages"):
|
||||
@@ -260,7 +306,9 @@ func _validate_languages_structure(data: Dictionary) -> bool:
|
||||
# Validate each language entry
|
||||
for lang_code in languages.keys():
|
||||
if not lang_code is String:
|
||||
DebugManager.log_error("Language code is not a string: %s" % str(lang_code), "SettingsManager")
|
||||
DebugManager.log_error(
|
||||
"Language code is not a string: %s" % str(lang_code), "SettingsManager"
|
||||
)
|
||||
return false
|
||||
|
||||
if lang_code.length() > MAX_SETTING_STRING_LENGTH:
|
||||
@@ -269,12 +317,16 @@ func _validate_languages_structure(data: Dictionary) -> bool:
|
||||
|
||||
var lang_data = languages[lang_code]
|
||||
if not lang_data is Dictionary:
|
||||
DebugManager.log_error("Language data for '%s' is not a dictionary" % lang_code, "SettingsManager")
|
||||
DebugManager.log_error(
|
||||
"Language data for '%s' is not a dictionary" % lang_code, "SettingsManager"
|
||||
)
|
||||
return false
|
||||
|
||||
# Validate required fields in language data
|
||||
if not lang_data.has("name") or not lang_data["name"] is String:
|
||||
DebugManager.log_error("Language '%s' missing valid 'name' field" % lang_code, "SettingsManager")
|
||||
DebugManager.log_error(
|
||||
"Language '%s' missing valid 'name' field" % lang_code, "SettingsManager"
|
||||
)
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
@@ -36,5 +36,6 @@ const FONT_SIZE_NORMAL := 18
|
||||
const FONT_SIZE_LARGE := 24
|
||||
const FONT_SIZE_TITLE := 32
|
||||
|
||||
|
||||
func _ready():
|
||||
DebugManager.log_info("UIConstants loaded successfully", "UIConstants")
|
||||
DebugManager.log_info("UIConstants loaded successfully", "UIConstants")
|
||||
|
||||
Reference in New Issue
Block a user