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>
33 KiB
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 stray9.strncpy((char *)&game.name, "sdl3-gametest", 256);— the exact callAGENTS.mdforbids 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:195saysakgl_game_initpasses thephysics.engineproperty toakgl_physics_factory.akgl_game_initnever calls the factory, and the stringphysics.engineappears nowhere insrc/.registry.h:54saysakgl_registry_initcreates the registries.akgl_game_initcalls the eight individual initializers and never callsakgl_registry_initat 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:
- What does libakgl add or constrain here?
- 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_LIMITis not a status code. It is the one-past-the-end sentinel used to computeAKGL_ERR_COUNT(261). There are five codes, not six:AKGL_ERR_COUNTis 5,akgl_error_initnames five, andtests/error.casserts five.AKGL_ERR_REGISTRYis not raised byakgl_actor_set_character. One raise site in the library:src/controller.c:491.akgl_actor_initializeraisesAKERR_KEYon a failed registry write;akgl_actor_set_characterraisesAKERR_NULLPOINTER.AKGL_ERR_BEHAVIORis never raised by the library at all — only bytests/.akgl_get_json_with_defaultdoes default onAKERR_OUTOFBOUNDS(src/json_helpers.c:208, fixed in 0.5.0). The status it deliberately does not default on isAKERR_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). AndAKERR_INDEXis never raised — it appears only as aHANDLE_GROUParm.
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_initdoes it first; a host with its own startup path must too.akgl_registry_iterate_actoris an SDL callback returningvoidthat ends inFINISH_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_defaulthands back the context it was given when it does not handle the status, so aCLEANUPthat also releases it double-releases and corrupts the failure instead of reporting it.AGENTS.mddocuments 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
#errorfeature tests ininclude/akgl/error.h. akgl.pcnames no dependencies at all — noRequires:— despiteakerror.hbeing 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;
2for 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
cds 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>: messageon stderr; output mismatches diffed throughcat -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_collideis not implemented — it raisesAKERR_API, andakgl_physics_simulatenever callscollideat all.arcade_movedoes 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 inmovementlogicfuncand says exactly why.- There is no terminal velocity. Gravity accumulates into
eyunbounded — the physics sim reaches 560 px/s in 0.7 s and keeps going.physics.drag.yis the only brake (eyapproachesgravity_y / drag_y) and is not documented as such anywhere today. - Releasing a direction stops the actor dead.
akgl_actor_cmhf_*_offzeroestx/tyand 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 shapemkcontrollermappings.shwas 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 insrc/tilemap.c; nothing in the library ever opens a.tsj.akgl_tilemap_load_tilesets_each(src/tilemap.c:139-159) readscolumns,firstgid,tilecountandimagedirectly off each element of the map'stilesetsarray, andtests/assets/testmap.tmjis embedded. An external stub fails withAKERR_KEYon the missingcolumns.Four further constraints verified against the loader, all of which bind the tutorial maps:
- Every object in an object group needs a
typestring, plain rectangles included. A missingtypefails the whole load.- Perspective markers need
"type": "perspective", not merely the namep_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:
- Read
src/, not the header prose. Thephysics.engineandakgl_registry_initclaims are both false and have been quoted forward. "A premise nobody has re-checked is where the next defect is hiding." - 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.
- Every fence carries an info string. Prefer
run=, thenwrap=, thenexcerpt=; anorunblock is a decision that has to be justified. - Do not restate signatures — link the Doxygen reference, or
excerpt=the header. - 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
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:
./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_examplespasses, and its census line accounts for every block. A risingnoruncount is the harness being talked out of its job, and is a review finding.ctest -R docs_screenshotspasses with no figure regenerated.- Every status code in
include/akgl/error.happears in Table 1, and every status the library actually raises appears in Table 2 — checked by greppingsrc/forFAIL_*andAKERR_status arguments, not by reading the headers. - Both example games build, and their headless smoke runs exit 0.
ctest -R api_surfaceand-R headersstill pass — writing a subsystem chapter tends to turn up a symbol declared nowhere, which is precisely whatapi_surfaceis for.doxygen Doxyfileis still clean;WARN_AS_ERROR = FAIL_ON_WARNINGSmeans a header touched while documenting must stay fully documented.scripts/reindent.sh --checkis clean —examples/andtools/are C sources in this tree and the pre-commit hook will reindent them.scripts/memcheck.sh -R docs_examples— therun=blocks execute real library code and are a genuine new memory-check vehicle, which is the shapeAGENTS.mdasks 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.mdinstead). - Fixing the false claims in
physics.handregistry.h. The chapters document what the code does; correcting the header comments is a separate commit, sinceAGENTS.mdrequires 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.