276 lines
23 KiB
Markdown
276 lines
23 KiB
Markdown
# CLAUDE.md
|
||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
||
## Project Overview
|
||
|
||
**Aventures En Egipte (AEE)** — a retro-style 2D game written in C++ using SDL3. The game uses a software-rendered 8-bit paletted graphics engine (320x200, 256 colors), custom audio (JailAudio), and GIF-based assets. The codebase and commit messages are in Valencian/Catalan.
|
||
|
||
## Build
|
||
|
||
```bash
|
||
# Linux
|
||
cmake -B build
|
||
cmake --build build
|
||
|
||
# Windows (MinGW)
|
||
cmake -B build -G "MinGW Makefiles"
|
||
cmake --build build
|
||
```
|
||
|
||
Dependencies: SDL3. Uses CMake (minimum 3.10) with C++20. SPIR-V shaders compiled automatically if `glslc` is available; precompiled headers used as fallback.
|
||
|
||
The executable is output to the project root. The `data/` folder must be in the working directory at runtime.
|
||
|
||
## Architecture
|
||
|
||
### New Rules (Modernization Phase)
|
||
|
||
The old "Golden Rule: Do Not Touch Gameplay" has been **revoked**. The original C-style code (jail engine + gameplay modules) is now a **modernization target**, not a sacred zone. The parallel-overlay approach has reached its ceiling: fades and cinematics are still blocking loops, audio relies on an async `SDL_AddTimer`, and the emulator-style game thread blocking at `publishFrame` is incompatible with an emscripten port.
|
||
|
||
The five current objectives are:
|
||
|
||
1. **Idiomatic C++**: RAII, `std::vector`/`std::string`/`std::optional`, classes with real constructors/destructors. No more raw `malloc/free` in structs.
|
||
2. **Zero blocking events**: no `while (...) { poll; }`, no `SDL_Delay` inside gameplay, no `cv.wait()` in `publishFrame`. Every subsystem must be able to advance in a single tick call.
|
||
3. **Time-based**: animations, cinematics and fades measured in milliseconds, not frames. `JG_ShouldUpdate()` as gameplay gate is on its way out.
|
||
4. **Overlay integrated**: overlay stops being a post-game layer painted by Director — it becomes part of the same render pass the game tick produces.
|
||
5. **SDL3 callbacks**: main loop handed over to `SDL_AppInit` / `SDL_AppIterate` / `SDL_AppEvent` / `SDL_AppQuit`, single-threaded, compatible with emscripten.
|
||
|
||
**Iron rule: zero gameplay regressions.** Each phase of the modernization must leave the game playing identically — same feel, same timings, same collisions, same scoring. See [docs/scenes-migration-plan.md](docs/scenes-migration-plan.md) for the phased plan.
|
||
|
||
The current emulator-thread architecture (Director + game thread + `publishFrame` mutex/cv) is **transitional**. It will be dismantled in Phase 5 and replaced by a single-threaded `SDL_AppIterate` loop in Phase 7.
|
||
|
||
### Migration Status (2026-04-16)
|
||
|
||
Phases 0–7b of the original runtime plan are **done**. Current effort is the **scene-by-scene rewrite of `source/game/modulesequence.cpp`** over a `scenes::` layer in [source/scenes/](source/scenes/):
|
||
|
||
- **Done**: `MortScene` (state 100), `BannerScene` (2..5), `MenuScene` (0), `IntroNewLogoScene` (255 when `use_new_logo`), `SlidesScene` (1, 7), `CreditsScene` (8), `SecretaScene` (6). Each registered in `Director::init` via `SceneRegistry`. Each removed from the legacy `ModuleSequence::Go()` switch and deleted from `modulesequence.cpp`.
|
||
- **Pending**: `IntroScene` (state 255 when `!use_new_logo` — the old JAILGAMES letter-by-letter), `IntroSpritesScene` (the Sam + momies animation with 3 random variants, hardest of the lot, currently still called from `IntroNewLogoScene::Phase::Delegate` via a temporary `doIntroSprites` exposed as `public` in `ModuleSequence`). Final cleanup of `modulesequence.cpp` comes after those two.
|
||
- `SceneRegistry` lookup happens inside `gameFiberEntry()` before falling back to legacy `ModuleSequence::Go()`, with a redirect `num_piramide == 6 && diners < 200 → 7` replicated ahead of the lookup to match the legacy flow.
|
||
- For quick tests, `Options::game` exposes `piramide_inicial`, `habitacio_inicial`, `vides`, `diamants_inicial`, `diners_inicial`, `use_new_logo`, `show_title_credits` — all persisted in `config.yaml`.
|
||
|
||
The scenes layer itself lives in [source/scenes/](source/scenes/): `scene.hpp` (interface), `scene_registry.hpp/.cpp`, `timeline.hpp/.cpp`, `sprite_mover.hpp/.cpp`, `frame_animator.hpp/.cpp`, `palette_fade.hpp/.cpp`, `surface_handle.hpp/.cpp`, `scene_utils.hpp/.cpp` (`playMusic`). Scenes are pure tick-based (no fibers, no `while`, no `JG_ShouldUpdate`) — the cooperative fiber still runs underneath them but `JD8_Flip()` inside the mini-while in `gameFiberEntry` is what yields. Once `IntroScene` + `IntroSpritesScene` are migrated, the fiber can be dismantled along with `ModuleGame`.
|
||
|
||
### Modernization Targets
|
||
|
||
**Invariants to preserve** (touch these and you broke the game):
|
||
- Gameplay feel, movement speed, enemy AI behavior
|
||
- Collision detection, scoring, lives, level progression
|
||
- Visible animation cadence (once translated to ms, must look identical)
|
||
- Difficulty curves and cinematic timings
|
||
- Cheat codes (`reviu`, `alone`, `obert`) — currently broken, should be restored
|
||
- Original palettes, fades, music cues
|
||
|
||
**Free to change** (internal representation):
|
||
- Data structures (structs → classes with RAII)
|
||
- Ownership (raw pointers → `std::unique_ptr`/`std::vector`/`std::string`)
|
||
- Timing representation (frame counters → ms accumulators)
|
||
- Threading model (game thread → single-threaded state machine)
|
||
- Global state (the old `info::` namespace is now an `inline` singleton `info::ctx` of type `GameContext`; access is `info::ctx.X` instead of `info::X`. Can be reset with `info::ctx.reset()`)
|
||
- API shapes of jail subsystems (as long as callers are updated consistently)
|
||
|
||
### Boundary: Original vs New Code
|
||
|
||
| Path | Owner | Rule |
|
||
|------|-------|------|
|
||
| `source/core/jail/` | Legacy engine, modernization target | Free to modify with care — preserve external behavior |
|
||
| `source/game/*.cpp/hpp` | Legacy gameplay, modernization target | Free to modify with care — preserve gameplay invariants |
|
||
| `source/core/rendering/` | New presentation layer | Free to modify |
|
||
| `source/core/input/` | New input layer | Free to modify |
|
||
| `source/utils/` | New utilities | Free to modify |
|
||
| `source/game/options,defines,defaults` | New config system | Free to modify |
|
||
| `data/*.gif, *.ogg` | Original assets | **Do not modify** — assets remain untouchable |
|
||
| `data/fonts/, data/ui/, data/shaders/` | New assets | Free to modify |
|
||
|
||
### Legacy "Jail" Engine (`source/core/jail/`) — modernization target
|
||
|
||
Flat C-style APIs (no classes), prefixed by subsystem. Being progressively converted to idiomatic C++ (see Phase 1 of the plan). External API names are kept stable during the transition to avoid churning call sites.
|
||
|
||
- **JG** (`jgame`) — Game loop timing: init/finalize, fixed-timestep update via `JG_ShouldUpdate()`
|
||
- **JD8** (`jdraw8`) — 8-bit paletted software renderer. 320x200 screen buffer (`JD8_Surface` = `Uint8*`), palette-indexed blitting with color-key transparency, fade effects. `JD8_Flip()` converts palette→ARGB and delegates to `Screen::present()`
|
||
- **JA** (`jail_audio`) — Custom audio mixing using SDL3 audio streams (OGG via stb_vorbis, WAV)
|
||
- **JI** (`jinput`) — Input: keyboard state polling, key debouncing, cheat code detection. Filters GUI keys from game, calls `GlobalInputs::handle()` and `Mouse::updateCursorVisibility()` each update
|
||
- **JF** (`jfile`) — File I/O: filesystem folder mode (`data/`) or packed resource file (`.jrf`). Config folder at `~/.config/jailgames/aee/`
|
||
|
||
### System Layer (`source/core/system/`)
|
||
|
||
- **Director** (`director.hpp/cpp`) — **Orchestrator singleton**. Owns main thread. Launches game thread that runs `ModuleGame`/`ModuleSequence::Go()`. Emulator-style architecture: Director runs at ~60 FPS independently, polls SDL events, updates overlay, presents frames. Game thread blocks at `JD8_Flip()` → `Director::publishFrame()` until Director consumes the frame. Director is **non-blocking**: if no new frame is available, it re-presents the last known game frame with fresh overlay on top
|
||
|
||
### Presentation Layer (`source/core/rendering/`)
|
||
|
||
- **Screen** (`screen.hpp/cpp`) — Singleton. Manages SDL_Window, SDL_Renderer, SDL_Texture. Dual rendering path: SDL3GPU with shaders (primary) or SDL_Renderer fallback. Handles fullscreen, zoom, aspect ratio 4:3, integer scaling, VSync. Counts FPS and updates render info segments
|
||
- **Overlay** (`overlay.hpp/cpp`) — Paints directly on the ARGB pixel buffer before presentation. Handles notifications (slide-in animation), animated render info (4 independent segments with per-segment anim + vertical slide state machine), persistent PAUSA indicator, and double-ESC-to-quit logic
|
||
- **Text** (`text.hpp/cpp`) — Bitmap font renderer. Loads `.fnt` + `.gif` pairs, renders UTF-8 glyphs directly on `Uint32*` ARGB buffer. Supports `drawClipped(x, y, text, color, clip_xmin, clip_xmax, clip_ymin, clip_ymax)` for per-pixel 2D clipping (used by menu transitions)
|
||
- **Menu** (`menu.hpp/cpp`) — Floating options menu with stack-based page navigation (root → VIDEO/AUDIO/CONTROLS). Uses ItemKind enum: Toggle/Cycle/IntRange/Submenu/KeyBind. Features: vertical expand animation on open (outQuad), horizontal slide + height interpolation on page transitions (forward/backward direction), key capture mode for remapping. Callbacks delegate to `Screen::*` / `Overlay::*` / `Options::applyAudio()` to avoid duplication
|
||
- **SDL3GPUShader** (`sdl3gpu/`) — GPU shader backend (Vulkan/Metal). PostFX and CRT-Pi shaders with presets, supersampling (3×/6×/9×), Lanczos downscaling. Supports 4:3 aspect ratio stretch fused into the upscale pass to avoid artifacts
|
||
|
||
### Input Layer (`source/core/input/`)
|
||
|
||
- **GlobalInputs** (`global_inputs.hpp/cpp`) — Maps configurable function keys to presentation actions. Uses debounce. Returns whether a key was consumed (to suppress from game layer)
|
||
- **Mouse** (`mouse.hpp/cpp`) — Auto-hides cursor after 3 seconds of inactivity
|
||
- **Gamepad** (`gamepad.hpp/cpp`) — First-gamepad support with hot-plug. Poll-based each frame: D-pad/left stick (deadzone 12000) → virtual arrow keys for game movement; A/B buttons, Start, Back translate to synthetic SDL key events (F12/ESC/Enter/Backspace) when menu is open, so Director handles them exactly like keyboard. Loads extra mappings from `gamecontrollerdb.txt` (next to the executable) at init via `SDL_AddGamepadMappingsFromFile`, extending SDL's built-in controller database
|
||
- **KeyRemap** (`key_remap.hpp/cpp`) — Each frame, reads `Options::keys_game.*` and mirrors physical keyboard state to virtual standard scancodes (`SDL_SCANCODE_UP`/DOWN/LEFT/RIGHT). Allows full movement key remapping without touching hardcoded game code in `prota.cpp`/`mapa.cpp`
|
||
|
||
### Locale Layer (`source/core/locale/`)
|
||
|
||
- **Locale** (`locale.hpp/cpp`) — Flat key → string map loaded from YAML at boot. Keys use dot notation (`menu.items.zoom`, `notifications.pause`). Returns the key itself when missing (visible fallback for debugging). Strings live in [data/locale/ca.yaml](data/locale/ca.yaml) (Valencian, default). Designed for future multilanguage support
|
||
|
||
### Configuration System (`source/game/`)
|
||
|
||
Follows the pattern from `jaildoctors_dilemma`, persists to YAML:
|
||
|
||
- **defines.hpp** — Game constants: `Texts::WINDOW_TITLE`, `Texts::VERSION`, `GameScreen::WIDTH/HEIGHT`
|
||
- **defaults.hpp** — Default values: `Defaults::KeysGUI`, `Defaults::KeysGame`, `Defaults::Video`, `Defaults::Audio`, `Defaults::Window`, `Defaults::Game`
|
||
- **options.hpp/cpp** — `Options` namespace with inline globals and YAML load/save. Structs: `KeysGUI`, `KeysGame`, `Video`, `RenderInfo`, `Audio`, `Window`, `Game`, `PostFXPreset`, `CrtPiPreset`
|
||
|
||
### Utilities (`source/utils/`)
|
||
|
||
- **utils.hpp/cpp** — `toLower()` and other helpers
|
||
- **easing.hpp/cpp** — Easing functions for animations: `linear`, `outQuad`, `inQuad`, `inOutQuad`, `outCubic`, `inCubic`, `lerp`, `lerpInt`. Used by Menu transitions, render info slide, and segment animations
|
||
|
||
### Function Key Map
|
||
|
||
| Key | Action |
|
||
|-----|--------|
|
||
| F1 | Decrease window zoom |
|
||
| F2 | Increase window zoom |
|
||
| F3 | Toggle fullscreen |
|
||
| F4 | Toggle shaders on/off |
|
||
| F5 | Toggle aspect ratio (square pixels ↔ 4:3 CRT) |
|
||
| F6 | Toggle supersampling |
|
||
| F7 | Cycle shader type (PostFX ↔ CRT-Pi) |
|
||
| F8 | Cycle shader presets |
|
||
| F9 | Toggle stretch filter (nearest ↔ linear) |
|
||
| F10 | Cycle render info (off → top → bottom → off) |
|
||
| F11 | Toggle pause (Director stops resuming the game fiber + `JA_PauseMusic`/`JA_ResumeMusic`) |
|
||
| F12 | Toggle floating options menu |
|
||
| ESC | Double-press to quit (with overlay notification) / close menu if open |
|
||
| Backspace | Go up one menu level / close menu if at root |
|
||
| ↑↓←→ / Enter | Menu navigation |
|
||
|
||
All key bindings are configurable via `Options::keys_gui` and stored in `config.yaml` (section `controls:` with SDL scancode names). Game movement keys (`Options::keys_game.up/down/left/right`) can be remapped via the CONTROLS submenu — the `KeyRemap` module mirrors custom physical keys to virtual standard scancodes so hardcoded game code keeps working.
|
||
|
||
### Execution Model (Single-threaded Fibers)
|
||
|
||
Since Phase 4+5, the old game thread + `publishFrame` mutex/cv has been **removed**. The game code (`ModuleGame`, `ModuleSequence`, all their `Go()` methods with internal `while` loops) runs inside a **cooperative fiber** (see [fiber.hpp](source/core/system/fiber.hpp) / [fiber.cpp](source/core/system/fiber.cpp)). The whole process is single-threaded.
|
||
|
||
```
|
||
Main thread (only thread)
|
||
─────────────────────────
|
||
Director::run() loop {
|
||
SDL_PollEvent()
|
||
GlobalInputs, Mouse, KeyRemap
|
||
JA_Update() ← audio pump
|
||
if !paused:
|
||
GameFiber::resume() ← hands control to game code
|
||
↓ (runs until next JD8_Flip)
|
||
... game code runs ...
|
||
JD8_Flip():
|
||
palette → ARGB → pixel_data
|
||
GameFiber::yield() ← returns control to Director
|
||
↓
|
||
copy JD8_GetFramebuffer() → game_frame
|
||
memcpy game_frame → presentation_buffer
|
||
Overlay::render(presentation_buffer)
|
||
Screen::present(presentation_buffer)
|
||
SDL_Delay to hit 60fps
|
||
}
|
||
```
|
||
|
||
**Fiber backend** ([fiber.cpp](source/core/system/fiber.cpp)):
|
||
- **Linux / macOS**: `ucontext_t` + `makecontext`/`swapcontext` (deprecated in POSIX.1-2008 but still functional in glibc and macOS libc; warning silenced with `#pragma`).
|
||
- **Windows**: `ConvertThreadToFiber` / `CreateFiber` / `SwitchToFiber` (native Fibers API).
|
||
- **Emscripten**: not yet. Phase 7 will add an `emscripten_fiber_*` or Asyncify backend.
|
||
|
||
**Key points:**
|
||
- Single-threaded: zero `std::thread`, zero `std::mutex`, zero `std::condition_variable`.
|
||
- `JD8_Flip()` is the natural sync point: it calls `GameFiber::yield()` instead of the old blocking `publishFrame`.
|
||
- Pause (F11) works by Director skipping `resume()`: the fiber stays frozen at its last yield, and Director keeps repainting the last frame with fresh overlay.
|
||
- Double buffer still exists (`game_frame` + `presentation_buffer`) because Director can present multiple frames per game frame during pause or slow sections. Eliminating it is marginal work and the extra 256 KB copy is cheap at 320×200.
|
||
- The state machine alternating `ModuleSequence` (state=1) and `ModuleGame` (state=0) now lives in `gameFiberEntry()` inside an anonymous namespace in [director.cpp](source/core/system/director.cpp), called once as the fiber entry point.
|
||
- SDL events still processed only on the main thread (which is now the only thread anyway).
|
||
|
||
### Rendering Pipeline (inside Screen::present)
|
||
|
||
```
|
||
Screen::present(pixel_data):
|
||
1. FPS count + render info text update
|
||
2. IF GPU + shaders enabled:
|
||
- uploadPixels → scene_texture (320×200)
|
||
- [IF 4:3] stretch pass fused with upscale: scene → scaled_texture (W×factor, H×factor×1.2)
|
||
- [IF SS] upscale pass: scene → scaled_texture (W×factor, H×factor)
|
||
- PostFX or CRT-Pi shader → swapchain (with viewport letterboxing)
|
||
3. ELSE IF GPU without shaders:
|
||
- uploadPixels → clean render → swapchain
|
||
4. ELSE (fallback):
|
||
- SDL_UpdateTexture → SDL_RenderPresent
|
||
```
|
||
|
||
### Pixel Format
|
||
|
||
JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL texture uses `SDL_PIXELFORMAT_ABGR8888`. GPU textures use `SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM` (same byte layout on little-endian). Overlay colors are ABGR format.
|
||
|
||
### Persistence Files
|
||
|
||
| File | Content |
|
||
|------|---------|
|
||
| `~/.config/jailgames/aee/config.yaml` | Main config: video (incl. `vsync`, `integer_scale`), audio (incl. `enabled` master + `music_*` + `sound_*`), window, render_info (incl. `show_time`), game, shader selection, controls (movement keys + menu_toggle + pause_toggle) |
|
||
| `~/.config/jailgames/aee/postfx.yaml` | PostFX shader presets (6 defaults: CRT, NTSC, CURVED, SCANLINES, SUBTLE, CRT LIVE) |
|
||
| `~/.config/jailgames/aee/crtpi.yaml` | CRT-Pi shader presets (4 defaults: DEFAULT, CURVED, SHARP, MINIMAL) |
|
||
|
||
### External Libraries (`source/external/`)
|
||
|
||
- `gif.h` — Header-only GIF decoder. **Cannot be included from more than one .cpp** (no include guards on functions). Other files use `extern` declarations for `LoadGif()`
|
||
- `stb_vorbis.h` — stb single-header OGG decoder
|
||
- `fkyaml_node.hpp` — Header-only YAML parser (fkYAML v0.4.2)
|
||
|
||
### Data Assets (`data/`)
|
||
|
||
- `*.gif`, `*.ogg` — Original game assets (**do not modify**)
|
||
- `fonts/8bithud.fnt + .gif` — Bitmap font for overlay (8×8, 124 glyphs, UTF-8 with accents)
|
||
- `shaders/` — GLSL sources: `postfx.vert`, `postfx.frag`, `upscale.frag`, `downscale.frag`, `crtpi_frag.glsl`
|
||
- `locale/ca.yaml` — UI strings in Valencian (menu titles/items/values, notifications). Edit freely; reload at restart
|
||
- `ui/` — Reserved for future UI graphics
|
||
|
||
### Known Issues & Technical Debt
|
||
|
||
1. **gif.h cannot be included twice**: Functions are not `static` or `inline`, causing multiple definition errors. Text class uses `extern` forward declarations as workaround
|
||
2. **Cheats are broken (`reviu`, `alone`, `obert`)**: `JI_CheatActivated` in [jinput.cpp:46](source/core/jail/jinput.cpp#L46) compares `SDL_Scancode` values (e.g. `SDL_SCANCODE_R`=21) against ASCII chars (`'r'`=114). They never match. Regression from SDL3 migration. **Now fixable** — scheduled for Phase 1 of modernization since jail is no longer off-limits.
|
||
3. **No sound effects in game**: Game code never calls `JA_PlaySound*`/`JA_LoadSound` — only music via `JA_PlayMusic`/`JA_FadeOutMusic`. The SONS and VOL SONS menu items control volume of an empty channel pool. Infrastructure ready for future SFX.
|
||
4. **Raw `malloc`/`free` in gameplay structs**: `Sprite`/`Entitat` use `malloc` for `Frame[]` and `Animacio[]`; `jfile.cpp` uses a global `scratch[255]` buffer (UB under concurrent calls); `jail_audio.cpp` mixes `new`/`malloc`/`SDL_malloc`. Scheduled for Phase 1 (RAII pass).
|
||
5. **Blocking loops in cinematics and fades**: `ModuleSequence::doIntro()` has 15+ `while(!JG_ShouldUpdate())` spin-waits; `JD8_FadeOut`/`JD8_FadeToPal` run 32 internal iterations calling `JD8_Flip`. Incompatible with `SDL_AppIterate`. Scheduled for Phase 2 (state-machine refactor).
|
||
6. ~~**`SDL_AddTimer` in audio**~~: Fixed in Phase 3. `jail_audio` is now a header-only `inline` module (single `.cpp` stub only hosts the `stb_vorbis` implementation to avoid multiple definitions). Music uses true streaming via `stb_vorbis_open_memory` + `JA_PumpMusic` with a 0.5s low-water-mark instead of decoding the whole OGG into RAM. Mixing/fade update runs manually via `JA_Update()` called once per frame from `Director::run()`. Ported from the `jaildoctors_dilemma` codebase.
|
||
7. ~~**Game thread + `publishFrame` mutex/cv**~~: Fixed in Phase 4+5. Replaced by a cooperative `GameFiber` (ucontext on POSIX, Fibers API on Windows). `JD8_Flip()` calls `GameFiber::yield()`, Director calls `GameFiber::resume()` once per frame. Zero threads, zero mutexes. Emscripten fiber backend still pending for Phase 7.
|
||
|
||
### Pending / Ideas for Later
|
||
|
||
- **Gamepad**: map Y button (North) to `P` key for Pepe character selection at title screen (only input path not covered by current mapping).
|
||
- **Menu items for game**: habitacio_inicial, piramide_inicial, vides (already in `Options::game`, not exposed).
|
||
- **Multi-language**: add `data/locale/es.yaml` / `en.yaml` and a language selector item in menu. `Locale::load()` already handles arbitrary files.
|
||
- **Game keys remap**: currently only UP/DOWN/LEFT/RIGHT + `menu_toggle`. Could add remap for `pause_toggle`, `keys_game.exit` (needs care with ESC double-press flow).
|
||
- **Notification persistence**: notifications clear on each new one (`showNotification` does `notifications_.clear()`). Could queue instead.
|
||
- **FPS counter jitter**: time segment width changes per frame (100 Hz centi updates) causes ~1-2 px horizontal jitter in centered layout. Could lock to max-width or use monospace digits.
|
||
- **Notification messages partially hardcoded**: overlay/global_inputs/director now use Locale, but the window title (`Texts::WINDOW_TITLE`) and some game-layer strings remain hardcoded.
|
||
|
||
### Previously Fixed (kept for reference)
|
||
|
||
- **ESC double-press**: Fixed by intercepting KEY_DOWN in Director, setting atomic `esc_blocked_` immediately. `JI_KeyPressed(ESCAPE)` consults this flag. No race condition possible because Director's flag wins before game polls
|
||
- **Overlay freeze during intros**: Fixed by threading model. Director runs independently at 60 FPS regardless of game delays. Double buffer avoids overlay smearing on re-presented frames
|
||
- **ESC-closes-menu then closes game**: When menu closes via ESC, `esc_swallow_until_release_` flag blocks `JI_KeyPressed(ESC)` until physical key release. Cleared on ESC `KEY_UP`
|
||
- **Backspace-closes-menu skipping cinematics**: `menu_keys_held_[SDL_SCANCODE_COUNT]` array tracks scancodes consumed by menu on KEY_DOWN; matching KEY_UP is swallowed so game polling (`JI_AnyKey`) doesn't see them. Also covers F12, Backspace, cursor keys, capture-mode keys
|
||
- **Key remap not working after Backspace-close**: `JI_SetInputBlocked(false)` now also called when `Menu::handleKey` causes menu to close via Backspace (previously only cleared on ESC/F12 close paths)
|
||
|
||
### Virtual Keystates (OR'd sources)
|
||
|
||
`jinput.cpp` maintains `virtual_keystates[JI_VSRC_COUNT][SDL_SCANCODE_COUNT]` with two sources: `JI_VSRC_GAMEPAD` (from Gamepad::update) and `JI_VSRC_REMAP` (from KeyRemap::update). `JI_KeyPressed` returns true if either physical keystate OR any virtual source has the key set. `JI_SetInputBlocked` still overrides everything (menu open = input suppressed). `JI_SetVirtualKey(scancode, source, pressed)` is the write API — sources are independent so gamepad and keymap can't clobber each other.
|
||
|
||
### Variable FPS Cap
|
||
|
||
Director loop uses `FRAME_MS_VSYNC = 16` (60 FPS) or `FRAME_MS_NO_VSYNC = 4` (~250 FPS) depending on `Options::video.vsync`. Selected each iteration, so toggling VSync from the menu updates cap immediately. The GPU swapchain is also reconfigured via `shader_backend_->setVSync()` (IMMEDIATE/MAILBOX vs VSYNC present modes).
|
||
|
||
### Main Entry (`source/main.cpp`)
|
||
|
||
Init order: `file_setconfigfolder` → `Options::load` → `Locale::load("locale/ca.yaml")` → `Options::loadPostFX/CrtPi` → `JG_Init` → `Screen::init` → `JD8_Init` → `JA_Init` → `Options::applyAudio()` → `Overlay::init` → `Menu::init` → `Director::init` (also calls `Gamepad::init()`) → `Director::run()` (blocks until quit). Shutdown: `Options::save` → `Director::destroy` → `Menu::destroy` → `Overlay::destroy` → `JA_Quit` → `JD8_Quit` → `Screen::destroy` → `JG_Finalize`.
|
||
|
||
The state machine (alternating `ModuleSequence` state=1 and `ModuleGame` state=0) lives inside `gameFiberEntry()` in an anonymous namespace in [director.cpp](source/core/system/director.cpp), invoked as the entry of the cooperative fiber (single-threaded).
|