diff --git a/.claude/animation.md b/.claude/animation.md new file mode 100644 index 00000000..f1e745d9 --- /dev/null +++ b/.claude/animation.md @@ -0,0 +1,74 @@ +# Animation System + +Source: `src/dusk/animation/` + +Lightweight keyframe-based value interpolation using fixed-point math throughout. + +--- + +## Easing (`animation/easing.h`) + +`easingApply(type, t)` applies an easing function to a normalized time value `t ∈ [0, FIXED_ONE]` and returns the eased value in the same range. + +All functions are also callable directly: + +```c +fixed_t t = FIXED(0.5f); +fixed_t out = easingApply(EASING_IN_OUT_CUBIC, t); +``` + +Available easing types (all in `easingtype_t`): + +| Enum value | Curve | +|---|---| +| `EASING_LINEAR` | straight line | +| `EASING_IN_SINE` / `OUT` / `IN_OUT` | sinusoidal | +| `EASING_IN_QUAD` / `OUT` / `IN_OUT` | quadratic | +| `EASING_IN_CUBIC` / `OUT` / `IN_OUT` | cubic | +| `EASING_IN_QUART` / `OUT` / `IN_OUT` | quartic | +| `EASING_IN_BACK` / `OUT` / `IN_OUT` | overshoots slightly | + +`EASING_FUNCTIONS[EASING_COUNT]` is a table of `easingfn_t` function pointers for when you need to pick an easing at runtime without a switch. + +--- + +## Keyframes (`animation/keyframe.h`) + +```c +typedef struct { + fixed_t time; // time point this keyframe is at + fixed_t value; // output value at this time + easingtype_t easing; // easing to apply when interpolating toward the NEXT keyframe +} keyframe_t; +``` + +Keyframe arrays should be sorted ascending by `time`. + +--- + +## Animation (`animation/animation.h`) + +`animation_t` wraps a keyframe array and provides value lookup: + +```c +keyframe_t frames[] = { + { FIXED(0.0f), FIXED(0.0f), EASING_LINEAR }, + { FIXED(1.0f), FIXED(1.0f), EASING_IN_OUT_CUBIC }, + { FIXED(2.0f), FIXED(0.0f), EASING_LINEAR }, +}; + +animation_t anim; +animationInit(&anim, frames, 3); + +fixed_t value = animationGetValue(&anim, FIXED(0.75f)); // interpolated +``` + +`animationGetValue` finds the surrounding keyframes for the given `time`, computes the local `t` within that segment, applies the keyframe's easing, and linearly interpolates between the two keyframe values. + +The animation does not own the keyframe array — it holds a pointer. Pass a static or long-lived array. + +--- + +## Usage in the engine + +Entity animations (`entityanim_t`) do NOT use this system — they use a simple countdown timer (`animTime`) and a state enum. The `animation_t` system is intended for property animation: UI transitions, camera easing, visual effects, anything that needs a time → value curve. diff --git a/.claude/architecture.md b/.claude/architecture.md new file mode 100644 index 00000000..132041db --- /dev/null +++ b/.claude/architecture.md @@ -0,0 +1,81 @@ +# Architecture + +## Platform abstraction + +Every subsystem that differs across platforms (display, input, asset loading, save, time, network, log) follows the same pattern: + +1. `src/dusk//platform.h` — included by the public header. Contains `#include "path/to/platform-specific-header.h"` resolved by the build system include path. +2. `src/dusk{platform}//platform.h` — the actual platform-specific header included above (e.g. `src/duskgl/display/framebuffer/framebufferplatform.h`). +3. The shared header (`src/dusk//.h`) `#error`s at compile time if the platform doesn't define the expected macros/types. + +The active platform backends are selected by `DUSK_TARGET_SYSTEM` in CMake, which includes `cmake/targets/.cmake`. That file sets compile definitions (`DUSK_LINUX`, `DUSK_SDL2`, `DUSK_OPENGL`, …) and links platform libraries. + +Platform source directories: +- `src/duskgl/` — OpenGL rendering (used on Linux and as the GL layer for SDL2) +- `src/dusksdl2/` — SDL2 window/input/time (Linux desktop) +- `src/dusklinux/` — Linux filesystem/save/network +- `src/duskdolphin/` — GameCube & Wii (GX renderer, libogc) +- `src/duskpsp/` — PSP (GU renderer, PSPSDK) +- `src/duskvita/` — PS Vita + +## Subsystem lifecycle + +All subsystems follow `init → update (per frame) → dispose`. Engine initialization order matters and is centralized in `engine.c`: + +``` +systemInit → timeInit → consoleInit → inputInit → assetInit → +localeManagerInit → displayInit → uiInit → uiTextboxInit → +cutsceneInit → rpgInit → networkInit → sceneInit +``` + +Dispose runs in reverse. Each call uses `errorChain()` to propagate failures. + +## Error handling + +Functions that can fail return `errorret_t` (a code + pointer to thread-local error state). Three core macros: + +```c +errorThrow("message %s", arg); // sets error, returns from current function +errorChain(someCall()); // if someCall() fails, propagates and returns +errorOk(); // returns success +``` + +Check with `errorIsOk(ret)` / `errorIsNotOk(ret)`. The error state carries file/function/line info for a stack-like trace. + +## Fixed-point math + +`fixed_t` is `int32_t` with Q24.8 format (8 fractional bits, ~0.004 resolution). Use it for all world/game values: + +```c +fixed_t x = FIXED(1.5); // compile-time literal +fixed_t y = fixedFromI32(3); // runtime conversion +fixed_t z = fixedMul(x, y); // arithmetic +float_t f = fixedToFloat(z); // only where float is needed (e.g. GL uniforms) +``` + +## Code generation from CSV + +Several subsystems define their data in CSV files and have corresponding Python tools that generate C headers at build time (via CMake `add_custom_command`): + +| CSV | Tool | Output | +|-----|------|--------| +| `src/dusk/input/input.csv` | `tools/input/csv/` | input action enum + names | +| `src/dusk/display/color.csv` | `tools/color/csv/` | color constants | +| `src/dusk/rpg/item/item.csv` | `tools/item/csv/` | item enum + metadata | +| `src/dusk/rpg/story/storyflag.csv` | `tools/story/csv/` | story flag enum + initial values | + +Generated headers are written to `build-/generated/` and included via `target_include_directories`. + +## Asset system + +Assets are packed into `dusk.dsk` (a zip archive) at build time from the `assets/` directory. At runtime `asset.c` opens the archive and serves files from it. + +Loading is asynchronous: `assetLock()` registers a load request; the background thread calls the appropriate loader; call `assetRequireLoaded()` to block until ready. `assetUnlock()` / `assetUnlockEntry()` releases the entry so it can be reclaimed. + +Loaders are registered per type (`assetloadertype_t`) and live under `src/dusk/asset/loader/`. Platform-specific asset init (finding the .dsk file) is in `src/dusk{platform}/asset/`. + +## Display subsystem + +The display system is currently organized around immediate GPU-style rendering: `mesh_t` (vertex buffers), `shader_t` (GLSL on GL / TEV state on Dolphin), `texture_t`, and `framebuffer_t`. See [display-refactor.md](display-refactor.md) for the planned move to a render-queue model (needed for a future Saturn port). + +The `spritebatch_t` (`display/spritebatch/`) accumulates 2D quads and flushes in batches — the primary 2D drawing primitive used by the RPG layer. diff --git a/.claude/asset.md b/.claude/asset.md new file mode 100644 index 00000000..4519e465 --- /dev/null +++ b/.claude/asset.md @@ -0,0 +1,115 @@ +# Asset System + +Source: `src/dusk/asset/` + +All game assets are packed into `dusk.dsk` (a zip archive) at build time and served from it at runtime. The asset system manages async loading, reference counting, and platform-specific archive location. + +See [architecture.md](architecture.md#asset-system) for the high-level overview. + +--- + +## Asset archive + +The archive is opened at `assetInit()`. `assetFileExists(filename)` checks for a file without loading it. The file path format inside the archive matches the layout of the `assets/` source directory. + +On each platform, `assetInitPlatform()` locates the `.dsk` file (e.g. adjacent to the binary on Linux, on the SD card on PSP). + +--- + +## Entry lifecycle + +An `assetentry_t` represents one file being managed by the system. States: + +``` +NOT_STARTED → PENDING_ASYNC → LOADING_ASYNC → PENDING_SYNC → LOADING_SYNC → LOADED + └→ ERROR +``` + +- **PENDING_ASYNC / LOADING_ASYNC**: the background thread is handling I/O (file reads, decompression). +- **PENDING_SYNC / LOADING_SYNC**: the main thread needs to finish loading (e.g. uploading to GPU), triggered during `assetUpdate()`. + +The async/sync split exists because GPU operations must happen on the main thread. + +--- + +## Using assets + +```c +// Acquire a loaded entry (blocks until loaded): +assetentry_t *entry = assetLock(filename, ASSET_LOADER_TYPE_TEXTURE, &input); +errorChain(assetRequireLoaded(entry)); + +// Use the loaded data: +texture_t *tex = &entry->data.texture.texture; + +// Release when done: +assetUnlockEntry(entry); +``` + +`assetLock` finds-or-creates an entry and increments its reference count. `assetUnlock` / `assetUnlockEntry` decrements it; when it reaches zero the entry is reclaimed at the next `assetUpdate()`. + +To subscribe to async completion instead of blocking: +```c +eventSubscribe(&entry->onLoaded, myCallback, myUser); +``` + +--- + +## Loader types + +| Type constant | File | Output struct accessed via | +|---|---|---| +| `ASSET_LOADER_TYPE_TEXTURE` | `.png` etc. | `entry->data.texture.texture` | +| `ASSET_LOADER_TYPE_TILESET` | tileset descriptor | `entry->data.tileset.tileset` | +| `ASSET_LOADER_TYPE_MESH` | mesh data | `entry->data.mesh.mesh` | +| `ASSET_LOADER_TYPE_LOCALE` | `.po` file | internal to locale manager | +| `ASSET_LOADER_TYPE_JSON` | `.json` | `entry->data.json.*` | + +Each loader type registers `loadAsync`, `loadSync`, and `dispose` callbacks in `ASSET_LOADER_CALLBACKS[]`. + +The async callback runs on the loader thread; the sync callback runs on the main thread during `assetUpdate()`. Most loaders do file I/O async and GPU upload sync. + +### Error handling inside loaders + +Use these macros instead of `errorThrow` / `errorChain` inside loader callbacks — they also set the entry state to ERROR: + +```c +assetLoaderErrorChain(loading, someCall()); +assetLoaderErrorThrow(loading, "Descriptive message"); +``` + +--- + +## Low-level file I/O (`asset/assetfile.h`) + +`assetfile_t` wraps a `zip_file_t` handle and provides streaming reads: + +```c +assetFileInit(&file, "textures/player.png", NULL, NULL); +assetFileOpen(&file); +assetFileRead(&file, buffer, size); +assetFileClose(&file); +assetFileDispose(&file); + +// Read entire file into a malloc'd buffer: +uint8_t *buf; size_t size; +assetFileReadEntire(&file, &buf, &size); // caller frees buf +``` + +For line-by-line text parsing (`assetfilelinereader_t`): +```c +assetFileLineReaderInit(&reader, &file, readBuf, readBufSize, outBuf, outBufSize); +while(!reader.eof) { + errorChain(assetFileLineReaderNext(&reader)); + // reader.outBuffer contains the line, reader.lineLength its length +} +``` + +--- + +## Background loader thread + +`assetUpdateAsync(thread)` is the thread entry point. It calls `assetUpdate()` in a loop, sleeping briefly between iterations, until `threadShouldStop()` returns true. The main thread also calls `assetUpdate()` once per frame to process the sync phase. + +Up to `ASSET_LOADING_COUNT_MAX` (4) entries can be loading concurrently. +Up to `ASSET_ENTRY_COUNT_MAX` (128) entries can exist at once. diff --git a/.claude/display-refactor.md b/.claude/display-refactor.md new file mode 100644 index 00000000..de61ac23 --- /dev/null +++ b/.claude/display-refactor.md @@ -0,0 +1,714 @@ +# Display Layer Refactor + +## Vision + +The goal is to remove the implicit assumption that all platforms render +through a GL-like API, and replace it with a system where each platform +owns its rendering stack completely. The scene describes *what* to draw +in platform-neutral terms; the platform decides *how* to draw it. + +This unlocks: +- Saturn (VDP1/VDP2 command-list, no Z-buffer, affine-only) +- PlayStation 1 (ordering table, affine textures, GTE fixed-point, CMake SDK) +- Nintendo 64 (RSP display list, hardware Z-buffer, perspective-correct, + real FPU -- closer to modern GL than to Saturn) +- SNES (PPU tile engine, Mode7 for overworld, no real 3D) +- Vulkan (explicit, modern, no legacy GL baggage) +- Native PSP GU (drop PSPGL which is just a compatibility shim) +- Legacy fixed-function GL as its own standalone target +- A real first-class 2D UI system not bolted onto 3D space + +--- + +## Why + +### The current abstraction assumes GPU-style rendering + +The current display layer was designed around a GL-like mental model: +vertex buffers, shaders, Z-buffered triangle rasterization, and texture +objects. `duskgl` implements this with real OpenGL. `duskdolphin` does its +own GX thing but still matches the same interface (mesh, shader, texture, +framebuffer). PSP uses PSPGL -- a library that *emulates* GL on top of +the PSP's native GE/GU hardware, which is entirely different underneath. + +Problems this creates: + +**PSPGL is a lie.** The PSP has a native graphics engine (GE/GU) with its +own command list, its own vertex formats, and its own display list model. +PSPGL translates GL calls into GU calls, but imperfectly -- and we end up +paying the abstraction cost without getting GL correctness. Writing directly +to GU gives better performance, access to native formats, and correct +behavior on edge cases that PSPGL gets wrong. + +**Legacy GL should not share code with modern GL.** The fixed-function +pipeline (no shaders, matrix stacks via glMatrixMode, glTexEnv) is +meaningfully different from modern GL (VAO/VBO, GLSL, explicit uniform +locations). Treating them as "the same thing with a flag" creates a tangle +of `#ifdef DUSK_OPENGL_LEGACY` guards throughout the rendering code. +They are separate targets and should be separate platform directories. + +**Saturn cannot fit the model at all.** VDP1 is a command-list processor: +you write 32-byte command structs (sprites, quads, lines) into VRAM, then +poke a register to trigger execution. There are no vertex buffers, no +shaders, no Z-buffer. Depth is pure painter's algorithm -- command order +IS the depth. VDP2 composites up to 6 background planes at scanline time; +these are tile maps and rotation parameter tables, not meshes. Nothing +about the current API maps onto this hardware. + +**SNES is even further removed.** The PPU renders tiles. VRAM holds 8x8 +or 16x16 pixel tiles and tile maps; the PPU references these during +scanline rendering. There are no draw calls. Mode7 is an affine transform +applied to a single background layer (the basis for the overworld map and +road perspective effects). Sprites are entries in OAM (Object Attribute +Memory). The 65816 CPU writes to memory-mapped registers and VRAM; the +PPU does the rest. The concept of "mesh" or "shader" is meaningless here. + +**Textures loaded as RGBA waste memory and exclude platforms.** Loading +every texture as 32-bit RGBA and converting at runtime is expensive on +memory-constrained platforms (Saturn has ~1 MB total RAM; SNES has 64 KB +VRAM) and simply wrong for platforms that have native formats incompatible +with RGBA (e.g., PSP's ABGR8888 / BGR5650, Saturn's RGB555 / CI4 / CI8, +SNES's 2bpp/4bpp/8bpp indexed). The asset pipeline must compile textures +to platform-native formats at build time. + +**UI in 3D space is wasteful and limiting.** Currently UI elements are +rendered as geometry projected into screen space, going through the full +3D pipeline. On platforms with dedicated 2D hardware (Saturn VDP2, +SNES BG layers), this is actively wrong -- UI should map to a hardware +plane, not a 3D draw call. On modern platforms it should be a clean +screen-space pass that never touches the 3D depth buffer. + +--- + +## Current Model (Summary) + +``` +Scene + -> shaderBind(shader) + -> textureBind(texture) + -> meshDraw(mesh) <-- immediate draw call per object + -> meshDraw(mesh) + -> ... +Platform receives each draw call immediately. +Depth is handled by Z-buffer hardware. +All textures live in GPU memory as RGBA (or Dolphin's tiled RGBA). +UI is rendered as 3D geometry with an orthographic projection. +``` + +Key current concepts: +- `mesh_t` -- vertex array (triangles/quads), in GPU VBO (GL) or CPU + memory (Dolphin) +- `shader_t` -- GLSL program (modern GL), GL fixed-function state + (legacy GL), or GX matrix + TEV config (Dolphin) +- `texture_t` -- GPU texture handle (GL) or tiled CPU buffer (Dolphin); + always RGBA at the engine level +- `framebuffer_t` -- FBO (GL) or fixed hardware XFB (Dolphin) +- `spritebatch_t` -- accumulates 2D quads and flushes in batches of 32; + the only existing deferred-submission system in the engine + +The spritebatch hints at the right model. Everything needs to work this way. + +--- + +## The Core Shift: Platform-Native Rendering + +### Before + +``` +src/dusk/ Core engine + GL-like rendering API definition +src/duskgl/ OpenGL implementation +src/dusksdl2/ SDL2 window/input (shared) +src/duskpsp/ PSP via PSPGL (shim over GU) +src/duskvita/ Vita via GL ES (similar path to duskgl) +src/duskdolphin/ GameCube/Wii via GX (already custom) +src/dusklinux/ Linux (uses dusksdl2 + duskgl) +``` + +### After + +``` +src/dusk/ Core engine logic + render intent API ONLY +src/dusksdl2/ SDL2 window/input (unchanged) +src/duskgl/ Modern OpenGL (Linux, Vita modern path) +src/duskgllegacy/ Fixed-function OpenGL (older hardware, PSP with PSPGL + as a last resort) +src/duskvulkan/ Vulkan (Linux modern, future) +src/duskpsp/ PSP native GU (no PSPGL, direct command lists) +src/duskvita/ Vita native GXM (TBD) +src/duskdolphin/ GameCube/Wii GX (already custom, mostly kept) +src/dusksaturn/ Saturn VDP1/VDP2 (new) +src/duskps1/ PlayStation 1 ordering table + GTE (new) +src/duskn64/ Nintendo 64 RSP/RDP display list (new) +src/dusksnes/ SNES PPU/Mode7 (new, extremely constrained) +``` + +`src/dusk/` no longer knows about meshes, shaders, or framebuffers. +It defines the *render intent* system: what the scene wants to draw. +Each platform directory is entirely self-contained and responsible for +translating intents to its native API. + +--- + +## Render Intent System (new) + +Instead of the scene calling `meshDraw()` or `shaderBind()`, it submits +render intents into a `renderqueue_t`. An intent describes what should +appear on screen without prescribing how to draw it. + +### Primitive intents (3D world) + +``` +RENDER_INTENT_QUAD -- textured quad, 4 vertices or transform + size +RENDER_INTENT_POLYGON -- filled polygon (convex, up to N vertices) +RENDER_INTENT_LINE -- line segment or polyline +RENDER_INTENT_SPRITE -- 2D billboard (always faces camera) +RENDER_INTENT_MESH -- arbitrary vertex array (GL/GX only; degraded + on command-list platforms) +``` + +Each intent carries: texture reference, color/tint, depth hint (for +painter's algorithm sorting), blend mode, and cull flags. + +### Background plane intents (2D layers) + +``` +RENDER_INTENT_BGPLANE -- configure a background/tilemap layer +``` + +Carries: layer index, tile map data reference, scroll offset, palette, +and transform (for Mode7-style affine). + +### UI intents (screen space) + +``` +RENDER_INTENT_UI_RECT -- solid colored rectangle +RENDER_INTENT_UI_SPRITE -- textured rectangle (UI image) +RENDER_INTENT_UI_TEXT -- text string at screen position +``` + +UI intents are always screen-space. They are never mixed into the 3D +world queue. See UI System section below. + +### Platform translation + +| Intent | Modern GL | PSP GU | Saturn VDP1 | PS1 OT | N64 RSP | SNES PPU | +|---|---|---|---|---|---|---| +| QUAD | VAO + glDraw | GU display list | distorted-sprite cmd | GPU quad packet | RSP display list | OAM + BG tile | +| POLYGON | VAO + glDraw | GU display list | polygon cmd | GPU poly packet | RSP display list | OAM | +| BGPLANE | fullscreen quad | fullscreen quad | VDP2 config | fullscreen quad | fullscreen quad | BG layer config | +| UI_SPRITE | 2D ortho quad | 2D GU quad | VDP2 BG plane | GPU rect packet | RDP rectangle | BG layer tile | +| MESH | VAO/VBO | GU buffers | (degrade: quads) | (degrade: tris/quads) | RSP display list | (not supported) | + +Note: N64 supports both triangles and axis-aligned rectangles natively via +RDP. PS1 supports triangles and quads (4-vertex) natively, so neither needs +the dead-vertex trick that Saturn requires. + +--- + +## Asset Pipeline: Platform-Native Formats + +### The problem + +All textures currently enter the engine as RGBA and are converted at +runtime by each platform (Dolphin retiles to 4x4 blocks; GL uploads as-is). +This wastes memory and CPU time, and is impossible for platforms where RGBA +is not a valid intermediate format at all. + +### The solution + +The asset compiler (offline, run at build time) produces platform-specific +binary bundles. A texture asset has one source (PNG or similar) but N +compiled outputs, one per target. + +### Texture formats by platform + +| Platform | Native Formats | Notes | +|---|---|---| +| Modern GL | RGBA8, RGB8, BC1-BC7 (compressed) | Upload directly, GPU handles | +| Legacy GL | RGBA8, RGB8, CI8 (palette via extension) | No compressed formats | +| Vulkan | VkFormat variants (RGBA8, BC, ASTC) | Chosen at compile time | +| PSP GU | ABGR8888, BGR5650, ABGR1555, ABGR4444, CI4, CI8 | Native swizzled format | +| Saturn VDP1/VDP2 | RGB555, CI4, CI8 (15-bit palette in CRAM) | Big-endian, packed | +| PlayStation 1 | RGB555 / CI4 / CI8 (CLUT in VRAM) | Little-endian; VRAM flat; CLUT at coord | +| Nintendo 64 | RGBA16, RGBA32, IA4-IA16, I4-I8, CI4, CI8 | 4 KB TMEM; tiles must fit in TMEM banks | +| GameCube/Wii GX | I4, I8, IA4, IA8, RGB565, RGB5A3, RGBA8, CMPR | 4x4 tiled, big-endian | +| SNES PPU | 2bpp, 4bpp, 8bpp indexed (CGRAM palette) | Tile-packed, no direct access | + +### Asset bundle structure + +The `.dsk` bundle gains a platform tag. The loader picks the right section +at runtime (or the build produces a single-platform bundle for constrained +targets like SNES/Saturn where there is no spare storage for unused data). + +--- + +## UI System (first-class) + +### Current problem + +UI elements go through the 3D pipeline: they are meshes with an orthographic +shader, rendered in the same pass as the world. This means: +- UI competes for Z-buffer depth with world geometry +- On Saturn/SNES, UI cannot use dedicated hardware planes +- Text rendering is tied to the sprite batch which is tied to the 3D pass +- No separation between "draw the world" and "draw the HUD" + +### New model + +UI is a completely separate rendering context. The world renders first, +then the UI renders on top. They share no state. + +UI coordinates are always in screen space (pixels or a logical resolution +that the platform scales to its native display size). No camera matrix, +no projection, no depth buffer involvement. + +### Platform mapping + +| Platform | UI implementation | +|---|---| +| Modern GL | Separate 2D ortho pass, screen-space quads, no depth test | +| Legacy GL | Same, using fixed-function | +| PSP GU | Separate GU display list, 2D mode | +| Saturn | VDP2 background plane(s) dedicated to UI | +| PlayStation 1 | Separate GPU packet chain, no Z; ordered after world OT | +| Nintendo 64 | RDP rectangle commands in a separate display list segment | +| GameCube/Wii | GX 2D mode or dedicated GX pass | +| SNES | Dedicated BG layer(s) for HUD tiles | + +On Saturn, the UI occupying VDP2 planes is a genuine hardware win -- the +PPU composites it for free at scanline time, costing zero VDP1 commands. +On SNES, the HUD must live in a BG layer because there is no alternative. + +### UI API (proposed) + +```c +uiBegin(); + uiDrawRect(x, y, w, h, color); + uiDrawSprite(x, y, w, h, texture, uvMin, uvMax); + uiDrawText(x, y, font, string); +uiEnd(); // platform flushes UI to hardware +``` + +The `uiBegin`/`uiEnd` block collects intents; the platform submits them +at frame end in whatever way is appropriate. + +--- + +## SNES / Mode7 + +SNES is the most constrained platform the engine will ever support and +needs its own section because it breaks assumptions that even Saturn keeps. + +### Hardware + +- **CPU**: 65816 @ ~3.58 MHz (16-bit, no FPU, no cache) +- **PPU**: Tile-based scanline renderer. VRAM holds tile graphics and + tile maps. BG layers reference tiles by index. +- **Mode7**: A single BG layer with a 2D affine matrix applied per + scanline. Used for overworld maps, road perspective (F-Zero), rotation + effects. The matrix is set via HDMA (scanline DMA) for per-scanline + variation, enabling horizon-perspective effects. +- **Sprites/OAM**: Up to 128 sprites (8x8, 16x16, 32x32, 64x64 pixels), + 4bpp indexed, up to 8 per scanline. +- **Palette**: CGRAM holds 256 entries of 15-bit RGB (512 bytes total). + BG layers use sub-palettes of 4/16/256 colors depending on bit depth. +- **VRAM**: 64 KB (tiles + tile maps) +- **WRAM**: 128 KB work RAM + usually 8 KB SRAM on cart for saves +- **No frame buffer.** The PPU renders scanlines directly. You cannot + read back what was drawn. +- **No general-purpose draw calls.** You configure registers and VRAM + before the frame and the PPU does the rest. + +### What "3D" means on SNES + +True 3D is not possible. What can be approximated: +- **Overworld map**: Mode7 with a flat texture and HDMA scroll gives a + top-down perspective with a horizon line (the classic JRPG overworld). +- **Depth illusion**: Mode7 matrix manipulation can simulate a moving + camera over flat terrain. Objects are sprites placed at screen positions + calculated by software perspective projection. +- **Sprite scaling**: Software-scaled sprites using pre-rendered frames + or the RSP-style tricks used in Super FX games (Star Fox). Super FX + is a co-processor on the cartridge -- base SNES cannot do this. +- **Basic 3D effects**: Some games use HDMA color gradient + Mode7 floor + with overlaid sprites to create a pseudo-3D look. + +The engine plan for SNES: Mode7 overworld (confirmed), sprite-based world +objects, BG layer UI. "Basic 3D effects" (pseudo-perspective with sprites) +is aspirational -- implementation complexity TBD. + +### SNES constraints on the engine + +- **No dynamic allocation.** With 128 KB WRAM, a general-purpose allocator + is risky. The engine memory system may need a static pool mode for SNES. +- **No floating point.** `float_t` must resolve to integer or fixed-point. +- **No scripting (JerryScript).** The JS engine requires far more than + 128 KB RAM. SNES scenes must be compiled C. +- **Asset data in ROM, not a .dsk bundle.** SNES loads from cartridge ROM + mapped into the address space. The asset system needs a ROM-mapped loader. +- **Tile pipeline.** Textures must be pre-converted to SNES tile format + (2bpp/4bpp/8bpp, 8x8 pixel tiles, CGRAM palette) at build time. This + is a completely different asset output from every other platform. + +--- + +## Platform Inventory + +A summary of what each platform's native rendering looks like after the +refactor, for reference when designing the intent API. + +### Modern OpenGL (duskgl) + +VAO + VBO mesh storage, GLSL shaders, FBO render targets, Z-buffer. +No fixed-function. Targets: Linux, possibly Vita (GXM is preferred). + +### Legacy OpenGL (duskgllegacy) + +Fixed-function pipeline: `glMatrixMode`, `glTexEnv`, client-side vertex +arrays. No VAO/VBO. Used for: very old desktop hardware, maybe PSP as +last resort (PSPGL is this). Targets: legacy desktop, embedded Linux. + +### Vulkan (duskvulkan) + +Explicit pipeline state objects, render passes, descriptor sets, command +buffers. Highest ceiling for performance and control. Targets: Linux +(modern), future platforms. Not immediate priority but the architecture +should not block it. + +### PSP native GU (duskpsp) + +The GE/GU is a display-list GPU. You build a command list in memory and +the GU DMA engine processes it asynchronously. Native vertex formats are +PSP-specific (ABGR byte order, swizzled textures for cache efficiency). +No PSPGL. Targets: PSP hardware and emulators. + +### Vita (duskvita) + +GXM is Sony's Vita GPU API -- closer to modern GL than GU, with explicit +shader binaries (.gxp), ring buffers, and GPU sync primitives. + +### GameCube/Wii GX (duskdolphin) + +Already a custom renderer. GX uses immediate-mode vertex submission +(`GX_Begin` / `GX_Position1x16` loops), TEV for texture compositing, and +hardware XFB double-buffering. Big-endian. Mostly kept as-is; may benefit +from being expressed in terms of render intents for consistency. + +### Saturn VDP1/VDP2 (dusksaturn) + +VDP1: command-list (32-byte structs), quad-based, affine texture mapping, +no Z-buffer (painter's algorithm). VDP2: up to 6 background planes +composited at scanline time. Big-endian dual SH-2, no FPU. Fixed-point +math required throughout. + +### PlayStation 1 (duskps1) + +MIPS R3000A @ 33.87 MHz, little-endian, no FPU. GTE (coprocessor 2) +handles fixed-point matrix math, perspective divide, and lighting. +GPU receives packets via DMA linked-list (the Ordering Table). Primitives: +triangles and quads natively (no dead-vertex needed). Texture mapping: +affine, same limitation as Saturn. No Z-buffer; depth is OT slot order. +VRAM is 1 MB flat (frame buffers + textures + CLUTs share it). SDK: +PSn00bSDK, which is CMake-native -- a direct fit for the dusk build system. + +### Nintendo 64 (duskn64) + +VR4300 @ 93.75 MHz, big-endian, real IEEE 754 FPU. Rendering is split +between the RSP (geometry: programmable MIPS SIMD, runs microcode up to +~1000 instructions in 4 KB IMEM) and the RDP (rasterization: fixed +hardware). RSP produces triangle commands from a CPU-built display list +in RDRAM. RDP features: perspective-correct texture mapping, bilinear +filtering, hardware Z-buffer. Primitives: triangles and axis-aligned rects. +TMEM is 4 KB on-chip texture cache; textures must be loaded into tiles +before drawing -- a significant memory management constraint. +SDK: libdragon (Unlicense, GCC 14, Makefile-based -- not CMake; this +requires a wrapper toolchain file for dusk's build system). + +### SNES PPU/Mode7 (dusksnes) + +Tile-based. VRAM holds tiles and tile maps. Mode7 provides affine transform +for one BG layer. Sprites via OAM. No frame buffer. All configuration is +memory-mapped registers. 65816 CPU, no FPU, extremely limited RAM. + +--- + +## Threading Model + +### Current model + +The engine uses OS threads for async asset loading (`assetXxxLoaderAsync`). +Platforms that have pthreads or an equivalent RTOS (Linux, PSP, Vita) run +worker threads that load data in the background while the game loop runs. +The main thread polls or blocks on completion. + +### The problem + +Several target platforms have no OS threading whatsoever, and others have +hardware-specific async mechanisms that are nothing like pthreads. + +### Per-platform reality + +| Platform | Threading | Async mechanism | +|---|---|---| +| Linux | pthreads | Worker threads (current) | +| Vita | SceKernelThread | Per-SDK threads | +| PSP | SceKernelThread | Per-SDK threads | +| GameCube/Wii | libogc LWP | Lightweight processes | +| Saturn | None (OS) | Slave SH-2 for fixed jobs; CD-ROM via interrupt/callback | +| PlayStation 1 | None (OS) | V-blank ISR, 7 DMA channels, CD-ROM callbacks | +| Nintendo 64 | libdragon preview only | PI DMA for cartridge; RSP for parallel compute | +| SNES | None | DMA (GPDMA/HDMA); NMI V-blank; SPC700 audio is a separate CPU | + +**Saturn slave SH-2**: The second SH-2 is not a general-purpose thread. +It runs a fixed subroutine you hand-load. The typical use is offloading +heavy per-frame computation (geometry transforms, depth sort) while the +master SH-2 handles game logic. Communication is via shared WRAM with +cache-through addresses to avoid coherency bugs. There is no scheduler +and no yield -- it runs to completion. + +**SNES DMA**: GPDMA copies blocks of data (ROM to WRAM, WRAM to VRAM) +and halts the CPU for the duration -- it is synchronous from the game's +perspective. HDMA runs per-scanline during H-blank, writing to PPU +registers without CPU involvement; this is how Mode7 perspective is +achieved. Neither is "async" in the programming sense. + +**SNES NMI**: The V-blank NMI fires at the start of every V-blank period. +This is the only safe window to write to VRAM and PPU registers. All +critical PPU updates must complete within ~1.2ms (the V-blank window). + +### Proposed model + +Introduce a compile-time threading capability flag: + +``` +DUSK_THREAD_PTHREAD -- Linux, maybe Vita +DUSK_THREAD_SCEKERNEL -- PSP, Vita SDK +DUSK_THREAD_LWP -- GameCube/Wii libogc +DUSK_THREAD_SLAVE_SH2 -- Saturn slave CPU (job dispatch only) +DUSK_THREAD_NONE -- SNES (and Saturn master thread view) +``` + +The asset loader's async path is gated on having a threading capability. +When `DUSK_THREAD_NONE` is defined, `assetXxxLoaderAsync` either does not +exist or is an alias for the synchronous version. On Saturn, the slave SH-2 +is exposed as a distinct API (`sh2JobDispatch`, `sh2JobWait`) used only for +compute-heavy work, not for I/O. + +### Asset loading without threads + +**Saturn**: CD-ROM access is initiated via SBL/CDC routines and completes +via interrupt callback. The engine's asset loading loop can poll the +callback flag in the main loop rather than blocking a thread. This is +interrupt-driven cooperative async, not preemptive. + +**SNES**: There is no loading. Assets live in ROM, mapped directly into the +65816 address space. "Loading a texture" means computing a pointer into ROM +and copying the tile data to VRAM during V-blank via GPDMA. The asset system +on SNES is essentially a VRAM/CGRAM allocator and a DMA scheduler, not a +file loader. + +### Asset system changes + +The asset pipeline needs to accommodate three loading models: + +1. **File-based** (Linux, PSP, Vita, Saturn CD): open file, read bytes, + close. Can be sync or thread-async. +2. **DMA/interrupt** (Saturn CD-ROM, GC DVD): initiate transfer, poll or + callback on completion, no thread blocked. +3. **ROM-mapped** (SNES): data is already in the address space; "loading" + is a VRAM DMA copy scheduled for V-blank, not file I/O. + +The `assetstream_t` abstraction that currently wraps file I/O needs a third +backend for ROM-mapped data, and the async path needs to support +callback-based completion as an alternative to thread-based blocking. + +--- + +## What Needs to Change + +### 1. Render intent API (new, in src/dusk/) + +Replace `mesh_t` / `shader_t` / `meshDraw()` as scene-facing APIs with +`renderqueue_t` and intent submission functions. `src/dusk/` defines the +intent types and submission API; platforms implement the flush. + +### 2. Platform renderer directories + +Move rendering implementations out of `duskgl/` as a shared layer and +into fully self-contained platform directories. `duskgl/` becomes the +*modern GL* platform only. Add `duskgllegacy/`, `duskvulkan/` as peers. + +### 3. Asset pipeline: platform-native texture formats + +The offline asset compiler must produce per-platform texture bundles in +native formats. The runtime texture loader expects pre-converted data, +not RGBA. `textureformat_t` grows to cover all platform formats but each +platform only ever sees the formats it natively supports. + +### 4. UI system (first-class, separate from 3D) + +New `src/dusk/ui/` subsystem with `uiBegin` / `uiEnd` and intent types +for rects, sprites, and text. Platforms implement the flush independently. +The 3D spritebatch is retired or scoped to world-space billboards only. + +### 5. Fixed-point / no-FPU math + +`float_t` needs a fixed-point mode. Proposed: define `fixed_t` as a +16.16 signed integer; define `DUSK_MATH_FIXED` for platforms that require +it (Saturn, SNES). Engine math utilities (`mathSin`, `mathCos`, etc.) +have fixed-point implementations selected by this flag. `float_t` on +FPU-less platforms becomes a typedef for `fixed_t`. + +### 6. Background plane abstraction (bgplane_t) + +New concept in `src/dusk/display/bgplane/`. A BG plane has a tile map or +bitmap source, scroll offsets, a palette reference, and optional affine +parameters (for Mode7-style use). On GL platforms: rendered as a +fullscreen textured quad or shader pass. On Saturn: VDP2 config. On SNES: +PPU BG layer config. + +### 7. Memory system: static pool mode + +For SNES (and possibly Saturn), the general-purpose allocator may be +unviable. A compile-time static pool mode (`DUSK_MEMORY_STATIC`) that uses +a fixed-size arena instead of dynamic allocation. All `memoryAllocate` +calls hit the pool; `memoryFree` is a no-op or a stack pop. + +### 8. Script runtime: optional + +JerryScript requires too much RAM for SNES and is marginal on Saturn. +The scripting system should be compile-time optional (`DUSK_SCRIPTING`), +not assumed present. SNES/Saturn scenes would be compiled C. + +--- + +## What to Keep + +- Platform macro abstraction pattern (`displayplatform.h`, etc.) -- works, + no reason to change. +- Directory structure convention for platform directories. +- Entity-component system -- platform-agnostic, unaffected. +- Asset loading + `.dsk` bundle concept (extended for platform formats). +- The broad subsystem layout: asset, input, display, log, network, save, + system, time. + +--- + +## Open Questions + +1. **Render intent granularity**: How much does the intent API need to + express? A MESH intent works on GL/N64 but degrades poorly on Saturn + (must split into quads) and is impossible on SNES. Should MESH be a + valid intent with a "best effort" contract, or excluded from the portable + API entirely? + +2. **Threading abstraction depth**: Should `DUSK_THREAD_SLAVE_SH2` be a + first-class concept in the engine's job system, or a Saturn-internal + implementation detail the core never sees? Same question applies to N64's + RSP as a compute co-processor. + +3. **Asset loading async contract**: When a platform has no threads, should + `assetLoadAsync` be a no-op alias for `assetLoadSync`, or return + immediately with a completion flag to poll? The polling model is more + honest but requires all call sites to handle it. + +4. **N64 build system**: libdragon uses GNU Make, not CMake. Options are: + (a) write a CMake toolchain file that wraps n64.mk, (b) maintain a + parallel Makefile just for N64, or (c) wait for upstream CMake support. + Which is acceptable long-term? + +5. **N64 RSP microcode**: Standard libdragon microcodes (Fast3D/F3DEX2) or + Tiny3D (community microcode with full T&L + skinning)? Writing custom + microcode is powerful but limited to ~1000 MIPS SIMD instructions. + This decision gates what 3D features the N64 port can support. + +6. **PSPGL fate**: Drop immediately in favor of native GU, or keep as a + fallback (`duskgllegacy`) while native GU is built? The two can coexist + during transition. + +7. **Vulkan priority**: Design the intent API with Vulkan in mind from the + start, or add it later? Vulkan's explicit pipeline state model may + conflict with how stateful platforms (Saturn, SNES) expect things to work. + +8. **Background planes on modern platforms**: Does `bgplane_t` degrade to a + fullscreen textured quad on GL/Vulkan/N64, or should modern platforms + support actual background scene rendering (3D world behind the foreground)? + +9. **PS1 ordering table depth**: The OT is a fixed-size array (e.g. 4096 + slots). Depth precision = number of slots. How deep should the engine's + default OT be, and should this be configurable per-scene? + +10. **Fixed-point strategy**: Does `float_t` transparently become `fixed_t` + on FPU-less platforms (Saturn, PS1, SNES), or do we require explicit + `fixed_t` in math-heavy paths? Transparent is easiest to port; explicit + is faster. + +11. **SNES V-blank budget**: All VRAM writes must finish within ~1.2ms. + Does the engine need a V-blank work queue with a budget checker, or is + this left to the game to manage manually? + +12. **SNES scripting**: JerryScript is out. Pure compiled C, or a lighter + scripting layer (Lua is ~100 KB -- tight but possible)? + +13. **Asset compiler**: New standalone tool, or an extension of the existing + asset pipeline? Part of the CMake build or a separate pre-build step? + +--- + +## Proposed Sequence (Draft) + +### Phase 1 -- Intent API (no behavior change) +1. Design and stabilize `renderqueue_t` and intent types +2. Refactor modern GL path to submit through render intents (same output, + new plumbing) +3. Refactor Dolphin path the same way +4. Validate no regressions on Linux + GameCube + +### Phase 2 -- UI system +5. Extract UI rendering from the 3D path into `src/dusk/ui/` +6. Implement UI flush for GL and Dolphin +7. Wire existing UI elements through the new system + +### Phase 3 -- Platform splits +8. Split `duskgl/` into `duskgl/` (modern) and `duskgllegacy/` (fixed-func) +9. Port PSP to native GU (`duskpsp/display/` rewrite, drop PSPGL dependency) +10. Stub `duskvulkan/` structure for future implementation + +### Phase 4 -- Asset pipeline +11. Design platform-native texture format system +12. Extend asset compiler for per-platform output +13. Update texture loader to expect pre-converted data + +### Phase 5 -- Saturn +14. CMake toolchain for SH-2 cross-compile (yaul / libyaul toolchain) +15. `src/dusksaturn/` -- input (SMPC), asset (CD-ROM), log, system +16. VDP1 backend for render queue (quads, polygons, painter's sort) +17. VDP2 backend for bgplane_t (tile maps, scroll, palette) +18. Fixed-point math mode (`DUSK_MATH_FIXED`) +19. UI backend (VDP2 plane(s)) + +### Phase 6 -- PlayStation 1 +20. CMake toolchain wrapping PSn00bSDK (already CMake-native) +21. `src/duskps1/` -- input (BIOS pad), asset (CD-ROM libpsxcd), log, system +22. GTE integration for fixed-point math (reuse `DUSK_MATH_FIXED` path) +23. Ordering table builder for render queue (painter's sort, DMA linked-list) +24. GPU packet backend for intents (tris, quads, rects) +25. UI backend (separate GPU packet chain after world OT) + +### Phase 7 -- Nintendo 64 +26. CMake toolchain wrapping libdragon (n64.mk wrapper or toolchain file) +27. `src/duskn64/` -- input (N64 controller via PIF), asset (PI DMA / + DragonFS), log, system +28. RSP display list builder for render queue (Z-buffer path, no sorting) +29. TMEM tile management for textures +30. RDP rectangle backend for UI +31. Decide on RSP microcode (Tiny3D vs standard F3DEX2) + +### Phase 8 -- SNES +32. SNES toolchain (cc65 or llvm-mos 65816 target) +33. Static memory pool mode (`DUSK_MEMORY_STATIC`) +34. PPU tile pipeline + VRAM management +35. Mode7 overworld implementation +36. OAM sprite system +37. BG layer UI +38. Scripting-optional build (`DUSK_SCRIPTING` off) diff --git a/.claude/display.md b/.claude/display.md new file mode 100644 index 00000000..31816404 --- /dev/null +++ b/.claude/display.md @@ -0,0 +1,215 @@ +# Display System + +Source: `src/dusk/display/` + +The display system is the rendering pipeline. It is abstracted across platforms via `displayplatform.h` — see [architecture.md](architecture.md) for the abstraction pattern. The current concrete backends are OpenGL (`src/duskgl/`) and GX/Dolphin (`src/duskdolphin/`). + +For the planned render-queue refactor (required for Saturn), see [display-refactor.md](display-refactor.md). + +--- + +## Initialization order + +Within the display system, init must follow this order (enforced in `engine.c`): + +``` +displayInit → uiInit → uiTextboxInit +``` + +Within `displayInit`, the platform typically initialises: framebuffer → screen → shader list → textures → spritebatch → text system. + +--- + +## `display_t` / `displaystate_t` + +`display_t DISPLAY` is the global display instance (type alias for `displayplatform_t`). + +`displaystate_t` carries per-draw-call render state flags: + +```c +DISPLAY_STATE_FLAG_CULL // face culling +DISPLAY_STATE_FLAG_DEPTH_TEST // depth testing +DISPLAY_STATE_FLAG_BLEND // alpha blending +``` + +Set state before drawing with `displaySetState(state)`. + +--- + +## Screen (`display/screen/`) + +`screen_t SCREEN` manages the logical viewport that game content renders into. On dynamic-size platforms (Linux/SDL2) the screen can differ from the native window/framebuffer resolution. + +Screen modes: +``` +SCREEN_MODE_BACKBUFFER — maps 1:1 to backbuffer +SCREEN_MODE_FIXED_SIZE — fixed pixel dimensions +SCREEN_MODE_ASPECT_RATIO — fixed aspect, scale to fit +SCREEN_MODE_FIXED_HEIGHT — fixed height, width scales +SCREEN_MODE_FIXED_WIDTH — fixed width, height scales +SCREEN_MODE_FIXED_VIEWPORT_HEIGHT — fixed viewport height +``` + +The linux target defines `DUSK_DISPLAY_SCREEN_HEIGHT=240`, producing a 240p fixed-height viewport. + +Render loop usage: +```c +screenBind(); // set up viewport, projection +// ... draw game content ... +screenUnbind(); +screenRender(); // blit to backbuffer / current framebuffer +``` + +`SCREEN.width` / `SCREEN.height` are the logical dimensions used for world-to-screen math — always prefer these over the framebuffer dimensions. + +--- + +## Framebuffer (`display/framebuffer/`) + +`framebuffer_t FRAMEBUFFER_BACKBUFFER` is the platform backbuffer. `FRAMEBUFFER_BOUND` points to the currently-bound framebuffer (or `NULL` for backbuffer). + +```c +frameBufferInitBackBuffer(); // called once at startup +frameBufferBind(fb); // NULL → backbuffer +frameBufferClear(FRAMEBUFFER_CLEAR_COLOR | FRAMEBUFFER_CLEAR_DEPTH, COLOR_BLACK); +frameBufferGetWidth(fb) / frameBufferGetHeight(fb) / frameBufferGetAspect(fb); +``` + +On platforms with `DUSK_DISPLAY_SIZE_DYNAMIC`, off-screen framebuffers can be created with `frameBufferInit(fb, w, h)` and disposed with `frameBufferDispose(fb)`. Fixed-resolution platforms (PSP, GameCube) only ever use the backbuffer. + +--- + +## Mesh (`display/mesh/`) + +`mesh_t` is a vertex buffer. The type is `meshplatform_t` (e.g. a VAO+VBO on GL, a GX display list on Dolphin). + +```c +meshInit(&mesh, MESH_PRIMITIVE_TYPE_TRIANGLES, vertexCount, verticesPtr); +meshFlush(&mesh, offset, count); // upload CPU vertices → GPU +meshDraw(&mesh, offset, count); // draw; pass -1 for count to draw all +meshDispose(&mesh); +``` + +**Key distinction**: `meshFlush` uploads data to GPU memory; `meshDraw` issues the draw call. For static geometry (chunk meshes) you call `meshFlush` once on load, then `meshDraw` every frame. For dynamic geometry (spritebatch) you `meshFlush` + `meshDraw` each frame. + +`meshvertex_t` (`display/mesh/meshvertex.h`) contains: +- `float_t uv[2]` — texture coordinates +- `float_t pos[3]` — position +- Optionally `color_t color` if `MESH_ENABLE_COLOR` is defined (off by default) + +Primitive mesh generators live alongside `mesh.h`: `quad.h`, `plane.h`, `cube.h`, `sphere.h`, `capsule.h`, `triprism.h`. + +--- + +## Shader (`display/shader/`) + +`shader_t` is `shaderplatform_t` (GLSL program on GL, TEV state block on Dolphin). + +```c +shaderInit(&shader, &definition); +shaderBind(&shader); +shaderSetMatrix(&shader, "uModel", modelMat); +shaderSetTexture(&shader, "uTexture", &texture); +shaderSetColor(&shader, "uColor", COLOR_WHITE); +shaderSetMaterial(&shader, &material); +shaderDispose(&shader); +``` + +### Shader list (`display/shader/shaderlist.h`) + +The engine maintains a small set of built-in shaders in `SHADER_LIST_DEFS[]`. Currently only one is defined: + +- `SHADER_LIST_SHADER_UNLIT` → `SHADER_UNLIT` — unlit textured/colored rendering, used for all world and entity drawing. + +`shaderListInit()` compiles/uploads all built-in shaders and sets shared projection/view matrices. Call once after display init. + +### Materials (`display/shader/shadermaterial.h`) + +`shadermaterial_t` is a union of all shader-specific material structs. Currently only `shaderunlitmaterial_t`: + +```c +shadermaterial_t mat = { + .unlit = { + .color = COLOR_WHITE, + .texture = &myTexture, // NULL for solid color + } +}; +shaderSetMaterial(&SHADER_UNLIT, &mat); +``` + +--- + +## Texture (`display/texture/`) + +```c +textureInit(&texture, width, height, format, data); +textureDispose(&texture); +``` + +Width and height **must be powers of two** (asserted at init time). + +`textureformat_t` is `textureformatplatform_t`. Supported formats vary by platform; the common ones are `TEXTURE_FORMAT_RGBA` and `TEXTURE_FORMAT_PALETTE`. + +`texturedata_t` is a union: +```c +// RGBA: +data.rgbaColors = colorArray; + +// Paletted: +data.paletted.indices = indexArray; +data.paletted.palette = &palette; // palette color count must be power of two +``` + +**Built-in textures** (defined in `texture.c`, no asset loading needed): +- `TEXTURE_WHITE` — 4×4 solid white +- `TEXTURE_TEST` — 4×4 black/magenta checkerboard + +### Palette (`display/texture/palette.h`) + +Up to `PALETTE_COUNT` (6) global palettes in `PALETTES[]`, each holding up to `PALETTE_COLOR_COUNT` (255) `color_t` entries. + +### Tileset (`display/texture/tileset.h`) + +A tileset slices a texture into a grid of equal-sized tiles. Used by fonts and UI frames. The tileset does not own the texture — it references a `texture_t *`. + +--- + +## SpriteBatch (`display/spritebatch/`) + +The primary 2D/billboard drawing primitive. Accumulates `spritebatchsprite_t` quads and flushes them in batches of `SPRITEBATCH_FLUSH_COUNT` (16) sprites at a time. + +```c +// Per frame: +spriteBatchClear(); +spriteBatchBuffer(sprites, count, &SHADER_UNLIT, material); // auto-flushes when batch full +spriteBatchFlush(); // flush remaining + +// Low-level: write directly to an external mesh (for baking static geometry): +spriteBatchBufferToMesh(sprites, count, vertices, verticesSize); +``` + +`spritebatchsprite_t`: +```c +typedef struct { + vec3 min, max; // 3D bounding box + vec2 uvMin, uvMax; // texture region +} spritebatchsprite_t; +``` + +The global `SPRITEBATCH` and its vertex backing array `SPRITEBATCH_VERTICES[]` are defined externally to the struct to satisfy alignment requirements on certain platforms. + +--- + +## Text (`display/text/`) + +Text rendering uses `FONT_DEFAULT` (loaded during `textInit()`), which references a texture and a tileset. Characters start at ASCII `!` (`TEXT_CHAR_START`). + +```c +textDraw(x, y, "Hello", COLOR_WHITE, &FONT_DEFAULT); +textMeasure("Hello", &FONT_DEFAULT, &outWidth, &outHeight); + +// Single-char sprite for manual layout: +spritebatchsprite_t s = textGetSprite(pos, 'A', &FONT_DEFAULT); +``` + +`font_t` holds a `texture_t *` and a `tileset_t *` — both are owned by the asset system, not the font struct. diff --git a/.claude/input.md b/.claude/input.md new file mode 100644 index 00000000..a5f21400 --- /dev/null +++ b/.claude/input.md @@ -0,0 +1,93 @@ +# Input System + +Source: `src/dusk/input/` + +The input system decouples physical hardware buttons from logical game actions via a binding layer. Actions are defined in `src/dusk/input/input.csv` and code-generated into `inputaction_t` enum values — see [architecture.md](architecture.md#code-generation-from-csv). + +--- + +## Concepts + +**Button** (`inputbutton_t`) — a physical input source: a keyboard scancode, gamepad button, gamepad axis, or pointer axis. The available button types depend on which `DUSK_INPUT_*` defines are active for the target platform. + +**Action** (`inputaction_t`) — a logical game input (e.g. `INPUT_ACTION_UP`, `INPUT_ACTION_CONFIRM`, `INPUT_ACTION_RAGEQUIT`). Each action accumulates a float value `[0.0, 1.0]` from all buttons bound to it. + +**Binding** — a many-to-one mapping from buttons to actions. Bindings are registered at runtime with `inputBind(button, action)`. + +--- + +## Querying actions + +```c +// Boolean helpers (current frame): +inputIsDown(action) // value > 0 this frame +inputPressed(action) // down this frame but not last +inputReleased(action) // down last frame but not this + +// Last frame state: +inputWasDown(action) + +// Raw float value: +inputGetCurrentValue(action) // [0.0, 1.0] +inputGetLastValue(action) + +// Axis helpers — combine two opposing actions into a signed float: +float_t h = inputAxis(INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT); // -1.0 to 1.0 +inputAxis2D(negX, posX, negY, posY, result); // fills vec2 +inputAngle2D(negX, posX, negY, posY, result); // atan2-based normalized direction + +// Deadzone: +float_t clean = inputDeadzone(rawValue, 0.1f); +``` + +--- + +## Dynamic values (`DUSK_TIME_DYNAMIC`) + +On platforms with variable frame rates, each action also tracks `dynamicDelta`-scaled values: + +```c +inputGetCurrentValueDynamic(action) +inputGetLastValueDynamic(action) +``` + +These account for the actual time elapsed since the last frame, so movement calculated from them is frame-rate independent. + +--- + +## Events on actions + +Each `inputactiondata_t` exposes `onPressed` and `onReleased` events: + +```c +eventSubscribe(&INPUT.actions[INPUT_ACTION_CONFIRM].onPressed, myCallback, myUser); +``` + +The callback signature is `void cb(void *params, void *user)`. `params` is always `NULL` for input events. + +--- + +## Buttons and bindings + +Physical buttons are typed via `inputbuttontype_t`: + +| Constant | When available | Payload | +|---|---|---| +| `INPUT_BUTTON_TYPE_KEYBOARD` | `DUSK_INPUT_KEYBOARD` | `inputscancode_t` | +| `INPUT_BUTTON_TYPE_GAMEPAD` | `DUSK_INPUT_GAMEPAD` | `inputgamepadbutton_t` | +| `INPUT_BUTTON_TYPE_GAMEPAD_AXIS` | `DUSK_INPUT_GAMEPAD` | axis + positive direction flag | +| `INPUT_BUTTON_TYPE_POINTER` | `DUSK_INPUT_POINTER` | `inputpointeraxis_t` | + +Button names and default bindings are defined in `input.csv`. Look up a button by name: +```c +inputbutton_t btn = inputButtonGetByName("keyboard_w"); +inputBind(btn, INPUT_ACTION_UP); +``` + +`INPUT_BUTTON_DATA[]` holds runtime state (current/last raw values) for every physical button. + +--- + +## Platform platform-specific reads + +`inputButtonGetValuePlatform(button)` is the one required platform function — it returns the current raw `[0.0, 1.0]` value for a button. The platform implementations live in `src/dusk{platform}/input/`. diff --git a/.claude/rpg/cutscene.md b/.claude/rpg/cutscene.md new file mode 100644 index 00000000..782f94fc --- /dev/null +++ b/.claude/rpg/cutscene.md @@ -0,0 +1,149 @@ +# Cutscenes + +Two distinct layers: a low-level engine sequencer (`src/dusk/cutscene/`) and a higher-level RPG wrapper (`src/dusk/rpg/cutscene/`). Almost all game code works with the RPG layer. + +--- + +## Engine sequencer (`src/dusk/cutscene/`) + +`cutscene_t CUTSCENE` is a minimal event runner with up to `CUTSCENE_EVENT_COUNT_MAX` (16) `cutsceneevent_t` slots. Each event has three callbacks: + +```c +typedef struct { + errorret_t (*onStart)(void); + errorret_t (*onUpdate)(void); + errorret_t (*onEnd)(void); +} cutsceneevent_t; +``` + +API: +```c +cutscenePlay(events, count); // copy events array and start from index 0 +cutsceneAdvance(); // end current event, start next (deactivates after last) +cutsceneStop(); // abort immediately +cutsceneIsActive(); // bool +``` + +This layer is primarily used by the RPG cutscene system — game code doesn't normally touch it directly. + +--- + +## RPG cutscene layer (`src/dusk/rpg/cutscene/`) + +### Data structures + +A `cutscene_t` is just a pointer to an item array and a count: + +```c +typedef struct cutscene_s { + const cutsceneitem_t *items; + uint8_t itemCount; +} cutscene_t; +``` + +A `cutsceneitem_t` is a tagged union of all item types: + +```c +typedef struct cutsceneitem_s { + cutsceneitemtype_t type; + union { + cutscenetext_t text; // display text in textbox + cutscenecallback_t callback; // call a void(*)(void) function + cutscenewait_t wait; // pause for a fixed_t duration (seconds) + const cutscene_t *cutscene; // nest another cutscene + }; +} cutsceneitem_t; +``` + +### Item types + +| Type constant | Payload | Behaviour | +|---|---|---| +| `CUTSCENE_ITEM_TYPE_TEXT` | `cutscenetext_t` — `text[256]` + `rpgtextboxpos_t position` | Shows textbox; advances on player confirm input | +| `CUTSCENE_ITEM_TYPE_CALLBACK` | `cutscenecallback_t` (function pointer) | Calls the function once, then immediately advances | +| `CUTSCENE_ITEM_TYPE_WAIT` | `cutscenewait_t` (a `fixed_t` in seconds) | Counts down `animTime` each frame, then advances | +| `CUTSCENE_ITEM_TYPE_CUTSCENE` | `const cutscene_t *` | Plays the nested cutscene before continuing | + +### Runtime state + +`cutscenesystem_t CUTSCENE_SYSTEM` tracks: +- `scene` — pointer to the active `cutscene_t` +- `currentItem` — index into `scene->items[]` +- `data` — per-item runtime data (`cutsceneitemdata_t`, currently just `cutscenewaitdata_t`) +- `mode` — the current `cutscenemode_t` + +API: +```c +cutsceneSystemStartCutscene(cutscene); // begin playing a cutscene +cutsceneSystemNext(); // advance to next item +cutsceneSystemUpdate(); // called each frame from rpgUpdate +cutsceneSystemGetCurrentItem(); // inspect active item +``` + +### Cutscene mode (`cutscenemode.h`) + +Each item can run in one of three modes: + +```c +CUTSCENE_MODE_NONE // no cutscene active +CUTSCENE_MODE_FULL_FREEZE // pause everything (not yet used) +CUTSCENE_MODE_INPUT_FREEZE // player input blocked (default: CUTSCENE_MODE_INITIAL) +CUTSCENE_MODE_GAMEPLAY // player can still move during cutscene +``` + +`cutsceneModeIsInputAllowed()` is checked by `entityUpdate()` before invoking the movement callback — the player cannot walk when in INPUT_FREEZE mode. + +### Defining a cutscene + +Cutscenes are defined as `static const` arrays in header files under `rpg/cutscene/scene/`. Example (`testcutscene.h`): + +```c +static const cutsceneitem_t MY_CUTSCENE_ITEMS[] = { + { + .type = CUTSCENE_ITEM_TYPE_TEXT, + .text = { .text = "Hello!", .position = RPG_TEXTBOX_POS_BOTTOM } + }, + { + .type = CUTSCENE_ITEM_TYPE_WAIT, + .wait = FIXED(1.5f) + }, + { + .type = CUTSCENE_ITEM_TYPE_CUTSCENE, + .cutscene = &ANOTHER_CUTSCENE + }, +}; + +static const cutscene_t MY_CUTSCENE = { + .items = MY_CUTSCENE_ITEMS, + .itemCount = sizeof(MY_CUTSCENE_ITEMS) / sizeof(cutsceneitem_t) +}; +``` + +Attach to an NPC via its interact component: +```c +entity->interact.type = ENTITY_INTERACT_CUTSCENE; +entity->interact.data.cutscene = &MY_CUTSCENE; +``` + +--- + +## Textbox (`src/dusk/rpg/rpgtextbox.h`) + +`rpgtextbox_t RPG_TEXTBOX` is the global textbox state: + +```c +typedef struct { + rpgtextboxpos_t position; // RPG_TEXTBOX_POS_TOP or RPG_TEXTBOX_POS_BOTTOM + bool_t visible; + char_t text[RPG_TEXTBOX_MAX_CHARS]; // 256 chars +} rpgtextbox_t; +``` + +API: +```c +rpgTextboxShow(position, text); // copies text, sets visible = true +rpgTextboxHide(); // sets visible = false +rpgTextboxIsVisible(); // bool +``` + +The textbox state is read by `ui/uitextbox.c` during the UI render pass to draw the dialogue box on screen. `rpgtextbox.c` itself does no rendering. diff --git a/.claude/rpg/entity.md b/.claude/rpg/entity.md new file mode 100644 index 00000000..1ffcd7d6 --- /dev/null +++ b/.claude/rpg/entity.md @@ -0,0 +1,140 @@ +# Entities + +Source: `src/dusk/rpg/entity/` + +--- + +## Storage + +Entities live in a single fixed global array: + +```c +entity_t ENTITIES[ENTITY_COUNT]; // ENTITY_COUNT = 64 +``` + +A slot is "empty" when `entity->type == ENTITY_TYPE_NULL`. Never allocate entity memory dynamically — always find a free slot with `entityGetAvailable()`, which returns its index (`0xFF` if none free). + +--- + +## `entity_t` structure + +```c +typedef struct entity_s { + uint8_t id; // index in ENTITIES[] + entitytype_t type; // ENTITY_TYPE_NULL / PLAYER / NPC + entitytypedata_t data; // union: player_t | npc_t + + entitydir_t direction; // facing direction (N/S/E/W) + fixed_t position[3]; // current sub-tile position (x, y, z) + fixed_t lastPosition[3]; // position before last move (for interpolation) + + entityanim_t animation; // IDLE / TURN / WALK + fixed_t animTime; // countdown timer for current animation + + entityinteract_t interact; // optional interact component +} entity_t; +``` + +--- + +## Type system + +Entity types are defined in `entitytype.h` using the enum+integer-typedef pattern: + +```c +typedef enum { ENTITY_TYPE_NULL, ENTITY_TYPE_PLAYER, ENTITY_TYPE_NPC, ENTITY_TYPE_COUNT } entitytype_enum_t; +typedef uint8_t entitytype_t; +``` + +Each type has a `entitycallback_t` entry in the `ENTITY_CALLBACKS[ENTITY_TYPE_COUNT]` static table: + +```c +typedef struct { + void (*init)(entity_t *entity); + void (*movement)(entity_t *entity); + bool_t (*interact)(entity_t *player, entity_t *entity); +} entitycallback_t; +``` + +Callbacks not applicable to a type are `NULL`; `entityUpdate()` guards against this before calling. + +Type-specific data sits in `entitytypedata_t` (a union of `player_t` and `npc_t`). Currently both are stubs (`void *nothing`). + +--- + +## Direction (`entitydir.h`) + +```c +ENTITY_DIR_NORTH / EAST / SOUTH / WEST +``` + +Aliases: `UP = NORTH`, `DOWN = SOUTH`, `LEFT = WEST`, `RIGHT = EAST`. + +Utilities: +- `entityDirGetOpposite(dir)` — returns the opposite direction. +- `entityDirGetRelative(dir, &relX, &relY)` — fills in the ±1 XY delta for that direction. +- `assertValidEntityDir(dir, msg)` — assertion macro. + +--- + +## Animation (`entityanim.h`) + +```c +ENTITY_ANIM_IDLE // standing still +ENTITY_ANIM_TURN // turning to a new direction (ENTITY_ANIM_TURN_DURATION = FIXED(0.06)) +ENTITY_ANIM_WALK // mid-step (ENTITY_ANIM_WALK_DURATION = FIXED(0.1)) +``` + +`entityAnimUpdate(entity)` decrements `animTime` each frame and transitions back to `IDLE` when it reaches zero. + +`entityCanWalk(entity)` / `entityCanTurn(entity)` both return true only when `animation == ENTITY_ANIM_IDLE`. + +The renderer interpolates between `lastPosition` and `position` using `animTime / WALK_DURATION` to produce smooth motion even at low frame rates. + +--- + +## Movement + +`entityWalk(entity, direction)`: + +1. Converts `entity->position` to a `worldpos_t` (truncates fractional part). +2. Applies the directional delta to get `newPos`. +3. Checks the current and target tiles for ramp raise/fall logic (see [world.md](world.md)). +4. Checks `ENTITIES[]` for another entity occupying `newPos` — blocks if found. +5. On success: copies `position` to `lastPosition`, updates `position` to `newPos` (via `worldPosToFixed`), sets `animation = ENTITY_ANIM_WALK`. + +`entityTurn(entity, direction)`: sets `direction` and starts a brief turn animation. + +--- + +## Interaction (`entityinteract.h`) + +The `entityinteract_t` component is embedded in every entity. It is optional — set `type = ENTITY_INTERACT_NULL` for non-interactable entities. + +```c +typedef enum { + ENTITY_INTERACT_NULL, + ENTITY_INTERACT_CUTSCENE, // plays a cutscene_t * + ENTITY_INTERACT_PRINT, // prints a short message[32] +} entityinteracttype_t; +``` + +`entityInteractWith(player, target)` dispatches: +1. If the interact component's `type != NULL`, handles it (starts the cutscene or prints the message). +2. Otherwise falls back to `ENTITY_CALLBACKS[type].interact` if set. + +--- + +## Player (`player.h` / `player.c`) + +`playerInit()` is called via `ENTITY_CALLBACKS[ENTITY_TYPE_PLAYER].init`. + +`playerInput(entity)` is the movement callback. It reads `PLAYER_INPUT_DIR_MAP[]` — a static table mapping input actions (`INPUT_ACTION_UP/DOWN/LEFT/RIGHT`) to entity directions — and calls `entityWalk` or `entityTurn` accordingly. + +The player entity is normally `ENTITIES[0]` but there is no hardcoded assumption about its index beyond being initialized with `ENTITY_TYPE_PLAYER`. + +--- + +## NPC (`npc.h` / `npc.c`) + +`npcInit()`, `npcMovement()`, and `npcInteract()` provide the NPC type callbacks. Currently stubs; movement does nothing, interact returns false. diff --git a/.claude/rpg/index.md b/.claude/rpg/index.md new file mode 100644 index 00000000..d8318e10 --- /dev/null +++ b/.claude/rpg/index.md @@ -0,0 +1,18 @@ +# RPG Layer + +The RPG layer lives in `src/dusk/rpg/` and is the game-logic tier above the engine. It is initialized and ticked by `engine.c` via `rpgInit` / `rpgUpdate` / `rpgDispose`. The `rpg_t` struct is currently a stub; all meaningful state lives in the subsystems below. + +## Contents + +- [world.md](world.md) — scene manager, overworld map, chunks, tiles, coordinate system, camera +- [entity.md](entity.md) — entity pool, types, direction, animation, interaction, player, NPC +- [cutscene.md](cutscene.md) — cutscene system, item types, mode control, textbox +- [story.md](story.md) — story flags, items, inventory, backpack, save system + +## Scene system + +The scene manager (`src/dusk/scene/`) sits above the RPG layer and owns the single active scene. Scenes are identified by `scenetype_t` and registered in `SCENE_TYPES[]` (`scene/scenetype.c`) as `scenecallbacks_t` (init / update / render / dispose). + +`scenedata_t` is a union so all scene structs share memory. `sceneSet(type)` defers the transition — the old scene disposes before the new one inits. + +Currently the only scene is `SCENE_TYPE_OVERWORLD` → `src/dusk/scene/overworld/sceneoverworld.c`. diff --git a/.claude/rpg/story.md b/.claude/rpg/story.md new file mode 100644 index 00000000..dd6b7347 --- /dev/null +++ b/.claude/rpg/story.md @@ -0,0 +1,124 @@ +# Story, Items & Save + +--- + +## Story flags (`src/dusk/rpg/story/`) + +Story flags are the primary mechanism for tracking game-world state (quest progress, one-time events, unlocks). Each flag is a `uint8_t` value (`storyflagvalue_t`), so they can hold booleans or small counts. + +### Defining flags + +Flags are defined in `src/dusk/rpg/story/storyflag.csv`: + +``` +id,description,initial +test,"Test flag for debugging purposes",1 +``` + +The build tool generates: +- A `storyflag_t` enum (e.g. `STORY_FLAG_TEST`) in the generated header. +- `STORY_FLAG_VALUES[]` — the runtime array, pre-populated with the `initial` column values. + +To add a flag: add a row to the CSV. The build re-runs the Python tool automatically on the next CMake build. + +### Access + +```c +storyflagvalue_t v = storyFlagGet(STORY_FLAG_TEST); // macro: array read +storyFlagSet(STORY_FLAG_TEST, 1); // function: also marks save dirty +``` + +`storyFlagGet` is a macro that directly indexes `STORY_FLAG_VALUES[]` — no function call overhead. + +--- + +## Items (`src/dusk/rpg/item/`) + +### Item definitions + +Items are defined in `src/dusk/rpg/item/item.csv`. The build tool generates `itemid_t` enum values and item metadata. `itemid_t` is a generated `uint8_t` typedef. + +### Inventory (`inventory.h`) + +`inventory_t` is a generic container backed by a caller-supplied `inventorystack_t` array: + +```c +typedef struct { + itemid_t item; + uint8_t quantity; // max ITEM_STACK_QUANTITY_MAX (255) +} inventorystack_t; + +typedef struct { + inventorystack_t *storage; + uint8_t storageSize; +} inventory_t; +``` + +Key operations: + +```c +inventoryInit(&inv, storageArray, size); +inventoryAdd(&inv, ITEM_POTION, 3); +inventoryRemove(&inv, ITEM_POTION); +inventorySet(&inv, ITEM_POTION, 10); +inventoryGetCount(&inv, ITEM_POTION); // returns 0 if not present +inventoryItemExists(&inv, ITEM_POTION); +inventoryIsFull(&inv); +inventorySort(&inv, INVENTORY_SORT_BY_ID, false); +``` + +`inventory_t` itself holds no data — the backing array is always external. This avoids fixed-size struct limits and lets different inventories (backpack, shop, chest) share the same logic. + +### Backpack (`backpack.h`) + +The player's inventory is the global `BACKPACK` instance: + +```c +extern inventorystack_t BACKPACK_STORAGE[BACKPACK_STORAGE_SIZE_MAX]; // 20 slots +extern inventory_t BACKPACK; + +backpackInit(); // wires BACKPACK_STORAGE into BACKPACK +``` + +--- + +## Save system (`src/dusk/save/`) + +The save system is stubbed out — it exists and compiles but is commented out of engine init (`engine.c`). What follows describes the design as implemented. + +### Slots + +`save_t SAVE` holds `SAVE_FILE_COUNT_MAX` slots: + +```c +typedef struct { + savefile_t files[SAVE_FILE_COUNT_MAX]; + saveplatform_t platform; // platform-specific state (paths, card handles) +} save_t; +``` + +### Streams + +`savestream_t` (`save/savestream.h`) is a raw byte cursor used to serialize/deserialize `savefile_t`. Platform backends in `src/dusk{platform}/save/` implement the actual I/O: +- Linux: filesystem files in a save directory. +- GameCube/Wii: memory card via libogc. + +### API + +```c +saveInit(); +saveLoad(slot); // reads platform storage → savefile_t +saveWrite(slot); // writes savefile_t → platform storage +saveDelete(slot); +saveExists(slot); // bool +saveGet(slot); // returns savefile_t * +saveDispose(); +``` + +--- + +## Locale / i18n (`src/dusk/locale/`) + +Translations are loaded from `.po` files in `assets/locale/` (e.g. `en_US.po`). `localemanager.c` manages the active locale and exposes a key→string lookup. `assetlocaleloader.c` parses the PO format via the asset system. + +All player-visible strings must go through the locale system rather than being hardcoded. The locale is loaded asynchronously via the asset system so it is available before the first scene renders. diff --git a/.claude/rpg/world.md b/.claude/rpg/world.md new file mode 100644 index 00000000..c518e283 --- /dev/null +++ b/.claude/rpg/world.md @@ -0,0 +1,114 @@ +# World + +Source: `src/dusk/rpg/overworld/` + +--- + +## Coordinate system + +Three nested coordinate spaces, each defined in `worldpos.h`: + +| Type | Description | Unit | +|---|---|---| +| `worldpos_t` | Tile-level absolute position `{x, y, z}` | `worldunit_t` (int16) | +| `chunkpos_t` | Chunk-grid position `{x, y, z}` | `chunkunit_t` (int16) | +| `fixed_t[3]` | Smooth sub-tile position used by entities | Q24.8 fixed-point | + +One chunk = `CHUNK_WIDTH × CHUNK_HEIGHT × CHUNK_DEPTH` tiles (16 × 16 × 8). +The loaded world window = `MAP_CHUNK_WIDTH × MAP_CHUNK_HEIGHT × MAP_CHUNK_DEPTH` chunks (5 × 5 × 3). + +Conversion helpers (all in `worldpos.c`): + +```c +worldPosToChunkPos(&worldPos, &chunkPos); // tile → chunk grid +chunkPosToWorldPos(&chunkPos, &worldPos); // chunk grid → tile origin +worldPosToChunkTileIndex(&worldPos); // tile → index within its chunk +chunkPosToIndex(&chunkPos); // chunk grid → linear index in MAP.chunks[] +worldPosToFixed(&worldPos, fixedOut); // tile → entity fixed position +fixedToWorldPos(fixedPos); // entity fixed → tile (truncates frac) +``` + +--- + +## Tiles + +Defined in `tile.h` as a plain `tile_t` enum: + +``` +TILE_SHAPE_NULL — empty / unloaded +TILE_SHAPE_GROUND — solid flat tile +TILE_SHAPE_RAMP_* — directional ramps (N/S/E/W + diagonals NE/NW/SE/SW) +``` + +Key predicates: +- `tileIsWalkable(tile)` — true for GROUND and all ramp shapes. +- `tileIsRamp(tile)` — true only for ramp shapes. + +Entity walk code (`entity.c`) checks both the current tile and the target tile to decide whether the entity steps forward flat, raises one Z level (walking up a ramp), or falls one Z level (stepping onto a downward ramp from above). + +--- + +## Chunks + +`chunk_t` (`chunk.h`) holds: +- `position` — its `chunkpos_t` in the world grid +- `tiles[CHUNK_TILE_COUNT]` — flat array of `tile_t`, indexed by `chunkGetTileIndex()` +- `vertices[CHUNK_VERTEX_COUNT]` / `mesh` — pre-baked mesh uploaded to GPU on load +- `entities[CHUNK_ENTITY_COUNT_MAX]` — indices into `ENTITIES[]` currently in this chunk (sentinel `0xFF`) +- `testColor` — temporary debug color (checkerboard), will be replaced by real tileset data + +Tile layout within a chunk is `z * W*H + y * W + x` (Z-major, row-major in XY). + +--- + +## Map + +`map_t MAP` (`map.h`) is the single global map instance. + +```c +chunk_t chunks[MAP_CHUNK_COUNT]; // flat storage — index is NOT world position +chunk_t *chunkOrder[MAP_CHUNK_COUNT]; // draw-order sorted pointers into chunks[] +chunkpos_t chunkPosition; // world-grid origin of the loaded window +bool_t loaded; +``` + +### Load / unload + +`mapInit()` allocates chunk meshes and performs the initial load of all chunks in the starting window. + +`mapPositionSet(newPos)` shifts the window: +1. Determines which of the `MAP_CHUNK_COUNT` slots remain within the new window vs. fall outside it. +2. Calls `mapChunkUnload()` on every chunk that falls outside (nulls its entity slots, zeroes `vertCount`). +3. Reuses freed slots for newly-in-range chunks; calls `mapChunkLoad()` on each. +4. Rebuilds `chunkOrder[]` in XYZ order for the new position. + +### Chunk load (current stub) + +`mapChunkLoad()` currently: +- Fills all tiles with `TILE_SHAPE_GROUND` +- Assigns a checkerboard debug color based on chunk XY parity +- Bakes a flat sprite-batch quad mesh for the z=0 layer and uploads it via `meshFlush()` +- Skips mesh generation for z > 0 chunks (they're empty) + +### Tile lookup + +```c +tile_t mapGetTile(const worldpos_t position); +``` + +Converts `position` to its chunk, looks up the chunk in `chunkOrder`, then indexes into `chunk->tiles[]`. Returns `TILE_SHAPE_NULL` for any out-of-bounds position or when the map is not loaded. + +--- + +## Camera + +`rpgcamera_t RPG_CAMERA` (`rpgcamera.h`) has two modes: + +```c +RPG_CAMERA_MODE_FREE // free worldpos; camera.free holds the position +RPG_CAMERA_MODE_FOLLOW_ENTITY // tracks ENTITIES[followEntityId] +``` + +`rpgCameraGetPosition()` returns the active world tile position in either mode. + +The scene renderer (`sceneoverworld.c`) uses `rpgCameraGetPosition()` to build the `glm_lookat` view matrix. When following an entity, it sub-tile interpolates between `entity->lastPosition` and `entity->position` using `entity->animTime / ENTITY_ANIM_WALK_DURATION` to smooth movement. diff --git a/.claude/save.md b/.claude/save.md new file mode 100644 index 00000000..b57b9c8a --- /dev/null +++ b/.claude/save.md @@ -0,0 +1,96 @@ +# Save & Locale + +--- + +## Save system (`src/dusk/save/`) + +Slot-based persistent storage. Currently disabled in engine init (commented out in `engine.c`) — the system is fully implemented but not yet wired up. + +### Slots + +Up to `SAVE_FILE_COUNT_MAX` (3) save slots. The global `SAVE` holds all slots: + +```c +saveInit(); +saveExists(slot); // bool_t — check before load +saveLoad(slot); // read from platform storage → SAVE.files[slot] +saveWrite(slot); // write SAVE.files[slot] → platform storage +saveDelete(slot); +savefile_t *f = saveGet(slot); +saveDispose(); +``` + +### Save file format + +`savefile_t` is the serialized struct stored per slot. Currently minimal: + +```c +typedef struct { + char_t header[3]; // "DSK" + uint32_t version; // SAVE_FILE_VERSION = 1 + bool_t exists; +} savefile_t; +``` + +Extend this struct to add game-specific save data (player position, story flags, etc.). + +### Stream serialization (`save/savestream.h`) + +`savestream_t` is a cursor used to read/write a save slot's bytes. It CRC32-checksums all data written through it and verifies the checksum on read. + +Write a save: +```c +savestream_t stream; +// (platform opens stream for slot) +saveFileWriteHeader(&stream, SAVE_FILE_HEADER); +saveFileWriteVersion(&stream, SAVE_FILE_VERSION); +saveFileWriteBool(&stream, myFlag); +saveFileWriteInt32(&stream, myInt); +saveFileWriteString(&stream, myString, sizeof(myString)); +saveStreamFinalizeWriteImpl(&stream); // writes CRC +``` + +Read a save: +```c +saveFileReadHeader(&stream, headerBuf); +saveFileReadVersion(&stream, &version); +saveFileReadBool(&stream, &myFlag); +saveFileReadInt32(&stream, &myInt); +saveFileReadString(&stream, myString, sizeof(myString)); +saveStreamVerifyChecksumImpl(&stream, slot); // returns error if CRC mismatch +``` + +All multi-byte values are stored in little-endian byte order. The `saveFile*` macros are thin wrappers over the `*Impl` functions that integrate `errorChain` — always use the macros. + +### Platform backends + +Each `src/dusk{platform}/save/` provides `saveplatform_t` (e.g. a file path on Linux, a memory-card handle on GameCube). The stream implementations (`savestream{platform}.c`) do the actual I/O. + +--- + +## Locale / i18n (`src/dusk/locale/`) + +Translations are stored as GNU `.po` files in `assets/locale/`. Only `en_US.po` currently exists. + +### Loading + +`localemanager_t LOCALE` tracks the active locale and its in-progress asset entry: + +```c +localeManagerInit(); // loads en_US by default +localeManagerSetLocale(&LOCALE_EN_US); // switch locale (async load) +localeManagerDispose(); +``` + +`LOCALE_EN_US` is a predefined `localeinfo_t` constant (`name = "en-US"`, `file = "locale/en_US.po"`). + +### Looking up strings + +```c +char_t buf[128]; +localeManagerGetText("my.key", buf, sizeof(buf), 1, /* format args */ ); +``` + +The macro handles plural forms and `printf`-style format arguments. Pass plural `1` for singular, any other value for plural. + +`assetlocaleloader.c` parses the `.po` format (msgid / msgstr pairs) into a key→string table during the async asset load phase. diff --git a/.claude/style.md b/.claude/style.md new file mode 100644 index 00000000..9f918a67 --- /dev/null +++ b/.claude/style.md @@ -0,0 +1,382 @@ +# Coding Style + +All source is C11. Everything below is derived from the existing codebase — match it exactly. + +--- + +## File structure + +### Headers (`.h`) + +```c +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "direct/dependency.h" +``` + +- `#pragma once` always, never `#ifndef` guards. +- No blank line between the license block and `#pragma once`. +- One blank line between `#pragma once` and the first `#include`. + +### Sources (`.c`) + +```c +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "thisfile.h" +#include "assert/assert.h" +#include "util/memory.h" +``` + +- First include is always the matching `.h` for this `.c` file. +- Remaining includes follow with no separator unless logically grouped (then one blank line between groups — see [Include order](#include-order)). + +--- + +## Include order + +In `.c` files, include in this order with a blank line between each group: + +1. The matching header (e.g. `#include "entity.h"`) +2. Core utilities (`assert/assert.h`, `util/memory.h`, `util/math.h`, etc.) +3. Engine subsystems (`display/...`, `input/...`, etc.) +4. Domain subsystems (`rpg/...`, `scene/...`, etc.) + +In `.h` files, only include what the header directly requires. Never include more than necessary to make the type definitions in that header compile. + +All include paths are relative to `src/dusk/` (the root include directory). Use the full path: +```c +#include "rpg/overworld/map.h" // correct +#include "map.h" // wrong +``` + +--- + +## Line length + +80-character limit. Break before it, not after. + +Multi-parameter function signatures break one param per line, with 2-space indent, closing `)` on its own line before `{`: + +```c +errorret_t textureInit( + texture_t *texture, + const int32_t width, + const int32_t height, + const textureformat_t format, + const texturedata_t data +) { +``` + +Same rule for calls that don't fit on one line: + +```c +assertTrue( + data.paletted.palette->count == + mathNextPowTwo(data.paletted.palette->count), + "Palette color count must be a power of 2" +); +``` + +--- + +## Indentation and spacing + +- **2 spaces** per indent level. No tabs. +- No space between a control keyword and its `(`: + ```c + if(x) { // correct + if (x) { // wrong + ``` +- No space between a function name and its `(` in either declarations or calls. +- Opening brace on the same line: + ```c + void entityUpdate(entity_t *entity) { + if(x) { + for(int i = 0; i < n; i++) { + ``` +- Closing brace always on its own line, except `} else {` and `} while(...)`. +- One blank line between function definitions in `.c` files. +- No trailing whitespace. + +--- + +## Naming + +| Kind | Convention | Example | +|---|---|---| +| Types (struct/union/typedef) | `snake_case_t` | `entity_t`, `worldpos_t` | +| Struct tags | `struct name_s` | `struct entity_s` | +| Union tags | `union name_u` | `union texturedata_u` | +| Enum tags (when typedef'd separately) | `name_enum_t` | `entitytype_enum_t` | +| Functions | `subsystemVerb` (camelCase, noun-first) | `entityInit`, `mapGetTile` | +| Macro constants | `UPPER_SNAKE_CASE` | `CHUNK_WIDTH`, `FIXED_ONE` | +| Function-like macros | `camelCase` (same as functions) | `errorThrow`, `assertNotNull` | +| Global subsystem instances | `UPPER_SNAKE_CASE` | `ENGINE`, `MAP`, `ENTITIES` | +| Local variables | `camelCase` | `tileNew`, `spriteCount` | +| Parameters | `camelCase` | `texture`, `worldPos` | + +Subsystem prefix always comes first in function names: `textureInit`, `shaderBind`, `spriteBatchFlush`. The verb describes the action: `Init`, `Update`, `Dispose`, `Get`, `Set`, `Is`, etc. + +--- + +## Typedefs + +### Structs + +```c +typedef struct { + uint8_t id; + entitytype_t type; +} entity_t; +``` + +Use a named tag (`struct entity_s`) only when forward declaration is required: + +```c +typedef struct entity_s { + // ... +} entity_t; +``` + +### Unions + +```c +typedef union texturedata_u { + struct { + uint8_t *indices; + palette_t *palette; + } paletted; + color_t *rgbaColors; +} texturedata_t; +``` + +### Enums + +When the enum values need to be a compact integer (common for arrays and flags), declare the enum separately and typedef an integer type: + +```c +typedef enum { + ENTITY_TYPE_NULL, + ENTITY_TYPE_PLAYER, + ENTITY_TYPE_NPC, + ENTITY_TYPE_COUNT +} entitytype_enum_t; + +typedef uint8_t entitytype_t; // actual type used everywhere +``` + +Always include `_NULL` as the first value (zero) and `_COUNT` as the last value. + +--- + +## `#define` constants + +All-caps, underscores. Wrap multi-token expressions in parentheses: + +```c +#define CHUNK_WIDTH 16 +#define CHUNK_HEIGHT CHUNK_WIDTH +#define CHUNK_TILE_COUNT (CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH) +``` + +Multi-line macros: backslash continuation, body indented 2 spaces, closing line has no backslash: + +```c +#define errorThrow(message, ...) \ + return errorThrowImpl(\ + &ERROR_STATE, ERROR_NOT_OK, __FILE__, __func__, __LINE__, (message), \ + ##__VA_ARGS__ \ + ) +``` + +--- + +## `const` usage + +Mark every pointer and value parameter `const` unless the function modifies it: + +```c +void entityTurn(entity_t *entity, const entitydir_t direction); +errorret_t textureInit(texture_t *texture, const int32_t width, ...); +``` + +`entity_t *entity` is non-const because the function writes to it; `direction` is const because it is read-only. + +--- + +## `void` in no-argument functions + +Use `(void)` in definitions and declarations of zero-parameter functions: + +```c +errorret_t engineUpdate(void); +errorret_t spriteBatchFlush(void); +``` + +--- + +## Global subsystem state + +Each subsystem exposes a single global instance declared `extern` in the header and defined (once) in the `.c` file: + +```c +// entity.h +extern entity_t ENTITIES[ENTITY_COUNT]; + +// entity.c +entity_t ENTITIES[ENTITY_COUNT]; +``` + +Never define a subsystem global as `static` in a header. + +--- + +## Assertions + +Place assertions at the very top of a function, before any logic: + +```c +void entityInit(entity_t *entity, const entitytype_t type) { + assertNotNull(entity, "Entity pointer cannot be NULL"); + assertTrue(type < ENTITY_TYPE_COUNT, "Invalid entity type"); + // ... actual logic +} +``` + +Available assertion macros (from `assert/assert.h`): +- `assertNotNull(ptr, msg)` +- `assertNull(ptr, msg)` +- `assertTrue(expr, msg)` +- `assertFalse(expr, msg)` +- `assertUnreachable(msg)` +- `assertStringEqual(a, b, msg)` +- `assertIsMainThread(msg)` / `assertNotMainThread(msg)` + +--- + +## Error handling style + +Functions that can fail return `errorret_t`. Three patterns: + +```c +// Propagate a child call's failure and return from this function: +errorChain(someCall()); + +// Return success: +errorOk(); + +// Return failure: +errorThrow("Descriptive message %s", variable); +``` + +`errorChain` is used inline — do not capture the result first: +```c +errorChain(textureInitPlatform(texture, width, height, format, data)); // correct +errorret_t r = textureInitPlatform(...); errorChain(r); // wrong +``` + +--- + +## Struct initialization + +Use C99 designated initializers for any struct literal with more than one field: + +```c +static const entitycallback_t ENTITY_CALLBACKS[ENTITY_TYPE_COUNT] = { + [ENTITY_TYPE_NULL] = { NULL }, + + [ENTITY_TYPE_PLAYER] = { + .init = playerInit, + .movement = playerInput + }, +}; +``` + +```c +shadermaterial_t material = { + .unlit = { + .color = COLOR_WHITE, + .texture = NULL + } +}; +``` + +--- + +## Fixed-size array iteration + +Prefer pointer arithmetic with `do/while` over index loops for iterating through fixed global arrays: + +```c +entity_t *ent = ENTITIES; +do { + if(ent->type == ENTITY_TYPE_NULL) continue; + // ... +} while(++ent, ent < &ENTITIES[ENTITY_COUNT]); +``` + +Use `for` loops when an index variable is actually needed. + +--- + +## Comments + +Comments explain *why*, not *what*. One short inline comment is fine; multi-line block comments for non-obvious invariants only. + +```c +// Walking up a ramp — only the direction the ramp faces is valid. +if(tileIsRamp(tileCurrent) && ...) { +``` + +Section labels inside long functions are acceptable: + +```c +// Chunks +{ + ... +} + +// Entities +{ + ... +} +``` + +Doc comments on public functions use Javadoc style with `@param` / `@return`: + +```c +/** + * Gets the tile at the given world position. + * + * @param position The world position. + * @return The tile at that position, or TILE_NULL if the chunk is unloaded. + */ +tile_t mapGetTile(const worldpos_t position); +``` + +--- + +## Platform-conditional code + +Use the `DUSK_*` compile-definition macros set by `cmake/targets/.cmake`: + +```c +#ifdef DUSK_THREAD_PTHREAD + #include "thread/thread.h" + extern pthread_t ASSERT_MAIN_THREAD_ID; +#endif +``` + +Never use `#ifdef __linux__`, `#ifdef _WIN32`, etc. directly — go through the engine macros. diff --git a/.claude/systems.md b/.claude/systems.md new file mode 100644 index 00000000..b8ee5939 --- /dev/null +++ b/.claude/systems.md @@ -0,0 +1,177 @@ +# Engine Systems + +Smaller systems that support the engine but don't warrant their own file each. + +--- + +## Time (`src/dusk/time/`) + +`dusktime_t TIME` tracks fixed and dynamic delta time. + +```c +TIME.delta // fixed_t: always DUSK_TIME_STEP (16ms default) on fixed-rate platforms +TIME.time // fixed_t: total elapsed time in seconds +``` + +On platforms with `DUSK_TIME_DYNAMIC` (Linux/SDL2): +```c +TIME.dynamicDelta // fixed_t: actual time since last frame +TIME.dynamicTime // fixed_t: total elapsed (dynamic) +TIME.dynamicUpdate // bool_t: true when a real tick occurred +``` + +Call `timeUpdate()` once per frame (before input/logic). `timeGetEpoch()` returns the current wall-clock time as a `dusktimeepoch_t`. + +### Epoch time (`time/timeepoch.h`) + +`dusktimeepoch_t` stores a Unix timestamp (double) with timezone offset. Utilities: + +```c +dusktimeepoch_t e = timeGetEpoch(); +timeEpochGetHours(e) // 0–23 +timeEpochGetMinutes(e) // 0–59 +timeEpochGetSeconds(e) // 0–59 +timeEpochGetDayOfMonth(e) // 0–30 +timeEpochGetMonth(e) // 0–11 +timeEpochGetYear(e) + +// Format: %Y year, %m month, %d day, %H hour, %M minute, %S second +timeEpochFormat(e, "%Y-%m-%d %H:%M:%S", buf, sizeof(buf)); + +// Compare: returns -1, 0, 1 +timeEpochCompare(a, b); + +// Timezone shift: +dusktimeepoch_t local = timeEpochSwitchTimeZone(e, offsetHours); +``` + +--- + +## Thread (`src/dusk/thread/`) + +Currently only the pthread backend (`DUSK_THREAD_PTHREAD`) exists. Thread objects are `thread_t` — used primarily by the asset loader. + +```c +threadInit(&thread, myCallback); // myCallback: void (*)(thread_t *) +threadStart(&thread); // blocks until thread is RUNNING +threadStop(&thread); // requests stop, blocks until STOPPED + +// Inside the thread callback: +while(!threadShouldStop(&thread)) { /* work */ } +``` + +State flow: `STOPPED → STARTING → RUNNING → (STOP_REQUESTED) → STOPPED`. + +### Mutex (`thread/threadmutex.h`) + +```c +threadMutexInit(&lock); +threadMutexLock(&lock); +threadMutexUnlock(&lock); +threadMutexTryLock(&lock); // non-blocking; returns false if already held +threadMutexWaitLock(&lock); // release lock and sleep until signalled +threadMutexSignal(&lock); // wake a thread waiting on this mutex +threadMutexDispose(&lock); +``` + +### Thread-local storage + +`THREAD_LOCAL` expands to `__thread` (GCC) on pthread platforms. Used for the per-thread error state (`ERROR_STATE` in `error/error.h`). + +--- + +## Event (`src/dusk/event/`) + +A fixed-capacity multicast callback list. + +```c +// Declare backing arrays (choose a size): +eventcallback_t cbs[4]; +void *users[4]; +event_t myEvent; +eventInit(&myEvent, cbs, users, 4); + +eventSubscribe(&myEvent, myCallback, myUser); +eventUnsubscribe(&myEvent, myCallback); +eventInvoke(&myEvent, params); // calls all subscribers; params passed as-is +``` + +Callback signature: `void cb(void *params, void *user)`. + +`event_t` does not own its callback/user arrays. Always declare them alongside the event in the same struct or as static arrays. + +--- + +## Console (`src/dusk/console/`) + +`CONSOLE` is a scrolling in-game terminal for debug output. On POSIX platforms (`DUSK_CONSOLE_POSIX`) it also polls stdin in a thread so commands can be typed during a running session. + +```c +consolePrint("Value is %d", x); // printf-style; thread-safe +consoleDraw(); // renders visible history lines to screen +``` + +Configuration constants (`consoledefs.h`): +- `CONSOLE_LINE_MAX` — 512 chars per line +- `CONSOLE_HISTORY_MAX` — 16 lines of scrollback +- `CONSOLE_EXEC_BUFFER_MAX` — 32 pending exec commands + +`CONSOLE.visible` controls whether `consoleDraw()` actually renders anything. + +--- + +## Log (`src/dusk/log/`) + +Two simple output functions, implemented per-platform: + +```c +logDebug("format %s", arg); // debug output (stdout on Linux, debug channel on consoles) +logError("format %s", arg); // error output; may pause execution on some platforms +``` + +These go directly to the platform's native output and are not buffered by the console history. + +--- + +## System / Platform (`src/dusk/system/`) + +`systemInit()` runs platform-specific startup (e.g. Wii PAD init, PSP kernel setup). Must be the first call in `engineInit()`. + +```c +systemplatform_t p = systemGetPlatform(); // SYSTEM_PLATFORM_LINUX, _PSP, etc. +systemdialogtype_t d = systemGetActiveDialogType(); +// SYSTEM_DIALOG_TYPE_NONE / RENDER_BLOCKING / TICK_BLOCKING +``` + +The full platform list is defined via X-macro in `system/systemplatformlist.h`: + +| Constant | Value | +|---|---| +| `SYSTEM_PLATFORM_LINUX` | 0 | +| `SYSTEM_PLATFORM_KNULLI` | 1 | +| `SYSTEM_PLATFORM_PSP` | 2 | +| `SYSTEM_PLATFORM_GAMECUBE` | 3 | +| `SYSTEM_PLATFORM_WII` | 4 | + +--- + +## Network (`src/dusk/network/`) + +`network_t NETWORK` manages platform connection state. The engine calls `networkInit` / `networkUpdate` / `networkDispose`; game code uses the request API: + +```c +networkRequestConnection(onConnected, onFailed, onDisconnect, user); +networkRequestDisconnection(onComplete, user); +networkIsConnected(); // bool_t +``` + +State machine: `DISCONNECTED → CONNECTING → CONNECTED → DISCONNECTING → DISCONNECTED`. + +Network address info (after connection): +```c +networkinfo_t info = networkGetInfo(); +// info.type = NETWORK_TYPE_IPV4 / IPV6 +// info.ipv4.ip[4] or info.ipv6.ip[16] +``` + +Platform backends implement `networkPlatformInit/Update/Dispose/IsConnected`. Currently only Linux (socket-based) and PSP/Vita are implemented. diff --git a/.claude/ui.md b/.claude/ui.md new file mode 100644 index 00000000..814432fe --- /dev/null +++ b/.claude/ui.md @@ -0,0 +1,123 @@ +# UI System + +Source: `src/dusk/ui/` + +The UI system is an immediate-mode layer drawn on top of the game scene each frame. Elements register themselves; `uiUpdate()` ticks all elements and `uiRender()` draws them. All coordinates are in screen pixels. + +--- + +## Lifecycle + +``` +uiInit() → uiTextboxInit() // init order matters — textbox depends on display being ready +uiUpdate() // each frame, before rendering +uiRender() // each frame, after scene render +uiDispose() +uiTextboxDispose() +``` + +--- + +## Element registration (`ui/uielement.h`) + +Elements are stored in `UI_ELEMENTS[]`. Each has a type and a `draw` callback: + +```c +typedef struct { + uielementtype_t type; + errorret_t (*draw)(); +} uielement_t; +``` + +Currently `UI_ELEMENT_TYPE_NATIVE` elements call their `draw` function directly. New debug/HUD elements are registered by adding to this array. + +--- + +## Textbox (`ui/uitextbox.h`) + +`UI_TEXTBOX` is the global dialogue box. It word-wraps text, paginates it, and plays a typewriter scroll effect. + +```c +uiTextboxSetText("Long dialogue string..."); // wraps to charsPerLine, paginates +uiTextboxUpdate(); // each frame: advance scroll, check input +uiTextboxDraw(); // draw box + text + +// Pagination: +uiTextboxPageIsComplete() // true when all chars of current page are visible +uiTextboxHasNextPage() +uiTextboxNextPage() + +// Subscibe to page events: +eventSubscribe(&UI_TEXTBOX.onPageComplete, cb, user); +eventSubscribe(&UI_TEXTBOX.onLastPage, cb, user); +``` + +`UI_TEXTBOX.advanceAction` defaults to the input action that advances dialogue — set it before calling `uiTextboxInit()` if the default doesn't suit. + +Layout constants: +- `UI_TEXTBOX_TEXT_MAX` — 1024 chars +- `UI_TEXTBOX_LINES_MAX` — 64 lines +- `UI_TEXTBOX_LINES_PER_PAGE_MAX` — 3 lines visible at once +- `UI_TEXTBOX_SCROLL_CHARS_PER_TICK` — 1 char per tick (typewriter speed) + +The textbox uses `UI_TEXTBOX.frame` (a `uiframe_t`) for its border rendering and `UI_TEXTBOX.font` for text. + +--- + +## UI Frame (`ui/uiframe.h`) + +9-slice bordered box rendered with a tileset: + +```c +uiFrameInit(&frame); +uiFrameDraw(&frame, x, y, width, height); +uiFrameDispose(&frame); +``` + +The tileset is loaded from the asset system during `uiFrameInit()`. The 9 slices are arranged in the tileset grid as: top-left corner, top edge, top-right corner, left edge, fill, right edge, bottom-left, bottom edge, bottom-right. + +--- + +## Loading overlay (`ui/uiloading.h`) + +`UI_LOADING` is a full-screen loading indicator with fade-in/fade-out transitions: + +```c +uiLoadingShow(onShownCallback, user); // fade in; calls callback when fully opaque +uiLoadingHide(onHiddenCallback, user); // fade out; calls callback when fully transparent +uiLoadingUpdate(delta); // each frame +uiLoadingDraw(); // each frame, over everything +``` + +`UI_LOADING_FADE_DURATION` is `FIXED(0.5f)` seconds. Subscribe to `UI_LOADING.onTransitionEnd` for completion events. + +--- + +## Full-box overlay (`ui/uifullbox.h`) + +Two global full-screen color overlays: `UI_FULLBOX_UNDER` (drawn before game content) and `UI_FULLBOX_OVER` (drawn after). Used for scene transitions (fade to black, etc.): + +```c +uiFullboxTransition( + &UI_FULLBOX_OVER, + COLOR_TRANSPARENT, COLOR_BLACK, + FIXED(0.5f), + EASING_IN_OUT_CUBIC +); +eventSubscribe(&UI_FULLBOX_OVER.onTransitionEnd, myCallback, NULL); + +uiFullboxUnderDraw(); // draw under layer +uiFullboxOverDraw(); // draw over layer +``` + +--- + +## FPS counter (`ui/uifps.h`) + +`UIFPS` tracks a rolling average FPS. `uiFPSDraw()` renders it in the corner. Currently drawn as part of the debug HUD (not wired to an element slot). + +--- + +## Player position HUD (`ui/uiplayerpos.h`) + +`uiplayerpos.c` draws the player's current world tile coordinates. Debug overlay, currently drawn directly in the scene render. diff --git a/.claude/util.md b/.claude/util.md new file mode 100644 index 00000000..cc409886 --- /dev/null +++ b/.claude/util.md @@ -0,0 +1,162 @@ +# Utilities + +Source: `src/dusk/util/` + +--- + +## String (`util/string.h`) + +**Always use these instead of stdlib equivalents** (`strcmp`, `strcpy`, `sprintf`, etc.). + +```c +stringCopy(dest, src, destSize); // safe strncpy; always null-terminates +stringCompare(a, b); // -1 / 0 / 1 +stringEquals(a, b); // bool_t +stringCompareInsensitive(a, b); // case-insensitive -1 / 0 / 1 +stringTrim(str); // in-place strip leading/trailing whitespace +stringFindLastChar(str, c); // last occurrence pointer or NULL +stringFormat(dest, destSize, fmt, ...); // snprintf wrapper; pass NULL dest to get length +stringFormatVA(dest, destSize, fmt, args); // va_list version + +stringIsWhitespace(c); // bool_t + +// Parse: +stringToI32(str, &out) → bool_t +stringToI64(str, &out) → bool_t +stringToI16(str, &out) → bool_t +stringToU16(str, &out) → bool_t +stringToF32(str, &out) → bool_t + +// Suffix checks: +stringEndsWith(str, suffix) → bool_t +stringEndsWithCaseInsensitive(str, suffix) → bool_t +stringIncludesString(haystack, needle) → bool_t +``` + +--- + +## Memory (`util/memory.h`) + +**Always use these instead of `malloc`/`free`/`memcpy`/`memset` directly.** + +```c +memoryAllocate(size) → void * // malloc + tracks pointer count +memoryAlign(alignment, size) → void * // aligned malloc +memoryFree(ptr) // free + decrements count +memoryReallocate(&ptr, size) // realloc +memoryResize(&ptr, oldSize, newSize) // realloc + copy (safe reshape) +memoryTrack(ptr) // track externally-malloc'd pointer + +memoryCopy(dest, src, size) // memcpy +memoryMove(dest, src, size) // memmove +memorySet(dest, value, size) // memset +memoryZero(dest, size) // memset 0 +memoryCompare(a, b, size) → int_t // memcmp + +// Useful for uploading vertex data with different source/dest layouts: +memoryCopyInterleaved(dest, destStride, src, srcStride, elementSize, count); +memoryCopyRangeSafe(dest, start, end, sizeMax); // copy with bounds check + +memoryGetAllocatedCount() → size_t // current malloc'd block count +``` + +--- + +## Math (`util/math.h`) + +```c +mathNextPowTwo(value) → uint32_t // next power of two >= value +mathMax(a, b) // macro +mathMin(a, b) // macro +mathClamp(x, lower, upper) // macro +mathAbs(amt) // macro +mathModFloat(x, y) → float_t // always non-negative modulo +mathLerp(a, b, t) → float_t // linear interpolation (floats) +``` + +For fixed-point lerp use `fixedLerp(a, b, t)` from `util/fixed.h`. + +`MATH_PI` is defined as `M_PI`. + +--- + +## Fixed-point (`util/fixed.h`) + +Q24.8 format: 24-bit integer part, 8-bit fractional part (`int32_t`). See [architecture.md](architecture.md#fixed-point-math) for the full API. + +Quick reference: +```c +FIXED(1.5f) // compile-time literal +fixedFromI32(3) // runtime int → fixed +fixedToFloat(f) // fixed → float (only for GL/platform APIs) +fixedMul(a, b) // multiplication (not just addition) +fixedDiv(a, b) // division +fixedLerp(a, b, t) // lerp where t ∈ [0, FIXED_ONE] +``` + +--- + +## Array (`util/array.h`) + +```c +arrayReverse(array, count, size); // in-place reverse; size = sizeof(element) +``` + +--- + +## Sort (`util/sort.h`) + +```c +sortQuick(array, count, size, compare); // quicksort +sortBubble(array, count, size, compare); // bubble sort (small arrays) +sort(array, count, size, compare); // macro alias for sortQuick + +// Convenience uint8_t sorter: +sortArrayU8(array, count); +int sortArrayU8Compare(const void *a, const void *b); // comparator +``` + +`sortcompare_t` matches `qsort` comparator signature: `int (*)(const void *, const void *)`. + +--- + +## Reference counting (`util/ref.h`) + +```c +refInit(&ref, dataPtr, onLock, onUnlock, onAllUnlocked); +refLock(&ref); // increments count, calls onLock +bool_t hit_zero = refUnlock(&ref); // decrements; calls onAllUnlocked when 0 +``` + +Used internally by the asset entry system to track how many callers hold a reference to a loaded asset entry. + +--- + +## CRC32 (`util/crypt.h`) + +```c +// One-shot: +uint32_t crc = cryptCRC32(data, size); + +// Streaming: +uint32_t acc = cryptCRC32Begin(); +cryptCRC32Update(&acc, chunk1, len1); +cryptCRC32Update(&acc, chunk2, len2); +uint32_t final = cryptCRC32End(acc); +``` + +Used by the save stream to checksum save files. + +--- + +## Endian (`util/endian.h`) + +All serialized data (save files, asset headers) is little-endian. Convert to/from host byte order: + +```c +uint32_t val = endianLittleToHost32(rawU32); +uint16_t val = endianLittleToHost16(rawU16); +float_t val = endianLittleToHostFloat(rawFloat); +``` + +`isHostLittleEndian()` returns a bool at runtime. The compile-time defines `DUSK_PLATFORM_ENDIAN_LITTLE` / `DUSK_PLATFORM_ENDIAN_BIG` are set by `cmake/targets/.cmake`. diff --git a/CLAUDE.md b/CLAUDE.md index ed7bea5e..fb635cf5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,432 +1,65 @@ -# Dusk — Claude Code rules +# CLAUDE.md -## File headers -Every C, H, and JS file starts with: +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -```c -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ +## Project + +Dusk is a C11 RPG game targeting resource-constrained hardware (PSP, GameCube, Wii, PS Vita, Knulli handhelds) and Linux/OpenGL. All game code lives in `src/dusk/`; platform-specific backends live in `src/dusk{platform}/` (e.g. `src/duskgl/`, `src/duskpsp/`, `src/duskdolphin/`). + +Assets are zipped into `dusk.dsk` at build time and loaded at runtime via the asset system. + +## Build + +```bash +# Linux (host) +./scripts/build-linux.sh # outputs build-linux/Dusk + +# Other targets (require Docker) +./scripts/build-psp-docker.sh +./scripts/build-gamecube-docker.sh +./scripts/build-wii-docker.sh +./scripts/build-knulli-docker.sh ``` -JS files use `//` comment style instead. - ---- - -## C conventions - -### Types -Always use the project-defined aliases instead of bare C primitives: - -| Use | Not | -|-----------|--------------| -| `bool_t` | `bool` | -| `int_t` | `int` | -| `float_t` | `float` | -| `char_t` | `char` | - -Use `uint8_t`, `uint16_t`, `int32_t`, etc. for fixed-width integers. -All struct and enum types end in `_t` (`animation_t`, `errorret_t`, …). - -### Naming -- **Functions** — snake_case, prefixed with their module: - `assetLock()`, `entityPositionInit()`, `moduleAssetBatchCtor()` -- **Struct fields** — camelCase: `keyframeCount`, `localPosition` -- **Macros / constants** — UPPER_SNAKE_CASE: - `ENTITY_ID_INVALID`, `ERROR_OK`, `COMPONENT_TYPE_COUNT` -- **Files** — snake_case matching the primary type: `entityposition.c`, - `moduleassetbatch.c` - -### Header files (`.h`) -- Use `#pragma once` — no include guards. -- Declare every public function, `#define`, and `extern` global. -- Write a JSDoc block (`/** … */`) above every declaration explaining - purpose, `@param`s, and `@returns`. -- Only include headers that the `.h` file itself strictly requires for - the types it exposes. Move everything else to the `.c` file. - Do not use forward declarations as a workaround — use the real - include in the `.c` file instead. - -### Implementation files (`.c`) -- Contain function bodies only; no declarations. -- Pull in whatever additional includes the implementation needs. -- Do not use `static` or `inline` on **functions**. Every function, - including internal helpers, must be declared in the matching `.h` and - defined in the `.c` file. Internal helpers belong near the bottom of - the `.c` file, not at the top with a `static` qualifier. - `static` and `inline` on functions are only appropriate when the - function body is written directly inside a `.h` file. - `static` on **variables** (file-scope state) is fine and expected. - -### Formatting -- Hard-wrap all lines at **80 characters**. - -### Error handling -Return `errorret_t` from fallible functions. Use these macros: - -```c -errorOk(); // return success -errorThrow("msg %d", val); // return failure with message -errorChain(someCall()); // propagate failure, continue on success -errorIsOk(ret) / errorIsNotOk(ret) // test a result -errorCatch(ret); // handle + free an error +Each script is a thin wrapper around: +```bash +cmake -S . -B build- -DDUSK_TARGET_SYSTEM= +cmake --build build- -- -j$(nproc) ``` -Never return raw error codes or use `errno` for in-engine errors. +## Tests -### Memory -Use the project allocator — never raw `malloc`/`free`: +```bash +./scripts/test-linux.sh # builds and runs all tests -```c -memoryAllocate(size) // allocate -memoryFree(ptr) // free -memoryZero(dest, size) // zero a block -memoryCopy(dest, src, size) // copy +# Manually run a single test binary after building: +./build-tests/test//test_ ``` -### Asserts -Prefer specific assert macros over bare `assert()`: +Tests use [cmocka](https://cmocka.org/) and are only compiled when `DUSK_BUILD_TESTS=ON`. Test sources live under `test/`. -```c -assertNotNull(ptr, "msg"); -assertTrue(cond, "msg"); -assertFalse(cond, "msg"); -assertUnreachable("msg"); -assertIsMainThread("msg"); -``` +## Key conventions ---- - -## Build system -Each subdirectory has its own `CMakeLists.txt` that adds sources with: - -```cmake -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - myfile.c -) -``` - -Never add source files to the root `CMakeLists.txt` directly. - ---- - -## Platform support - -### Targets -Set `DUSK_TARGET_SYSTEM` at CMake configure time to select a platform: - -| `DUSK_TARGET_SYSTEM` | Macro defined | Platform | -|----------------------|-------------------|------------------| -| `linux` | `DUSK_LINUX` | Linux desktop | -| `knulli` | `DUSK_KNULLI` | Knulli (handheld)| -| `psp` | `DUSK_PSP` | Sony PSP | -| `vita` | `DUSK_VITA` | PlayStation Vita | -| `gamecube` | `DUSK_GAMECUBE` | Nintendo GameCube| -| `wii` | `DUSK_WII` | Nintendo Wii | - -### Layer structure -``` -src/dusk/ core, platform-agnostic game logic -src/duskgl/ OpenGL abstraction (Linux, Knulli, PSP, Vita) -src/dusksdl2/ SDL2 window + input (Linux, Knulli, PSP, Vita) -src/dusklinux/ Linux + Knulli platform impl -src/duskpsp/ PSP platform impl -src/duskvita/ Vita platform impl -src/duskdolphin/ GameCube / Wii platform impl (no SDL2/OpenGL) -``` - -Dolphin is the only target that bypasses SDL2 and OpenGL entirely — -it uses native GameCube/Wii rendering and input APIs. - -### Platform guards -Use the compile-time macros for platform-specific code: - -```c -#ifdef DUSK_PSP - // PSP-only path -#elif defined(DUSK_GAMECUBE) || defined(DUSK_WII) - // GameCube / Wii path -#else - // Generic / Linux fallback -#endif -``` - -Additional capability macros set per-target: -`DUSK_SDL2`, `DUSK_OPENGL`, `DUSK_OPENGL_ES`, `DUSK_OPENGL_LEGACY`, -`DUSK_INPUT_GAMEPAD`, `DUSK_INPUT_KEYBOARD`, `DUSK_INPUT_POINTER`, -`DUSK_PLATFORM_ENDIAN_BIG` / `DUSK_PLATFORM_ENDIAN_LITTLE`. - -### Abstraction pattern -Platform-specific implementations are wired in via `#define` macros in -each platform's `displayplatform.h` / `inputplatform.h` etc., which -the core calls through. Functions that a platform does not support are -simply left undefined — the core guards calls with `#ifdef`. - -### Adding platform-specific code -- Put it under `src/dusk/` in the matching subsystem folder. -- Gate any core call-site with the appropriate `#ifdef DUSK_` - or capability macro. -- Keep the `src/dusk/` core free of platform ifdefs — delegate through - the platform header macros instead. - ---- - -## Adding a new asset loader type -1. Add an enum value to `assetloadertype_t` (before `_COUNT`) in - `src/dusk/asset/loader/assetloader.h`. -2. Add fields to the input/loading/output unions in `assetloader.h`. -3. Implement `assetXxxLoaderSync`, `assetXxxLoaderAsync`, and - `assetXxxDispose` in a new `src/dusk/asset/loader/xxx/` directory. -4. Register the three callbacks in `ASSET_LOADER_CALLBACKS[]` in - `src/dusk/asset/loader/assetloader.c`. -5. If user-facing, create a JS module (see below) and a `.d.ts` file. - ---- - -## Adding a new entity component -1. Create `src/dusk/entity/component//entityMyComp.h/.c` with - struct `entityMyComp_t`, `entityMyCompInit()`, and optionally - `entityMyCompDispose()`. -2. Add the include to `src/dusk/entity/componentlist.h` header block. -3. Add a row to `src/dusk/entity/componentlist.h`: - ```c - X(MYCOMP, entityMyComp_t, myComp, entityMyCompInit, NULL, NULL) - ``` - This auto-generates the enum, union field, and definition entry. -4. If JS-facing, create the script module and `.d.ts` (see below). - ---- - -## Adding a new script (JS) module -1. Create `src/dusk/script/module//moduleMyMod.h/.c`. - - Declare `extern scriptproto_t MODULE_MYMOD_PROTO;` in the header. - - Use `moduleBaseFunction(name)` to define JS-callable functions. - - Register props/funcs in `moduleMyModInit()` with - `scriptProtoDefineProp` / `scriptProtoDefineFunc` / - `scriptProtoDefineStaticFunc`. -2. `#include` the header in - `src/dusk/script/module/modulelist.c` and call - `moduleMyModInit()` in `moduleListInit()` (and `Dispose` in - `moduleListDispose()`). -3. For component modules also register in - `src/dusk/script/module/entity/component/modulecomponentlist.c` - so `entity.add()` returns the typed wrapper. -4. Create `types//mymod.d.ts` and add a - `/// ` line to `types/index.d.ts`. - ---- - -## Script module type declarations -Whenever a `src/dusk/script/module/**/*.c` file is created or modified, -check whether the corresponding `types/**/*.d.ts` needs updating and -apply any changes before finishing the task. - ---- - -## JavaScript (asset scripts) -- Use `var` for module-level state; `const` for values that never - change. -- Always use semicolons. -- Scene objects are plain objects (`var scene = {}`) with assigned - methods. -- Export via `module.exports = scene`. -- Async scene init should use `async function` and `await`. - ---- +- Use `stringCompare`, `stringCopy`, `stringEquals`, etc. from `util/string.h` — never `strcmp`, `strcpy`, and friends directly. +- All functions that can fail return `errorret_t`. Use `errorThrow(...)`, `errorChain(call())`, and `errorOk()` macros — see `error/error.h`. +- Positions and game-world values use `fixed_t` (Q24.8 fixed-point, `int32_t`) — not `float`. Use `FIXED(x)` for literals and the `fixedFrom*`/`fixedTo*` helpers in `util/fixed.h`. ## Coding style -### ASCII only -Source files (`.c`, `.h`, `.js`) must contain only ASCII characters (U+0000–U+007F). -Non-ASCII characters are banned even in comments and string literals. -Use ASCII-only substitutes instead: -- `--` or `-` instead of `—` (em dash) -- `->` instead of `→` (arrow) -- `x` or `*` instead of `×` (multiplication) +See [`.claude/style.md`](.claude/style.md) for the full style guide: indentation, line length, naming, typedefs, defines, include order, `const` usage, assertion placement, error handling, struct initialization, and platform conditionals. -Only non-script asset files (e.g. `.po` locale files) may contain non-ASCII text. +## Architecture & systems -### Indentation -2 spaces. No tabs. - -### Keyword and operator spacing -No space between a keyword or function name and its opening parenthesis: - -```c -if(!ptr) return; -for(uint8_t i = 0; i < count; i++) { -while(entry->state != DONE) { -switch(type) { -sizeof(assetbatch_t) -memoryZero(ptr, size) -``` - -Spaces around all binary operators and after every comma: - -```c -pos->flags |= ENTITY_POSITION_FLAG_WORLD_DIRTY; -(size_t)end - (size_t)start -foo(a, b, c) -``` - -### Braces -Opening brace on the **same line** as the statement (K&R style) for all -constructs — functions, `if`, `else`, `for`, `while`, `switch`: - -```c -void assetEntryLock(assetentry_t *entry) { - ... -} - -if(dirty) { - ... -} else { - ... -} -``` - -### Guard returns -Short guards go on one line with no braces: - -```c -if(!ptr) return; -if(!b || !b->batch) return jerry_undefined(); -if(!(flags & DIRTY)) return; -``` - -### Blank lines -- One blank line between functions; no blank line at the start or end of - a function body. -- One blank line between logical blocks inside a function body. -- No trailing blank lines at the end of a file. - -### Pointer placement -`*` is attached to the variable name, not the type: - -```c -assetentry_t *entry -const char_t *name -void *ptr -uint8_t *d = (uint8_t *)dest; -``` - -### Casts -Space between cast and operand: - -```c -(assetbatch_t *)user -(uint8_t *)dest -(textureformat_t)v -``` - -### Return -No parentheses around the return value: - -```c -return ptr; -return MEMORY_POINTERS_IN_USE; -``` - -### switch / case -`case` indented 2 spaces from `switch`; body indented 2 more from `case`: - -```c -switch(type) { - case ASSET_LOADER_TYPE_TEXTURE: - descs[i].input.texture = (textureformat_t)v; - break; - default: - break; -} -``` - -### Multi-line function signatures -When parameters don't fit on one line, put each on its own line indented -2 spaces; the closing `) {` (definition) or `);` (declaration) goes on -its own line at column 0: - -```c -void assetEntryInit( - assetentry_t *entry, - const char_t *name, - const assetloadertype_t type, - assetloaderinput_t *input -) { - -errorret_t memoryCompare( - const void *a, - const void *b, - const size_t size -); -``` - -### Structs and enums -Anonymous inner struct or enum with a `typedef`, `_t` suffix, closing -brace and name on the same line: - -```c -typedef struct { - errorcode_t code; - char_t *message; -} errorstate_t; - -typedef enum { - ASSET_LOADER_TYPE_NULL, - ASSET_LOADER_TYPE_COUNT -} assetloadertype_t; -``` - -### Designated initialisers -Spaces inside braces; `.field = value`: - -```c -jsassetentry_t e = { .entry = entry }; -assetbatchloadedpend_t init = { .batch = batch }; -``` - -### Ternary operator -Spaces around `?` and `:`: - -```c -const float val = psx > 0.0f ? pt[0][0] / psx : 0.0f; -``` - -### const placement -`const` before the type, `*` attached to the variable: - -```c -const char_t *name -const void *src -const size_t size -``` - -### Comments in `.c` files -- Do not use section dividers (`/* ---- ... ---- */`). Just let the - functions follow one another with a single blank line between them. -- Multi-line explanatory comments inside function bodies use `//` lines: - ```c - // Script modules are freed; orphaned JS wrapper objects now get GC'd - // so their finalizers fire before assetDispose() checks ref counts. - jerry_heap_gc(JERRY_GC_PRESSURE_HIGH); - ``` -- Do not use `/* */` for inline or inline-block comments inside `.c` - function bodies. - -### Comments in `.h` files -Every public declaration gets a Javadoc block (`/** … */`) with -`@param` and `@returns` where relevant. Keep it on the lines immediately -above the declaration with no blank line in between. - ---- - -## Tests -- Tests live in `test/` mirroring `src/dusk/` structure. -- Use cmocka; include `dusktest.h`. -- Test functions: `static void test_something(void **state)`. -- After each test, assert `memoryGetAllocatedCount() == 0` to catch - leaks. -- Build with `-DDUSK_BUILD_TESTS=ON`. +| Doc | Covers | +|---|---| +| [`.claude/architecture.md`](.claude/architecture.md) | Platform abstraction pattern, subsystem lifecycle, error handling, code-generation pipeline | +| [`.claude/display.md`](.claude/display.md) | Rendering pipeline: display state, screen, framebuffer, mesh, shader, texture, spritebatch, text | +| [`.claude/input.md`](.claude/input.md) | Input actions, buttons, bindings, axis helpers, events | +| [`.claude/asset.md`](.claude/asset.md) | Asset archive, entry lifecycle, loader types, async/sync split, low-level file I/O | +| [`.claude/ui.md`](.claude/ui.md) | UI element system, textbox, frames, loading overlay, fullbox transitions, FPS counter | +| [`.claude/animation.md`](.claude/animation.md) | Keyframe animation, easing functions | +| [`.claude/systems.md`](.claude/systems.md) | Time, threading, mutex, events, console, logging, system/platform, network | +| [`.claude/util.md`](.claude/util.md) | String, memory, math, fixed-point, array, sort, ref counting, CRC32, endian | +| [`.claude/save.md`](.claude/save.md) | Save slots, stream serialization with CRC, locale/i18n | +| [`.claude/rpg/index.md`](.claude/rpg/index.md) | RPG layer overview → [world](.claude/rpg/world.md), [entities](.claude/rpg/entity.md), [cutscenes](.claude/rpg/cutscene.md), [story/items](.claude/rpg/story.md) | +| [`.claude/display-refactor.md`](.claude/display-refactor.md) | Planned render-queue refactor (Saturn port context) |