10 KiB
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
# 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
Golden Rule: Do Not Touch Gameplay
The original game logic (gameplay, entities, map, scoring, collisions, animations) must remain untouched. All modernization work targets the presentation layer and infrastructure only. Any new feature must be implemented as an overlay on top of the existing game, never by modifying original gameplay code.
Boundary: Original vs New Code
| Path | Owner | Rule |
|---|---|---|
source/core/jail/ |
Original engine | Do not modify gameplay behavior |
source/game/*.cpp/hpp (except options/defines/defaults) |
Original game | Do not modify |
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 |
data/fonts/, data/ui/, data/shaders/ |
New assets | Free to modify |
Original "Jail" Engine (source/core/jail/)
Flat C-style APIs (no classes), prefixed by subsystem. Do not touch gameplay logic.
- JG (
jgame) — Game loop timing: init/finalize, fixed-timestep update viaJG_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 toScreen::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, callsGlobalInputs::handle()andMouse::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 runsModuleGame/ModuleSequence::Go(). Emulator-style architecture: Director runs at ~60 FPS independently, polls SDL events, updates overlay, presents frames. Game thread blocks atJD8_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 text - Overlay (
overlay.hpp/cpp) — Paints directly on the ARGB pixel buffer before presentation. Handles notifications (slide-in animation), render info display (top/bottom/off, configurable colors), and double-ESC-to-quit logic - Text (
text.hpp/cpp) — Bitmap font renderer. Loads.fnt+.gifpairs, renders UTF-8 glyphs directly onUint32*ARGB buffer. No dependency on SDL_Texture or palettes - 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
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 —
Optionsnamespace 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
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) |
| ESC | Double-press to quit (with overlay notification) |
All key bindings are configurable via Options::keys_gui and stored in config.yaml.
Threading Model (Emulator Architecture)
Main thread (Director) Game thread (ModuleGame/Sequence::Go())
──────────────────── ────────────────────────────────────
loop at ~60 FPS { loop {
SDL_PollEvent() ... game logic ...
GlobalInputs, Mouse JD8_Flip():
if new_frame_available: palette→ARGB in pixel_data
copy to game_frame publishFrame(pixel_data) ⏸
signal → ────────────────────→ (blocks until Director consumes)
copy game_frame → present_buffer ←──── signal_consumed
Overlay::render(present_buffer) continue game loop
Screen::present(present_buffer) }
SDL_Delay to hit 60fps
}
Key points:
- Director is NON-BLOCKING: if no new frame, re-presents the last one with fresh overlay
- Double buffer:
game_frame(untouched copy from game) +presentation_buffer(regenerated with overlay each frame) - Game thread pauses at
JD8_Flip()waiting for Director — natural sync point - SDL events processed ONLY on main thread (SDL requirement)
JI_Update()no longer polls events — reads Director's state
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, audio, window, render_info, game, shader selection) |
~/.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 useexterndeclarations forLoadGif()stb_vorbis.h— stb single-header OGG decoderfkyaml_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.glslui/— Reserved for future UI graphics
Known Issues & Technical Debt
- gif.h cannot be included twice: Functions are not
staticorinline, causing multiple definition errors. Text class usesexternforward declarations as workaround
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
Main Entry (source/main.cpp)
Init order: file_setconfigfolder → Options::load → Options::loadPostFX/CrtPi → JG_Init → Screen::init → JD8_Init → JA_Init → Overlay::init → Director::init → Director::run() (blocks until quit). Shutdown: Options::save → Director::destroy → Overlay::destroy → JA_Quit → JD8_Quit → Screen::destroy → JG_Finalize.
The state machine (alternating ModuleSequence state=1 and ModuleGame state=0) now lives inside Director::gameThreadFunc(), running on the game thread.