A sweep of all 265 tracked files, not just the ones the migration touched. Two cross-repository citations named deps/libakerror/TODO.md item 8, a numbering that has not existed for two releases; both now name libakerror issue #18. Three tutorial and plan references pointed at TODO.md for defects that are tracked: the invisible-on-frame-one actor is #39, the control map traps are #81, and the pkg-config gap is #17. plan.md is a live instruction document for writing the manual and still told its reader to file findings in TODO.md; it says the tracker now. Filed while sweeping: a zeroed akgl_ControlMap matches gamepad South, because SDL_GAMEPAD_BUTTON_SOUTH is 0 and the two arms of the match are one expression, and handler_on/handler_off are called without a NULL check. Both are documented as workarounds in chapter 21 and neither had an issue. #81. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
527 lines
33 KiB
Markdown
527 lines
33 KiB
Markdown
# 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 the issue that tracks it |
|
||
|
||
---
|
||
|
||
## 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. (Issue #17.)
|
||
|
||
---
|
||
|
||
## 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 the defect record:
|
||
|
||
- **`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 the tracker
|
||
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 its issue.
|
||
|
||
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, file the issues the writing turned
|
||
up, 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 (issue #52 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.
|