lint fixes
Some checks failed
Continuous Integration / Code Formatting (pull_request) Successful in 27s
Continuous Integration / Code Quality Check (pull_request) Successful in 29s
Continuous Integration / Test Execution (pull_request) Failing after 33s
Continuous Integration / CI Summary (pull_request) Failing after 5s
Some checks failed
Continuous Integration / Code Formatting (pull_request) Successful in 27s
Continuous Integration / Code Quality Check (pull_request) Successful in 29s
Continuous Integration / Test Execution (pull_request) Failing after 33s
Continuous Integration / CI Summary (pull_request) Failing after 5s
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
extends Node
|
||||
|
||||
const MUSIC_PATH := "res://assets/audio/music/Space Horror InGame Music (Exploration) _Clement Panchout.wav"
|
||||
const MUSIC_BASE := "res://assets/audio/music/"
|
||||
const MUSIC_FILE := "Space Horror InGame Music (Exploration) _Clement Panchout.wav"
|
||||
const MUSIC_PATH := MUSIC_BASE + MUSIC_FILE
|
||||
const UI_CLICK_SOUND_PATH := "res://assets/audio/sfx/817587__silverdubloons__tick06.wav"
|
||||
|
||||
var music_player: AudioStreamPlayer
|
||||
|
||||
@@ -72,21 +72,15 @@ func _should_log(level: LogLevel) -> bool:
|
||||
|
||||
func _log_level_to_string(level: LogLevel) -> String:
|
||||
"""Convert LogLevel enum to string representation"""
|
||||
match level:
|
||||
LogLevel.TRACE:
|
||||
return "TRACE"
|
||||
LogLevel.DEBUG:
|
||||
return "DEBUG"
|
||||
LogLevel.INFO:
|
||||
return "INFO"
|
||||
LogLevel.WARN:
|
||||
return "WARN"
|
||||
LogLevel.ERROR:
|
||||
return "ERROR"
|
||||
LogLevel.FATAL:
|
||||
return "FATAL"
|
||||
_:
|
||||
return "UNKNOWN"
|
||||
var level_strings := {
|
||||
LogLevel.TRACE: "TRACE",
|
||||
LogLevel.DEBUG: "DEBUG",
|
||||
LogLevel.INFO: "INFO",
|
||||
LogLevel.WARN: "WARN",
|
||||
LogLevel.ERROR: "ERROR",
|
||||
LogLevel.FATAL: "FATAL"
|
||||
}
|
||||
return level_strings.get(level, "UNKNOWN")
|
||||
|
||||
|
||||
func _format_log_message(level: LogLevel, message: String, category: String = "") -> String:
|
||||
|
||||
@@ -39,29 +39,8 @@ func start_clickomania_game() -> void:
|
||||
|
||||
func start_game_with_mode(gameplay_mode: String) -> void:
|
||||
"""Load game scene with specified gameplay mode and safety validation"""
|
||||
# Input validation
|
||||
if not gameplay_mode or gameplay_mode.is_empty():
|
||||
DebugManager.log_error("Empty or null gameplay mode provided", "GameManager")
|
||||
return
|
||||
|
||||
if not gameplay_mode is String:
|
||||
DebugManager.log_error(
|
||||
"Invalid gameplay mode type: " + str(typeof(gameplay_mode)), "GameManager"
|
||||
)
|
||||
return
|
||||
|
||||
# Prevent concurrent scene changes (race condition protection)
|
||||
if is_changing_scene:
|
||||
DebugManager.log_warn("Scene change already in progress, ignoring request", "GameManager")
|
||||
return
|
||||
|
||||
# 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"
|
||||
)
|
||||
# Combined input validation
|
||||
if not _validate_game_mode_request(gameplay_mode):
|
||||
return
|
||||
|
||||
is_changing_scene = true
|
||||
@@ -149,3 +128,33 @@ func exit_to_main_menu() -> void:
|
||||
# Wait for scene to be ready, then mark scene change as complete
|
||||
await get_tree().process_frame
|
||||
is_changing_scene = false
|
||||
|
||||
|
||||
func _validate_game_mode_request(gameplay_mode: String) -> bool:
|
||||
"""Validate gameplay mode request with combined checks"""
|
||||
# Input validation
|
||||
if not gameplay_mode or gameplay_mode.is_empty():
|
||||
DebugManager.log_error("Empty or null gameplay mode provided", "GameManager")
|
||||
return false
|
||||
|
||||
if not gameplay_mode is String:
|
||||
DebugManager.log_error(
|
||||
"Invalid gameplay mode type: " + str(typeof(gameplay_mode)), "GameManager"
|
||||
)
|
||||
return false
|
||||
|
||||
# Prevent concurrent scene changes (race condition protection)
|
||||
if is_changing_scene:
|
||||
DebugManager.log_warn("Scene change already in progress, ignoring request", "GameManager")
|
||||
return false
|
||||
|
||||
# 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"
|
||||
)
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
@@ -14,10 +14,6 @@ const MAX_SCORE: int = 999999999
|
||||
const MAX_GAMES_PLAYED: int = 100000
|
||||
const MAX_FILE_SIZE: int = 1048576 # 1MB limit
|
||||
|
||||
# Save operation protection - prevents race conditions
|
||||
var _save_in_progress: bool = false
|
||||
var _restore_in_progress: bool = false
|
||||
|
||||
var game_data: Dictionary = {
|
||||
"high_score": 0,
|
||||
"current_score": 0,
|
||||
@@ -32,6 +28,10 @@ var game_data: Dictionary = {
|
||||
}
|
||||
}
|
||||
|
||||
# Save operation protection - prevents race conditions
|
||||
var _save_in_progress: bool = false
|
||||
var _restore_in_progress: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
"""Initialize SaveManager and load existing save data on startup"""
|
||||
@@ -98,12 +98,22 @@ func load_game() -> void:
|
||||
# Reset restore flag
|
||||
_restore_in_progress = false
|
||||
|
||||
var loaded_data = _load_and_parse_save_file()
|
||||
if loaded_data == null:
|
||||
return
|
||||
|
||||
# Process the loaded data
|
||||
_process_loaded_data(loaded_data)
|
||||
|
||||
|
||||
func _load_and_parse_save_file() -> Variant:
|
||||
"""Load and parse the save file, returning null on failure"""
|
||||
var save_file: FileAccess = FileAccess.open(SAVE_FILE_PATH, FileAccess.READ)
|
||||
if save_file == null:
|
||||
DebugManager.log_error(
|
||||
"Failed to open save file for reading: %s" % SAVE_FILE_PATH, "SaveManager"
|
||||
)
|
||||
return
|
||||
return null
|
||||
|
||||
# Check file size
|
||||
var file_size: int = save_file.get_length()
|
||||
@@ -112,14 +122,14 @@ func load_game() -> void:
|
||||
"Save file too large: %d bytes (max %d)" % [file_size, MAX_FILE_SIZE], "SaveManager"
|
||||
)
|
||||
save_file.close()
|
||||
return
|
||||
return null
|
||||
|
||||
var json_string: Variant = save_file.get_var()
|
||||
save_file.close()
|
||||
|
||||
if not json_string is String:
|
||||
DebugManager.log_error("Save file contains invalid data type", "SaveManager")
|
||||
return
|
||||
return null
|
||||
|
||||
var json: JSON = JSON.new()
|
||||
var parse_result: Error = json.parse(json_string)
|
||||
@@ -127,48 +137,33 @@ func load_game() -> void:
|
||||
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"
|
||||
)
|
||||
return
|
||||
_handle_load_failure("JSON parse failed")
|
||||
return null
|
||||
|
||||
var loaded_data: Variant = json.data
|
||||
if not loaded_data is Dictionary:
|
||||
DebugManager.log_error("Save file root is not a dictionary", "SaveManager")
|
||||
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
|
||||
_handle_load_failure("Invalid data format")
|
||||
return null
|
||||
|
||||
return loaded_data
|
||||
|
||||
|
||||
func _process_loaded_data(loaded_data: Variant) -> void:
|
||||
"""Process and validate the loaded data"""
|
||||
# Validate checksum first
|
||||
if not _validate_checksum(loaded_data):
|
||||
DebugManager.log_error(
|
||||
"Save file checksum validation failed - possible tampering", "SaveManager"
|
||||
)
|
||||
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"
|
||||
)
|
||||
_handle_load_failure("Checksum validation failed")
|
||||
return
|
||||
|
||||
# Handle version migration
|
||||
var migrated_data: Variant = _handle_version_migration(loaded_data)
|
||||
if migrated_data == null:
|
||||
DebugManager.log_error("Save file version migration failed", "SaveManager")
|
||||
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"
|
||||
)
|
||||
_handle_load_failure("Migration failed")
|
||||
return
|
||||
|
||||
# Validate and fix loaded data
|
||||
@@ -176,19 +171,21 @@ func load_game() -> void:
|
||||
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"
|
||||
)
|
||||
_handle_load_failure("Validation failed")
|
||||
return
|
||||
|
||||
# Use migrated data
|
||||
loaded_data = migrated_data
|
||||
|
||||
# Safely merge validated data
|
||||
_merge_validated_data(loaded_data)
|
||||
_merge_validated_data(migrated_data)
|
||||
|
||||
|
||||
func _handle_load_failure(reason: String) -> void:
|
||||
"""Handle load failure with backup restoration attempt"""
|
||||
if not _restore_in_progress:
|
||||
var backup_restored = _restore_backup_if_exists()
|
||||
if not backup_restored:
|
||||
DebugManager.log_warn(
|
||||
"%s and backup restore failed, using defaults" % reason, "SaveManager"
|
||||
)
|
||||
|
||||
DebugManager.log_info(
|
||||
(
|
||||
@@ -375,6 +372,28 @@ func reset_all_progress() -> bool:
|
||||
# Security and validation helper functions
|
||||
func _validate_save_data(data: Dictionary) -> bool:
|
||||
# Check required fields exist and have correct types
|
||||
if not _validate_required_fields(data):
|
||||
return false
|
||||
|
||||
# Validate numeric fields
|
||||
if not _validate_score_fields(data):
|
||||
return false
|
||||
|
||||
# Validate games_played field
|
||||
if not _validate_games_played_field(data):
|
||||
return false
|
||||
|
||||
# Validate grid state
|
||||
var grid_state: Variant = data.get("grid_state", {})
|
||||
if not grid_state is Dictionary:
|
||||
DebugManager.log_error("Grid state is not a dictionary", "SaveManager")
|
||||
return false
|
||||
|
||||
return _validate_grid_state(grid_state)
|
||||
|
||||
|
||||
func _validate_required_fields(data: Dictionary) -> bool:
|
||||
"""Validate that all required fields exist"""
|
||||
var required_fields: Array[String] = [
|
||||
"high_score", "current_score", "games_played", "total_score", "grid_state"
|
||||
]
|
||||
@@ -382,19 +401,21 @@ func _validate_save_data(data: Dictionary) -> bool:
|
||||
if not data.has(field):
|
||||
DebugManager.log_error("Missing required field: %s" % field, "SaveManager")
|
||||
return false
|
||||
return true
|
||||
|
||||
# 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
|
||||
func _validate_score_fields(data: Dictionary) -> bool:
|
||||
"""Validate all score-related fields"""
|
||||
var score_fields = ["high_score", "current_score", "total_score"]
|
||||
for field in score_fields:
|
||||
if not _is_valid_score(data.get(field, 0)):
|
||||
DebugManager.log_error("Invalid %s validation failed" % field, "SaveManager")
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _validate_games_played_field(data: Dictionary) -> bool:
|
||||
"""Validate the games_played field"""
|
||||
var games_played: Variant = data.get("games_played", 0)
|
||||
if not (games_played is int or games_played is float):
|
||||
DebugManager.log_error(
|
||||
@@ -418,13 +439,7 @@ func _validate_save_data(data: Dictionary) -> bool:
|
||||
)
|
||||
return false
|
||||
|
||||
# Validate grid state
|
||||
var grid_state: Variant = data.get("grid_state", {})
|
||||
if not grid_state is Dictionary:
|
||||
DebugManager.log_error("Grid state is not a dictionary", "SaveManager")
|
||||
return false
|
||||
|
||||
return _validate_grid_state(grid_state)
|
||||
return true
|
||||
|
||||
|
||||
func _validate_and_fix_save_data(data: Dictionary) -> bool:
|
||||
@@ -522,30 +537,71 @@ func _validate_and_fix_save_data(data: Dictionary) -> bool:
|
||||
|
||||
|
||||
func _validate_grid_state(grid_state: Dictionary) -> bool:
|
||||
# Check grid size
|
||||
# Validate grid size
|
||||
var grid_size_validation = _validate_grid_size(grid_state)
|
||||
if not grid_size_validation.valid:
|
||||
return false
|
||||
var width = grid_size_validation.width
|
||||
var height = grid_size_validation.height
|
||||
|
||||
# Validate tile types
|
||||
var tile_types = _validate_tile_types(grid_state)
|
||||
if tile_types == -1:
|
||||
return false
|
||||
|
||||
# Validate active gem types
|
||||
if not _validate_active_gem_types(grid_state, tile_types):
|
||||
return false
|
||||
|
||||
# Validate grid layout if present
|
||||
var layout: Variant = grid_state.get("grid_layout", [])
|
||||
if not layout is Array:
|
||||
DebugManager.log_error("grid_layout is not an array", "SaveManager")
|
||||
return false
|
||||
|
||||
if layout.size() > 0:
|
||||
return _validate_grid_layout(layout, width, height, tile_types)
|
||||
|
||||
return true
|
||||
|
||||
|
||||
func _validate_grid_size(grid_state: Dictionary) -> Dictionary:
|
||||
"""Validate grid size and return validation result with dimensions"""
|
||||
var result = {"valid": false, "width": 0, "height": 0}
|
||||
|
||||
if not grid_state.has("grid_size") or not grid_state.grid_size is Dictionary:
|
||||
DebugManager.log_error("Invalid grid_size in save data", "SaveManager")
|
||||
return false
|
||||
return result
|
||||
|
||||
var size: Variant = grid_state.grid_size
|
||||
if not size.has("x") or not size.has("y"):
|
||||
return false
|
||||
return result
|
||||
|
||||
var width: Variant = size.x
|
||||
var height: Variant = size.y
|
||||
if not width is int or not height is int:
|
||||
return false
|
||||
return result
|
||||
if width < 3 or height < 3 or width > MAX_GRID_SIZE or height > MAX_GRID_SIZE:
|
||||
DebugManager.log_error("Grid size out of bounds: %dx%d" % [width, height], "SaveManager")
|
||||
return false
|
||||
return result
|
||||
|
||||
# Check tile types
|
||||
result.valid = true
|
||||
result.width = width
|
||||
result.height = height
|
||||
return result
|
||||
|
||||
|
||||
func _validate_tile_types(grid_state: Dictionary) -> int:
|
||||
"""Validate tile types count and return it, or -1 if invalid"""
|
||||
var tile_types: Variant = grid_state.get("tile_types_count", 0)
|
||||
if not tile_types is int or tile_types < 3 or tile_types > MAX_TILE_TYPES:
|
||||
DebugManager.log_error("Invalid tile_types_count: %s" % str(tile_types), "SaveManager")
|
||||
return false
|
||||
return -1
|
||||
return tile_types
|
||||
|
||||
# Validate active_gem_types if present
|
||||
|
||||
func _validate_active_gem_types(grid_state: Dictionary, tile_types: int) -> bool:
|
||||
"""Validate active gem types array"""
|
||||
var active_gems: Variant = grid_state.get("active_gem_types", [])
|
||||
if not active_gems is Array:
|
||||
DebugManager.log_error("active_gem_types is not an array", "SaveManager")
|
||||
@@ -565,16 +621,6 @@ func _validate_grid_state(grid_state: Dictionary) -> bool:
|
||||
"active_gem_types[%d] out of range: %d" % [i, gem_type], "SaveManager"
|
||||
)
|
||||
return false
|
||||
|
||||
# Validate grid layout if present
|
||||
var layout: Variant = grid_state.get("grid_layout", [])
|
||||
if not layout is Array:
|
||||
DebugManager.log_error("grid_layout is not an array", "SaveManager")
|
||||
return false
|
||||
|
||||
if layout.size() > 0:
|
||||
return _validate_grid_layout(layout, width, height, tile_types)
|
||||
|
||||
return true
|
||||
|
||||
|
||||
@@ -757,22 +803,30 @@ func _normalize_value_for_checksum(value: Variant) -> String:
|
||||
"""
|
||||
if value == null:
|
||||
return "null"
|
||||
elif value is bool:
|
||||
|
||||
if value is bool:
|
||||
return str(value)
|
||||
elif value is int:
|
||||
|
||||
if value is String:
|
||||
return value
|
||||
|
||||
if 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)
|
||||
|
||||
if value is float:
|
||||
return _normalize_float_for_checksum(value)
|
||||
|
||||
return str(value)
|
||||
|
||||
|
||||
func _normalize_float_for_checksum(value: float) -> String:
|
||||
"""Normalize float values for checksum calculation"""
|
||||
# Convert float to int if it's a whole number (handles JSON conversion)
|
||||
if value == int(value):
|
||||
return str(int(value))
|
||||
# For actual floats, use consistent precision
|
||||
return "%.10f" % value
|
||||
|
||||
|
||||
func _validate_checksum(data: Dictionary) -> bool:
|
||||
@@ -790,15 +844,15 @@ func _validate_checksum(data: Dictionary) -> bool:
|
||||
# Try to be more lenient with existing saves to prevent data loss
|
||||
var data_version: Variant = data.get("_version", 0)
|
||||
if data_version <= 1:
|
||||
(
|
||||
DebugManager
|
||||
. log_warn(
|
||||
(
|
||||
"Checksum mismatch in v%d save file - may be due to JSON serialization issue (stored: %s, calculated: %s)"
|
||||
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"
|
||||
)
|
||||
)
|
||||
),
|
||||
"SaveManager"
|
||||
)
|
||||
(
|
||||
DebugManager
|
||||
@@ -810,15 +864,14 @@ func _validate_checksum(data: Dictionary) -> bool:
|
||||
# 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
|
||||
DebugManager.log_error(
|
||||
(
|
||||
"Checksum mismatch - stored: %s, calculated: %s"
|
||||
% [stored_checksum, calculated_checksum]
|
||||
),
|
||||
"SaveManager"
|
||||
)
|
||||
return false
|
||||
|
||||
return is_valid
|
||||
|
||||
@@ -880,7 +933,7 @@ func _handle_version_migration(data: Dictionary) -> Variant:
|
||||
"Save file is current version (%d)" % SAVE_FORMAT_VERSION, "SaveManager"
|
||||
)
|
||||
return data
|
||||
elif data_version > SAVE_FORMAT_VERSION:
|
||||
if data_version > SAVE_FORMAT_VERSION:
|
||||
# Future version - cannot handle
|
||||
DebugManager.log_error(
|
||||
(
|
||||
@@ -890,13 +943,12 @@ func _handle_version_migration(data: Dictionary) -> Variant:
|
||||
"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)
|
||||
# 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:
|
||||
|
||||
@@ -131,43 +131,64 @@ func set_setting(key: String, value) -> bool:
|
||||
func _validate_setting_value(key: String, value) -> bool:
|
||||
match key:
|
||||
"master_volume", "music_volume", "sfx_volume":
|
||||
# Enhanced numeric validation with NaN/Infinity checks
|
||||
if not (value is float or value is int):
|
||||
return false
|
||||
# Convert to float for validation
|
||||
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"
|
||||
)
|
||||
return false
|
||||
# Range validation
|
||||
return float_value >= 0.0 and float_value <= 1.0
|
||||
return _validate_volume_setting(key, value)
|
||||
"language":
|
||||
if not value is String:
|
||||
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"
|
||||
)
|
||||
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"
|
||||
)
|
||||
return false
|
||||
# Check if language is supported
|
||||
if languages_data.has("languages") and languages_data.languages is Dictionary:
|
||||
return value in languages_data.languages
|
||||
else:
|
||||
# Fallback to basic validation if languages not loaded
|
||||
return value in ["en", "ru"]
|
||||
return _validate_language_setting(value)
|
||||
_:
|
||||
return _validate_default_setting(key, value)
|
||||
|
||||
|
||||
func _validate_volume_setting(key: String, value) -> bool:
|
||||
## Validate volume settings with numeric validation.
|
||||
##
|
||||
## Validates audio volume values are numbers within range (0.0 to 1.0).
|
||||
## Handles edge cases like NaN and infinity values.
|
||||
##
|
||||
## Args:
|
||||
## key: The setting key being validated (for error reporting)
|
||||
## value: The volume value to validate
|
||||
##
|
||||
## Returns:
|
||||
## bool: True if the value is a valid volume setting, False otherwise
|
||||
if not (value is float or value is int):
|
||||
return false
|
||||
# Convert to float for validation
|
||||
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"
|
||||
)
|
||||
return false
|
||||
# Range validation
|
||||
return float_value >= 0.0 and float_value <= 1.0
|
||||
|
||||
|
||||
func _validate_language_setting(value) -> bool:
|
||||
if not value is String:
|
||||
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"
|
||||
)
|
||||
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"
|
||||
)
|
||||
return false
|
||||
# Check if language is supported
|
||||
if languages_data.has("languages") and languages_data.languages is Dictionary:
|
||||
return value in languages_data.languages
|
||||
# Fallback to basic validation if languages not loaded
|
||||
return value in ["en", "ru"]
|
||||
|
||||
|
||||
func _validate_default_setting(key: String, value) -> bool:
|
||||
# Default validation: accept if type matches default setting type
|
||||
var default_value = default_settings.get(key)
|
||||
if default_value == null:
|
||||
@@ -193,14 +214,34 @@ func _apply_setting_side_effect(key: String, value) -> void:
|
||||
|
||||
|
||||
func load_languages():
|
||||
var file_content = _load_languages_file()
|
||||
if file_content.is_empty():
|
||||
_load_default_languages_with_fallback("File loading failed")
|
||||
return
|
||||
|
||||
var parsed_data = _parse_languages_json(file_content)
|
||||
if not parsed_data:
|
||||
_load_default_languages_with_fallback("JSON parsing failed")
|
||||
return
|
||||
|
||||
if not _validate_languages_structure(parsed_data):
|
||||
_load_default_languages_with_fallback("Structure validation failed")
|
||||
return
|
||||
|
||||
languages_data = parsed_data
|
||||
DebugManager.log_info(
|
||||
"Languages loaded successfully: " + str(languages_data.languages.keys()), "SettingsManager"
|
||||
)
|
||||
|
||||
|
||||
func _load_languages_file() -> String:
|
||||
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"
|
||||
)
|
||||
_load_default_languages()
|
||||
return
|
||||
return ""
|
||||
|
||||
# Check file size to prevent memory exhaustion
|
||||
var file_size = file.get_length()
|
||||
@@ -210,14 +251,12 @@ func load_languages():
|
||||
"SettingsManager"
|
||||
)
|
||||
file.close()
|
||||
_load_default_languages()
|
||||
return
|
||||
return ""
|
||||
|
||||
if file_size == 0:
|
||||
DebugManager.log_error("Languages.json file is empty", "SettingsManager")
|
||||
file.close()
|
||||
_load_default_languages()
|
||||
return
|
||||
return ""
|
||||
|
||||
var json_string = file.get_as_text()
|
||||
var file_error = file.get_error()
|
||||
@@ -227,14 +266,16 @@ func load_languages():
|
||||
DebugManager.log_error(
|
||||
"Error reading languages.json (Error code: %d)" % file_error, "SettingsManager"
|
||||
)
|
||||
_load_default_languages()
|
||||
return
|
||||
return ""
|
||||
|
||||
return json_string
|
||||
|
||||
|
||||
func _parse_languages_json(json_string: String) -> Dictionary:
|
||||
# Validate the JSON string is not empty
|
||||
if json_string.is_empty():
|
||||
DebugManager.log_error("Languages.json contains empty content", "SettingsManager")
|
||||
_load_default_languages()
|
||||
return
|
||||
return {}
|
||||
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(json_string)
|
||||
@@ -243,24 +284,18 @@ func load_languages():
|
||||
"JSON parsing failed at line %d: %s" % [json.error_line, json.error_string],
|
||||
"SettingsManager"
|
||||
)
|
||||
_load_default_languages()
|
||||
return
|
||||
return {}
|
||||
|
||||
if not json.data or not json.data is Dictionary:
|
||||
DebugManager.log_error("Invalid JSON data structure in languages.json", "SettingsManager")
|
||||
_load_default_languages()
|
||||
return
|
||||
return {}
|
||||
|
||||
# Validate the structure of the JSON data
|
||||
if not _validate_languages_structure(json.data):
|
||||
DebugManager.log_error("Languages.json structure validation failed", "SettingsManager")
|
||||
_load_default_languages()
|
||||
return
|
||||
return json.data
|
||||
|
||||
languages_data = json.data
|
||||
DebugManager.log_info(
|
||||
"Languages loaded successfully: " + str(languages_data.languages.keys()), "SettingsManager"
|
||||
)
|
||||
|
||||
func _load_default_languages_with_fallback(reason: String):
|
||||
DebugManager.log_warn("Loading default languages due to: " + reason, "SettingsManager")
|
||||
_load_default_languages()
|
||||
|
||||
|
||||
func _load_default_languages():
|
||||
@@ -289,7 +324,25 @@ func reset_settings_to_defaults() -> void:
|
||||
|
||||
|
||||
func _validate_languages_structure(data: Dictionary) -> bool:
|
||||
"""Validate the structure and content of languages.json data"""
|
||||
## Validate the structure and content of languages.json data.
|
||||
##
|
||||
## Validates language data loaded from the languages.json file.
|
||||
## Ensures the data structure is valid and contains required fields.
|
||||
##
|
||||
## Args:
|
||||
## data: Dictionary containing the parsed languages.json data
|
||||
##
|
||||
## Returns:
|
||||
## bool: True if data structure is valid, False if validation fails
|
||||
if not _validate_languages_root_structure(data):
|
||||
return false
|
||||
|
||||
var languages = data["languages"]
|
||||
return _validate_individual_languages(languages)
|
||||
|
||||
|
||||
func _validate_languages_root_structure(data: Dictionary) -> bool:
|
||||
"""Validate the root structure of languages data"""
|
||||
if not data.has("languages"):
|
||||
DebugManager.log_error("Languages.json missing 'languages' key", "SettingsManager")
|
||||
return false
|
||||
@@ -303,30 +356,40 @@ func _validate_languages_structure(data: Dictionary) -> bool:
|
||||
DebugManager.log_error("Languages dictionary is empty", "SettingsManager")
|
||||
return false
|
||||
|
||||
# Validate each language entry
|
||||
return true
|
||||
|
||||
|
||||
func _validate_individual_languages(languages: Dictionary) -> bool:
|
||||
"""Validate each individual 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"
|
||||
)
|
||||
if not _validate_single_language_entry(lang_code, languages[lang_code]):
|
||||
return false
|
||||
return true
|
||||
|
||||
if lang_code.length() > MAX_SETTING_STRING_LENGTH:
|
||||
DebugManager.log_error("Language code too long: %s" % lang_code, "SettingsManager")
|
||||
return false
|
||||
|
||||
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"
|
||||
)
|
||||
return false
|
||||
func _validate_single_language_entry(lang_code: Variant, lang_data: Variant) -> bool:
|
||||
"""Validate a single language entry"""
|
||||
if not lang_code is String:
|
||||
DebugManager.log_error(
|
||||
"Language code is not a string: %s" % str(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"
|
||||
)
|
||||
return false
|
||||
if lang_code.length() > MAX_SETTING_STRING_LENGTH:
|
||||
DebugManager.log_error("Language code too long: %s" % lang_code, "SettingsManager")
|
||||
return false
|
||||
|
||||
if not lang_data is Dictionary:
|
||||
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"
|
||||
)
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
Reference in New Issue
Block a user