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 `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.
|