Compare commits

...

4 Commits

Author SHA1 Message Date
0791b80f15 change init window size
Some checks failed
GDScript Auto-Formatting / Auto-Format GDScript Code (pull_request) Failing after 2m7s
GDScript Linting / GDScript Code Quality Check (pull_request) Failing after 12s
2025-09-27 21:02:01 +04:00
ca6111cd28 Add gdformat pipeline 2025-09-27 21:01:49 +04:00
0cf76d595f Comments and documentation update 2025-09-27 21:00:45 +04:00
821d9aa42c add lint workflow 2025-09-27 20:42:51 +04:00
7 changed files with 529 additions and 31 deletions

View File

@@ -0,0 +1,278 @@
name: GDScript Auto-Formatting
on:
# Trigger on pull requests to main branch
pull_request:
branches: ['main']
paths:
- '**/*.gd'
- '.gdformatrc'
- '.gitea/workflows/gdformat.yml'
# Allow manual triggering
workflow_dispatch:
inputs:
target_branch:
description: 'Target branch to format (leave empty for current branch)'
required: false
default: ''
jobs:
gdformat:
name: Auto-Format GDScript Code
runs-on: ubuntu-latest
# Grant write permissions for pushing changes
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
# Use the PR head ref for pull requests, or current branch for manual runs
ref: ${{ github.event.pull_request.head.ref || github.ref }}
# Need token with write permissions to push back
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install --upgrade "setuptools<81"
pip install gdtoolkit==4
- name: Verify gdformat installation
run: |
gdformat --version
echo "✅ gdformat installed successfully"
- name: Get target branch info
id: branch-info
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
target_branch="${{ github.event.pull_request.head.ref }}"
echo "🔄 Processing PR branch: $target_branch"
elif [[ -n "${{ github.event.inputs.target_branch }}" ]]; then
target_branch="${{ github.event.inputs.target_branch }}"
echo "🎯 Manual target branch: $target_branch"
git checkout "$target_branch" || (echo "❌ Branch not found: $target_branch" && exit 1)
else
target_branch="${{ github.ref_name }}"
echo "📍 Current branch: $target_branch"
fi
echo "target_branch=$target_branch" >> $GITHUB_OUTPUT
- name: Count GDScript files
id: count-files
run: |
file_count=$(find . -name "*.gd" -not -path "./.git/*" | wc -l)
echo "file_count=$file_count" >> $GITHUB_OUTPUT
echo "📊 Found $file_count GDScript files to format"
- name: Run GDScript formatting
id: format-files
run: |
echo "🎨 Starting GDScript formatting..."
echo "================================"
# Initialize counters
total_files=0
formatted_files=0
skipped_files=0
failed_files=0
# Track if any files were actually changed
files_changed=false
# Find all .gd files except TestHelper.gd (static var syntax incompatibility)
while IFS= read -r -d '' file; do
filename=$(basename "$file")
# Skip TestHelper.gd due to static var syntax incompatibility with gdformat
if [[ "$filename" == "TestHelper.gd" ]]; then
echo "⚠️ Skipping $file (static var syntax not supported by gdformat)"
((total_files++))
((skipped_files++))
continue
fi
echo "🎨 Formatting: $file"
((total_files++))
# Get file hash before formatting
before_hash=$(sha256sum "$file" | cut -d' ' -f1)
# Run gdformat
if gdformat "$file" 2>/dev/null; then
# Get file hash after formatting
after_hash=$(sha256sum "$file" | cut -d' ' -f1)
if [[ "$before_hash" != "$after_hash" ]]; then
echo "✅ Formatted (changes applied)"
files_changed=true
else
echo "✅ Already formatted"
fi
((formatted_files++))
else
echo "❌ Failed to format"
((failed_files++))
fi
done < <(find . -name "*.gd" -not -path "./.git/*" -print0)
# Print summary
echo ""
echo "================================"
echo "📋 Formatting Summary"
echo "================================"
echo "📊 Total files: $total_files"
echo "✅ Successfully formatted: $formatted_files"
echo "⚠️ Skipped files: $skipped_files"
echo "❌ Failed files: $failed_files"
echo ""
# Export results for next step
echo "files_changed=$files_changed" >> $GITHUB_OUTPUT
echo "total_files=$total_files" >> $GITHUB_OUTPUT
echo "formatted_files=$formatted_files" >> $GITHUB_OUTPUT
echo "failed_files=$failed_files" >> $GITHUB_OUTPUT
# Exit with error if any files failed
if [[ $failed_files -gt 0 ]]; then
echo "❌ Formatting FAILED - $failed_files file(s) could not be formatted"
exit 1
else
echo "✅ All files processed successfully!"
fi
- name: Check for changes
id: check-changes
run: |
if git diff --quiet; then
echo "📝 No formatting changes detected"
echo "has_changes=false" >> $GITHUB_OUTPUT
else
echo "📝 Formatting changes detected"
echo "has_changes=true" >> $GITHUB_OUTPUT
# Show what changed
echo "🔍 Changed files:"
git diff --name-only
echo ""
echo "📊 Diff summary:"
git diff --stat
fi
- name: Commit and push changes
if: steps.check-changes.outputs.has_changes == 'true'
run: |
echo "💾 Committing formatting changes..."
# Configure git
git config user.name "Gitea Actions"
git config user.email "actions@gitea.local"
# Add all changed files
git add -A
# Create commit with detailed message
commit_message="🎨 Auto-format GDScript code
Automated formatting applied by gdformat workflow
📊 Summary:
- Total files processed: ${{ steps.format-files.outputs.total_files }}
- Successfully formatted: ${{ steps.format-files.outputs.formatted_files }}
- Files with changes: $(git diff --cached --name-only | wc -l)
🤖 Generated by Gitea Actions
Workflow: ${{ github.workflow }}
Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
git commit -m "$commit_message"
# Push changes back to the branch
target_branch="${{ steps.branch-info.outputs.target_branch }}"
echo "📤 Pushing changes to branch: $target_branch"
git push origin HEAD:"$target_branch"
echo "✅ Changes pushed successfully!"
- name: Summary comment (PR only)
if: github.event_name == 'pull_request'
uses: actions/github-script@v6
with:
script: |
const hasChanges = '${{ steps.check-changes.outputs.has_changes }}' === 'true';
const totalFiles = '${{ steps.format-files.outputs.total_files }}';
const formattedFiles = '${{ steps.format-files.outputs.formatted_files }}';
const failedFiles = '${{ steps.format-files.outputs.failed_files }}';
let message;
if (hasChanges) {
message = `🎨 **GDScript Auto-Formatting Complete**
✅ Code has been automatically formatted and pushed to this branch.
📊 **Summary:**
- Total files processed: ${totalFiles}
- Successfully formatted: ${formattedFiles}
- Files with changes applied: ${hasChanges ? 'Yes' : 'No'}
🔄 **Next Steps:**
The latest commit contains the formatted code. You may need to pull the changes locally.
[View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})`;
} else {
message = `🎨 **GDScript Formatting Check**
✅ All GDScript files are already properly formatted!
📊 **Summary:**
- Total files checked: ${totalFiles}
- Files needing formatting: 0
🎉 No changes needed - code style is consistent.
[View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})`;
}
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: message
});
- name: Upload formatting artifacts
if: failure()
uses: actions/upload-artifact@v3
with:
name: gdformat-results
path: |
**/*.gd
retention-days: 7
- name: Workflow completion status
run: |
echo "🎉 GDScript formatting workflow completed!"
echo ""
echo "📋 Final Status:"
if [[ "${{ steps.format-files.outputs.failed_files }}" != "0" ]]; then
echo "❌ Some files failed to format"
exit 1
elif [[ "${{ steps.check-changes.outputs.has_changes }}" == "true" ]]; then
echo "✅ Code formatted and changes pushed"
else
echo "✅ Code already properly formatted"
fi

147
.gitea/workflows/gdlint.yml Normal file
View File

@@ -0,0 +1,147 @@
name: GDScript Linting
on:
# Trigger on push to any branch
push:
branches: ['*']
paths:
- '**/*.gd'
- '.gdlintrc'
- '.gitea/workflows/gdlint.yml'
# Trigger on pull requests
pull_request:
branches: ['*']
paths:
- '**/*.gd'
- '.gdlintrc'
- '.gitea/workflows/gdlint.yml'
# Allow manual triggering
workflow_dispatch:
jobs:
gdlint:
name: GDScript Code Quality Check
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install --upgrade "setuptools<81"
pip install gdtoolkit==4
- name: Verify gdlint installation
run: |
gdlint --version
echo "✅ gdlint installed successfully"
- name: Count GDScript files
id: count-files
run: |
file_count=$(find . -name "*.gd" -not -path "./.git/*" | wc -l)
echo "file_count=$file_count" >> $GITHUB_OUTPUT
echo "📊 Found $file_count GDScript files to lint"
- name: Run GDScript linting
run: |
echo "🔍 Starting GDScript linting..."
echo "================================"
# Initialize counters
total_files=0
clean_files=0
warning_files=0
error_files=0
# Find all .gd files except TestHelper.gd (static var syntax incompatibility)
while IFS= read -r -d '' file; do
filename=$(basename "$file")
# Skip TestHelper.gd due to static var syntax incompatibility with gdlint
if [[ "$filename" == "TestHelper.gd" ]]; then
echo "⚠️ Skipping $file (static var syntax not supported by gdlint)"
((total_files++))
((clean_files++))
continue
fi
echo "🔍 Linting: $file"
((total_files++))
# Run gdlint and capture output
if output=$(gdlint "$file" 2>&1); then
if [[ -z "$output" ]]; then
echo "✅ Clean"
((clean_files++))
else
echo "⚠️ Warnings found:"
echo "$output"
((warning_files++))
fi
else
echo "❌ Errors found:"
echo "$output"
((error_files++))
fi
echo ""
done < <(find . -name "*.gd" -not -path "./.git/*" -print0)
# Print summary
echo "================================"
echo "📋 Linting Summary"
echo "================================"
echo "📊 Total files: $total_files"
echo "✅ Clean files: $clean_files"
echo "⚠️ Files with warnings: $warning_files"
echo "❌ Files with errors: $error_files"
echo ""
# Set exit code based on results
if [[ $error_files -gt 0 ]]; then
echo "❌ Linting FAILED - $error_files file(s) have errors"
echo "Please fix the errors above before merging"
exit 1
elif [[ $warning_files -gt 0 ]]; then
echo "⚠️ Linting PASSED with warnings - Consider fixing them"
echo "✅ No blocking errors found"
exit 0
else
echo "✅ All GDScript files passed linting!"
echo "🎉 Code quality check complete - ready for merge"
exit 0
fi
- name: Upload linting results
if: failure()
uses: actions/upload-artifact@v3
with:
name: gdlint-results
path: |
**/*.gd
retention-days: 7
- name: Comment on PR (if applicable)
if: github.event_name == 'pull_request' && failure()
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '❌ **GDScript Linting Failed**\n\nPlease check the workflow logs and fix the linting errors before merging.\n\n[View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})'
})

View File

@@ -33,8 +33,8 @@ UIConstants="*res://src/autoloads/UIConstants.gd"
[display]
window/size/viewport_width=1920
window/size/viewport_height=1080
window/size/viewport_width=1024
window/size/viewport_height=768
window/stretch/mode="canvas_items"
window/handheld/orientation=4

View File

@@ -3,7 +3,12 @@ extends Node2D
signal score_changed(points: int)
signal grid_state_loaded(grid_size: Vector2i, tile_types: int)
enum GameState { WAITING, SELECTING, SWAPPING, PROCESSING } # Waiting for player input # First tile selected # Animating tile swap # Processing matches and cascades
## Match-3 Game State Machine
## WAITING: Idle state, accepting player input for tile selection
## SELECTING: First tile selected, waiting for second tile to complete swap
## SWAPPING: Animation in progress, input blocked during tile swap
## PROCESSING: Detecting matches, clearing tiles, dropping new ones, checking cascades
enum GameState { WAITING, SELECTING, SWAPPING, PROCESSING }
var GRID_SIZE := Vector2i(8, 8)
var TILE_TYPES := 5
@@ -124,6 +129,7 @@ func _initialize_grid():
func _has_match_at(pos: Vector2i) -> bool:
"""Check if tile at position is part of a 3+ match horizontally or vertically"""
# Bounds and null checks
if not _is_valid_grid_position(pos):
return false
@@ -151,8 +157,8 @@ func _has_match_at(pos: Vector2i) -> bool:
return matches_vertical.size() >= 3
# Check for any matches on the board
func _check_for_matches() -> bool:
"""Scan entire grid to detect if any matches exist (used for cascade detection)"""
for y in range(GRID_SIZE.y):
for x in range(GRID_SIZE.x):
if _has_match_at(Vector2i(x, y)):
@@ -161,6 +167,12 @@ func _check_for_matches() -> bool:
func _get_match_line(start: Vector2i, dir: Vector2i) -> Array:
"""Find all consecutive matching tiles in a line from start position in given direction.
Returns array of tile nodes that form a continuous match.
Direction must be unit vector (1,0), (-1,0), (0,1), or (0,-1).
Searches bidirectionally from start position.
"""
# Validate input parameters
if not _is_valid_grid_position(start):
DebugManager.log_error(
@@ -185,13 +197,14 @@ func _get_match_line(start: Vector2i, dir: Vector2i) -> Array:
if not "tile_type" in start_tile:
return []
var line = [start_tile]
var type = start_tile.tile_type
var line = [start_tile] # Initialize with start tile
var type = start_tile.tile_type # Type to match against
# Check both directions with safety limits
for offset in [1, -1]:
# Check both directions from start position (bidirectional search)
for offset in [1, -1]: # offset 1 = forward direction, -1 = backward direction
var current = start + dir * offset
var steps = 0
# Safety limit prevents infinite loops in case of logic errors
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
@@ -200,17 +213,23 @@ func _get_match_line(start: Vector2i, dir: Vector2i) -> Array:
if not neighbor or not is_instance_valid(neighbor):
break
# Stop if tile type doesn't match (end of matching sequence)
if not "tile_type" in neighbor or neighbor.tile_type != type:
break
line.append(neighbor)
current += dir * offset
steps += 1
line.append(neighbor) # Add matching tile to sequence
current += dir * offset # Move to next position
steps += 1 # Increment safety counter
return line
func _clear_matches():
func _clear_matches() -> void:
"""Find and remove all match groups of 3+ tiles, calculating score and triggering effects.
Uses flood-fill approach to group connected matches, prevents double-counting tiles.
Handles tile removal, score calculation, and visual effects.
"""
# Check grid integrity
if not _validate_grid_integrity():
DebugManager.log_error("Grid integrity check failed in _clear_matches", "Match3")
@@ -693,6 +712,14 @@ func _select_tile_at_cursor() -> void:
func _on_tile_selected(tile: Node2D) -> void:
"""Handle tile selection with state machine logic for match-3 gameplay.
State transitions:
WAITING -> SELECTING: First tile selected
SELECTING -> WAITING: Same tile clicked (deselect)
SELECTING -> SWAPPING: Different tile clicked (attempt swap)
"""
# Block tile selection during busy states
if current_state == GameState.SWAPPING or current_state == GameState.PROCESSING:
DebugManager.log_debug(
"Tile selection ignored - game busy (state: %s)" % [GameState.keys()[current_state]],
@@ -736,7 +763,7 @@ func _on_tile_selected(tile: Node2D) -> void:
func _select_tile(tile: Node2D) -> void:
selected_tile = tile
tile.is_selected = true
current_state = GameState.SELECTING
current_state = GameState.SELECTING # State transition: WAITING -> SELECTING
DebugManager.log_debug(
"Selected tile at (%d, %d)" % [tile.grid_position.x, tile.grid_position.y], "Match3"
)
@@ -816,8 +843,8 @@ func _attempt_swap(tile1: Node2D, tile2: Node2D) -> void:
"Match3"
)
current_state = GameState.SWAPPING
await _swap_tiles(tile1, tile2)
current_state = GameState.SWAPPING # State transition: SELECTING -> SWAPPING
await _swap_tiles(tile1, tile2) # Animate tile swap
# Check if swap creates matches
if _has_match_at(tile1.grid_position) or _has_match_at(tile2.grid_position):

View File

@@ -1,3 +1,9 @@
## Debug Manager - Global Debug and Logging System
##
## Provides centralized debug functionality and structured logging for the Skelly project.
## Manages debug state, overlay visibility, and log levels with formatted output.
## Replaces direct print() and push_error() calls with structured logging system.
extends Node
signal debug_toggled(enabled: bool)
@@ -9,52 +15,63 @@ var debug_overlay_visible: bool = false
var current_log_level: LogLevel = LogLevel.INFO
func _ready():
func _ready() -> void:
"""Initialize the DebugManager on game startup"""
log_info("DebugManager loaded")
func toggle_debug():
func toggle_debug() -> void:
"""Toggle debug mode on/off and emit signal to connected systems"""
debug_enabled = !debug_enabled
debug_toggled.emit(debug_enabled)
log_info("Debug mode: " + ("ON" if debug_enabled else "OFF"))
func set_debug_enabled(enabled: bool):
func set_debug_enabled(enabled: bool) -> void:
"""Set debug mode to specific state without toggling"""
if debug_enabled != enabled:
debug_enabled = enabled
debug_toggled.emit(debug_enabled)
func is_debug_enabled() -> bool:
"""Check if debug mode is currently enabled"""
return debug_enabled
func toggle_overlay():
func toggle_overlay() -> void:
"""Toggle debug overlay visibility"""
debug_overlay_visible = !debug_overlay_visible
func set_overlay_visible(visible: bool):
func set_overlay_visible(visible: bool) -> void:
"""Set debug overlay visibility to specific state"""
debug_overlay_visible = visible
func is_overlay_visible() -> bool:
"""Check if debug overlay is currently visible"""
return debug_overlay_visible
func set_log_level(level: LogLevel):
func set_log_level(level: LogLevel) -> void:
"""Set minimum log level for output filtering"""
current_log_level = level
log_info("Log level set to: " + _log_level_to_string(level))
func get_log_level() -> LogLevel:
"""Get current minimum log level"""
return current_log_level
func _should_log(level: LogLevel) -> bool:
"""Determine if message should be logged based on current log level"""
return level >= current_log_level
func _log_level_to_string(level: LogLevel) -> String:
"""Convert LogLevel enum to string representation"""
match level:
LogLevel.TRACE:
return "TRACE"
@@ -73,47 +90,54 @@ func _log_level_to_string(level: LogLevel) -> String:
func _format_log_message(level: LogLevel, message: String, category: String = "") -> String:
"""Format log message with timestamp, level, category, and content"""
var timestamp = Time.get_datetime_string_from_system()
var level_str = _log_level_to_string(level)
var category_str = (" [" + category + "]") if category != "" else ""
return "[%s] %s%s: %s" % [timestamp, level_str, category_str, message]
func log_trace(message: String, category: String = ""):
func log_trace(message: String, category: String = "") -> void:
"""Log trace-level message (lowest priority, only shown in debug mode)"""
if _should_log(LogLevel.TRACE):
var formatted = _format_log_message(LogLevel.TRACE, message, category)
if debug_enabled:
print(formatted)
func log_debug(message: String, category: String = ""):
func log_debug(message: String, category: String = "") -> void:
"""Log debug-level message (development information, only shown in debug mode)"""
if _should_log(LogLevel.DEBUG):
var formatted = _format_log_message(LogLevel.DEBUG, message, category)
if debug_enabled:
print(formatted)
func log_info(message: String, category: String = ""):
func log_info(message: String, category: String = "") -> void:
"""Log info-level message (general information, always shown)"""
if _should_log(LogLevel.INFO):
var formatted = _format_log_message(LogLevel.INFO, message, category)
print(formatted)
func log_warn(message: String, category: String = ""):
func log_warn(message: String, category: String = "") -> void:
"""Log warning-level message (potential issues that don't break functionality)"""
if _should_log(LogLevel.WARN):
var formatted = _format_log_message(LogLevel.WARN, message, category)
print(formatted)
push_warning(formatted)
func log_error(message: String, category: String = ""):
func log_error(message: String, category: String = "") -> void:
"""Log error-level message (serious issues that may break functionality)"""
if _should_log(LogLevel.ERROR):
var formatted = _format_log_message(LogLevel.ERROR, message, category)
print(formatted)
push_error(formatted)
func log_fatal(message: String, category: String = ""):
func log_fatal(message: String, category: String = "") -> void:
"""Log fatal-level message (critical errors that prevent normal operation)"""
if _should_log(LogLevel.FATAL):
var formatted = _format_log_message(LogLevel.FATAL, message, category)
print(formatted)

View File

@@ -1,3 +1,9 @@
## Game Manager - Centralized Scene Transition System
##
## Manages all scene transitions with race condition protection and input validation.
## Provides safe scene switching for different gameplay modes with error handling.
## Never call get_tree().change_scene_to_file() directly - use GameManager methods.
extends Node
const GAME_SCENE_PATH := "res://scenes/game/game.tscn"
@@ -8,26 +14,31 @@ var is_changing_scene: bool = false
func start_new_game() -> void:
"""Start a new match-3 game with fresh save data"""
SaveManager.start_new_game()
start_game_with_mode("match3")
func continue_game() -> void:
"""Continue existing match-3 game with saved score intact"""
# Don't reset score
start_game_with_mode("match3")
func start_match3_game() -> void:
"""Start new match-3 gameplay mode"""
SaveManager.start_new_game()
start_game_with_mode("match3")
func start_clickomania_game() -> void:
"""Start new clickomania gameplay mode"""
SaveManager.start_new_game()
start_game_with_mode("clickomania")
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")
@@ -39,7 +50,7 @@ func start_game_with_mode(gameplay_mode: String) -> void:
)
return
# Prevent concurrent scene changes
# Prevent concurrent scene changes (race condition protection)
if is_changing_scene:
DebugManager.log_warn("Scene change already in progress, ignoring request", "GameManager")
return
@@ -72,7 +83,7 @@ func start_game_with_mode(gameplay_mode: String) -> void:
# Wait for scene instantiation and tree addition
await get_tree().process_frame
await get_tree().process_frame # Additional frame for complete initialization
await get_tree().process_frame # Additional frame ensures complete initialization
# Validate scene was loaded successfully
if not get_tree().current_scene:
@@ -80,7 +91,7 @@ func start_game_with_mode(gameplay_mode: String) -> void:
is_changing_scene = false
return
# Set gameplay mode with timeout protection
# Configure game scene with requested gameplay mode
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)
@@ -97,6 +108,7 @@ func start_game_with_mode(gameplay_mode: String) -> void:
func save_game() -> void:
"""Save current game state and score via SaveManager"""
# 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"):
@@ -107,6 +119,7 @@ func save_game() -> void:
func exit_to_main_menu() -> void:
"""Exit to main menu with race condition protection"""
# Prevent concurrent scene changes
if is_changing_scene:
DebugManager.log_warn(

View File

@@ -1,3 +1,9 @@
## Save Manager - Secure Game Data Persistence System
##
## Provides secure save/load functionality with tamper detection, race condition protection,
## and permissive validation. Features backup/restore, version migration, and data integrity.
## Implements multi-layered security: checksums, size limits, type validation, and bounds checking.
extends Node
const SAVE_FILE_PATH: String = "user://savegame.save"
@@ -8,7 +14,7 @@ const MAX_SCORE: int = 999999999
const MAX_GAMES_PLAYED: int = 100000
const MAX_FILE_SIZE: int = 1048576 # 1MB limit
# Save operation protection
# Save operation protection - prevents race conditions
var _save_in_progress: bool = false
var _restore_in_progress: bool = false
@@ -28,10 +34,12 @@ var game_data: Dictionary = {
func _ready() -> void:
"""Initialize SaveManager and load existing save data on startup"""
load_game()
func save_game() -> bool:
"""Save current game data with race condition protection and error handling"""
# Prevent concurrent saves
if _save_in_progress:
DebugManager.log_warn("Save already in progress, skipping", "SaveManager")
@@ -82,6 +90,7 @@ func _perform_save() -> bool:
func load_game() -> void:
"""Load game data from disk with comprehensive validation and error recovery"""
if not FileAccess.file_exists(SAVE_FILE_PATH):
DebugManager.log_info("No save file found, using defaults", "SaveManager")
return