From f858f9b6336465baad61ccc3fe879fbf6e09aa57 Mon Sep 17 00:00:00 2001 From: Vladimir nett00n Budylnikov Date: Thu, 25 Sep 2025 22:47:59 +0400 Subject: [PATCH] saves and score are added --- localization/MainStrings.csv | 10 + localization/MainStrings.en.translation | Bin 658 -> 1238 bytes localization/MainStrings.ru.translation | Bin 777 -> 1821 bytes project.godot | 1 + scenes/game/game.gd | 46 +- scenes/game/gameplays/match3_gameplay.gd | 605 ++++++++++++++++++++--- scenes/ui/DebugMenuBase.gd | 95 +++- scenes/ui/MainMenu.gd | 24 +- scenes/ui/SettingsMenu.gd | 77 +++ scenes/ui/SettingsMenu.tscn | 26 +- src/autoloads/GameManager.gd | 21 +- src/autoloads/SaveManager.gd | 402 +++++++++++++++ src/autoloads/SaveManager.gd.uid | 1 + 13 files changed, 1190 insertions(+), 118 deletions(-) create mode 100644 src/autoloads/SaveManager.gd create mode 100644 src/autoloads/SaveManager.gd.uid diff --git a/localization/MainStrings.csv b/localization/MainStrings.csv index 8fc4d4a..683102e 100644 --- a/localization/MainStrings.csv +++ b/localization/MainStrings.csv @@ -6,3 +6,13 @@ music_volume;"Music Volume";"Громкость музыки" sfx_volume;"SFX Volume";"Громкость эффектов" language;"Language";"Язык" back;"Back";"Назад" +reset_progress;"Reset All Progress";"Сбросить весь прогресс" +confirm_reset_title;"Confirm Progress Reset";"Подтвердите сброс прогресса" +confirm_reset_message;"Are you sure you want to delete ALL your progress?\n\nThis will permanently remove:\n• All scores and high scores\n• Game statistics\n• Saved game states\n\nThis action cannot be undone!";"Вы уверены, что хотите удалить ВЕСЬ свой прогресс?\n\nЭто навсегда удалит:\n• Все очки и рекорды\n• Игровую статистику\n• Сохранённые игровые состояния\n\nЭто действие нельзя отменить!" +reset_confirm;"Yes, Reset Everything";"Да, сбросить всё" +cancel;"Cancel";"Отмена" +reset_success_title;"Progress Reset";"Прогресс сброшен" +reset_success_message;"All progress has been successfully deleted.\nYour game has been reset to the beginning.";"Весь прогресс был успешно удалён.\nВаша игра была сброшена к началу." +reset_error_title;"Reset Failed";"Ошибка сброса" +reset_error_message;"There was an error resetting your progress.\nPlease try again.";"Произошла ошибка при сбросе вашего прогресса.\nПопробуйте ещё раз." +ok;"OK";"ОК" diff --git a/localization/MainStrings.en.translation b/localization/MainStrings.en.translation index b950f4afc55176a33ab04e46e01ae1cd70037e18..719377dfcaf7aae9d10b60db3f515c21495da5c4 100644 GIT binary patch delta 764 zcmbQldW~~~A7i~Z0|fjB0xlp^0EoqaSO$pIfEXmM1EoQ1BOu!Xh@FtcK;j-qd_N#N z1c;-6m=%Z>fH(n&nShuPh^J`t-;D#(Kzp_w7Z8IKfv|S>(#Y>X zS`diECi62%>%wI2%QN>k06EM+84!jU{iS|qoD`7H3&ebr_cKaQp2a9o57Sk})p#WU z$l-yifQip2`4s636a(2K48$0mase%l-rVsd5p&*bRi!w`I<7Oc(Q(C}Y~&*vGn(rdc|jw|Z2<^QuX{Ll2LVG&U447*ar zHTG%NU0kJ#Trg30mK?hiUiH4saFJk^d^u~7s4ExPG)b9WO&O_{RSXX#*8OKxWcU|p zrUG%U>+@hiw|@-IiFwJXIT$XT0d~?!@tE^mYBC+mbkt_JXN9{;_7}s85A7k3;@WFG}!XgE^sxjCU|xFH5oh$+A-Ku0PsE&W@k!Q*9Bn>UGdY>Vfgf=tIDi(%f; zMlGTQwHV;%C!PS_3xGmkDX06Qjd%n*cbaS`8L!{ z-&G23p^v7zGV4ALPh!t7{@>q@Jvs%RuLZ;KKdje}I>AeD48#9?Q~0bI`_(=SzoPZa z6;Oh6G7P__vhw*h)O`XRgFl+(Kn@&TCzqa3jJJ=fIC>D>z*IlaiEE3CgIX=RNscomH)>mC`h* zYe@-u?3sJTW>9dypj*g_B2?U~Oj3%{$k}D;=2Do53%bc|AQoFRqan0HX$en1!Xy`R z1zmsysMGUF)38b!b);%T-WC%kfru_7(gI1kK$lV50}h{JjV1-(9thAw?o84!98)k% zQy&N6eEC@Afe+2M5sr@~^0l*E5`Wy678tFlq-S;2S3ac@>KsxdysInZRTe@fJiA*6 z)P?{q`sqP>bj8uLd3W9-qn;lAcf%&Dx_?*PkK6+~ehOZ29R@L^x-cM&pH<0$U?<#` z6@o623)%8Y8cP#)+e7f1sB1(QqQ~~ztAitAApe-JABY7>k z&yoPVT^zWkS1hy_i*BmZdDvu7PX>x(tSV8$b#|cJUZ_67JkGboD>_RV^xlC>0B>Fx zWBmkQfMv0mT%c;;c}%+T=JUX;CI1j}eFs zvsqpAfwV9PPxfPyj`~u+GtLRflY?TIO2w8B(}6+^Ak`rJ|AyG%1|Y2h#URr__{-yu f3TuHhHxz^1CJ)3r7$yfZ@8V$xGC|6Mf`gm^m>4%s diff --git a/project.godot b/project.godot index e9b1792..626f0be 100644 --- a/project.godot +++ b/project.godot @@ -26,6 +26,7 @@ AudioManager="*res://src/autoloads/AudioManager.gd" GameManager="*res://src/autoloads/GameManager.gd" LocalizationManager="*res://src/autoloads/LocalizationManager.gd" DebugManager="*res://src/autoloads/DebugManager.gd" +SaveManager="*res://src/autoloads/SaveManager.gd" [input] diff --git a/scenes/game/game.gd b/scenes/game/game.gd index 7420fce..81b94db 100644 --- a/scenes/game/game.gd +++ b/scenes/game/game.gd @@ -16,20 +16,29 @@ func _ready() -> void: if not back_button.pressed.is_connected(_on_back_button_pressed): back_button.pressed.connect(_on_back_button_pressed) - # Default to match3 for now - set_gameplay_mode("match3") + # GameManager will set the gameplay mode, don't set default here + DebugManager.log_debug("Game _ready() completed, waiting for GameManager to set gameplay mode", "Game") func set_gameplay_mode(mode: String) -> void: + DebugManager.log_info("set_gameplay_mode called with mode: %s" % mode, "Game") current_gameplay_mode = mode - load_gameplay(mode) + await load_gameplay(mode) + DebugManager.log_info("set_gameplay_mode completed for mode: %s" % mode, "Game") func load_gameplay(mode: String) -> void: DebugManager.log_debug("Loading gameplay mode: %s" % mode, "Game") - # Clear existing gameplay - for child in gameplay_container.get_children(): - DebugManager.log_debug("Removing existing child: %s" % child.name, "Game") - child.queue_free() + # Clear existing gameplay and wait for removal + var existing_children = gameplay_container.get_children() + if existing_children.size() > 0: + DebugManager.log_debug("Removing %d existing children" % existing_children.size(), "Game") + for child in existing_children: + DebugManager.log_debug("Removing existing child: %s" % child.name, "Game") + child.queue_free() + + # Wait for children to be properly removed from scene tree + await get_tree().process_frame + DebugManager.log_debug("Children removal complete, container count: %d" % gameplay_container.get_child_count(), "Game") # Load new gameplay if GAMEPLAY_SCENES.has(mode): @@ -54,11 +63,32 @@ func set_global_score(value: int) -> void: func _on_score_changed(points: int) -> void: self.global_score += points + SaveManager.update_current_score(self.global_score) + +func get_global_score() -> int: + return global_score + +func _get_current_gameplay_instance() -> Node: + if gameplay_container.get_child_count() > 0: + return gameplay_container.get_child(0) + return null func _on_back_button_pressed() -> void: DebugManager.log_debug("Back button pressed in game scene", "Game") AudioManager.play_ui_click() - GameManager.save_game() + + # Save current grid state if we have an active match3 gameplay + var gameplay_instance = _get_current_gameplay_instance() + if gameplay_instance and gameplay_instance.has_method("save_current_state"): + DebugManager.log_info("Saving grid state before exit", "Game") + # Make sure the gameplay instance is still valid and not being destroyed + if is_instance_valid(gameplay_instance) and gameplay_instance.is_inside_tree(): + gameplay_instance.save_current_state() + else: + DebugManager.log_warn("Gameplay instance invalid, skipping grid save on exit", "Game") + + # Save the current score immediately before exiting + SaveManager.finish_game(global_score) GameManager.exit_to_main_menu() func _input(event: InputEvent) -> void: diff --git a/scenes/game/gameplays/match3_gameplay.gd b/scenes/game/gameplays/match3_gameplay.gd index b53efbe..378b8ad 100644 --- a/scenes/game/gameplays/match3_gameplay.gd +++ b/scenes/game/gameplays/match3_gameplay.gd @@ -1,6 +1,7 @@ extends Node2D signal score_changed(points: int) +signal grid_state_loaded(grid_size: Vector2i, tile_types: int) enum GameState { WAITING, # Waiting for player input @@ -13,6 +14,24 @@ var GRID_SIZE := Vector2i(8, 8) var TILE_TYPES := 5 const TILE_SCENE := preload("res://scenes/game/gameplays/tile.tscn") +# Safety constants +const MAX_GRID_SIZE := 15 +const MAX_TILE_TYPES := 10 +const MAX_CASCADE_ITERATIONS := 20 +const MIN_GRID_SIZE := 3 +const MIN_TILE_TYPES := 3 + +# Layout constants (replacing magic numbers) +const SCREEN_WIDTH_USAGE := 0.8 # Use 80% of screen width +const SCREEN_HEIGHT_USAGE := 0.7 # Use 70% of screen height +const GRID_LEFT_MARGIN := 50.0 +const GRID_TOP_MARGIN := 50.0 + +# Timing constants +const CASCADE_WAIT_TIME := 0.1 +const SWAP_ANIMATION_TIME := 0.3 +const TILE_DROP_WAIT_TIME := 0.2 + var grid := [] var tile_size: float = 48.0 var grid_offset: Vector2 @@ -20,35 +39,54 @@ var current_state: GameState = GameState.WAITING var selected_tile: Node2D = null var cursor_position: Vector2i = Vector2i(0, 0) var keyboard_navigation_enabled: bool = false +var grid_initialized: bool = false +var instance_id: String func _ready(): - DebugManager.log_debug("Match3 _ready() started", "Match3") + # Generate unique instance ID for debugging + instance_id = "Match3_%d" % get_instance_id() - # Gem pool will be set individually on each tile during creation + if grid_initialized: + DebugManager.log_warn("[%s] Match3 _ready() called multiple times, skipping initialization" % instance_id, "Match3") + return + DebugManager.log_debug("[%s] Match3 _ready() started" % instance_id, "Match3") + grid_initialized = true + + # Always calculate grid layout first _calculate_grid_layout() - _initialize_grid() + + # Try to load saved state first, otherwise use default initialization + var loaded_saved_state = await load_saved_state() + if not loaded_saved_state: + DebugManager.log_info("No saved state found, using default grid initialization", "Match3") + _initialize_grid() + else: + DebugManager.log_info("Successfully loaded saved grid state", "Match3") DebugManager.log_debug("Match3 _ready() completed, calling debug structure check", "Match3") + # Emit signal to notify UI components (like debug menu) that grid state is fully loaded + grid_state_loaded.emit(GRID_SIZE, TILE_TYPES) + # Debug: Check scene tree structure immediately call_deferred("_debug_scene_structure") func _calculate_grid_layout(): var viewport_size = get_viewport().get_visible_rect().size - var available_width = viewport_size.x * 0.8 # Use 80% of screen width - var available_height = viewport_size.y * 0.7 # Use 70% of screen height + var available_width = viewport_size.x * SCREEN_WIDTH_USAGE + var available_height = viewport_size.y * SCREEN_HEIGHT_USAGE # Calculate tile size based on available space var max_tile_width = available_width / GRID_SIZE.x var max_tile_height = available_height / GRID_SIZE.y tile_size = min(max_tile_width, max_tile_height) - # Align grid to left side with some margin + # Align grid to left side with configurable margins var total_grid_height = tile_size * GRID_SIZE.y grid_offset = Vector2( - 50, # Left margin - (viewport_size.y - total_grid_height) / 2 + 50 # Vertically centered with top margin + GRID_LEFT_MARGIN, + (viewport_size.y - total_grid_height) / 2 + GRID_TOP_MARGIN ) func _initialize_grid(): @@ -79,10 +117,21 @@ func _initialize_grid(): grid[y].append(tile) func _has_match_at(pos: Vector2i) -> bool: - # Fixed: Add bounds and null checks - if pos.x < 0 or pos.y < 0 or pos.x >= GRID_SIZE.x or pos.y >= GRID_SIZE.y: + # Comprehensive bounds and null checks + if not _is_valid_grid_position(pos): return false - if not grid[pos.y][pos.x]: + + if pos.y >= grid.size() or pos.x >= grid[pos.y].size(): + DebugManager.log_error("Grid array bounds exceeded at (%d,%d)" % [pos.x, pos.y], "Match3") + return false + + var tile = grid[pos.y][pos.x] + if not tile or not is_instance_valid(tile): + return false + + # Check if tile has required properties + if not "tile_type" in tile: + DebugManager.log_warn("Tile at (%d,%d) missing tile_type property" % [pos.x, pos.y], "Match3") return false var matches_horizontal = _get_match_line(pos, Vector2i(1, 0)) @@ -101,57 +150,145 @@ func _check_for_matches() -> bool: return false func _get_match_line(start: Vector2i, dir: Vector2i) -> Array: - var line = [grid[start.y][start.x]] - var type = grid[start.y][start.x].tile_type + # Validate input parameters + if not _is_valid_grid_position(start): + DebugManager.log_error("Invalid start position for match line: (%d,%d)" % [start.x, start.y], "Match3") + return [] - # Fixed: Check in both directions separately to avoid infinite loops + if abs(dir.x) + abs(dir.y) != 1 or (dir.x != 0 and dir.y != 0): + DebugManager.log_error("Invalid direction vector for match line: (%d,%d)" % [dir.x, dir.y], "Match3") + return [] + + # Check grid bounds and tile validity + if start.y >= grid.size() or start.x >= grid[start.y].size(): + return [] + + var start_tile = grid[start.y][start.x] + if not start_tile or not is_instance_valid(start_tile): + return [] + + if not "tile_type" in start_tile: + return [] + + var line = [start_tile] + var type = start_tile.tile_type + + # Check in both directions separately with safety limits for offset in [1, -1]: var current = start + dir * offset - while current.x >= 0 and current.y >= 0 and current.x < GRID_SIZE.x and current.y < GRID_SIZE.y: - var neighbor = grid[current.y][current.x] - # Fixed: Add null check to prevent crashes - if not neighbor or neighbor.tile_type != type: + var steps = 0 + while steps < GRID_SIZE.x + GRID_SIZE.y and _is_valid_grid_position(current): + if current.y >= grid.size() or current.x >= grid[current.y].size(): break + + var neighbor = grid[current.y][current.x] + if not neighbor or not is_instance_valid(neighbor): + break + + if not "tile_type" in neighbor or neighbor.tile_type != type: + break + line.append(neighbor) current += dir * offset + steps += 1 return line func _clear_matches(): - var to_clear := [] + # Safety check for grid integrity + if not _validate_grid_integrity(): + DebugManager.log_error("Grid integrity check failed in _clear_matches", "Match3") + return + + var match_groups := [] + var processed_tiles := {} for y in range(GRID_SIZE.y): + if y >= grid.size(): + continue + for x in range(GRID_SIZE.x): - # Fixed: Add null check before processing - if not grid[y][x]: + if x >= grid[y].size(): continue + + # Safe tile access with validation + var tile = grid[y][x] + if not tile or not is_instance_valid(tile): + continue + if processed_tiles.has(tile): + continue + var pos = Vector2i(x, y) var horiz = _get_match_line(pos, Vector2i(1, 0)) - if horiz.size() >= 3: - to_clear.append_array(horiz) var vert = _get_match_line(pos, Vector2i(0, 1)) - if vert.size() >= 3: - to_clear.append_array(vert) - # Remove duplicates using dictionary keys trick + if horiz.size() >= 3: + match_groups.append(horiz) + for match_tile in horiz: + if is_instance_valid(match_tile): + processed_tiles[match_tile] = true + + if vert.size() >= 3: + match_groups.append(vert) + for match_tile in vert: + if is_instance_valid(match_tile): + processed_tiles[match_tile] = true + + # Calculate total score from all matches + # Formula: 3 gems = 3 points, 4 gems = 6 points, 5 gems = 8 points, etc. + # Pattern: n gems = n + (n-2) points for n >= 3, but 3 gems = exactly 3 points + var total_score := 0 + for match_group in match_groups: + var match_size = match_group.size() + var match_score: int + if match_size == 3: + match_score = 3 # Exactly 3 points for 3 gems + else: + match_score = match_size + max(0, match_size - 2) # n + (n-2) for n >= 4 + total_score += match_score + print("Debug: Match of ", match_size, " gems = ", match_score, " points") + + # Remove duplicates from all matches combined + var to_clear := [] var unique_dict := {} - for tile in to_clear: - unique_dict[tile] = true + for match_group in match_groups: + for tile in match_group: + unique_dict[tile] = true to_clear = unique_dict.keys() # Fixed: Only proceed if there are matches to clear if to_clear.size() == 0: return - # Clear grid references first, then queue nodes for removal + # Emit score before clearing tiles + if total_score > 0: + print("Debug: Total score for this turn: ", total_score) + score_changed.emit(total_score) + + # Safe tile removal with validation for tile in to_clear: - grid[tile.grid_position.y][tile.grid_position.x] = null + if not is_instance_valid(tile): + continue + + # Validate tile has grid_position property + if not "grid_position" in tile: + DebugManager.log_warn("Tile missing grid_position during removal", "Match3") + tile.queue_free() + continue + + var tile_pos = tile.grid_position + # Validate grid position before clearing reference + if _is_valid_grid_position(tile_pos) and tile_pos.y < grid.size() and tile_pos.x < grid[tile_pos.y].size(): + grid[tile_pos.y][tile_pos.x] = null + else: + DebugManager.log_warn("Invalid grid position during tile removal: (%d,%d)" % [tile_pos.x, tile_pos.y], "Match3") + tile.queue_free() # Wait one frame for nodes to be queued properly await get_tree().process_frame _drop_tiles() - await get_tree().create_timer(0.2).timeout + await get_tree().create_timer(TILE_DROP_WAIT_TIME).timeout _fill_empty_cells() func _drop_tiles(): @@ -173,83 +310,173 @@ func _drop_tiles(): moved = true func _fill_empty_cells(): + # Safety check for grid integrity + if not _validate_grid_integrity(): + DebugManager.log_error("Grid integrity check failed in _fill_empty_cells", "Match3") + return + # Create gem pool for current tile types var gem_indices = [] for i in range(TILE_TYPES): gem_indices.append(i) + var tiles_created = 0 for y in range(GRID_SIZE.y): + if y >= grid.size(): + DebugManager.log_error("Grid row %d does not exist" % y, "Match3") + continue + for x in range(GRID_SIZE.x): + if x >= grid[y].size(): + DebugManager.log_error("Grid column %d does not exist in row %d" % [x, y], "Match3") + continue + if not grid[y][x]: var tile = TILE_SCENE.instantiate() + if not tile: + DebugManager.log_error("Failed to instantiate tile at (%d,%d)" % [x, y], "Match3") + continue + tile.grid_position = Vector2i(x, y) tile.position = grid_offset + Vector2(x, y) * tile_size add_child(tile) - # Set gem types for this tile - tile.set_active_gem_types(gem_indices) + # Set gem types for this tile with error handling + if tile.has_method("set_active_gem_types"): + tile.set_active_gem_types(gem_indices) + else: + DebugManager.log_warn("Tile missing set_active_gem_types method", "Match3") - # Set random tile type - tile.tile_type = randi() % TILE_TYPES + # Set random tile type with bounds checking + if TILE_TYPES > 0: + tile.tile_type = randi() % TILE_TYPES + else: + DebugManager.log_error("TILE_TYPES is 0, cannot set tile type", "Match3") + tile.queue_free() + continue grid[y][x] = tile - # Connect tile signals for new tiles - tile.tile_selected.connect(_on_tile_selected) + # Connect tile signals for new tiles with error handling + if tile.has_signal("tile_selected"): + tile.tile_selected.connect(_on_tile_selected) + else: + DebugManager.log_warn("Tile missing tile_selected signal", "Match3") - # Fixed: Add recursion protection to prevent stack overflow - await get_tree().create_timer(0.1).timeout - var max_iterations = 10 + tiles_created += 1 + + DebugManager.log_debug("Created %d new tiles" % tiles_created, "Match3") + + # Enhanced cascade protection with better limits + await get_tree().create_timer(CASCADE_WAIT_TIME).timeout var iteration = 0 - while _check_for_matches() and iteration < max_iterations: + while _check_for_matches() and iteration < MAX_CASCADE_ITERATIONS: _clear_matches() - await get_tree().create_timer(0.1).timeout + await get_tree().create_timer(CASCADE_WAIT_TIME).timeout iteration += 1 + if iteration >= MAX_CASCADE_ITERATIONS: + DebugManager.log_warn("Maximum cascade iterations reached (%d), stopping to prevent infinite loop" % MAX_CASCADE_ITERATIONS, "Match3") + + # Save grid state after cascades complete + save_current_state() + func regenerate_grid(): + # Validate grid size before regeneration + if GRID_SIZE.x < MIN_GRID_SIZE or GRID_SIZE.y < MIN_GRID_SIZE or GRID_SIZE.x > MAX_GRID_SIZE or GRID_SIZE.y > MAX_GRID_SIZE: + DebugManager.log_error("Invalid grid size for regeneration: %dx%d" % [GRID_SIZE.x, GRID_SIZE.y], "Match3") + return + + if TILE_TYPES < 3 or TILE_TYPES > MAX_TILE_TYPES: + DebugManager.log_error("Invalid tile types count for regeneration: %d" % TILE_TYPES, "Match3") + return + # Use time-based seed to ensure different patterns each time var new_seed = Time.get_ticks_msec() seed(new_seed) DebugManager.log_debug("Regenerating grid with seed: " + str(new_seed), "Match3") - # Fixed: Clear ALL tile children, not just ones in grid array - # This handles any orphaned tiles that might not be tracked in the grid + # Safe tile cleanup with improved error handling var children_to_remove = [] + var removed_count = 0 + for child in get_children(): + if not is_instance_valid(child): + continue + + # More robust tile detection if child.has_method("get_script") and child.get_script(): var script_path = child.get_script().resource_path if script_path == "res://scenes/game/gameplays/tile.gd": children_to_remove.append(child) + removed_count += 1 + elif "grid_position" in child: # Fallback detection + children_to_remove.append(child) + removed_count += 1 + + DebugManager.log_debug("Found %d tile children to remove" % removed_count, "Match3") # First clear grid array references to prevent access to nodes being freed for y in range(grid.size()): - if grid[y] and grid[y] is Array: + if y < grid.size() and grid[y] and grid[y] is Array: for x in range(grid[y].size()): - grid[y][x] = null + if x < grid[y].size(): + grid[y][x] = null - # Clear the grid array + # Clear the grid array safely grid.clear() # Remove all found tile children using queue_free for safe cleanup for child in children_to_remove: - child.queue_free() + if is_instance_valid(child): + child.queue_free() # Wait for nodes to be properly freed from the scene tree await get_tree().process_frame + await get_tree().process_frame # Extra frame for complex hierarchies - # Fixed: Recalculate grid layout before regenerating tiles + # Recalculate grid layout before regenerating tiles _calculate_grid_layout() - # Regenerate the grid (gem pool is updated in _initialize_grid) + # Regenerate the grid with safety checks _initialize_grid() func set_tile_types(new_count: int): + # Input validation + if new_count < 3: + DebugManager.log_error("Tile types count too low: %d (minimum 3)" % new_count, "Match3") + return + + if new_count > MAX_TILE_TYPES: + DebugManager.log_error("Tile types count too high: %d (maximum %d)" % [new_count, MAX_TILE_TYPES], "Match3") + return + + if new_count == TILE_TYPES: + DebugManager.log_debug("Tile types count unchanged, skipping regeneration", "Match3") + return + + DebugManager.log_debug("Changing tile types from %d to %d" % [TILE_TYPES, new_count], "Match3") TILE_TYPES = new_count + # Regenerate grid with new tile types (gem pool is updated in regenerate_grid) await regenerate_grid() func set_grid_size(new_size: Vector2i): - DebugManager.log_debug("Changing grid size from " + str(GRID_SIZE) + " to " + str(new_size), "Match3") + # Comprehensive input validation + if new_size.x < MIN_GRID_SIZE or new_size.y < MIN_GRID_SIZE: + DebugManager.log_error("Grid size too small: %dx%d (minimum %dx%d)" % [new_size.x, new_size.y, MIN_GRID_SIZE, MIN_GRID_SIZE], "Match3") + return + + if new_size.x > MAX_GRID_SIZE or new_size.y > MAX_GRID_SIZE: + DebugManager.log_error("Grid size too large: %dx%d (maximum %dx%d)" % [new_size.x, new_size.y, MAX_GRID_SIZE, MAX_GRID_SIZE], "Match3") + return + + if new_size == GRID_SIZE: + DebugManager.log_debug("Grid size unchanged, skipping regeneration", "Match3") + return + + DebugManager.log_debug("Changing grid size from %s to %s" % [GRID_SIZE, new_size], "Match3") GRID_SIZE = new_size + # Regenerate grid with new size await regenerate_grid() @@ -336,6 +563,11 @@ func _move_cursor(direction: Vector2i) -> void: DebugManager.log_error("Diagonal cursor movement not supported: " + str(direction), "Match3") return + # Validate grid integrity before cursor operations + if not _validate_grid_integrity(): + DebugManager.log_error("Grid integrity check failed in cursor movement", "Match3") + return + var old_pos = cursor_position var new_pos = cursor_position + direction @@ -344,27 +576,33 @@ func _move_cursor(direction: Vector2i) -> void: new_pos.y = clamp(new_pos.y, 0, GRID_SIZE.y - 1) if new_pos != cursor_position: - # Validate old position before accessing grid - if old_pos.x >= 0 and old_pos.y >= 0 and old_pos.x < GRID_SIZE.x and old_pos.y < GRID_SIZE.y: - var old_tile = grid[cursor_position.y][cursor_position.x] - if old_tile and not old_tile.is_selected: + # Safe access to old tile + var old_tile = _safe_grid_access(old_pos) + if old_tile and "is_selected" in old_tile and "is_highlighted" in old_tile: + if not old_tile.is_selected: old_tile.is_highlighted = false DebugManager.log_debug("Cursor moved from (%d,%d) to (%d,%d)" % [old_pos.x, old_pos.y, new_pos.x, new_pos.y], "Match3") cursor_position = new_pos - # Validate new position before accessing grid - if cursor_position.x >= 0 and cursor_position.y >= 0 and cursor_position.x < GRID_SIZE.x and cursor_position.y < GRID_SIZE.y: - var new_tile = grid[cursor_position.y][cursor_position.x] - if new_tile and not new_tile.is_selected: + # Safe access to new tile + var new_tile = _safe_grid_access(cursor_position) + if new_tile and "is_selected" in new_tile and "is_highlighted" in new_tile: + if not new_tile.is_selected: new_tile.is_highlighted = true func _select_tile_at_cursor() -> void: - if cursor_position.x >= 0 and cursor_position.y >= 0 and cursor_position.x < GRID_SIZE.x and cursor_position.y < GRID_SIZE.y: - var tile = grid[cursor_position.y][cursor_position.x] - if tile: - DebugManager.log_debug("Keyboard selection at cursor (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3") - _on_tile_selected(tile) + # Validate cursor position and grid integrity + if not _is_valid_grid_position(cursor_position): + DebugManager.log_warn("Invalid cursor position for selection: (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3") + return + + var tile = _safe_grid_access(cursor_position) + if tile: + DebugManager.log_debug("Keyboard selection at cursor (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3") + _on_tile_selected(tile) + else: + DebugManager.log_warn("No valid tile at cursor position (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3") func _on_tile_selected(tile: Node2D) -> void: if current_state == GameState.SWAPPING or current_state == GameState.PROCESSING: @@ -394,25 +632,32 @@ func _select_tile(tile: Node2D) -> void: DebugManager.log_debug("Selected tile at (%d, %d)" % [tile.grid_position.x, tile.grid_position.y], "Match3") func _deselect_tile() -> void: - if selected_tile: - DebugManager.log_debug("Deselecting tile at (%d,%d)" % [selected_tile.grid_position.x, selected_tile.grid_position.y], "Match3") + if selected_tile and is_instance_valid(selected_tile): + # Safe access to tile properties + if "grid_position" in selected_tile: + DebugManager.log_debug("Deselecting tile at (%d,%d)" % [selected_tile.grid_position.x, selected_tile.grid_position.y], "Match3") + else: + DebugManager.log_debug("Deselecting tile (no grid position available)", "Match3") - # Clear selection state - selected_tile.is_selected = false + # Safe property access for selection state + if "is_selected" in selected_tile: + selected_tile.is_selected = false # Handle cursor highlighting for keyboard navigation if keyboard_navigation_enabled: - # Clear all highlighting first to avoid conflicts - selected_tile.is_highlighted = false + # Clear highlighting on selected tile + if "is_highlighted" in selected_tile: + selected_tile.is_highlighted = false # Re-highlight cursor position if it's different from the deselected tile - var cursor_tile = grid[cursor_position.y][cursor_position.x] - if cursor_tile: + var cursor_tile = _safe_grid_access(cursor_position) + if cursor_tile and "is_highlighted" in cursor_tile: cursor_tile.is_highlighted = true DebugManager.log_debug("Restored cursor highlighting at (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3") else: # For mouse navigation, just clear highlighting - selected_tile.is_highlighted = false + if "is_highlighted" in selected_tile: + selected_tile.is_highlighted = false selected_tile = null current_state = GameState.WAITING @@ -478,8 +723,218 @@ func _swap_tiles(tile1: Node2D, tile2: Node2D) -> void: var tween = create_tween() tween.set_parallel(true) - tween.tween_property(tile1, "position", target_pos1, 0.3) - tween.tween_property(tile2, "position", target_pos2, 0.3) + tween.tween_property(tile1, "position", target_pos1, SWAP_ANIMATION_TIME) + tween.tween_property(tile2, "position", target_pos2, SWAP_ANIMATION_TIME) await tween.finished DebugManager.log_trace("Tile swap animation completed", "Match3") + +func serialize_grid_state() -> Array: + # Convert the current grid to a serializable 2D array + DebugManager.log_info("Starting serialization: grid.size()=%d, GRID_SIZE=(%d,%d)" % [grid.size(), GRID_SIZE.x, GRID_SIZE.y], "Match3") + + if grid.size() == 0: + DebugManager.log_error("Grid array is empty during serialization!", "Match3") + return [] + + var serialized_grid = [] + var valid_tiles = 0 + var null_tiles = 0 + + for y in range(GRID_SIZE.y): + var row = [] + for x in range(GRID_SIZE.x): + if y < grid.size() and x < grid[y].size() and grid[y][x]: + row.append(grid[y][x].tile_type) + valid_tiles += 1 + # Only log first few for brevity + if valid_tiles <= 5: + DebugManager.log_debug("Serializing tile (%d,%d): type %d" % [x, y, grid[y][x].tile_type], "Match3") + else: + row.append(-1) # Invalid/empty tile + null_tiles += 1 + # Only log first few nulls for brevity + if null_tiles <= 5: + DebugManager.log_debug("Serializing tile (%d,%d): NULL/empty (-1)" % [x, y], "Match3") + serialized_grid.append(row) + + DebugManager.log_info("Serialized grid state: %dx%d grid, %d valid tiles, %d null tiles" % [GRID_SIZE.x, GRID_SIZE.y, valid_tiles, null_tiles], "Match3") + return serialized_grid + +func get_active_gem_types() -> Array: + # Get active gem types from the first available tile + if grid.size() > 0 and grid[0].size() > 0 and grid[0][0]: + return grid[0][0].active_gem_types.duplicate() + else: + # Fallback to default + var default_types = [] + for i in range(TILE_TYPES): + default_types.append(i) + return default_types + +func save_current_state(): + # Save complete game state + var grid_layout = serialize_grid_state() + var active_gems = get_active_gem_types() + + DebugManager.log_info("Saving match3 state: size(%d,%d), %d tile types, %d active gems" % [GRID_SIZE.x, GRID_SIZE.y, TILE_TYPES, active_gems.size()], "Match3") + + SaveManager.save_grid_state(GRID_SIZE, TILE_TYPES, active_gems, grid_layout) + +func load_saved_state() -> bool: + # Check if there's a saved grid state + if not SaveManager.has_saved_grid(): + DebugManager.log_info("No saved grid state found, using default generation", "Match3") + return false + + var saved_state = SaveManager.get_saved_grid_state() + + # Restore grid settings + var saved_size = Vector2i(saved_state.grid_size.x, saved_state.grid_size.y) + TILE_TYPES = saved_state.tile_types_count + var saved_gems = saved_state.active_gem_types + var saved_layout = saved_state.grid_layout + + DebugManager.log_info("[%s] Loading saved grid state: size(%d,%d), %d tile types, layout_size=%d" % [instance_id, saved_size.x, saved_size.y, TILE_TYPES, saved_layout.size()], "Match3") + + # Debug: Print first few rows of loaded layout + for y in range(min(3, saved_layout.size())): + var row_str = "" + for x in range(min(8, saved_layout[y].size())): + row_str += str(saved_layout[y][x]) + " " + DebugManager.log_debug("Loading row %d: %s" % [y, row_str], "Match3") + + # Validate saved data + if saved_layout.size() != saved_size.y: + DebugManager.log_error("Saved grid layout height mismatch: expected %d, got %d" % [saved_size.y, saved_layout.size()], "Match3") + return false + + if saved_layout.size() > 0 and saved_layout[0].size() != saved_size.x: + DebugManager.log_error("Saved grid layout width mismatch: expected %d, got %d" % [saved_size.x, saved_layout[0].size()], "Match3") + return false + + # Apply the saved settings + var old_size = GRID_SIZE + GRID_SIZE = saved_size + + # Recalculate layout if size changed + if old_size != saved_size: + DebugManager.log_info("Grid size changed from %s to %s, recalculating layout" % [old_size, saved_size], "Match3") + _calculate_grid_layout() + + await _restore_grid_from_layout(saved_layout, saved_gems) + + DebugManager.log_info("Successfully loaded saved grid state", "Match3") + return true + +func _restore_grid_from_layout(grid_layout: Array, active_gems: Array): + DebugManager.log_info("[%s] Starting grid restoration: layout_size=%d, active_gems=%s" % [instance_id, grid_layout.size(), active_gems], "Match3") + + # Clear ALL existing tile children, not just ones in grid array + # This ensures no duplicate layers are created + var all_tile_children = [] + for child in get_children(): + if child.has_method("get_script") and child.get_script(): + var script_path = child.get_script().resource_path + if script_path == "res://scenes/game/gameplays/tile.gd": + all_tile_children.append(child) + + DebugManager.log_debug("Found %d existing tile children to remove" % all_tile_children.size(), "Match3") + + # Remove all found tile children + for child in all_tile_children: + child.queue_free() + + # Clear existing grid array + for y in range(grid.size()): + if grid[y] and grid[y] is Array: + for x in range(grid[y].size()): + grid[y][x] = null + + # Clear grid array + grid.clear() + + # Wait for nodes to be freed + await get_tree().process_frame + + # Restore grid from saved layout + for y in range(GRID_SIZE.y): + grid.append([]) + for x in range(GRID_SIZE.x): + var tile = TILE_SCENE.instantiate() + var tile_position = grid_offset + Vector2(x, y) * tile_size + tile.position = tile_position + tile.grid_position = Vector2i(x, y) + add_child(tile) + + # Set gem types for this tile + tile.set_active_gem_types(active_gems) + + # Set the saved tile type + var saved_tile_type = grid_layout[y][x] + DebugManager.log_debug("Setting tile (%d,%d): saved_type=%d, TILE_TYPES=%d" % [x, y, saved_tile_type, TILE_TYPES], "Match3") + + if saved_tile_type >= 0 and saved_tile_type < TILE_TYPES: + tile.tile_type = saved_tile_type + DebugManager.log_debug("✓ Restored tile (%d,%d) with saved type %d" % [x, y, saved_tile_type], "Match3") + else: + # Fallback for invalid tile types + tile.tile_type = randi() % TILE_TYPES + DebugManager.log_error("✗ Invalid saved tile type %d at (%d,%d), using random %d" % [saved_tile_type, x, y, tile.tile_type], "Match3") + + # Connect tile signals + tile.tile_selected.connect(_on_tile_selected) + grid[y].append(tile) + + DebugManager.log_info("Completed grid restoration: %d tiles restored" % [GRID_SIZE.x * GRID_SIZE.y], "Match3") + +# Safety and validation helper functions +func _is_valid_grid_position(pos: Vector2i) -> bool: + return pos.x >= 0 and pos.y >= 0 and pos.x < GRID_SIZE.x and pos.y < GRID_SIZE.y + +func _validate_grid_integrity() -> bool: + # Check if grid array structure is valid + if not grid is Array: + DebugManager.log_error("Grid is not an array", "Match3") + return false + + if grid.size() != GRID_SIZE.y: + DebugManager.log_error("Grid height mismatch: %d vs %d" % [grid.size(), GRID_SIZE.y], "Match3") + return false + + for y in range(grid.size()): + if not grid[y] is Array: + DebugManager.log_error("Grid row %d is not an array" % y, "Match3") + return false + + if grid[y].size() != GRID_SIZE.x: + DebugManager.log_error("Grid row %d width mismatch: %d vs %d" % [y, grid[y].size(), GRID_SIZE.x], "Match3") + return false + + return true + +func _safe_grid_access(pos: Vector2i) -> Node2D: + # Safe grid access with comprehensive bounds checking + if not _is_valid_grid_position(pos): + return null + + if pos.y >= grid.size() or pos.x >= grid[pos.y].size(): + DebugManager.log_warn("Grid bounds exceeded: (%d,%d)" % [pos.x, pos.y], "Match3") + return null + + var tile = grid[pos.y][pos.x] + if not tile or not is_instance_valid(tile): + return null + + return tile + +func _safe_tile_access(tile: Node2D, property: String): + # Safe property access on tiles + if not tile or not is_instance_valid(tile): + return null + + if not property in tile: + DebugManager.log_warn("Tile missing property: %s" % property, "Match3") + return null + + return tile.get(property) diff --git a/scenes/ui/DebugMenuBase.gd b/scenes/ui/DebugMenuBase.gd index 964fb13..7548a9b 100644 --- a/scenes/ui/DebugMenuBase.gd +++ b/scenes/ui/DebugMenuBase.gd @@ -12,8 +12,16 @@ extends Control @export var target_script_path: String = "res://scenes/game/gameplays/match3_gameplay.gd" @export var log_category: String = "DebugMenu" +# Safety constants matching match3_gameplay.gd +const MAX_GRID_SIZE := 15 +const MAX_TILE_TYPES := 10 +const MIN_GRID_SIZE := 3 +const MIN_TILE_TYPES := 3 + var match3_scene: Node2D var search_timer: Timer +var last_scene_search_time: float = 0.0 +const SCENE_SEARCH_COOLDOWN := 0.5 # Prevent excessive scene searching func _exit_tree(): if search_timer: @@ -43,27 +51,27 @@ func _ready(): _find_target_scene() func _initialize_spinboxes(): - # Initialize gem types spinbox - gem_types_spinbox.min_value = 3 - gem_types_spinbox.max_value = 8 + # Initialize gem types spinbox with safety limits + gem_types_spinbox.min_value = MIN_TILE_TYPES + gem_types_spinbox.max_value = MAX_TILE_TYPES gem_types_spinbox.step = 1 gem_types_spinbox.value = 5 # Default value - # Initialize grid size spinboxes - grid_width_spinbox.min_value = 4 - grid_width_spinbox.max_value = 12 + # Initialize grid size spinboxes with safety limits + grid_width_spinbox.min_value = MIN_GRID_SIZE + grid_width_spinbox.max_value = MAX_GRID_SIZE grid_width_spinbox.step = 1 grid_width_spinbox.value = 8 # Default value - grid_height_spinbox.min_value = 4 - grid_height_spinbox.max_value = 12 + grid_height_spinbox.min_value = MIN_GRID_SIZE + grid_height_spinbox.max_value = MAX_GRID_SIZE grid_height_spinbox.step = 1 grid_height_spinbox.value = 8 # Default value func _setup_scene_finding(): - # Create timer for periodic scene search + # Create timer for periodic scene search with longer intervals to reduce CPU usage search_timer = Timer.new() - search_timer.wait_time = 0.1 + search_timer.wait_time = 0.5 # Reduced frequency from 0.1 to 0.5 seconds search_timer.timeout.connect(_find_target_scene) add_child(search_timer) @@ -92,6 +100,11 @@ func _update_ui_from_scene(): if not match3_scene: return + # Connect to grid state loaded signal if not already connected + if match3_scene.has_signal("grid_state_loaded") and not match3_scene.grid_state_loaded.is_connected(_on_grid_state_loaded): + match3_scene.grid_state_loaded.connect(_on_grid_state_loaded) + DebugManager.log_debug("Connected to grid_state_loaded signal", log_category) + # Update gem types display if match3_scene.has_method("get") and "TILE_TYPES" in match3_scene: gem_types_spinbox.value = match3_scene.TILE_TYPES @@ -105,6 +118,18 @@ func _update_ui_from_scene(): grid_width_label.text = "Width: " + str(grid_size.x) grid_height_label.text = "Height: " + str(grid_size.y) +func _on_grid_state_loaded(grid_size: Vector2i, tile_types: int): + DebugManager.log_debug("Grid state loaded signal received: size=%s, types=%d" % [grid_size, tile_types], log_category) + + # Update the UI with the actual loaded values + gem_types_spinbox.value = tile_types + gem_types_label.text = "Gem Types: " + str(tile_types) + + grid_width_spinbox.value = grid_size.x + grid_height_spinbox.value = grid_size.y + grid_width_label.text = "Width: " + str(grid_size.x) + grid_height_label.text = "Height: " + str(grid_size.y) + func _stop_search_timer(): if search_timer and search_timer.timeout.is_connected(_find_target_scene): search_timer.stop() @@ -122,6 +147,14 @@ func _on_debug_toggled(enabled: bool): if not match3_scene: _find_target_scene() _update_ui_from_scene() + # Force refresh the values in case they changed while debug was hidden + _refresh_current_values() + +func _refresh_current_values(): + # Refresh UI with current values from the scene + if match3_scene: + DebugManager.log_debug("Refreshing debug menu values from current scene state", log_category) + _update_ui_from_scene() func _on_regenerate_pressed(): if not match3_scene: @@ -138,17 +171,25 @@ func _on_regenerate_pressed(): DebugManager.log_error("Target scene does not have regenerate_grid method", log_category) func _on_gem_types_changed(value: float): + # Rate limiting for scene searches + var current_time = Time.get_ticks_msec() / 1000.0 + if current_time - last_scene_search_time < SCENE_SEARCH_COOLDOWN: + return + if not match3_scene: _find_target_scene() + last_scene_search_time = current_time if not match3_scene: DebugManager.log_error("Could not find target scene for gem types change", log_category) return var new_value = int(value) - # Input validation - if new_value < gem_types_spinbox.min_value or new_value > gem_types_spinbox.max_value: - DebugManager.log_error("Invalid gem types value: %d (range: %d-%d)" % [new_value, gem_types_spinbox.min_value, gem_types_spinbox.max_value], log_category) + # Enhanced input validation with safety constants + if new_value < MIN_TILE_TYPES or new_value > MAX_TILE_TYPES: + DebugManager.log_error("Invalid gem types value: %d (range: %d-%d)" % [new_value, MIN_TILE_TYPES, MAX_TILE_TYPES], log_category) + # Reset to valid value + gem_types_spinbox.value = clamp(new_value, MIN_TILE_TYPES, MAX_TILE_TYPES) return if match3_scene.has_method("set_tile_types"): @@ -163,17 +204,25 @@ func _on_gem_types_changed(value: float): gem_types_label.text = "Gem Types: " + str(new_value) func _on_grid_width_changed(value: float): + # Rate limiting for scene searches + var current_time = Time.get_ticks_msec() / 1000.0 + if current_time - last_scene_search_time < SCENE_SEARCH_COOLDOWN: + return + if not match3_scene: _find_target_scene() + last_scene_search_time = current_time if not match3_scene: DebugManager.log_error("Could not find target scene for grid width change", log_category) return var new_width = int(value) - # Input validation - if new_width < grid_width_spinbox.min_value or new_width > grid_width_spinbox.max_value: - DebugManager.log_error("Invalid grid width value: %d (range: %d-%d)" % [new_width, grid_width_spinbox.min_value, grid_width_spinbox.max_value], log_category) + # Enhanced input validation with safety constants + if new_width < MIN_GRID_SIZE or new_width > MAX_GRID_SIZE: + DebugManager.log_error("Invalid grid width value: %d (range: %d-%d)" % [new_width, MIN_GRID_SIZE, MAX_GRID_SIZE], log_category) + # Reset to valid value + grid_width_spinbox.value = clamp(new_width, MIN_GRID_SIZE, MAX_GRID_SIZE) return grid_width_label.text = "Width: " + str(new_width) @@ -188,17 +237,25 @@ func _on_grid_width_changed(value: float): DebugManager.log_error("Target scene does not have set_grid_size method", log_category) func _on_grid_height_changed(value: float): + # Rate limiting for scene searches + var current_time = Time.get_ticks_msec() / 1000.0 + if current_time - last_scene_search_time < SCENE_SEARCH_COOLDOWN: + return + if not match3_scene: _find_target_scene() + last_scene_search_time = current_time if not match3_scene: DebugManager.log_error("Could not find target scene for grid height change", log_category) return var new_height = int(value) - # Input validation - if new_height < grid_height_spinbox.min_value or new_height > grid_height_spinbox.max_value: - DebugManager.log_error("Invalid grid height value: %d (range: %d-%d)" % [new_height, grid_height_spinbox.min_value, grid_height_spinbox.max_value], log_category) + # Enhanced input validation with safety constants + if new_height < MIN_GRID_SIZE or new_height > MAX_GRID_SIZE: + DebugManager.log_error("Invalid grid height value: %d (range: %d-%d)" % [new_height, MIN_GRID_SIZE, MAX_GRID_SIZE], log_category) + # Reset to valid value + grid_height_spinbox.value = clamp(new_height, MIN_GRID_SIZE, MAX_GRID_SIZE) return grid_height_label.text = "Height: " + str(new_height) diff --git a/scenes/ui/MainMenu.gd b/scenes/ui/MainMenu.gd index 29c021b..29201dd 100644 --- a/scenes/ui/MainMenu.gd +++ b/scenes/ui/MainMenu.gd @@ -9,11 +9,17 @@ var original_button_scales: Array[Vector2] = [] func _ready(): DebugManager.log_info("MainMenu ready", "MainMenu") _setup_menu_navigation() + _update_new_game_button() func _on_new_game_button_pressed(): AudioManager.play_ui_click() - DebugManager.log_info("New Game pressed", "MainMenu") - GameManager.start_new_game() + var button_text = $MenuContainer/NewGameButton.text + if button_text == "Continue": + DebugManager.log_info("Continue pressed", "MainMenu") + GameManager.continue_game() + else: + DebugManager.log_info("New Game pressed", "MainMenu") + GameManager.start_new_game() func _on_settings_button_pressed(): AudioManager.play_ui_click() @@ -77,3 +83,17 @@ func _update_visual_selection(): else: button.scale = original_button_scales[i] button.modulate = Color.WHITE + +func _update_new_game_button(): + # Check if there's an existing save with progress + var current_score = SaveManager.get_current_score() + var games_played = SaveManager.get_games_played() + var has_saved_grid = SaveManager.has_saved_grid() + + var new_game_button = $MenuContainer/NewGameButton + if current_score > 0 or games_played > 0 or has_saved_grid: + new_game_button.text = "Continue" + DebugManager.log_info("Updated button to Continue (score: %d, games: %d, grid: %s)" % [current_score, games_played, has_saved_grid], "MainMenu") + else: + new_game_button.text = "New Game" + DebugManager.log_info("Updated button to New Game", "MainMenu") diff --git a/scenes/ui/SettingsMenu.gd b/scenes/ui/SettingsMenu.gd index 6588e1e..fc5fac3 100644 --- a/scenes/ui/SettingsMenu.gd +++ b/scenes/ui/SettingsMenu.gd @@ -6,10 +6,14 @@ signal back_to_main_menu @onready var music_slider = $SettingsContainer/MusicVolumeContainer/MusicVolumeSlider @onready var sfx_slider = $SettingsContainer/SFXVolumeContainer/SFXVolumeSlider @onready var language_stepper = $SettingsContainer/LanguageContainer/LanguageStepper +@onready var reset_progress_button = $ResetSettingsContainer/ResetProgressButton @export var settings_manager: Node = SettingsManager @export var localization_manager: Node = LocalizationManager +# Progress reset confirmation dialog +var confirmation_dialog: AcceptDialog + # Navigation system variables var navigable_controls: Array[Control] = [] @@ -39,6 +43,7 @@ func _ready(): _update_controls_from_settings() update_text() _setup_navigation_system() + _setup_confirmation_dialog() func _update_controls_from_settings(): master_slider.value = settings_manager.get_setting("master_volume") @@ -112,6 +117,7 @@ func update_text(): $SettingsContainer/SFXVolumeContainer/SFXVolume.text = tr("sfx_volume") $SettingsContainer/LanguageContainer/LanguageLabel.text = tr("language") $BackButtonContainer/BackButton.text = tr("back") + reset_progress_button.text = tr("reset_progress") func _on_reset_setting_button_pressed() -> void: @@ -133,6 +139,7 @@ func _setup_navigation_system(): navigable_controls.append(language_stepper) # Use the ValueStepper component navigable_controls.append($BackButtonContainer/BackButton) navigable_controls.append($ResetSettingsContainer/ResetSettingButton) + navigable_controls.append(reset_progress_button) # Store original visual properties for control in navigable_controls: @@ -218,3 +225,73 @@ func _get_control_name(control: Control) -> String: func _on_language_stepper_value_changed(new_value: String, new_index: int): DebugManager.log_info("Language changed via ValueStepper: " + new_value + " (index: " + str(new_index) + ")", "Settings") + +func _setup_confirmation_dialog(): + """Create confirmation dialog for progress reset""" + confirmation_dialog = AcceptDialog.new() + confirmation_dialog.title = tr("confirm_reset_title") + confirmation_dialog.dialog_text = tr("confirm_reset_message") + confirmation_dialog.ok_button_text = tr("reset_confirm") + confirmation_dialog.add_cancel_button(tr("cancel")) + + # Make dialog modal and centered + confirmation_dialog.set_flag(Window.FLAG_POPUP, true) + confirmation_dialog.popup_window = true + + # Connect signals + confirmation_dialog.confirmed.connect(_on_reset_progress_confirmed) + confirmation_dialog.canceled.connect(_on_reset_progress_canceled) + + add_child(confirmation_dialog) + +func _on_reset_progress_button_pressed(): + """Handle reset progress button press with confirmation""" + AudioManager.play_ui_click() + DebugManager.log_info("Reset progress button pressed", "Settings") + + # Update dialog text with current translations + confirmation_dialog.title = tr("confirm_reset_title") + confirmation_dialog.dialog_text = tr("confirm_reset_message") + confirmation_dialog.ok_button_text = tr("reset_confirm") + + # Show confirmation dialog + confirmation_dialog.popup_centered() + +func _on_reset_progress_confirmed(): + """Actually reset the progress after confirmation""" + AudioManager.play_ui_click() + DebugManager.log_info("Progress reset confirmed by user", "Settings") + + # Call SaveManager to reset all progress + if SaveManager.reset_all_progress(): + DebugManager.log_info("All progress successfully reset", "Settings") + + # Show success message + var success_dialog = AcceptDialog.new() + success_dialog.title = tr("reset_success_title") + success_dialog.dialog_text = tr("reset_success_message") + success_dialog.ok_button_text = tr("ok") + add_child(success_dialog) + success_dialog.popup_centered() + + # Auto-close success dialog and remove it after 3 seconds + success_dialog.confirmed.connect(func(): success_dialog.queue_free()) + await get_tree().create_timer(3.0).timeout + if is_instance_valid(success_dialog): + success_dialog.queue_free() + else: + DebugManager.log_error("Failed to reset progress", "Settings") + + # Show error message + var error_dialog = AcceptDialog.new() + error_dialog.title = tr("reset_error_title") + error_dialog.dialog_text = tr("reset_error_message") + error_dialog.ok_button_text = tr("ok") + add_child(error_dialog) + error_dialog.popup_centered() + error_dialog.confirmed.connect(func(): error_dialog.queue_free()) + +func _on_reset_progress_canceled(): + """Handle reset progress cancellation""" + AudioManager.play_ui_click() + DebugManager.log_info("Progress reset canceled by user", "Settings") diff --git a/scenes/ui/SettingsMenu.tscn b/scenes/ui/SettingsMenu.tscn index 98485b3..1814e64 100644 --- a/scenes/ui/SettingsMenu.tscn +++ b/scenes/ui/SettingsMenu.tscn @@ -2,7 +2,7 @@ [ext_resource type="Script" uid="uid://dftenhuhwskqa" path="res://scenes/ui/SettingsMenu.gd" id="1_oqkcn"] [ext_resource type="PackedScene" uid="uid://df2b4wn8j6cxl" path="res://scenes/ui/DebugToggle.tscn" id="2_debug"] -[ext_resource type="PackedScene" path="res://scenes/ui/components/ValueStepper.tscn" id="3_value_stepper"] +[ext_resource type="PackedScene" uid="uid://cb6k05r8t7l4l" path="res://scenes/ui/components/ValueStepper.tscn" id="3_value_stepper"] [node name="SettingsMenu" type="Control" groups=["localizable"]] layout_mode = 3 @@ -26,6 +26,7 @@ offset_right = 34.5 offset_bottom = 20.0 grow_horizontal = 2 grow_vertical = 2 +alignment = 1 [node name="SettingsTitle" type="Label" parent="SettingsContainer"] custom_minimum_size = Vector2(300, 0) @@ -105,30 +106,28 @@ grow_horizontal = 2 grow_vertical = 2 text = "back" -[node name="ResetSettingsContainer" type="Control" parent="."] +[node name="ResetSettingsContainer" type="VBoxContainer" parent="."] layout_mode = 1 anchors_preset = 7 anchor_left = 0.5 anchor_top = 1.0 anchor_right = 0.5 anchor_bottom = 1.0 -offset_left = -20.0 -offset_top = -40.0 -offset_right = 20.0 +offset_left = -98.0 +offset_top = -80.0 +offset_right = 98.0 grow_horizontal = 2 grow_vertical = 0 [node name="ResetSettingButton" type="Button" parent="ResetSettingsContainer"] -layout_mode = 1 -anchors_preset = 5 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -98.0 -offset_right = 98.0 -offset_bottom = 31.0 -grow_horizontal = 2 +layout_mode = 2 text = "Reset settings to default" +[node name="ResetProgressButton" type="Button" parent="ResetSettingsContainer"] +modulate = Color(1, 0.7, 0.7, 1) +layout_mode = 2 +text = "Reset All Progress" + [node name="DebugToggle" parent="." instance=ExtResource("2_debug")] layout_mode = 1 @@ -138,3 +137,4 @@ layout_mode = 1 [connection signal="value_changed" from="SettingsContainer/LanguageContainer/LanguageStepper" to="." method="_on_language_stepper_value_changed"] [connection signal="pressed" from="BackButtonContainer/BackButton" to="." method="_on_back_button_pressed"] [connection signal="pressed" from="ResetSettingsContainer/ResetSettingButton" to="." method="_on_reset_setting_button_pressed"] +[connection signal="pressed" from="ResetSettingsContainer/ResetProgressButton" to="." method="_on_reset_progress_button_pressed"] diff --git a/src/autoloads/GameManager.gd b/src/autoloads/GameManager.gd index 18723ce..dabd45b 100644 --- a/src/autoloads/GameManager.gd +++ b/src/autoloads/GameManager.gd @@ -7,12 +7,19 @@ var pending_gameplay_mode: String = "match3" var is_changing_scene: bool = false func start_new_game() -> void: + SaveManager.start_new_game() + start_game_with_mode("match3") + +func continue_game() -> void: + # Don't reset score - just load the game scene start_game_with_mode("match3") func start_match3_game() -> void: + SaveManager.start_new_game() start_game_with_mode("match3") func start_clickomania_game() -> void: + SaveManager.start_new_game() start_game_with_mode("clickomania") func start_game_with_mode(gameplay_mode: String) -> void: @@ -65,13 +72,25 @@ func start_game_with_mode(gameplay_mode: String) -> void: if get_tree().current_scene.has_method("set_gameplay_mode"): DebugManager.log_info("Setting gameplay mode to: %s" % pending_gameplay_mode, "GameManager") get_tree().current_scene.set_gameplay_mode(pending_gameplay_mode) + + # Load saved score + if get_tree().current_scene.has_method("set_global_score"): + var saved_score = SaveManager.get_current_score() + DebugManager.log_info("Loading saved score: %d" % saved_score, "GameManager") + get_tree().current_scene.set_global_score(saved_score) else: DebugManager.log_error("Game scene does not have set_gameplay_mode method", "GameManager") is_changing_scene = false func save_game() -> void: - DebugManager.log_info("Game saved (mock)", "GameManager") + # Get current score from the active game scene + var current_score = 0 + if get_tree().current_scene and get_tree().current_scene.has_method("get_global_score"): + current_score = get_tree().current_scene.get_global_score() + + SaveManager.finish_game(current_score) + DebugManager.log_info("Game saved with score: %d" % current_score, "GameManager") func exit_to_main_menu() -> void: # Prevent concurrent scene changes diff --git a/src/autoloads/SaveManager.gd b/src/autoloads/SaveManager.gd new file mode 100644 index 0000000..677a845 --- /dev/null +++ b/src/autoloads/SaveManager.gd @@ -0,0 +1,402 @@ +extends Node + +const SAVE_FILE_PATH = "user://savegame.save" +const SAVE_FORMAT_VERSION = 1 +const MAX_GRID_SIZE = 15 +const MAX_TILE_TYPES = 10 +const MAX_SCORE = 999999999 +const MAX_GAMES_PLAYED = 100000 + +var game_data = { + "high_score": 0, + "current_score": 0, + "games_played": 0, + "total_score": 0, + "grid_state": { + "grid_size": {"x": 8, "y": 8}, + "tile_types_count": 5, + "active_gem_types": [0, 1, 2, 3, 4], + "grid_layout": [] # 2D array of tile types + } +} + +func _ready(): + load_game() + +func save_game(): + # Create backup before saving + _create_backup() + + # Add version and validation data + var save_data = game_data.duplicate(true) + save_data["_version"] = SAVE_FORMAT_VERSION + save_data["_checksum"] = _calculate_checksum(save_data) + + var save_file = FileAccess.open(SAVE_FILE_PATH, FileAccess.WRITE) + if save_file == null: + DebugManager.log_error("Failed to open save file for writing: %s" % SAVE_FILE_PATH, "SaveManager") + return false + + var json_string = JSON.stringify(save_data) + + # Validate JSON was created successfully + if json_string.is_empty(): + DebugManager.log_error("Failed to serialize save data to JSON", "SaveManager") + save_file.close() + return false + + save_file.store_var(json_string) + save_file.close() + + DebugManager.log_info("Game saved successfully. High score: %d, Current score: %d" % [game_data.high_score, game_data.current_score], "SaveManager") + return true + +func load_game(): + if not FileAccess.file_exists(SAVE_FILE_PATH): + DebugManager.log_info("No save file found, using defaults", "SaveManager") + return + + var save_file = FileAccess.open(SAVE_FILE_PATH, FileAccess.READ) + if save_file == null: + DebugManager.log_error("Failed to open save file for reading: %s" % SAVE_FILE_PATH, "SaveManager") + return + + # Check file size to prevent memory exhaustion + var file_size = save_file.get_length() + if file_size > 1048576: # 1MB limit + DebugManager.log_error("Save file too large: %d bytes (max 1MB)" % file_size, "SaveManager") + save_file.close() + return + + var json_string = save_file.get_var() + save_file.close() + + if not json_string is String: + DebugManager.log_error("Save file contains invalid data type", "SaveManager") + return + + var json = JSON.new() + var parse_result = json.parse(json_string) + if parse_result != OK: + DebugManager.log_error("Failed to parse save file JSON: %s" % json.error_string, "SaveManager") + _restore_backup_if_exists() + return + + var loaded_data = json.data + if not loaded_data is Dictionary: + DebugManager.log_error("Save file root is not a dictionary", "SaveManager") + _restore_backup_if_exists() + return + + # Validate and sanitize loaded data + if not _validate_save_data(loaded_data): + DebugManager.log_error("Save file failed validation, using defaults", "SaveManager") + _restore_backup_if_exists() + return + + # Safely merge validated data + _merge_validated_data(loaded_data) + + DebugManager.log_info("Game loaded successfully. High score: %d, Games played: %d" % [game_data.high_score, game_data.games_played], "SaveManager") + +func update_current_score(score: int): + # Input validation + if score < 0: + DebugManager.log_warn("Negative score rejected: %d" % score, "SaveManager") + return + if score > MAX_SCORE: + DebugManager.log_warn("Score too high, capping at maximum: %d -> %d" % [score, MAX_SCORE], "SaveManager") + score = MAX_SCORE + + game_data.current_score = score + if score > game_data.high_score: + game_data.high_score = score + DebugManager.log_info("New high score: %d" % score, "SaveManager") + +func start_new_game(): + game_data.current_score = 0 + game_data.games_played += 1 + # Clear any saved grid state for fresh start + game_data.grid_state.grid_layout = [] + DebugManager.log_info("Started new game #%d (cleared grid state)" % game_data.games_played, "SaveManager") + +func finish_game(final_score: int): + # Input validation + if final_score < 0: + DebugManager.log_warn("Negative final score rejected: %d" % final_score, "SaveManager") + return + if final_score > MAX_SCORE: + DebugManager.log_warn("Final score too high, capping: %d -> %d" % [final_score, MAX_SCORE], "SaveManager") + final_score = MAX_SCORE + + DebugManager.log_info("Finishing game with score: %d (previous: %d)" % [final_score, game_data.current_score], "SaveManager") + game_data.current_score = final_score + + # Prevent total_score overflow + var new_total = game_data.total_score + final_score + if new_total < game_data.total_score: # Overflow check + DebugManager.log_warn("Total score overflow prevented", "SaveManager") + game_data.total_score = MAX_SCORE + else: + game_data.total_score = new_total + + if final_score > game_data.high_score: + game_data.high_score = final_score + DebugManager.log_info("New high score achieved: %d" % final_score, "SaveManager") + save_game() + +func get_high_score() -> int: + return game_data.high_score + +func get_current_score() -> int: + return game_data.current_score + +func get_games_played() -> int: + return game_data.games_played + +func get_total_score() -> int: + return game_data.total_score + +func save_grid_state(grid_size: Vector2i, tile_types_count: int, active_gem_types: Array, grid_layout: Array): + # Comprehensive input validation + if not _validate_grid_parameters(grid_size, tile_types_count, active_gem_types, grid_layout): + DebugManager.log_error("Grid state validation failed, not saving", "SaveManager") + return + + DebugManager.log_info("Saving grid state: size(%d,%d), types=%d, layout_rows=%d" % [grid_size.x, grid_size.y, tile_types_count, grid_layout.size()], "SaveManager") + + game_data.grid_state.grid_size = {"x": grid_size.x, "y": grid_size.y} + game_data.grid_state.tile_types_count = tile_types_count + game_data.grid_state.active_gem_types = active_gem_types.duplicate() + game_data.grid_state.grid_layout = grid_layout.duplicate(true) # Deep copy + + # Debug: Print first few rows of saved layout + for y in range(min(3, grid_layout.size())): + var row_str = "" + for x in range(min(8, grid_layout[y].size())): + row_str += str(grid_layout[y][x]) + " " + DebugManager.log_debug("Saved row %d: %s" % [y, row_str], "SaveManager") + + save_game() + +func get_saved_grid_state() -> Dictionary: + return game_data.grid_state + +func has_saved_grid() -> bool: + return game_data.grid_state.grid_layout.size() > 0 + +func clear_grid_state(): + DebugManager.log_info("Clearing saved grid state", "SaveManager") + game_data.grid_state.grid_layout = [] + save_game() + +func reset_all_progress(): + """Reset all game progress and delete save files completely""" + DebugManager.log_info("Starting complete progress reset", "SaveManager") + + # Reset all game data to initial values + game_data = { + "high_score": 0, + "current_score": 0, + "games_played": 0, + "total_score": 0, + "grid_state": { + "grid_size": {"x": 8, "y": 8}, + "tile_types_count": 5, + "active_gem_types": [0, 1, 2, 3, 4], + "grid_layout": [] # 2D array of tile types + } + } + + # Delete main save file + if FileAccess.file_exists(SAVE_FILE_PATH): + var error = DirAccess.remove_absolute(SAVE_FILE_PATH) + if error == OK: + DebugManager.log_info("Main save file deleted successfully", "SaveManager") + else: + DebugManager.log_error("Failed to delete main save file: error %d" % error, "SaveManager") + + # Delete backup file + var backup_path = SAVE_FILE_PATH + ".backup" + if FileAccess.file_exists(backup_path): + var error = DirAccess.remove_absolute(backup_path) + if error == OK: + DebugManager.log_info("Backup save file deleted successfully", "SaveManager") + else: + DebugManager.log_error("Failed to delete backup save file: error %d" % error, "SaveManager") + + DebugManager.log_info("Progress reset completed - all scores and save data cleared", "SaveManager") + return true + +# Security and validation helper functions +func _validate_save_data(data: Dictionary) -> bool: + # Check required fields exist and have correct types + var required_fields = ["high_score", "current_score", "games_played", "total_score", "grid_state"] + for field in required_fields: + if not data.has(field): + DebugManager.log_error("Missing required field: %s" % field, "SaveManager") + return false + + # Validate numeric fields + if not _is_valid_score(data.get("high_score", 0)): + return false + if not _is_valid_score(data.get("current_score", 0)): + return false + if not _is_valid_score(data.get("total_score", 0)): + return false + + var games_played = data.get("games_played", 0) + # Accept both int and float for games_played, convert to int for validation + if not (games_played is int or games_played is float): + DebugManager.log_error("Invalid games_played type: %s (type: %s)" % [str(games_played), typeof(games_played)], "SaveManager") + return false + + var games_played_int = int(games_played) + if games_played_int < 0 or games_played_int > MAX_GAMES_PLAYED: + DebugManager.log_error("Invalid games_played value: %d (range: 0-%d)" % [games_played_int, MAX_GAMES_PLAYED], "SaveManager") + return false + + # Validate grid state + var grid_state = data.get("grid_state", {}) + if not grid_state is Dictionary: + DebugManager.log_error("Grid state is not a dictionary", "SaveManager") + return false + + return _validate_grid_state(grid_state) + +func _validate_grid_state(grid_state: Dictionary) -> bool: + # Check grid size + if not grid_state.has("grid_size") or not grid_state.grid_size is Dictionary: + DebugManager.log_error("Invalid grid_size in save data", "SaveManager") + return false + + var size = grid_state.grid_size + if not size.has("x") or not size.has("y"): + return false + + var width = size.x + var height = size.y + if not width is int or not height is int: + return false + if width < 3 or height < 3 or width > MAX_GRID_SIZE or height > MAX_GRID_SIZE: + DebugManager.log_error("Grid size out of bounds: %dx%d" % [width, height], "SaveManager") + return false + + # Check tile types + var tile_types = grid_state.get("tile_types_count", 0) + if not tile_types is int or tile_types < 3 or tile_types > MAX_TILE_TYPES: + DebugManager.log_error("Invalid tile_types_count: %s" % str(tile_types), "SaveManager") + return false + + # Validate grid layout if present + var layout = grid_state.get("grid_layout", []) + if layout.size() > 0: + return _validate_grid_layout(layout, width, height, tile_types) + + return true + +func _validate_grid_layout(layout: Array, expected_width: int, expected_height: int, max_tile_type: int) -> bool: + if layout.size() != expected_height: + DebugManager.log_error("Grid layout height mismatch: %d vs %d" % [layout.size(), expected_height], "SaveManager") + return false + + for y in range(layout.size()): + var row = layout[y] + if not row is Array: + DebugManager.log_error("Grid layout row %d is not an array" % y, "SaveManager") + return false + if row.size() != expected_width: + DebugManager.log_error("Grid layout row %d width mismatch: %d vs %d" % [y, row.size(), expected_width], "SaveManager") + return false + + for x in range(row.size()): + var tile_type = row[x] + if not tile_type is int: + DebugManager.log_error("Grid tile [%d][%d] is not an integer: %s" % [y, x, str(tile_type)], "SaveManager") + return false + if tile_type < -1 or tile_type >= max_tile_type: + DebugManager.log_error("Grid tile [%d][%d] type out of range: %d" % [y, x, tile_type], "SaveManager") + return false + + return true + +func _validate_grid_parameters(grid_size: Vector2i, tile_types_count: int, active_gem_types: Array, grid_layout: Array) -> bool: + # Validate grid size + if grid_size.x < 3 or grid_size.y < 3 or grid_size.x > MAX_GRID_SIZE or grid_size.y > MAX_GRID_SIZE: + DebugManager.log_error("Invalid grid size: %dx%d (min 3x3, max %dx%d)" % [grid_size.x, grid_size.y, MAX_GRID_SIZE, MAX_GRID_SIZE], "SaveManager") + return false + + # Validate tile types count + if tile_types_count < 3 or tile_types_count > MAX_TILE_TYPES: + DebugManager.log_error("Invalid tile types count: %d (min 3, max %d)" % [tile_types_count, MAX_TILE_TYPES], "SaveManager") + return false + + # Validate active gem types + if active_gem_types.size() != tile_types_count: + DebugManager.log_error("Active gem types size mismatch: %d vs %d" % [active_gem_types.size(), tile_types_count], "SaveManager") + return false + + # Validate grid layout + return _validate_grid_layout(grid_layout, grid_size.x, grid_size.y, tile_types_count) + +func _is_valid_score(score) -> bool: + # Accept both int and float, but convert to int for validation + if not (score is int or score is float): + DebugManager.log_error("Score is not a number: %s (type: %s)" % [str(score), typeof(score)], "SaveManager") + return false + + var score_int = int(score) + if score_int < 0 or score_int > MAX_SCORE: + DebugManager.log_error("Score out of bounds: %d" % score_int, "SaveManager") + return false + return true + +func _merge_validated_data(loaded_data: Dictionary): + # Safely merge only validated fields, converting floats to ints for scores + for key in ["high_score", "current_score", "total_score"]: + if loaded_data.has(key): + var value = loaded_data[key] + # Convert float scores to integers + game_data[key] = int(value) if (value is float or value is int) else 0 + + # Games played should always be an integer + if loaded_data.has("games_played"): + var games_played = loaded_data["games_played"] + game_data["games_played"] = int(games_played) if (games_played is float or games_played is int) else 0 + + # Merge grid state carefully + var loaded_grid = loaded_data.get("grid_state", {}) + for grid_key in ["grid_size", "tile_types_count", "active_gem_types", "grid_layout"]: + if loaded_grid.has(grid_key): + game_data.grid_state[grid_key] = loaded_grid[grid_key] + +func _calculate_checksum(data: Dictionary) -> String: + # Simple checksum for save file integrity + var json_string = JSON.stringify(data) + return str(json_string.hash()) + +func _create_backup(): + # Create backup of current save file + if FileAccess.file_exists(SAVE_FILE_PATH): + var backup_path = SAVE_FILE_PATH + ".backup" + var original = FileAccess.open(SAVE_FILE_PATH, FileAccess.READ) + var backup = FileAccess.open(backup_path, FileAccess.WRITE) + if original and backup: + backup.store_var(original.get_var()) + backup.close() + if original: + original.close() + +func _restore_backup_if_exists(): + var backup_path = SAVE_FILE_PATH + ".backup" + if FileAccess.file_exists(backup_path): + DebugManager.log_info("Attempting to restore from backup", "SaveManager") + var backup = FileAccess.open(backup_path, FileAccess.READ) + var original = FileAccess.open(SAVE_FILE_PATH, FileAccess.WRITE) + if backup and original: + original.store_var(backup.get_var()) + original.close() + DebugManager.log_info("Backup restored successfully", "SaveManager") + load_game() # Try to load the restored backup + if backup: + backup.close() \ No newline at end of file diff --git a/src/autoloads/SaveManager.gd.uid b/src/autoloads/SaveManager.gd.uid new file mode 100644 index 0000000..29eaa29 --- /dev/null +++ b/src/autoloads/SaveManager.gd.uid @@ -0,0 +1 @@ +uid://d1fdah5x4rme0