docs/ is a narrative manual, not a second reference. Every header already carries a substantial @file/@brief block and Doxyfile sets WARN_IF_UNDOCUMENTED with WARN_AS_ERROR, so an undocumented symbol already fails CI. The gap was navigation and worked examples. Chapters teach a task and link to the Doxygen output; where a declaration or a constant table has to be in front of the reader it arrives as a `c excerpt=` block, so the text *is* the header and cannot diverge from it. That also preserves the hand-aligned bit-flag tables scripts/reindent.el goes out of its way not to destroy. The manual does not re-document its dependencies. libakerror owns the ATTEMPT/CLEANUP/PROCESS/HANDLE/FINISH protocol, SDL3 owns renderers and events, Tiled owns the map format, jansson owns json_t. A chapter that restated any of them would be wrong the day upstream changed and nothing here would notice -- the same drift this work exists to fix, arriving from a different direction. So each chapter says what libakgl adds or constrains and links out for the rest. Chapter 4 is the exception and the reason for it: libakerror documents the mechanism, but only libakgl can say which statuses its own functions raise and what they mean here, and that was written down nowhere. It carries three tables -- libakgl's five status codes, the libakerror statuses libakgl actually raises with their meaning in this library, and the exit-status trap where `exit(AKGL_ERR_SDL)` is a wait status of 0 because the band starts at 256. Every chapter was written against src/ rather than against the header comments, which is how 27 false claims in those comments came to light. Where a chapter documents a known defect rather than a design decision it says so and points at TODO.md. README.md keeps the development process and hands the reader to docs/. Its task-oriented FAQ is deleted rather than moved, because one source of truth per topic is the whole point and that FAQ's examples did not compile. Census: 39 compiled snippets, 89 verbatim header excerpts, 4 JSON documents run through the real loaders, one linked-and-executed program with its output compared byte for byte, one generated figure. 11 norun blocks, each justified. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
229 lines
13 KiB
Markdown
229 lines
13 KiB
Markdown
# 06. The registry
|
||
|
||
Nothing in this library is passed around by pointer where a name will do. An actor names
|
||
the character it instantiates, a character names the sprites it draws, a sprite names the
|
||
sheet it cuts frames from. All of it is resolved at load time through eight registries,
|
||
and that is what lets the whole asset graph be described in JSON files that reference each
|
||
other by name.
|
||
|
||
**The registries are SDL property sets, not akgl types.** They are `SDL_PropertiesID`
|
||
values with external linkage, so you can enumerate one with `SDL_EnumerateProperties`,
|
||
which is exactly what `akgl_registry_iterate_actor` and
|
||
`akgl_character_state_sprites_iterate` do.
|
||
|
||
## The eight registries
|
||
|
||
| Registry | Key | Value | Written by | Cleared by |
|
||
|---|---|---|---|---|
|
||
| `AKGL_REGISTRY_ACTOR` | actor name | `akgl_Actor *` | `akgl_actor_initialize` | `akgl_heap_release_actor` at refcount 0 |
|
||
| `AKGL_REGISTRY_SPRITE` | sprite name | `akgl_Sprite *` | `akgl_sprite_initialize` | `akgl_heap_release_sprite` at refcount 0 |
|
||
| `AKGL_REGISTRY_SPRITESHEET` | **resolved image path** | `akgl_SpriteSheet *` | `akgl_spritesheet_initialize` | `akgl_heap_release_spritesheet` at refcount 0 |
|
||
| `AKGL_REGISTRY_CHARACTER` | character name | `akgl_Character *` | `akgl_character_initialize` | `akgl_heap_release_character` at refcount 0 |
|
||
| `AKGL_REGISTRY_ACTOR_STATE_STRINGS` | state name | its bit value, as a number | `akgl_registry_init_actor_state_strings` | never |
|
||
| `AKGL_REGISTRY_FONT` | font name (yours) | `TTF_Font *` | `akgl_text_loadfont` | `akgl_text_unloadfont` |
|
||
| `AKGL_REGISTRY_MUSIC` | music name | audio handle | **nothing** — created but never populated | — |
|
||
| `AKGL_REGISTRY_PROPERTIES` | configuration key | string value | `akgl_set_property`, `akgl_registry_load_properties` | never |
|
||
|
||
Three of those rows are worth expanding.
|
||
|
||
**The spritesheet registry is keyed by path, not by name.** That is the sharing mechanism:
|
||
two sprites cutting frames out of the same image find the same `akgl_SpriteSheet` and the
|
||
same `SDL_Texture`, and the texture is loaded once.
|
||
|
||
**The actor-state-string registry exists so JSON can name a bit.**
|
||
`akgl_registry_init_actor_state_strings` walks `AKGL_ACTOR_STATE_STRING_NAMES` and maps
|
||
entry `i` to `1 << i`, which is what lets a character definition write
|
||
`"AKGL_ACTOR_STATE_FACE_LEFT"` instead of `2`. Two entries in that name table disagree with
|
||
`actor.h`: bits 11 and 12 are `UNDEFINED_11` and `UNDEFINED_12` rather than `MOVING_IN` and
|
||
`MOVING_OUT`, **so those two states cannot be named from JSON at all** (`TODO.md` items
|
||
24–26). The individual name registrations are not checked, either.
|
||
|
||
**The music registry is empty.** `akgl_registry_init_music` creates it and nothing in the
|
||
library ever writes to it. It is there for you.
|
||
|
||
## What actually runs at startup
|
||
|
||
`registry.h` documents `akgl_registry_init` as creating "the seven asset registries", with
|
||
a `@warning` that it does not create `AKGL_REGISTRY_PROPERTIES`. **Both halves of that are
|
||
now wrong**, and the correction matters because the warning is what a reader would design
|
||
around.
|
||
|
||
Read against `src/registry.c` and `src/game.c`, this is what is true:
|
||
|
||
- `akgl_registry_init` creates **eight**, including `AKGL_REGISTRY_PROPERTIES`. The
|
||
properties call was added in 0.5.0 (`TODO.md`, "Known and still open" item 3, marked
|
||
fixed). Its order is spritesheet, sprite, character, actor, actor-state-strings, font,
|
||
music, properties.
|
||
- **`akgl_game_init` never calls `akgl_registry_init`.** It calls the eight individual
|
||
initializers itself, in a different order: actor, sprite, spritesheet, character, font,
|
||
music, properties, actor-state-strings.
|
||
|
||
So there are two supported startup paths and they do not share code. If you call
|
||
`akgl_game_init`, you have all eight and need do nothing. If you are building your own
|
||
startup — an embedder binding an existing renderer, say — call `akgl_registry_init` and you
|
||
also have all eight. What you must not do is call some subset by hand and assume the rest
|
||
followed.
|
||
|
||
`akgl_registry_init_actor` is the one initializer that behaves differently in a way you
|
||
would notice: since 0.5.0 every initializer destroys the previous property set before
|
||
creating the replacement, so none of them leaks on a second call, but the *actor* one is
|
||
the one meant to be called again — between levels, paired with `akgl_heap_init_actor`. Note
|
||
that it destroys the **registry**, not the actors; releasing those is
|
||
`akgl_heap_release_actor`'s job.
|
||
|
||
## The id-0 trap
|
||
|
||
Every `SDL_PropertiesID` in `src/registry.c` starts at 0, and SDL treats 0 as "no such
|
||
property set". **Reads return the default and writes are refused.** An uninitialized
|
||
registry therefore does not fail the way you would want it to.
|
||
|
||
How loud that failure is depends entirely on whether libakgl checks SDL's return value, and
|
||
it does not check everywhere:
|
||
|
||
| Call | Behaviour against an id of 0 |
|
||
|---|---|
|
||
| `akgl_actor_initialize`, `akgl_sprite_initialize`, `akgl_spritesheet_initialize`, `akgl_character_initialize` | **Loud.** The `SDL_SetPointerProperty` result is checked; you get `AKERR_KEY`, "Unable to add … to registry" |
|
||
| `akgl_actor_set_character` | **Loud.** `SDL_GetPointerProperty` returns `NULL` and the result is checked; you get `AKERR_NULLPOINTER` |
|
||
| `akgl_set_property` | **Silent.** The `SDL_SetStringProperty` result is not checked. The value is discarded and the call returns success |
|
||
| `akgl_registry_load_properties` | **Silent, and worse.** Same unchecked write, and it logs `Set property x = y` for every entry it dropped, then logs `Properties loaded` |
|
||
| `akgl_get_property` | **Silent.** You get the default you passed, every time |
|
||
| `akgl_registry_init_actor_state_strings` | **Silent.** The `SDL_SetNumberProperty` results are not checked |
|
||
|
||
The consequence is the one `registry.h` warns about, and it is real even though the
|
||
attribution has moved: **a startup path that leaves `AKGL_REGISTRY_PROPERTIES` at 0 gives
|
||
you a no-op `akgl_set_property` and an `akgl_get_property` that always hands back your
|
||
default** — which in turn means `akgl_render_2d_init` builds a window from its fallback
|
||
size and `akgl_physics_init_arcade` runs with no gravity, both reporting success.
|
||
|
||
If you are debugging configuration that appears not to apply, check
|
||
`AKGL_REGISTRY_PROPERTIES != 0` before you check anything else.
|
||
|
||
## Truncated keys
|
||
|
||
Every name is copied into a fixed-width field with `aksl_strncpy` bounded to
|
||
`sizeof(field) - 1`, which is the "**truncated, not rejected**" contract those headers
|
||
document. The fields:
|
||
|
||
| Object | Field | Bytes |
|
||
|---|---|---|
|
||
| `akgl_Actor::name` | `AKGL_ACTOR_MAX_NAME_LENGTH` | 128 |
|
||
| `akgl_Sprite::name` | `AKGL_SPRITE_MAX_NAME_LENGTH` | 128 |
|
||
| `akgl_Character::name` | `AKGL_CHARACTER_MAX_NAME_LENGTH` | 128 |
|
||
| `akgl_SpriteSheet::name` | `AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH` | 512 |
|
||
|
||
**Two distinct names that agree on their first 127 bytes truncate to the same key**, and
|
||
the second `SDL_SetPointerProperty` silently replaces the first. The objects are different;
|
||
the registry cannot tell. That is recorded in `TODO.md`, "Truncated registry keys can
|
||
collide", and it is not being fixed because the fix is a contract change — refusing an
|
||
over-long name with `AKERR_OUTOFBOUNDS` — and every one of those headers currently promises
|
||
the opposite.
|
||
|
||
There is a second half that the `TODO.md` entry understates. It says the truncated name *is*
|
||
the registry key. That is true for sprites and spritesheets, which register under their own
|
||
copied field. It is **not** true for actors and characters:
|
||
|
||
- `akgl_actor_initialize` and `akgl_character_initialize` register under the **caller's**
|
||
string, untruncated.
|
||
- `akgl_heap_release_actor` and `akgl_heap_release_character` clear under the object's
|
||
**truncated** field.
|
||
|
||
For any name of 127 bytes or fewer those are the same string and nothing happens. Over that,
|
||
the entry is written under one key and cleared under another, so **releasing the object
|
||
leaves a live registry entry pointing at a zeroed pool slot** — and the next lookup of that
|
||
name hands out a pointer to freed storage. Keep asset names short; 127 bytes is generous for
|
||
something a human types into a JSON file.
|
||
|
||
## Configuration properties
|
||
|
||
`AKGL_REGISTRY_PROPERTIES` is a string-to-string store. **Everything is a string, including
|
||
numbers** — `game.screenwidth` is `"800"`, not `800` — and whoever reads a property parses
|
||
it.
|
||
|
||
These are every property the library itself reads, with the default it falls back to and
|
||
where it is read:
|
||
|
||
| Property | Default | Read by | Meaning |
|
||
|---|---|---|---|
|
||
| `game.screenwidth` | `"0"` | `akgl_render_2d_init` | Window and camera width, in pixels |
|
||
| `game.screenheight` | `"0"` | `akgl_render_2d_init` | Window and camera height, in pixels |
|
||
| `physics.gravity.x` | `"0.0"` | `akgl_physics_init_arcade` | Constant acceleration on x, px/s² |
|
||
| `physics.gravity.y` | `"0.0"` | `akgl_physics_init_arcade` | Constant acceleration on y, px/s². Positive is down |
|
||
| `physics.gravity.z` | `"0.0"` | `akgl_physics_init_arcade` | Constant acceleration on z, px/s² |
|
||
| `physics.drag.x` | `"0.0"` | `akgl_physics_init_arcade` | Fraction of environmental velocity shed per second on x |
|
||
| `physics.drag.y` | `"0.0"` | `akgl_physics_init_arcade` | Same for y. **This is the only brake on falling** — there is no terminal velocity |
|
||
| `physics.drag.z` | `"0.0"` | `akgl_physics_init_arcade` | Same for z |
|
||
| `physics.max_timestep` | `"0.05"` | `akgl_physics_init_arcade` | Upper bound on a single simulation `dt`, in seconds |
|
||
|
||
That is the whole list. Two things follow from it:
|
||
|
||
**The screen size defaults to 0×0.** `akgl_render_2d_init` passes whatever it parsed
|
||
straight to `SDL_CreateWindowAndRenderer`, so a program that never sets those two asks SDL
|
||
for a zero-sized window. Set them before calling it.
|
||
|
||
**`physics.engine` is not a property.** `physics.h` says in two places that `akgl_game_init`
|
||
passes a `physics.engine` property to `akgl_physics_factory`. It does not — `akgl_game_init`
|
||
never calls the factory at all, and the string `physics.engine` does not appear anywhere in
|
||
`src/`. Choosing a backend is an explicit call you make yourself, with a name of `"null"` or
|
||
`"arcade"`; see [Chapter 14](14-physics.md). The header comment is a known-false claim left
|
||
alone deliberately, since correcting it is a source change rather than a documentation one.
|
||
|
||
### Setting properties
|
||
|
||
Two ways in, and both must happen **after** `akgl_game_init` (which creates the registry)
|
||
and **before** `akgl_render_2d_init` or `akgl_physics_init_arcade` (which read it).
|
||
|
||
`akgl_set_property(name, value)` sets one. SDL copies the value, so your buffer can go away
|
||
afterwards.
|
||
|
||
`akgl_registry_load_properties(fname)` loads a JSON document. It expects a top-level
|
||
`properties` object whose members are **all strings**:
|
||
|
||
```json kind=properties
|
||
{
|
||
"properties": {
|
||
"game.screenwidth": "800",
|
||
"game.screenheight": "600",
|
||
"physics.gravity.y": "400.0",
|
||
"physics.drag.y": "0.9",
|
||
"physics.max_timestep": "0.05"
|
||
}
|
||
}
|
||
```
|
||
|
||
A member that is not a string is `AKERR_TYPE`; a document with no `properties` member is
|
||
`AKERR_KEY`; a file that will not open or will not parse is `AKERR_NULLPOINTER` with
|
||
jansson's line number and text in the message. Note that a failure part-way through leaks
|
||
one string-pool slot, because the per-property `CLEANUP` block is empty — a malformed
|
||
configuration file is worth failing on rather than retrying in a loop.
|
||
|
||
### Reading properties
|
||
|
||
`akgl_get_property(name, dest, def)` reads one into a pooled string. **Absence is not an
|
||
error**: an unset property yields `def`, which is how every caller inside the library gets a
|
||
working default without checking first.
|
||
|
||
Three details, all of which have caused a defect here before:
|
||
|
||
- **`*dest` decides who allocates.** `NULL` claims a slot from the string pool for you;
|
||
non-`NULL` is written in place. Either way **you** release it with
|
||
`akgl_heap_release_string`. Initialize your local to `NULL` on the first call.
|
||
- **`def` is effectively required.** A `NULL` default on an unset property is
|
||
`AKERR_NULLPOINTER`, not an empty result.
|
||
- **Only the value and its terminator are copied.** The rest of the destination keeps
|
||
whatever the previous holder left, so read the result as a C string and never as
|
||
`AKGL_MAX_STRING_LENGTH` bytes. A value of `AKGL_MAX_STRING_LENGTH` bytes or more is
|
||
`AKERR_OUTOFBOUNDS`; it will not fit an `akgl_String` with its terminator.
|
||
|
||
The full worked example — claim, read, parse, release in `CLEANUP` — is in
|
||
[Chapter 5](05-the-heap.md), because the ownership rule is the string pool's rather than the
|
||
registry's.
|
||
|
||
## Where to go next
|
||
|
||
- [Chapter 5](05-the-heap.md) — the pools the registries point into, and who releases what.
|
||
- [Chapter 7](07-the-game-and-the-frame.md) — where in startup the registries are created,
|
||
and where configuration has to be in place by.
|
||
- [Chapter 14](14-physics.md) — choosing a backend, and what the `physics.*` properties do.
|
||
- [Chapter 21](21-appendix-limits.md) — the configuration table again, alongside every
|
||
`AKGL_MAX_*`.
|