Files
skelly/docs/ARCHITECTURE.md
Vladimir nett00n Budylnikov 538459f323
Some checks failed
Continuous Integration / Code Formatting (push) Successful in 31s
Continuous Integration / Code Quality Check (push) Successful in 28s
Continuous Integration / Test Execution (push) Failing after 17s
Continuous Integration / CI Summary (push) Failing after 4s
codemap generation
2025-10-01 14:36:21 +04:00

12 KiB

System Architecture

High-level architecture guide for the Skelly project, explaining system design, architectural patterns, and design decisions.

Quick Links:

Overview

Skelly uses a service-oriented architecture with autoload managers providing global services, scene-based components implementing gameplay, and signal-based communication for loose coupling.

Key Architectural Principles:

  • Instance-based design: No global static state for testability
  • Signal-driven communication: Loose coupling between systems
  • Service layer pattern: Autoloads as singleton services
  • State machines: Explicit state management for complex flows
  • Defensive programming: Input validation, error recovery, fallback mechanisms

Autoload System

Autoloads provide singleton services accessible globally. Each has a specific responsibility.

GameManager

Responsibility: Scene transitions and game state coordination

Key Features:

  • Centralized scene loading with error handling
  • Race condition protection via is_changing_scene flag
  • Scene path constants for maintainability
  • Gameplay mode validation with whitelist
  • Never use get_tree().change_scene_to_file() directly

Usage:

# ✅ Correct
GameManager.start_game_with_mode("match3")

# ❌ Wrong
get_tree().change_scene_to_file("res://scenes/game/Game.tscn")

Files: src/autoloads/GameManager.gd

SaveManager

Responsibility: Save/load game state with security and data integrity

Key Features:

  • Tamper Detection: Deterministic checksums detect save file modification
  • Race Condition Protection: Save operation locking prevents concurrent conflicts
  • Permissive Validation: Auto-repair system fixes corrupted data
  • Type Safety: NaN/Infinity/bounds checking for numeric values
  • Memory Protection: File size limits prevent memory exhaustion
  • Version Migration: Backward-compatible save format upgrades
  • Error Recovery: Multi-layered backup and fallback systems

Security Model:

# Checksum validates data integrity
save_data["_checksum"] = _calculate_checksum(save_data)

# Auto-repair fixes corrupted fields
if not _validate_save_data(data):
    data = _repair_save_data(data)

# Race condition protection
if _is_saving:
    return  # Prevent concurrent saves

Testing: See TESTING.md for comprehensive test suites validating checksums, migration, and integration.

Files: src/autoloads/SaveManager.gd

SettingsManager

Responsibility: User settings persistence and validation

Key Features:

  • Input validation with NaN/Infinity checks
  • Bounds checking for numeric values
  • Security hardening against invalid inputs
  • Default fallback values
  • Type coercion with validation

Files: src/autoloads/SettingsManager.gd

DebugManager

Responsibility: Unified logging and debug UI coordination

Key Features:

  • Structured logging with log levels (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)
  • Category-based log organization
  • Global debug UI toggle (F12 key)
  • Log level filtering for development/production
  • Replaces all print() and push_error() calls

Usage: See CODE_OF_CONDUCT.md for logging best practices.

Files: src/autoloads/DebugManager.gd

AudioManager

Responsibility: Music and sound effect playback

Key Features:

  • Audio bus system (Music, SFX)
  • Volume control per bus
  • Loop configuration for music
  • UI click sounds

Files: src/autoloads/AudioManager.gd

LocalizationManager

Responsibility: Language switching and translation management

Key Features:

  • Multi-language support (English, Russian)
  • Runtime language switching
  • Translation file management

Files: src/autoloads/LocalizationManager.gd

Scene Management Pattern

All scene transitions flow through GameManager to ensure consistent error handling and state management.

Scene Loading Flow

User Action
    ↓
GameManager.start_game_with_mode(mode)
    ↓
Validate mode (whitelist check)
    ↓
Check is_changing_scene flag
    ↓
Load PackedScene with validation
    ↓
change_scene_to_packed()
    ↓
Reset is_changing_scene flag

Race Condition Prevention

var is_changing_scene: bool = false

func start_game_with_mode(gameplay_mode: String) -> void:
    # Prevent concurrent scene changes
    if is_changing_scene:
        DebugManager.log_warn("Scene change already in progress", "GameManager")
        return

    is_changing_scene = true
    # ... scene loading logic ...
    is_changing_scene = false

Why This Matters: Multiple rapid button clicks or input events could trigger concurrent scene loads, causing crashes or undefined state.

Modular Gameplay System

Game modes are implemented as separate gameplay modules in scenes/game/gameplays/.

Gameplay Architecture

Game.tscn (Main scene)
    ↓
    ├─> Match3Gameplay.tscn (Match-3 mode)
    ├─> ClickomaniaGameplay.tscn (Clickomania mode)
    └─> [Future gameplay modes]

Pattern: Each gameplay mode:

  • Extends Control or Node2D
  • Emits score_changed signal
  • Implements _ready() for initialization
  • Handles input independently
  • Includes optional debug menu

Files: scenes/game/gameplays/

Design Patterns Used

Instance-Based Architecture

Problem: Static variables prevent testing and create hidden dependencies.

Solution: Instance-based architecture with explicit dependencies.

