Files
aee/docs/ARQUITECTURA.md

22 KiB
Raw Permalink Blame History

Arquitectura de Aventures en Egipte (AEE)

Guía de orientación para un desarrollador nuevo en el proyecto.

Cada afirmación está anclada a código real: se cita el fichero (y, cuando ayuda, la función o el número de línea) que la respalda. Donde no he encontrado algo, lo digo explícitamente en lugar de inventarlo.

Aventures en Egipte es un juego de aventura/plataformas retro (el protagonista "Sam" explora pirámides esquivando momias y recogiendo tesoros), escrito en C++ + SDL3. Resolución de juego 320×200. El rasgo que más condiciona el código es que está en plena migración desde un motor legacy propio ("Jail") hacia C++ moderno (ver §3). Los comentarios del código están en valenciano/español; este documento está en castellano.

Existe además un docs/scenes-migration-plan.md (histórico) sobre la migración del sistema de escenas; aquí se documenta el estado actual del código, no el plan.


Índice

  1. Visión general
  2. Punto de entrada y bucle principal
  3. El motor legacy "Jail"
  4. Escenas y máquina de estados
  5. Renderizado: de la lógica al píxel
  6. Entrada
  7. Gameplay: ModuleGame y entidades
  8. Sistema de escenas cinemáticas
  9. Recursos
  10. Audio y localización
  11. Configuración y persistencia
  12. Modo demo / IA
  13. Convenciones y patrones recurrentes
  14. Guía de navegación: "si quieres tocar X, mira Y"

1. Visión general

El árbol source/ separa motor y juego, pero con una particularidad: el motor tiene dos generaciones conviviendo (legacy "Jail" + capas nuevas).

  • source/core/jail/ — el motor legacy "Jail" (APIs C planas): Jg (jgame), Jd8 (jdraw8), Ji (jinput), Jf (jfile). En modernización (§3).
  • source/core/ (resto) — capas nuevas: rendering (Screen, Overlay, Text, Menu, sdl3gpu), input, system (Director), resources, audio, locale.
  • source/game/ — el juego: gameplay legacy en la raíz (modulegame, prota, momia, engendro, bola, mapa, marcador, info, sprite) y el sistema de escenas nuevo en scenes/.
  • source/utils/easing, utils.
  • source/external/ — vendorizado (stb, fkyaml).

~117 ficheros C++, ~40.000 líneas.

