docs/22-ui.md covers the clay-backed UI: what clay owns against what libakgl owns, bring-up and the arena refusal contract, the frame bracket and where it sits in a real frame, the consumed-event rules (with the one-frame-stale hit test stated plainly), the three widgets, menus across keyboard, gamepad and mouse, writing raw CLAY() with an application-owned press edge, images and styles, and how layout errors surface from frame_end with a captured trace. Every listing is a checked block: one linked-and-run example with pinned output, eight excerpts quoting examples/uidemo and examples/jrpg/textbox.c, and the chapter figure is a frame out of uidemo via docs_game_figures. The chapter owes and pays the comparison the tutorials earn: the dialog widget's default style reproduces the JRPG textbox palette, the two listings sit side by side, and the trade is stated -- 125 lines you own completely against one call that costs you the subsystem. Neither is deprecated; chapter 21 keeps teaching the hand-rolled panel on purpose. The appendix moves to 23 (links in chapters 4, 5, 6, 7, 13 and the TOC updated) and gains the ui.h status cross-reference, the AKGL_UI_* limits table, and the new draw primitives' rows. Its status-band figures were stale at COUNT 5 / LIMIT 261 while the band already held six codes; now correct at 7 / 263. Chapter 4 gains AKGL_ERR_UI (262) in the excerpt, the table and the name list. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
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 23](23-appendix-limits.md) — the configuration table again, alongside every
|
||
`AKGL_MAX_*`.
|