# ❌ Bad: Static global state
static var current_gem_pool = [0, 1, 2, 3, 4]

# ✅ Good: Instance-based
var active_gem_types: Array = []

func set_active_gem_types(gem_indices: Array) -> void:
    active_gem_types = gem_indices.duplicate()

Benefits:

  • Each instance isolated for testing
  • No hidden global state
  • Explicit dependencies
  • Thread-safe by default

Signal-Based Communication

Pattern: Use signals for loose coupling between systems.

# Component emits signal
signal score_changed(new_score: int)

# Parent connects to signal
gameplay.score_changed.connect(_on_score_changed)

Benefits:

  • Loose coupling
  • Easy to test components in isolation
  • Clear event flow
  • Flexible subscription model

Service Layer Pattern

Autoloads act as singleton services providing global functionality.

Pattern:

  • Autoloads expose public API methods
  • Components call autoload methods
  • Autoloads emit signals for state changes
  • Never nest autoload calls deeply

State Machine Pattern

Complex workflows use explicit state machines.

Example: Match-3 tile swapping

WAITING → SELECTING → SWAPPING → PROCESSING → WAITING

Benefits:

  • Clear state transitions
  • Easy to debug
  • Prevents invalid state combinations
  • Self-documenting code

Critical Architecture Decisions

Memory Management: queue_free() Over free()

Decision: Always use queue_free() instead of free() for node cleanup.

Rationale:

  • free() causes immediate deletion (crashes if referenced)
  • queue_free() waits until safe deletion point
  • Prevents use-after-free bugs

Implementation:

# ✅ Correct
for child in children_to_remove:
    child.queue_free()
await get_tree().process_frame  # Wait for cleanup

# ❌ Dangerous
for child in children_to_remove:
    child.free()  # Can crash

Impact: Eliminated multiple potential crash points. See CODE_OF_CONDUCT.md for standards.

Race Condition Prevention: State Flags

Decision: Use state flags to prevent concurrent operations.

Rationale:

  • Async operations can overlap without protection
  • Multiple rapid inputs can trigger race conditions
  • State flags provide simple, effective protection

Implementation: Used in GameManager (scene loading), SaveManager (save operations), and gameplay systems (tile swapping).

Error Recovery: Fallback Mechanisms

Decision: Provide fallback behavior for all critical failures.

Rationale:

  • Games should degrade gracefully, not crash
  • User experience > strict validation
  • Log errors but continue operation

Examples:

  • Settings file corrupted? Load defaults
  • Scene load failed? Return to main menu
  • Audio file missing? Continue without sound

Input Validation: Whitelist Approach

Decision: Validate all user inputs against known-good values.

Rationale:

  • Security hardening
  • Prevent invalid state
  • Clear error messages
  • Self-documenting code

Implementation: Used in SettingsManager (volume, language), GameManager (gameplay modes), Match3Gameplay (grid movements).

System Interactions

Typical Game Flow

Main Menu
    ↓
    [GameManager.start_game_with_mode("match3")]
    ↓
Game Scene Loads
    ↓
Match3Gameplay initializes
    ↓
    ├─> SettingsManager (load difficulty)
    ├─> AudioManager (play background music)
    └─> DebugManager (setup debug UI)
    ↓
Gameplay Loop
    ↓
    ├─> Input handling
    ├─> Score updates (emit score_changed signal)
    ├─> SaveManager (autosave high score)
    └─> DebugManager (log important events)

Signal Flow Example: Score Change

Match3Gameplay detects match
    ↓
    [emit score_changed(new_score)]
    ↓
Game.gd receives signal
    ↓
Updates score display UI
    ↓
SaveManager.save_high_score(score)

Debug System Integration

User presses F12
    ↓
DebugManager.toggle_debug_ui()
    ↓
    [emit debug_ui_toggled(visible)]
    ↓
All debug menus receive signal
    ↓
Show/hide debug panels

Quality Improvements

The project implements high-quality code standards from the start:

Key Quality Features:

  • Memory Safety: Uses queue_free() pattern for safe node cleanup
  • Error Handling: Comprehensive error handling with fallbacks
  • Race Condition Protection: State flag protection for async operations
  • Instance-Based Architecture: No global static state for testability
  • Code Reuse: Base class architecture (DebugMenuBase) for common functionality
  • Input Validation: Complete validation coverage for all user inputs

These standards are enforced through the CODE_OF_CONDUCT.md quality checklist.

Best Practices

Extending the Architecture

When adding new features:

  1. New autoload?

    • Only if truly global and used by many systems
    • Consider dependency injection instead
    • Keep autoloads focused on single responsibility
  2. New gameplay mode?

    • Create in scenes/game/gameplays/
    • Extend appropriate base class
    • Emit score_changed signal
    • Add to GameManager mode whitelist
  3. New UI component?

    • Create in scenes/ui/components/
    • Follow UI_COMPONENTS.md patterns
    • Support keyboard/gamepad navigation
    • Emit signals for state changes

Architecture Review Checklist

Before committing architectural changes:

  • No new global static state introduced
  • All autoload access justified and documented
  • Signal-based communication used appropriately
  • Error handling with fallbacks implemented
  • Input validation in place
  • Memory management uses queue_free()
  • Race condition protection if async operations
  • Testing strategy defined