diff --git a/TODO.md b/TODO.md index 67586b6..815ac09 100644 --- a/TODO.md +++ b/TODO.md @@ -2184,3 +2184,180 @@ a live defect -- but nothing rejects it, and `max_timestep` is caller-settable. non-square sprite draws at its own proportions through the library's own `renderfunc`. This item is the half that remains: one uniform `scale`, with no way to expand a single axis. + +## Found while writing the manual + +Twenty-one chapters and two tutorial games were written against `src/` rather than +against the header comments, and the exercise turned up two distinct classes of problem. +Both are recorded here because publishing a problem you cannot fix yet is still a +contribution, and because the second class is the more dangerous one: **every item in it +was documented, in a header, incorrectly.** + +The manual documents each of these where a reader would hit it, and points here. + +### Defects with no prior entry + +1. **`akgl_game_update` calls `simulate` through a NULL pointer.** `akgl_game_update` + invokes `akgl_physics->simulate(akgl_physics, NULL)` with no NULL check, and + `akgl_default_physics` is zeroed BSS — all four method pointers are NULL. A program + that does not call `akgl_physics_init_arcade`/`_null` itself therefore **segfaults on + its first frame**, measured as exit 139, rather than raising `AKERR_NULLPOINTER`. + `physics.h:8-9,195` tells the reader `akgl_game_init` selects a backend from a + `physics.engine` property, so a caller who believes the header writes exactly the + program that crashes. This is the worst first-contact experience in the library and + the fix is a NULL check. Closing it touches `src/game.c` only. + +2. **Parent/child offset is double-counted at draw time.** `src/physics.c:192-197` writes + a child's `x` as `parent->x + vx` — an absolute world coordinate. `src/actor.c:273-279` + then draws it at `parent->x + obj->x`, while `actor_visible` three lines above tests + the camera against the raw `obj->x` as absolute. Two readings of one field inside one + function. Confirmed by measurement: with the player at (280,146) and a (-14,+10) + offset, the guarded draw lands at (130,114) and the unguarded one at (410,260), off a + 320x240 screen; rendering both and hashing `SDL_RenderReadPixels` gives different + images. Invisible only while the parent sits at the origin. `actor.h` documents **both** + readings, in two places. `examples/jrpg` works around it with a `renderfunc` that nulls + `obj->parent` for the duration of the draw. + +3. **Actors and characters unregister under a different key than they register.** + `akgl_actor_initialize` (`src/actor.c:46`) and `akgl_character_initialize` + (`src/character.c:39`) register under the caller's untruncated `name`, while + `akgl_heap_release_actor` (`src/heap.c:132`) and `_release_character` + (`src/heap.c:165`) clear using the object's truncated 128-byte field. Past 127 bytes + those are different keys, so releasing leaves a live registry entry pointing at a + zeroed pool slot. Distinct from "Truncated registry keys can collide" above, which + describes sprites and spritesheets, where the truncated name genuinely *is* the key. + +4. **Every actor spawned from a map is invisible on frame one.** + `akgl_actor_initialize` sets `movement_controls_face`, and the default `facefunc` + clears every facing bit and re-sets one only from a *movement* bit. An NPC has no + movement bit, so state 17 falls to 16, which maps to no sprite, and the actor is + silently skipped by the draw. The same thing makes a player character vanish the + moment they stop walking. Both tutorials clear the field on every live actor after + loading a map; that workaround should not be necessary. + +5. **`use_own_physics` is set and never read.** `akgl_tilemap_load_physics` builds a + complete `akgl_PhysicsBackend` on the map when it declares a `physics.model` property, + and sets `map->use_own_physics` — and nothing in the library ever consults it. A map's + declared physics is silently ignored unless the application writes the switch itself. + `README.md` used to show that `if`, which made a caller-side workaround read like a + library feature. + +6. **`akgl_TilemapLayer` does not record a layer's name.** `akgl_tilemap_load_layers` + reads `id`, `opacity`, `visible`, `x`, `y` and `type` and drops the `name` Tiled + wrote; the only `name` retained anywhere is per-object. A game therefore cannot ask + for "the terrain layer" and must hard-code a numeric layer id, which changes whenever + somebody reorders layers in the editor. Both tutorials hard-code one. + +7. **`akgl_character_sprite_add` leaks a reference when the same sprite is re-added.** + `ref->refcount += 1` is unconditional but the matching release is guarded by + `displaced != ref`, so re-adding a sprite to a state it already occupies leaks a pool + slot per call. + +8. **`akgl_tilemap_compute_tileset_offsets` silently requires `spacing == 0` and + `margin == 0`.** It adds `spacing` to the tile pitch but sets row 0's y offset to + `spacing` rather than 0, and ignores `margin` entirely. A tileset with a gutter — which + is most published tileset packs — renders misaligned with no diagnostic. This + materially constrains what art the library can consume; it ruled out several otherwise + suitable CC0 packs while sourcing the tutorial assets. + +9. **`akgl_actor_render` hard-codes `SDL_FLIP_NONE`** (`src/actor.c:283`). There is no + mirrored blit, so a side-on character needs both facings drawn in the sheet. That is + why `docs/tutorials/assets/sidescroller/player.png` carries six frames rather than + three, and it doubles the art cost of every such character. + +10. **`akgl_Actor::layer` is unbounded but `draw_world` stops at `AKGL_TILEMAP_MAX_LAYERS`.** + An actor assigned layer 16 or above is accepted, updated, simulated — and never drawn. + +11. **`akgl_registry_load_properties` leaks one string-pool slot per failed property.** + The per-property `CLEANUP` block is empty. The string pool is 256 slots, so a + sufficiently malformed properties file drains it. + +12. **`speedtime` is dead.** It is loaded from character JSON (`src/character.c:263`), + written through `(int *)&obj->speedtime` on a `uint64_t` field — correct only because + the struct was zeroed and the host is little-endian — and then read by nothing. Frame + timing comes from `sprite->speed`. + +13. **`AKGL_SPRITE_MAX_REGISTRY_SIZE` is dead.** Defined in `sprite.h`, referenced + nowhere in `src/`, `include/`, `tests/` or `util/`. + +14. **`AKGL_TILEMAP_MAX_TILES_PER_IMAGE` is checked nowhere**, and costs 512 KiB per + tileset regardless of the image's real tile count. + +15. **There is no sound-effect API.** `akgl_audio_*` is a synthesizer that reads no files; + `akgl_load_start_bgm` is the only file-audio entry point, and its infinite-loop + request is set on property set 0, so background music plays once. A game cannot load + and play a sound effect through this library at all. + +16. **A literal 512x512 tilemap is rejected.** Both bounds are `>=`, so the documented + `AKGL_TILEMAP_MAX_WIDTH`/`_HEIGHT` of 512 is off by one and 512x511 is the largest + map that loads. + +17. **Unverified asset provenance.** `tests/assets/World_A1.png` and + `util/assets/Actor1.png` carry the default filenames of RPG Maker's bundled art and + ship with no license file, while `tests/assets/akgl_test_mono.ttf` sits beside + `akgl_test_mono.LICENSE.txt`. RPG Maker's bundled assets are licensed to users of that + product; redistributing them inside a C library is not something that license covers. + The tutorial assets under `docs/tutorials/assets/` deliberately do not depend on + either file, and `tests/docs_setups/tilemap.sh` says why it stages a different image. + Closing this means replacing two fixtures and the maps that reference them. + +18. **`util/assets/littleguy.json` does not load.** The sample data for `charviewer`, the + one demo program the library ships, uses the pre-0.5.0 unprefixed state names + (`ACTOR_STATE_ALIVE`) and `velocity_x`/`velocity_y` instead of `speed_x`/`speed_y`. + The current loader accepts neither. + +### Header comments that describe code that has changed + +Twenty-seven claims across the public headers were false when checked against `src/`. +`AGENTS.md` already warns that this file "carried eleven entries describing code that had +already changed"; this is the same failure in the headers, and Doxygen publishes it. + +Nothing catches these. `WARN_IF_UNDOCUMENTED` proves a symbol *has* a comment, not that +the comment is true, and `api_surface` strips comments precisely because prose is not a +declaration. The full list is in the manual, each noted in the chapter that covers the +subsystem, but the ones that would actively mislead a caller are: + +- `physics.h:8-9,195` — the `physics.engine` property and `akgl_game_init` calling the + factory. Neither exists. See defect 1 above for what this costs. +- `README.md` (now corrected) — "ONLY supports TilED TMJ tilemaps with tileset **external** + references". Backwards: `"source"` appears nowhere in `src/tilemap.c`, and + `akgl_tilemap_load_tilesets_each` reads `columns`/`firstgid`/`tilecount`/`image` inline. + Only **embedded** tilesets load. +- `renderer.h` — `frame_start`/`frame_end`/`draw_texture` "dereference `self` before it is + checked". Each function's first statement is `FAIL_ZERO_RETURN(errctx, self, ...)`. +- `sprite.h` — `speed` is "seconds, scaled to milliseconds". It is milliseconds scaled to + nanoseconds. Also claims `frames` is unbounded (bounded at `src/sprite.c:207`) and that + `akgl_sprite_initialize` overreads via `memcpy` (it uses `aksl_strncpy`). +- `character.h` — `speedtime` "in seconds" (milliseconds), and `sprite_add` never releasing + a displaced sprite (`src/character.c:60,77-79` releases it). +- `actor.h` — the `cmhf` block comment says the `_off` handlers zero acceleration, thrust, + environmental *and* velocity. They zero only `ax`/`tx` or `ay`/`ty`; zeroing `ey` was the + gravity-cancel defect fixed in 0.6.0, and two `@note`s still describe it. +- `json_helpers.h` — the conventions block says `dest` is not NULL-checked and that only + `akgl_get_json_string_value` checks its key. All eleven accessors check `dest`, and all + seven key-taking accessors check `key`. +- `assets.h:17-18` — "`akgl_game_init` (or a bare `akgl_audio_init`) has to have run + first". `akgl_mixer` is created only in `akgl_game_init`; `akgl_audio_init` opens the + synthesizer's stream and never touches it, so `akgl_load_start_bgm` after only that + hands NULL to `MIX_LoadAudio`. +- `registry.h:54` — `akgl_registry_init` creating seven registries and not properties. It + creates eight including properties; the genuinely false part is that `akgl_game_init` + never calls it at all, calling the eight individually in a different order. +- `tilemap.h:59-60,354-357,447-450,462-468` — object and tileset counts unbounded, and + `akgl_tilemap_release` double-freeing. All fixed; the surviving half of the last one is + that `release` does not release the map's actors. +- `controller.h:232-234,262-263` — a negative `controlmapid` not rejected. Both call sites + check. + +### Entries in this file that are themselves stale + +- **"Known and still open" item 15** — `akgl_path_relative` leaking an error-context slot + per ENOENT call — is fixed. `src/util.c:115-129` sets a flag in the `HANDLE` block and + calls `path_relative_root` after `FINISH`, which is exactly the fix the item proposes. +- **`TODO.md:2027`**, under "akgl.pc names no dependencies", says "`akglConfig.cmake` + re-finds the targets". No such file is generated or installed — `install()` ships the + library, the headers and `akgl.pc` only, so `find_package(akgl)` cannot work against an + install tree at all. That makes the section's problem worse than it states. +- **`AGENTS.md:636`** says `sim_step()` sets `gravity_time` to `now - dt`. + `tests/physics_sim.c` sets it to `0` and drives the step through `max_timestep`; the + `now - dt` form was replaced because it went red under parallel `ctest`. diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..3b2eedc --- /dev/null +++ b/plan.md @@ -0,0 +1,526 @@ +# Write the libakgl manual + +## Context + +libakgl 0.7.0 exports **156 `akgl_*` functions** across 20 public headers, and the only +user-facing documentation is a 404-line `README.md` written as a FAQ. Half of it is +developer process (git hooks, mutation testing, perf suites, memcheck) rather than +anything a person building a game needs, and **the half that is user-facing does not +compile**: + +- `PASS(e, akgl_heap_next_spritesheet(&sheet);` — unbalanced parentheses, twice. +- `sprite->frameids = [0, 1, 2, 3];` — not C in any dialect. +- `myactor->state = 9AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT);` — a stray `9`. +- `strncpy((char *)&game.name, "sdl3-gametest", 256);` — the exact call `AGENTS.md` + forbids under **Copying Into Fixed-Width Fields**, in the first snippet a reader sees. +- `int screenwidth = NULL;` + +The prose has drifted the same way, and two claims were confirmed false against `src/`: + +- `physics.h:195` says `akgl_game_init` passes the `physics.engine` property to + `akgl_physics_factory`. `akgl_game_init` never calls the factory, and the string + `physics.engine` appears nowhere in `src/`. +- `registry.h:54` says `akgl_registry_init` creates the registries. `akgl_game_init` + calls the eight individual initializers and never calls `akgl_registry_init` at all. + +`util/assets/littleguy.json` — the sample data for the one shipped demo — still uses the +pre-prefix state names (`"ACTOR_STATE_ALIVE"`) and `velocity_x`, neither of which the +current loader accepts. + +This is not a reader routing around typos. It is what happens to samples and prose that +nothing executes, and `AGENTS.md` already makes the argument in another context — *"A +test that has not failed has not been tested"*, *"Do not trust a comment, a TODO entry, or +a CI exclusion that states a premise."* The sibling `akbasic` repository solved exactly +this: every example in its manual is compiled or run by CTest, and a chapter that drifts +from the code turns a job red. + +The outcome wanted: a `docs/` manual — introduction, design philosophy, a chapter per +subsystem, and two tutorials building a complete 2D sidescroller and a complete top-down +JRPG — with akbasic's harness ported so no sample in it can rot. + +## Decisions taken + +| Question | Answer | +|---|---| +| Tutorial assets | Vendor a curated Kenney.nl **CC0** subset, with `LICENSE`, a provenance manifest, and a refresh script | +| Tutorial code | Real compiling, runnable targets under `examples/`, built by default and in CI | +| Existing `README.md` | Split: the FAQ half seeds `docs/` (corrected); the developer-process half stays | +| Known defects | Documented inline in the owning chapter, cross-referenced to `TODO.md` | + +--- + +## The governing editorial rule: reference upstream, do not restate it + +**This manual documents libakgl. It does not re-document its dependencies.** + +libakgl sits on libakerror, libakstdlib, SDL3, SDL3_image, SDL3_mixer, SDL3_ttf, jansson +and the Tiled map format. Every one of those is documented by its own project, by people +who own the code. A chapter that restates the `ATTEMPT`/`CLEANUP`/`PROCESS`/`HANDLE`/ +`FINISH` protocol is a chapter that will be wrong the day libakerror changes it, and +nothing in this repository's test suite would notice — the drift this whole project exists +to fix, reintroduced from a different direction. + +So each chapter answers exactly two questions and links out for the rest: + +1. **What does libakgl add or constrain here?** +2. **What does a libakgl caller actually write?** — a small, harness-verified example. + +| Topic | Owned upstream — link, do not restate | What this manual owes the reader | +|---|---|---| +| `ATTEMPT`/`CLEANUP`/`PROCESS`/`HANDLE`/`FINISH`, `PASS`, `CATCH`, `IGNORE` | `deps/libakerror` | **The status-code tables below**; `akgl_error_init()` ordering; libakgl's own hazards | +| `aksl_strncpy`, `aksl_fclose`, `aksl_atoi` and friends | `deps/libakstdlib` | Which ones libakgl requires you to use, and why (`AGENTS.md` already argues it) | +| `SDL_Renderer`, `SDL_Texture`, events, `SDL_PropertiesID` | SDL3 wiki | The backend vtable, the frame contract, what libakgl does to the renderer's state | +| Audio decoding, `MIX_Audio`, mixers and tracks | SDL3_mixer | `akgl_load_start_bgm`, the track table, the separate three-voice synthesizer (ours) | +| TTF rasterizing and metrics | SDL3_ttf | The font registry, the teardown ordering trap, the rasterize-per-call cost | +| The TMJ map format, layers, tilesets, custom properties | Tiled documentation | libakgl's **extensions** and **limits** — actor objects, `physics.model`, the perspective band, `AKGL_TILEMAP_MAX_*` | +| `json_t`, `json_decref`, the jansson API | jansson manual | `akgl_get_json_*` — which status means "absent" vs "wrong type", and the borrowed-reference rule | + +Chapter 04 is the sharpest case. It does **not** teach the error protocol; it says "the +protocol is libakerror's and is documented there", then spends its length on the three +things that are genuinely libakgl's: the status-code tables, the libakgl-specific traps, +and one worked example of a real libakgl call sequence. + +### One more decision, forced by what the exploration found + +**`docs/` is a narrative manual, not a second reference.** Every header already carries a +substantial `@file`/`@brief` block explaining the subsystem's *design rationale*, and +`Doxyfile` sets `WARN_IF_UNDOCUMENTED = YES` with `WARN_AS_ERROR = FAIL_ON_WARNINGS`, so +an undocumented symbol already fails CI. The gap is navigation and worked examples, not +reference text. + +So the chapters teach a task and link to the generated Doxygen for per-function detail. +They do not restate 156 signatures — a hand-copied signature table is exactly the artifact +that drifts, and it would compete with a reference CI already keeps honest. Where a chapter +genuinely needs a declaration or a constant table in front of the reader, it uses a +```` ```c excerpt=include/akgl/heap.h ```` block, so the text *is* the header. + +--- + +## The error-code tables + +These are the exception to the rule above, and the reason the exception exists is +concrete: libakerror documents the *mechanism*, but only libakgl can say which statuses +its own 156 functions raise and what they mean here. A caller writing a `HANDLE` block +needs that, and it is written down nowhere today. **These tables are a required +deliverable, not a nice-to-have.** + +Three tables, in chapter 04, with the appendix carrying the full cross-reference. + +> **Corrected during execution.** The tables below were written from header prose and were +> wrong in five places; the chapter as built carries the verified version. Recorded here +> because being wrong about the error codes, in the plan that exists to fix wrong +> documentation, is the joke writing itself. +> +> - **`AKGL_ERR_LIMIT` is not a status code.** It is the one-past-the-end sentinel used to +> compute `AKGL_ERR_COUNT` (261). There are **five** codes, not six: `AKGL_ERR_COUNT` is +> 5, `akgl_error_init` names five, and `tests/error.c` asserts five. +> - **`AKGL_ERR_REGISTRY` is not raised by `akgl_actor_set_character`.** One raise site in +> the library: `src/controller.c:491`. `akgl_actor_initialize` raises `AKERR_KEY` on a +> failed registry write; `akgl_actor_set_character` raises `AKERR_NULLPOINTER`. +> - **`AKGL_ERR_BEHAVIOR` is never raised by the library at all** — only by `tests/`. +> - **`akgl_get_json_with_default` does default on `AKERR_OUTOFBOUNDS`** +> (`src/json_helpers.c:208`, fixed in 0.5.0). The status it deliberately does *not* +> default on is `AKERR_TYPE` — a missing key can take a default, a malformed one cannot. +> - **Three statuses were missing**: `AKERR_TYPE` (11 raise sites), `AKERR_VALUE` (8), +> `AKERR_RELATIONSHIP` (1). And **`AKERR_INDEX` is never raised** — it appears only as a +> `HANDLE_GROUP` arm. + +### Table 1 — libakgl's own status codes + +All six are offsets from `AKERR_FIRST_CONSUMER_STATUS`, declared in `include/akgl/error.h` +under owner string `AKGL_ERR_OWNER` (`"libakgl"`). The chapter renders this as an aligned +table; the values come in as an `excerpt=include/akgl/error.h` block so the constants +cannot drift from it. + +| Code | Value | Means | Typically raised by | What the caller does | +|---|---|---|---|---| +| `AKGL_ERR_SDL` | base + 0 | An SDL call failed; the message carries `SDL_GetError()` | Anything touching a window, texture, renderer or mixer | Usually fatal at startup; check the driver and the asset path | +| `AKGL_ERR_REGISTRY` | base + 1 | A name lookup or registration failed | `akgl_actor_set_character`, registry writes | Check the name and that the asset was loaded *before* the thing referencing it | +| `AKGL_ERR_HEAP` | base + 2 | A pool is exhausted | every `akgl_heap_next_*` | **Normally a missing release, not a small pool.** Raise the `AKGL_MAX_HEAP_*` override only after checking | +| `AKGL_ERR_BEHAVIOR` | base + 3 | A call was made in a state that forbids it | lifecycle and ordering violations | Fix the call order; see the startup sequence in chapter 07 | +| `AKGL_ERR_LOGICINTERRUPT` | base + 4 | **Not a failure — a control signal.** "Skip the rest of this tick for this actor" | your own `movementlogicfunc` | Raise it deliberately; `akgl_physics_simulate` swallows it. A backend's `gravity`/`move` must **never** raise it — there it aborts the whole step | +| `AKGL_ERR_LIMIT` | base + 5 | A fixed compile-time bound was exceeded | loaders hitting `AKGL_*_MAX_*` | Reduce the asset, or raise the bound and rebuild *everything* linking libakgl | + +### Table 2 — libakerror statuses libakgl raises, and what they mean *here* + +The statuses themselves are libakerror's; their libakgl meaning is not documented +anywhere. This table is what lets a caller write a `HANDLE` block with confidence. + +| Status | What it means when a libakgl function raises it | +|---|---| +| `AKERR_NULLPOINTER` | A required pointer argument was `NULL`, or a required field (`akgl_game.name`/`.version`/`.uri`) was empty. Also what `akgl_text_rendertextat` raises for an empty string — while `akgl_text_measure` accepts one | +| `AKERR_KEY` | **A key is absent.** The idiomatic "optional thing was not there" status: a missing JSON key, a character with no sprite for a state, a map with no properties. Frequently a `HANDLE` block rather than a failure | +| `AKERR_INDEX` | An array index was out of range | +| `AKERR_OUTOFBOUNDS` | A value did not fit its destination — `aksl_strncpy` truncation, a frame id past `uint8_t`, a flood fill past `AKGL_DRAW_MAX_FLOOD_SPANS`. **Note:** `akgl_get_json_with_default` does *not* default on this one, which is why it cannot currently give an array element a default | +| `AKERR_IO` | A read or write failed. Distinct from `AKERR_EOF` — that separation is the whole reason `aksl_fgetc` exists | +| `AKERR_EOF` | End of input. Sometimes the desired outcome, as in `require_at_eof` in `src/game.c` | +| `AKERR_API` | The function is not implemented. Currently `akgl_physics_arcade_collide` and `akgl_render_2d_draw_mesh` — both reached by ordinary-looking calls, so both get a chapter callout | + +### Table 3 — the exit-status trap + +Not a status list; a table because the failure is arithmetic and silent. + +| You write | Wait status the shell sees | Why | +|---|---|---| +| `exit(AKGL_ERR_SDL)` | **0 — a clean run** | An exit status is one byte; libakgl's band starts at 256 | +| `akerr_exit(status)` | 0→0, 1–255→itself, else 125 | `AKERR_EXIT_STATUS_UNREPRESENTABLE` | + +Every suite in `tests/` once reported success on the most common failure a library built +on SDL can have. This belongs in the manual because a reader writing their own `main` will +write the first line. + +### libakgl-specific traps that are ours to document + +Not the protocol — the places libakgl makes the protocol bite in a way libakerror's docs +cannot anticipate: + +- **`akgl_error_init()` must run before anything that can raise**, or every libakgl error + prints as "Unknown Error". `akgl_game_init` does it first; a host with its own startup + path must too. +- **`akgl_registry_iterate_actor` is an SDL callback returning `void`** that ends in + `FINISH_NORETURN`, so libakerror's default unhandled-error handler **exits the process**. + A reader meets this the first time a sprite name is wrong. +- **`akgl_get_json_with_default` hands back the context it was given** when it does not + handle the status, so a `CLEANUP` that also releases it double-releases and corrupts the + failure instead of reporting it. `AGENTS.md` documents the correct shape; the chapter + shows it as a verified example because a reader will hit it writing their first loader. +- **libakerror ≥ 2.0.1 is a hard floor**, enforced by two `#error` feature tests in + `include/akgl/error.h`. +- **`akgl.pc` names no dependencies at all** — no `Requires:` — despite `akerror.h` being + part of libakgl's public interface. Chapter 03 says so and gives the flags to add by + hand. (`TODO.md`.) + +--- + +## Part 1 — The verification harness + +Ported from `akbasic/tests/docs_examples.sh` (550 lines, bash + awk, no third-party deps). +The `c` / `excerpt` / `sh` / `output` / `norun` / `text` machinery is generic and transfers +directly; what drops out is everything about running an interpreter, and what gets added is +the ability to link and run, because libakgl's samples are C rather than BASIC. + +**New file: `tests/docs_examples.sh`.** Same contract, and these house conventions are +worth copying verbatim rather than reinventing: + +- **Exit status is the number of failed examples**; `2` for usage/setup errors. +- **A fence with no info string is a hard error**, and so is an unknown one. The failure + mode the whole harness exists to prevent is passing because it quietly ran nothing. +- Paths forced absolute, because the script `cd`s into sandboxes. +- An unreadable named document is `exit 2` — akbasic learned this when an empty generator + expression contributed an empty argument that replaced the entire document list, and the + suite passed having checked nothing. +- A closing census line every run, treated as part of the result. +- `FAIL :: message` on stderr; output mismatches diffed through `cat -A`, + because the bugs it catches are trailing spaces and missing newlines. + +### Block kinds + +| Info string | What happens | +|---|---| +| ```` ```c ```` | `cc -fsyntax-only -std=gnu99 -Wall -Werror` with the project include path | +| ```` ```c wrap=NAME ```` | The same, wrapped in `tests/docs_preludes/NAME.pre` / `.post` | +| ```` ```c run=NAME ```` | **New.** Compile, link against `akgl`, run headless; stdout compared to a following ```` ```output ```` block | +| ```` ```c excerpt=PATH ```` | Must appear verbatim in `PATH` (comment- and whitespace-insensitive); not compiled | +| ```` ```c screenshot=NAME ```` | **New.** Linked against the figure host; renders `docs/images/NAME.png` | +| ```` ```json kind=KIND ```` | **New.** Loaded through the real loader: `sprite`, `character`, `tilemap`, `properties` | +| ```` ```output ```` | Claimed by the preceding runnable block; an unclaimed one is a failure | +| ```` ```sh ```` / `sh norun` | Run in a sandbox and must exit 0 / shown only | +| ```` ```cmake ```` , ```` ```text ```` , `norun` | Never executed; counted as skipped, so the decision is explicit | + +`-Werror` on snippets, unlike the library: `AGENTS.md` keeps `AKGL_WERROR` off by default +precisely because libakgl is consumed with `add_subdirectory` and a new compiler's +diagnostic should not break someone else's build. A doc snippet is not a consumer, and a +sample that warns is a sample that teaches the warning. `gnu99` rather than `c99`, for +akbasic's reason — `akerror.h` uses `PATH_MAX`, which `` hides under +`__STRICT_ANSI__`. + +`#line` directives are emitted before each body, so a diagnostic reads +`docs/14-physics.md:112: error: ...` rather than pointing into a scratch file. + +### Three additions akbasic does not have + +**`c run=NAME`** is the reason to do this at all for a game library. `-fsyntax-only` proves +a call typechecks; it does not prove the startup order works, that a sprite loads, or that +an `ATTEMPT` block releases what it acquired. A `run` block links a real binary and +executes it under `SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy SDL_RENDER_DRIVER=software` +— the same forcing `scripts/memcheck.sh` already uses. It asserts a clean exit **and** +greps stderr for a raised context, because a libakgl program can fail and still exit 0. + +**`json kind=`** exists because the asset formats are documented in prose and read by +`src/sprite.c`, `src/character.c`, `src/tilemap.c` and `src/registry.c`, with nothing tying +the two together. That gap is not hypothetical: `util/assets/littleguy.json` is already +invalid against the loader it ships with. A small `tools/docs_checkjson.c` writes the block +to a temp file and calls the matching `akgl_*_load_json`, so the documented format is the +format the loader accepts. + +**`c screenshot=NAME`** — `tools/docs_screenshot.c` supplies `main`, brings the library up +headless at a stated size, calls the snippet's `docs_frame(void)`, then +`SDL_RenderReadPixels` and writes the PNG via SDL3_image. Copy two of akbasic's decisions +exactly: **any stdout during a render is a failure** (an image of a blank screen is worse +than no image), and **`--check` compares and never repairs** (a test that fixes what it +measures passes the second time for the wrong reason). + +Regeneration is `cmake --build build --target docs_screenshots`, never part of `all`. +Figures are tracked, and `docs/images/README.md` says they are generated — the same +tracked-generated-artifact contract `AGENTS.md` already spells out for +`SDL_GameControllerDB.h`. + +### Support files and CMake wiring + +``` +tests/docs_examples.sh the verifier +tests/docs_preludes/*.pre/.post akglbody, akglfile, akglapp, akglframe +tests/docs_setups/*.sh asset fixtures a chapter should not have to show +tools/docs_checkjson.c the json kind= validator +tools/docs_screenshot.c the figure host +tools/docs_screenshots.sh figure generation and --check +docs/images/ tracked, generated +``` + +Include paths come from a `file(GENERATE)`'d `docs_cflags.txt`, so the transitive path +through `akerror`, `akstdlib`, SDL3 and jansson has one source of truth. Register +`docs_examples` and `docs_screenshots` with `add_test`, `WORKING_DIRECTORY` at the source +root — and note `CMakeLists.txt:76-102` shadows `add_test` only when top-level, so follow +the file's existing idiom rather than calling the builtin. + +Follow akbasic's CI decision: **no docs-path filter.** Documentation goes stale because the +code moved, not because somebody edited a chapter. + +A prelude must never let a wrong example compile. A prelude declaring `akgl_game_init` +itself would defeat the check. + +--- + +## Part 2 — Chapter structure + +`docs/README.md` is a hand-maintained two-column TOC, matching akbasic's conventions: +`NN-kebab-case.md`, H1 repeating the ordinal, no front matter, H2/H3 only (no H4), relative +links between chapters. + +| | | +|---|---| +| `01-introduction.md` | What libakgl is, what it refuses to be, the 0.03 ms frame budget from `PERFORMANCE.md`, and the dependency map — who owns which documentation | +| `02-design-philosophy.md` | Pools not `malloc`, backends not branches, errors that carry context, bit flags, name-based registries | +| `03-getting-started.md` | `add_subdirectory` vs `akgl.pc` (and the missing `Requires:`), the first window | +| `04-errors.md` | **The three status tables above.** Protocol referenced, not restated; libakgl's own traps; one worked call sequence | +| `05-the-heap.md` | The five pools, `akgl_heap_next_*`, `akgl_String`, the refcount asymmetry, `AKGL_MAX_HEAP_*` as an ABI constraint | +| `06-the-registry.md` | The eight registries, configuration properties, the id-0 silent-no-op, key truncation | +| `07-the-game-and-the-frame.md` | Startup order, `akgl_game_update`, the state lock, iterators, what savegames do and do not do | +| `08-rendering.md` | `akgl_RenderBackend`, the frame contract, cameras, and **embedding via `akgl_render_2d_bind`** | +| `09-drawing.md` | `draw.h` primitives, colour-as-argument, regions, flood fill's reentrancy limit | +| `10-spritesheets-and-sprites.md` | Sheet sharing by resolved path, the sprite JSON format, animation | +| `11-characters.md` | State→sprite mappings, speeds and accelerations, the character JSON format | +| `12-actors.md` | The 32-bit state mask, the six behaviour hooks, parents and children, layers | +| `13-tilemaps.md` | libakgl's Tiled **extensions and limits** — actor objects, `physics.model`, the perspective band. Format itself referenced | +| `14-physics.md` | thrust/environmental/velocity, `null` and `arcade`, what is not implemented | +| `15-input.md` | Control maps, push-not-poll dispatch, the keystroke ring, the gamepad DB | +| `16-text-and-fonts.md` | The font registry, measuring, the teardown ordering trap | +| `17-audio.md` | The three-voice synthesizer (ours, in full), and the SDL_mixer asset path (referenced) | +| `18-utilities.md` | Collision helpers, path resolution, `akgl_get_json_*` status semantics, static strings | +| `19-tutorial-sidescroller.md` | The first game, start to finish | +| `20-tutorial-jrpg.md` | The second game, start to finish | +| `21-appendix-limits.md` | **Status cross-reference** (which functions raise what), every `AKGL_MAX_*`, the full configuration property table | + +Chapter 04 precedes every subsystem because all 156 functions return +`akerr_ErrorContext AKERR_NOIGNORE *`, and a reader who has not met the tables cannot read +a single example. Chapter 08 carries the embedding seam as a first-class topic rather than +a footnote — `akgl_render_2d_bind` is what akbasic actually consumes. + +Constant tables (`AKGL_MAX_HEAP_*`, the actor state bits, the iterator ops, the status +codes) go in as `excerpt=` blocks against their headers. Those headers hand-align their +bit-flag tables, and `scripts/reindent.el` deliberately avoids `tabify` to preserve that +alignment — an excerpt keeps the alignment and the values honest at once. + +`README.md` keeps its developer-process half and gains a link to `docs/`. Its FAQ half is +deleted, not copied: corrected content lives in the chapters, and one source of truth per +topic is the entire point of the exercise. + +--- + +## Part 3 — The two tutorial games + +Real targets: `examples/sidescroller/` and `examples/jrpg/`, each a complete program, built +by default and exercised in CI by a headless smoke run (*run N frames, exit 0*), so a +tutorial cannot silently stop working. + +The tutorial chapters quote these programs with ```` ```c excerpt=examples/... ```` blocks +rather than restating the code. A chapter then *cannot* drift from a program that builds — +the excerpt check fails the moment the source moves. + +**`examples/sidescroller/`** — gravity, a jump, platforms from a Tiled map, a coin pickup, +a hazard. Exercises arcade physics, `physics.gravity.y`, `AKGL_ACTOR_STATE_MOVING_*`, +tilemap layers, and a custom `movementlogicfunc` (which is where `AKGL_ERR_LOGICINTERRUPT` +stops being a table row and becomes something the reader writes). + +This game forces the chapter to make three honest statements, all from `TODO.md`: + +- **`akgl_physics_arcade_collide` is not implemented** — it raises `AKERR_API`, and + `akgl_physics_simulate` never calls `collide` at all. `arcade_move` does no clamping and + consults no tilemap: an actor walks through a wall and off the edge of the world. The + tutorial implements its own collision in `movementlogicfunc` and says exactly why. +- **There is no terminal velocity.** Gravity accumulates into `ey` unbounded — the physics + sim reaches 560 px/s in 0.7 s and keeps going. `physics.drag.y` is the only brake (`ey` + approaches `gravity_y / drag_y`) and is not documented as such anywhere today. +- **Releasing a direction stops the actor dead.** `akgl_actor_cmhf_*_off` zeroes `tx`/`ty` + and there is no friction or deceleration. Correct for Zelda, wrong for Mario; the chapter + shows the workaround rather than pretending. + +**`examples/jrpg/`** — four-direction walking with per-facing animation, a Tiled town map, +NPCs spawned from map objects, a text box, and a party member as a child actor. Exercises +null-gravity arcade physics, the full four-way state mask, characters with per-facing +sprite mappings, actors auto-created from object layers, `akgl_text_*`, parent/child actors +(children are snapped to the parent and never simulated — a documented behaviour this game +depends on). + +The two are deliberately complementary: the sidescroller is the physics tutorial, the JRPG +is the content-pipeline tutorial. + +--- + +## Part 4 — Assets + +`docs/tutorials/assets/` holds a curated **CC0** subset from Kenney.nl, with: + +- `LICENSE` — the CC0 deed text. +- `PROVENANCE.md` — an aligned table, one row per file: which pack, the source URL, and + what it was cropped or repacked into. +- `scripts/fetch_tutorial_assets.sh` — refreshes from upstream into a temp directory and + moves into place only after verifying the fetch and sanity-checking the contents. This is + exactly the shape `mkcontrollermappings.sh` was *fixed* into for 0.5.0: check curl's + status, check a plausible minimum, never overwrite a good tracked copy from a failed run, + exit non-zero. Do not reintroduce the version that silently destroyed its own fallback. + +CC0 specifically, not merely "free": a reader who copies a tutorial into their own game +inherits whatever obligation the assets carry, and CC0 carries none. + +**Asset contract, fixed up front** so the art and the game code can be built in parallel: +16×16 tiles, 32×32 character frames, sheets counted left-to-right from the top-left as +`akgl_spritesheet_initialize` expects, at most `AKGL_SPRITE_MAX_FRAMES` (16) frames per +animation with frame ids that fit a `uint8_t`. Maps are Tiled TMJ with **embedded** +tilesets, under `AKGL_TILEMAP_MAX_LAYERS` (16) and `AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER` +(128). Actor objects use `"type":"actor"` with a `character` string property and a `state` +**int** property; the string-array form works in character JSON and is *not* accepted here. + +> **Corrected during execution.** This contract originally said *external* tileset +> references, quoting `README.md`: *"The engine ONLY supports TilED TMJ tilemaps with +> tileset external references."* That is backwards, and it is one more stale claim of the +> kind this project exists to fix. `"source"` appears nowhere in `src/tilemap.c`; nothing +> in the library ever opens a `.tsj`. `akgl_tilemap_load_tilesets_each` +> (`src/tilemap.c:139-159`) reads `columns`, `firstgid`, `tilecount` and `image` directly +> off each element of the map's `tilesets` array, and `tests/assets/testmap.tmj` is +> embedded. An external stub fails with `AKERR_KEY` on the missing `columns`. +> +> Four further constraints verified against the loader, all of which bind the tutorial maps: +> +> - **Every object in an object group needs a `type` string**, plain rectangles included. +> A missing `type` fails the whole load. +> - **Perspective markers need `"type": "perspective"`**, not merely the name +> `p_foreground`/`p_vanishing`. The loader checks the type first and silently ignores +> the object otherwise. +> - **A literal 512×512 map is rejected.** Both bounds use `>=`, so 262,144 cells is one +> too many; 512×511 is the largest that loads. +> - **Tileset images resolve through `akgl_path_relative`** (canonicalized, absolute paths +> work) but **image-layer images resolve through a plain `"%s/%s"` join** (absolute paths +> do not). Keep every image path relative to the map file. + +### A defect found while planning + +`tests/assets/World_A1.png` and `util/assets/Actor1.png` carry the default filenames of +RPG Maker's bundled art and have **no license file**, while `tests/assets/akgl_test_mono.ttf` +sits beside `akgl_test_mono.LICENSE.txt`. RPG Maker's bundled assets are licensed to users +of that product; redistributing them inside a C library is not something that license +covers. + +This plan does not fix it — replacing test fixtures is a separate change with its own blast +radius, and it is not blocking the docs. It does two things: the tutorial assets get a clean +provenance story that does not depend on those files, and the finding goes into `TODO.md` +with file, functional consequence and blast radius, per the house practice of documenting +defects against yourself. + +--- + +## Part 5 — How the work is split across subagents + +Wave 1 runs entirely in parallel. Nothing waits on `docs/` existing, because the asset +contract, the block grammar and the status tables are all pinned above — that is what lets +the tutorial work start immediately rather than queueing behind the manual. + +| Agent | Deliverable | Depends on | +|---|---|---| +| **A — harness** | `tests/docs_examples.sh`, preludes, setups, `tools/docs_checkjson.c`, the screenshot pair, CMake wiring, the harness spec section in `README.md` | nothing | +| **B — assets** | Kenney CC0 subset, `LICENSE`, `PROVENANCE.md`, fetch script, both Tiled maps, sprite/character JSON | the asset contract | +| **C — sidescroller** | `examples/sidescroller/` + chapter 19 | asset contract (B's bytes arrive later) | +| **D — JRPG** | `examples/jrpg/` + chapter 20 | asset contract | +| **E — core** | 04 errors **(owns the three tables)**, 05 heap, 06 registry, 07 game/frame, 21 appendix | nothing | +| **F — presentation** | 08 rendering + embedding, 09 drawing, 10 sprites, 11 characters, 12 actors | nothing | +| **G — world** | 13 tilemaps, 14 physics, 15 input | nothing | +| **H — periphery + front matter** | 16 text, 17 audio, 18 utilities, 01 intro, 02 philosophy, 03 getting started | nothing | + +Agent E's status tables are the one wave-1 artifact other agents consume, so E publishes +them first, before writing prose. Every other chapter cites them rather than re-listing +codes locally. + +Every chapter agent gets the same five standing instructions, because the failure this +project exists to fix is documentation asserting things nobody checked: + +1. **Read `src/`, not the header prose.** The `physics.engine` and `akgl_registry_init` + claims are both false and have been quoted forward. *"A premise nobody has re-checked is + where the next defect is hiding."* +2. **Reference upstream, do not restate it.** If the sentence you are writing would still + be true in a project that does not use libakgl, it belongs in a link. Cite the specific + upstream document; do not paraphrase it. +3. **Every fence carries an info string.** Prefer `run=`, then `wrap=`, then `excerpt=`; a + `norun` block is a decision that has to be justified. +4. **Do not restate signatures** — link the Doxygen reference, or `excerpt=` the header. +5. **Note the known defect where a reader would hit it**, with the guard to apply, linked + to `TODO.md`. + +Wave 2 is the integration pass, and the only genuinely serial step: build the TOC, insert +cross-references between chapters and both tutorials, verify no chapter re-teaches upstream +material, reconcile terminology, cut the README's FAQ half, add the `TODO.md` entries, then +run the full gate. + +## Verification + +```sh +cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo +cmake --build build --parallel +ctest --test-dir build --output-on-failure +``` + +Iterating on one chapter without a full run: + +```sh +./tests/docs_examples.sh --root . --cflags-file build/docs_cflags.txt docs/14-physics.md +``` + +Specific things that must hold before this is done: + +- `ctest -R docs_examples` passes, **and its census line accounts for every block**. A + rising `norun` count is the harness being talked out of its job, and is a review finding. +- `ctest -R docs_screenshots` passes with no figure regenerated. +- **Every status code in `include/akgl/error.h` appears in Table 1**, and every status the + library actually raises appears in Table 2 — checked by grepping `src/` for `FAIL_*` and + `AKERR_` status arguments, not by reading the headers. +- Both example games build, and their headless smoke runs exit 0. +- `ctest -R api_surface` and `-R headers` still pass — writing a subsystem chapter tends to + turn up a symbol declared nowhere, which is precisely what `api_surface` is for. +- `doxygen Doxyfile` is still clean; `WARN_AS_ERROR = FAIL_ON_WARNINGS` means a header + touched while documenting must stay fully documented. +- `scripts/reindent.sh --check` is clean — `examples/` and `tools/` are C sources in this + tree and the pre-commit hook will reindent them. +- `scripts/memcheck.sh -R docs_examples` — the `run=` blocks execute real library code and + are a genuine new memory-check vehicle, which is the shape `AGENTS.md` asks for ("a new + path worth checking belongs in a benchmark, where it gets both"). +- Every commit names the agent program, model and version as co-author, per `AGENTS.md`. + +## Out of scope, deliberately + +- Replacing the RPG-Maker-named test fixtures (recorded in `TODO.md` instead). +- Fixing the false claims in `physics.h` and `registry.h`. The chapters document what the + code *does*; correcting the header comments is a separate commit, since `AGENTS.md` + requires style and behaviour changes to stay unbundled. +- Implementing `arcade_collide`, terminal velocity, or friction. The tutorials work around + them and say so. +- Documenting libakerror, libakstdlib, SDL3, jansson or the Tiled format. Linked, not + restated — see the governing editorial rule.