El CLAUDE.md define una frontera explícita "Original vs New Code" (§79): core/jail/ y game/*.cpp son legacy en modernización (modificar con cuidado, preservando comportamiento); core/rendering, core/input, utils/, options/defines/defaults son código nuevo. Interiorizar esa frontera es lo primero para no romper invariantes del juego original.

Ideas-fuerza:

  1. Un solo hilo, tick-based, vía callbacks de SDL3 (§2).
  2. El render es software paletizado 8-bit (Jd8, 320×200) que al final se convierte a ARGB y pasa por shaders GPU (§5).
  3. El flujo de pantallas es una máquina de estados apoyada en Info::ctx.num_piramide, con un SceneRegistry que va sustituyendo cinemáticas legacy por escenas nuevas (§4).
graph TD
    SDL[SDL3 callbacks · main.cpp] --> DIR[Director]
    DIR -->|game_state_ + Info::ctx| DISP{createNextScene}
    DISP -->|==0| MG["ModuleGame (gameplay)"]
    DISP -->|==1| REG["SceneRegistry::tryCreate(num_piramide)"]
    REG --> CINE["intro / banner / slides / mort / secreta / credits…"]
    MG --> MAPA[Mapa] & PROTA[Prota] & MOMIA[Momia] & BOLA[Bola] & MARC[Marcador]
    DIR --> INP["KeyConfig / GlobalInputs / Gamepad / KeyRemap / Mouse"]
    MG -->|dibuja índices| JD8["Jd8 (8-bit 320×200)"]
    CINE --> JD8
    JD8 -->|JD8_Flip → ARGB| SCREEN[Screen]
    OVL[Overlay] --> SCREEN
    SCREEN --> GPU["sdl3gpu PostFX / CrtPi / upscale"] --> WIN[Ventana]
    RES["Resource::Cache / List"] -.-> MG & CINE

2. Punto de entrada y bucle principal

2.1. SDL conduce el bucle (callbacks)

source/main.cpp define SDL_MAIN_USE_CALLBACKS. No hay while propio (clave para el port a Emscripten). SDL_AppInit monta todo y SDL_AppIterate llama a Director::iterate().

El arranque en SDL_AppInit inicializa, en orden: carpeta de config (Jf), sistema de recursos (resources.pack + fallback en Debug/WASM), Options, KeyConfig, Locale, presets de shaders, y luego el motor: Jg::init, Screen::init, Jd8::init, Audio::init, Overlay::init, Menu::init, Resource::List/Cache (con beginLoad()), y Director::init + setup().

2.2. El Director

source/core/system/director.{hpp,cpp}. Es el orquestador singleton, único hilo del runtime (sin fibers, mutex ni condition variables; el comentario de director.hpp:11 lo subraya). Posee el estado de escena como miembros: current_scene_ (std::unique_ptr<Scenes::Scene>), game_state_, last_tick_ms_, y dos buffers de frame [320*200] (game_frame_, presentation_buffer_).

Cada iterate() (modelo documentado en CLAUDE.md:161):

Gamepad/KeyRemap/GlobalInputs/Mouse::update
JA_Update()                                   ← bombeo de audio
if (!paused_) {
  if (scene && (scene->done() || JG_Quitting()))  game_state_ = scene->nextState(); scene.reset();
  if (!scene)  scene = createNextScene(); scene->onEnter();   ← ModuleGame o SceneRegistry
  JI_Update()
  scene->tick(now - last_tick_ms_)
  JD8_Flip()                                  ← índices → ARGB
  memcpy → game_frame
}
memcpy game_frame → presentation_buffer
Overlay::render(presentation_buffer)
Screen::present(presentation_buffer)
SDL_Delay(frame_target - elapsed)             ← cap de FPS

2.3. Tiempo, pausa y salida

Las escenas reciben delta_ms real (time-based). El gameplay (ModuleGame) gatea su Update() a 10 ms fijos vía Jg::shouldUpdate() (ticker del motor legacy, ya sin yield). La pausa (F11) simplemente salta el bloque de tick(): el overlay y el present siguen, re-presentando el último frame congelado. La salida se solicita con requestQuit() (doble ESC o SDL_QUIT) y requestRestart() hace un reinicio suave (para audio, resetea Info::ctx, vuelve a la intro).


3. El motor legacy "Jail"

source/core/jail/ es el sustrato heredado, APIs C planas con prefijo por subsistema (sin clases), que se está convirtiendo progresivamente a C++ idiomático manteniendo los nombres externos estables para no romper los call sites (CLAUDE.md:92). Es el rasgo más distintivo del proyecto.

Subsistema Fichero Qué hace
Jg (jgame) jail/jgame.* Timing: init/finalize, fixed-timestep (Jg::shouldUpdate() a 10 ms), flag de salida (JG_Quitting).
Jd8 (jdraw8) jail/jdraw8.* Renderer software 8-bit paletizado, buffer de pantalla 320×200 (Jd8::Surface = Uint8*), blits con color key, fades no bloqueantes (máquina de estados), flip() (paleta→ARGB) → Screen::present.
Ji (jinput) jail/jinput.* Sondeo de teclado, debounce, códigos cheat. Filtra las teclas de GUI del juego y llama a GlobalInputs/Mouse cada update.
Jf (jfile) jail/jfile.* I/O de ficheros: carpeta data/ o pack; carpeta de config en ~/.config/jailgames/aee/.

Jd8 en detalle (el corazón del render)

jail/jdraw8.hpp. API representativa:

  • using Surface = Uint8* (búfer de índices de paleta), using Palette = Color*.
  • loadSurface/newSurface/freeSurface, loadPalette/setScreenPalette.
  • Blits: blit, blitCK (con color key/transparencia), blitCKCut, blitCKScroll, blitToSurface/blitCKToSurface (blit entre surfaces).
  • fillRect/fillSquare/putPixel/getPixel.
  • Fade no bloqueante (fadeStart* + fadeTickStep que devuelve true al acabar): sustituye los JD8_FadeOut bloqueantes del código original.
  • flip() convierte el buffer indexado + paleta a ARGB y delega en Screen::present; getFramebuffer() devuelve el Uint32* resultante.

Mentalidad clave: todo el dibujo del juego ocurre sobre índices de 8 bits en un buffer 320×200; el color real y los shaders entran solo al final.


4. Escenas y máquina de estados

4.1. La base Scene

source/game/scenes/scene.hpp (namespace Scenes):

class Scene {
   public:
    virtual void onEnter() {}                 // una vez, antes del primer tick
    virtual void tick(int delta_ms) = 0;      // no bloquea, NO llama a Jd8::flip
    virtual auto done() const -> bool = 0;
    virtual auto nextState() const -> int { return 1; }  // 1=siguiente, 0=gameplay, -1=salir
};

El Director hace avanzar la escena hasta que done() es cierto, consulta nextState() y construye la siguiente.

4.2. El despachador: game_state_ + Info::ctx

Director::createNextScene() decide la siguiente escena:

  • game_state_ == 0new ModuleGame (gameplay puro, §7).
  • game_state_ == 1SceneRegistry::tryCreate(Info::ctx.num_piramide) (game/scenes/scene_registry.hpp): un mapa int → factory que devuelve la escena registrada para ese estado, o nullptr para caer al path legacy. Replica el viejo ModuleSequence::Go, incluido el redirect heredado num_piramide == 6 && diners < 200 → 7 (CLAUDE.md:104).
  • game_state_ == -1 → salir.

4.3. Info::ctx: el estado del juego

source/game/info.hppInfo::GameContext ctx (singleton inline) es la fuente de verdad del estado:

struct GameContext {
    int num_piramide, num_habitacio, diners, diamants, vida, momies, engendros;
    bool nou_personatge, pepe_activat;
    void reset();
};

num_piramide es la pieza central: hace de selector tanto de cinemáticas como de nivel jugable. El SceneRegistry va migrando cada estado de cinemática a una Scene nueva; lo que no esté registrado cae a legacy. Por eso el registro "crece" a medida que avanza la modernización.

graph LR
    SC[Scene activa] -->|done()| NS{nextState}
    NS -->|0| MG[ModuleGame]
    NS -->|1| REG["SceneRegistry(num_piramide)"]
    NS -->|-1| QUIT[salir]
    MG -->|muta Info::ctx| REG
    REG --> SC

5. Renderizado: de la lógica al píxel

El pipeline tiene dos mitades: software paletizado (Jd8) y GPU (Screen + shaders).

5.1. Software: dibujar índices

Escenas y ModuleGame dibujan sobre el buffer screen de Jd8 (índices 8-bit, 320×200) con blits y color key. JD8_Flip() aplica la paleta y produce un buffer ARGB (formato ABGR8888: 0xFF000000 + R + (G<<8) + (B<<16); CLAUDE.md:220).

5.2. Overlay: sobre el buffer ARGB

source/core/rendering/overlay.* pinta directamente sobre el buffer ARGB antes de presentar: notificaciones (slide-in), info de render animada (4 segmentos), indicador de PAUSA y la lógica de doble-ESC para salir.

5.3. Screen: a la ventana (GPU o fallback)

source/core/rendering/screen.* (singleton). Doble camino (Screen::present, CLAUDE.md:204):

  • GPU con shaders (primario, sdl3gpu/): sube los píxeles a una textura de escena 320×200; opcionalmente upscale/supersampling (3×/6×/9× con downscale Lanczos) y estiramiento 4:3 fusionado en el upscale; luego el shader PostFX o CRT-Pi (con presets) a la swapchain con letterboxing.
  • GPU sin shaders: subida limpia a la swapchain.
  • Fallback: SDL_UpdateTexture + SDL_RenderPresent (SDL_Renderer).

Screen gestiona ventana, fullscreen, zoom, 4:3, integer scaling, VSync, FPS. Apoyos de UI: Text (fuentes bitmap .fnt+.gif sobre el buffer ARGB, con clipping 2D) y Menu (menú flotante de opciones con navegación por páginas, animaciones y captura de teclas para remapping).


6. Entrada

Toda la entrada de UI/sistema converge en KeyConfig como fuente única de verdad de las teclas (source/core/input/key_config.*): carga data/input/keys.yaml (F1F10 GlobalInputs + F11 pausa + F12 menú) y aplica overrides del usuario; expone scancode("id"), isGuiKey(sc) (para que el Director no propague teclas de UI al juego), y persiste solo lo que difiere del default.

  • GlobalInputs (global_inputs.*) — mapea las F-keys a acciones de presentación (zoom, fullscreen, shaders, 4:3, supersampling, filtro, render info, pausa, menú). Tabla completa en CLAUDE.md:139.
  • Gamepad (gamepad.*) — primer mando con hot-plug y notificación; D-pad/stick → flechas virtuales; botones frontales → Enter sintético; SELECT → menú, START → pausa. Carga gamecontrollerdb.txt.
  • KeyRemap (key_remap.*) — cada frame copia Options::keys_game.* al estado virtual de scancodes estándar (UP/DOWN/LEFT/RIGHT). Permite remapear el movimiento sin tocar el código legacy de prota.cpp/mapa.cpp.
  • Mouse (mouse.*) — auto-oculta el cursor tras 3 s de inactividad.

Reparto deliberado: las teclas de UI/sistema viven en KeyConfig (keys.yaml); las de movimiento del jugador en Options::keys_game (config.yaml, sección controls:).


7. Gameplay: ModuleGame y entidades

source/game/modulegame.{hpp,cpp} es la escena de gameplay puro (game_state_ == 0). Hereda de Scenes::Scene y sustituye el viejo Go() bloqueante por un tick(). Tres fases internas (Phase): FADING_IN → PLAYING → FADING_OUT → DONE (modulegame.hpp:46), con un PaletteFade para los fundidos no bloqueantes.

Coordina las entidades del nivel, todas con una Jd8::Surface gfx_ compartida:

  • Mapa (game/mapa.*) — la sala/pirámide: tiles con contenido (CONTE_RES/CONTE_TRESOR/CONTE_FARAO…), colisión y dibujo del escenario.
  • Prota (game/prota.*) — el protagonista "Sam" (hereda de Sprite), movimiento y colisión.
  • Momia (game/momia.*) — enemigo que persigue al Prota (recibe Prota*); variante dimoni (demonio).
  • Bola (game/bola.*) — bola/roca que interactúa con el Prota.
  • Engendro (game/engendro.*) — entidad generada (su update() devuelve bool); Info::ctx.engendros lleva la cuenta.
  • Marcador (game/marcador.*) — el marcador (vidas, dinero, diamantes), lee del Prota/Info::ctx.

ModuleGame::tick() hace draw() (a screen, sin flip — lo hace el Director) y update() (gateado por Jg::shouldUpdate). Cuando la partida acaba (final_ != 0: muerte o cambio de sala), applyFinalTransitions() muta Info::ctx y nextState() devuelve el estado siguiente.

Todas las entidades de gameplay derivan de Sprite (game/sprite.*), clase base con draw()/update() sobre Jd8.


8. Sistema de escenas cinemáticas

Las pantallas no jugables (game_state_ == 1) son Scenes registradas en el SceneRegistry. Las concretas viven en source/game/scenes/: boot_loader (carga incremental, §9), banner, intro, intro_new_logo, intro_sprites, slides, menu (menu_scene), mort (secuencia de muerte), secreta (pantalla secreta) y credits.

Sobre ellas hay un conjunto de utilidades de animación reutilizables —el andamiaje "cinematográfico" del juego—:

  • Timeline (scenes/timeline.*) — secuenciación temporal de eventos.
  • FrameAnimator (scenes/frame_animator.*) — animación por fotogramas.
  • SpriteMover (scenes/sprite_mover.*) — movimiento interpolado de sprites (con easing).
  • PaletteFade (scenes/palette_fade.*) — fundidos por paleta no bloqueantes (también usado por ModuleGame).
  • SurfaceHandle (scenes/surface_handle.*) — propiedad RAII de una Jd8::Surface (las escenas poseen sus assets y los liberan en el destructor).
  • scene_utils — helpers compartidos.

9. Recursos

  • Resource::List (core/resources/resource_list.*) — registro de rutas de asset desde data/config/assets.yaml, con consulta O(1).
  • Resource::Cache (core/resources/resource_cache.*) — caché de assets con carga incremental: beginLoad() (en SDL_AppInit) + la escena BootLoaderScene que el Director arranca automáticamente mientras Resource::Cache::isLoadDone() sea falso (una barra de progreso con la ventana viva).
  • Pack y fallback: resource_pack.* + resource_helper.* sirven desde resources.pack (formato propio "AEE1", CLAUDE.md:237); en release nativo es estricto (solo pack), en Debug/WASM hay fallback a data/.
  • Formatos: GIF/paletas para gráficos (vía Jd8), fuentes .fnt+.gif, OGG/WAV para audio, GLSL para shaders.

10. Audio y localización

  • Audio: core/audio/audio.* (singleton, aplica Options::audio) sobre jail_audio (Ja, jail/jail_audio.hpp / core/audio/jail_audio.hpp), mezcla propia con streams SDL3 (OGG vía stb_vorbis, WAV). JA_Update() se bombea cada frame desde el Director; pausa/resume con F11.
  • Localización: core/locale/locale.* — mapa plano clave→cadena cargado de YAML (data/locale/ca.yaml, valenciano por defecto). Claves con notación de puntos (menu.items.zoom); si falta una clave, devuelve la propia clave (fallback visible para depurar).

11. Configuración y persistencia

  • Options (source/game/options.*) — namespace con globals inline y carga/guardado YAML. Structs: KeysGame (movimiento), Video, RenderInfo, Audio, Window, Game, PostFXPreset, CrtPiPreset.
  • defines.hpp — constantes del juego: Texts::WINDOW_TITLE/VERSION, GameScreen::WIDTH=320/HEIGHT=200/BUFFER_SIZE=64000.
  • defaults.hpp — valores por defecto (Defaults::KeysGame, Video, Audio, Window, Game).
  • KeyConfig — teclas de UI (§6).

Ficheros persistentes en ~/.config/jailgames/aee/ (CLAUDE.md:224): config.yaml (vídeo/audio/ventana/render_info/controles), keys.yaml (overrides de UI), postfx.yaml (6 presets), crtpi.yaml (4 presets), y en Debug debug.yaml (estado inicial de gameplay para pruebas).

Builds condicionales: NDEBUG (release: pack estricto, sin debug.yaml), __EMSCRIPTEN__ (WASM: fuerza ventana/zoom/4:3, mantiene fallback de assets, sin persistencia MEMFS).


12. Modo demo / IA

No he encontrado un modo demo, attract mode ni IA de demostración en este proyecto. Lo he buscado explícitamente: la única aparición de "replay" es un comentario en jail_audio.hpp sobre re-reproducción de pistas de audio, sin relación con un modo demo.

A diferencia de los proyectos hermanos coffee_crisis (playback de input grabado) o jaildoctors_dilemma (tour de habitaciones), aquí el flujo de atracción —si lo hubiera— se construiría sobre el SceneRegistry y las escenas cinemáticas (§8), pero hoy no existe tal cosa implementada.


13. Convenciones y patrones recurrentes

  • Frontera legacy/nuevo (CLAUDE.md:79): lo más importante. core/jail/ y game/*.cpp son legacy en modernización (preservar comportamiento); el resto es código nuevo. Los assets de data/gfx/data/music son intocables.
  • Namespaces legacy abreviados Jg/Jd8/Ji/Jf/Ja (APIs C planas), con nombres externos estables durante la transición.
  • Single-thread, tick-based: sin fibers/mutex; las escenas no bloquean ni llaman a Jd8::flip (lo hace el Director).
  • Render paletizado 8-bit sobre buffer 320×200; color y shaders solo al final (JD8_FlipScreen).
  • Singletons con init()/destroy()/get() (Screen, Audio, Director, Menu, Overlay, Resource::*, Jd8/Jg como módulos), creados/destruidos en orden explícito en SDL_AppInit/SDL_AppQuit.
  • Máquina de estados por Info::ctx.num_piramide + SceneRegistry que migra cinemáticas legacy a Scenes nuevas de forma incremental.
  • Fades y timing no bloqueantes: lo que antes eran bucles bloqueantes (JD8_FadeOut) ahora son máquinas de estados que avanzan un paso por tick.
  • Comentarios en valenciano/español; muchos #include con comentario de justificación (estilo IWYU).

14. Guía de navegación: "si quieres tocar X, mira Y"

Quiero… Empieza por…
Entender el arranque source/main.cpp (SDL_AppInit) + core/system/director.cpp
El bucle / orden del frame Director::iterate() (director.cpp)
Tocar el motor de dibujo core/jail/jdraw8.* (Jd8)
Timing / fixed-timestep core/jail/jgame.* (Jg::shouldUpdate)
Añadir/editar una pantalla game/scenes/ (hereda de Scenes::Scene) + SceneRegistry::registerScene
Cómo se decide la siguiente pantalla Director::createNextScene + Info::ctx.num_piramide + scene_registry.*
El estado del juego game/info.hpp (Info::ctx)
Gameplay game/modulegame.* (fases) + entidades prota/momia/bola/engendro/mapa/marcador
Animaciones de cinemáticas game/scenes/{timeline,frame_animator,sprite_mover,palette_fade,surface_handle}.*
Composición final / shaders core/rendering/screen.* + sdl3gpu/ + presets postfx.yaml/crtpi.yaml
Overlays (notificaciones/PAUSA/info) core/rendering/overlay.*
Texto / menú core/rendering/text.*, core/rendering/menu.*
Teclas de UI / F1F12 core/input/key_config.* + global_inputs.* + data/input/keys.yaml
Remapear movimiento core/input/key_remap.* + Options::keys_game
Mando core/input/gamepad.*
Cargar recursos / barra de carga core/resources/resource_cache.* + resource_list.* + scenes/boot_loader_scene.*
Audio core/audio/audio.* + jail_audio.hpp (Ja)
Idiomas core/locale/locale.* + data/locale/
Opciones / constantes game/options.*, game/defines.hpp, game/defaults.hpp
Carpetas y pack core/jail/jfile.* (Jf) + core/resources/resource_pack.*

Documento generado a partir de la lectura directa del código en el commit actual de la rama main. Si algo aquí no cuadra con el código, el código manda: actualiza este documento.