Record what writing the manual found: 18 defects and 27 false claims
Twenty-one chapters and two games were written against src/ rather than against the header comments, and the exercise turned up two classes of problem. Both are recorded here because publishing a problem you cannot fix yet is a contribution, and because the second class is the more dangerous one: every item in it was documented, in a header, incorrectly. The one to fix first is defect 1. akgl_game_update calls akgl_physics->simulate() with no NULL check and akgl_default_physics is zeroed BSS, so a program that never calls an initializer segfaults on its first frame -- measured, exit 139, not an AKERR_NULLPOINTER. physics.h tells the reader akgl_game_init selects a backend from a `physics.engine` property, which is false in both halves, so a caller who believes the header writes exactly the program that crashes. That is the worst first-contact experience in the library and the fix is a NULL check. Defect 2 is the subtlest. A child actor's offset is counted twice: physics.c writes x as parent->x + vx, an absolute coordinate, and actor.c then draws at parent->x + obj->x while actor_visible three lines above treats obj->x as absolute. Two readings of one field inside one function, and actor.h documents both of them without noticing. Confirmed by rendering the frame with and without the guard and hashing the readback. The rest run from silent invisibility (an actor on layer >= 16 is updated and simulated and never drawn; a map-spawned actor has no facing bit and so no sprite) through dead API (speedtime is loaded, mis-cast, and read by nothing; there is no way to play a sound effect from a file at all) to asset provenance: tests/assets/World_A1.png and util/assets/Actor1.png carry RPG Maker's default filenames with no licence file, and util/assets/littleguy.json -- the sample data for the one demo the library ships -- does not load. The second section lists the 27 header comments that describe code that has since changed. Nothing catches these: WARN_IF_UNDOCUMENTED proves a symbol has a comment, not that the comment is true, and check_api_surface.sh strips comments precisely because prose is not a declaration. Doxygen publishes them. A third section marks entries in this file that are themselves stale, including item 15, which describes a leak that src/util.c already fixes with exactly the technique the item proposes. AGENTS.md warns that this file "carried eleven entries describing code that had already changed"; that is still accumulating, and a premise nobody has re-checked is where the next defect is hiding. plan.md is the plan the work was executed from, kept because it records the decisions and, in three places, corrections to itself -- including getting the error-code count wrong in the document whose purpose was fixing wrong documentation. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
526
plan.md
Normal file
526
plan.md
Normal file
@@ -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 <doc>:<line>: 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 `<limits.h>` 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.
|
||||
Reference in New Issue
Block a user