Fix the type, macro and state-table defects, and the leftover debris
Closes internal-consistency items 19 through 36, 38 and 41. Item 37, the ~180 redundant casts, is deliberately left open with its reasoning in TODO.md: the benefit only arrives once the build turns on the warnings those casts suppress, and doing it before that is churn across the two files with the most outstanding functional defects. The two that were real bugs are in the actor state table. AKGL_ACTOR_STATE_STRING_NAMES was declared [AKGL_ACTOR_MAX_STATES+1] and defined [32], so a consumer trusting the declared bound read past the object; and indices 11 and 12 were named UNDEFINED_11 and UNDEFINED_12 where actor.h has MOVING_IN and MOVING_OUT, so no character JSON could bind a sprite to either state. tests/registry.c now walks the whole table -- every entry non-NULL, every entry resolving to its own bit, no two entries sharing a name. The bitmask macros are parenthesized and AKGL_BITMASK_CLEAR has lost the semicolon inside its body. Writing tests/bitmasks.c for that turned up something worth knowing: the obvious test does not catch it. For a bit that is set, the misparse `!(mask & bit) == bit` gives the same answer as the correct one. It only diverges for an unset bit whose value is not 1, and that is the shape the suite uses now. akgl_draw_background was the last public function outside the error protocol. It takes a backend like everything else in draw.h, restores the draw colour it found, and is tested -- TODO.md had it filed under "needs the offscreen renderer harness", which was never true; what it needed was to stop reading the global. All eight registry initializers go through one helper, so the seven that leaked an SDL_PropertiesID on every call after the first no longer do. Fixed in the same place because it is the same function: akgl_registry_init never called akgl_registry_init_properties, which made akgl_set_property a silent no-op for anyone not going through akgl_game_init -- Defects, Known and still open item 3. Also: AKGL_COLLIDE_RECTANGLES (three open parens, two closes) and akgl_Frame deleted, float32_t/float64_t used consistently, the developer-specific debug logging removed from the controller inner loop, the abandoned SDL_GetBasePath comments removed, nine unused locals removed, and dst renamed to dest. akgl_game_update's default flags no longer OR the same bit twice. That changes nothing today, and the reason is Performance item 32: the loop never reads either bit, which is why every actor is updated sixteen times a frame. Still open. 25/25 pass, memcheck clean, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
275
TODO.md
275
TODO.md
@@ -211,175 +211,172 @@ resolved for every pair it listed.
|
||||
|
||||
### 4. Types and macros
|
||||
|
||||
19. **`float`/`double` are used raw where `types.h` defines aliases.** `types.h`
|
||||
exports `float32_t` and `float64_t`, and the actor and character structs use
|
||||
`float32_t` throughout — but `akgl_get_json_number_value` takes `float *`
|
||||
(`json_helpers.h:55`), `akgl_get_json_double_value` takes `double *` (line 66),
|
||||
`akgl_PhysicsBackend`'s six drag/gravity fields are `double`
|
||||
(`physics.h:22-27`), and `akgl_Tilemap`'s perspective fields are `float`
|
||||
(`tilemap.h:105-108`). Either use the aliases consistently or delete them.
|
||||
**Items 19 through 23 are resolved in 0.5.0.**
|
||||
|
||||
20. **`AKGL_COLLIDE_RECTANGLES` (`include/akgl/util.h:27`) has unbalanced
|
||||
parentheses** — three opens, two closes — so any use is a syntax error. It has
|
||||
no callers and duplicates `akgl_collide_rectangles`. Delete it.
|
||||
19. **`float`/`double` used raw where `types.h` defines aliases.** The four
|
||||
signatures and twelve struct fields that spelled them out now use
|
||||
`float32_t` and `float64_t` like the actor and character structs. They are
|
||||
plain typedefs, so this is a spelling change and not an ABI one.
|
||||
`float64_t`'s doc no longer says "unused so far".
|
||||
|
||||
21. **Bitmask macros are unparenthesized.** `AKGL_BITMASK_HAS(x, y)` expands to
|
||||
`(x & y) == y` with no outer parens, so `!AKGL_BITMASK_HAS(a, b)` parses as
|
||||
`!(a & b) == b`. No in-tree caller negates it today (`AKGL_BITMASK_HASNOT`
|
||||
exists for that), so this is latent rather than live. `AKGL_BITMASK_CLEAR(x)`
|
||||
(`game.h:86`) also carries a trailing semicolon inside the macro body, so
|
||||
normal use produces an empty statement. Parenthesize all five and drop the
|
||||
semicolon.
|
||||
20. **`AKGL_COLLIDE_RECTANGLES` had unbalanced parentheses.** Deleted. Three
|
||||
opens against two closes meant any expansion was a syntax error, it had no
|
||||
callers, and it duplicated `akgl_collide_rectangles`.
|
||||
|
||||
22. **The state and iterator bit macros mix forms.** `AKGL_ITERATOR_OP_UPDATE`
|
||||
(`iterator.h:15`) is written `1` while its 31 siblings are `1 << n`, and none
|
||||
of the `1 << n` values in `iterator.h` or `actor.h:15-49` are parenthesized.
|
||||
The hand-maintained trailing bit-pattern comments in `actor.h:34-49` are also
|
||||
wrong for the high word — they restart at `0000 0000 0000 0001` for bit 16
|
||||
rather than showing the full 32-bit value.
|
||||
21. **Bitmask macros were unparenthesized.** All five are fully parenthesized,
|
||||
and `AKGL_BITMASK_CLEAR` no longer carries a semicolon inside its body.
|
||||
|
||||
23. **`akgl_Frame` (`game.h:27-31`) is defined and never used** anywhere in
|
||||
`src/`, `include/`, `tests/`, or `util/`.
|
||||
`tests/bitmasks.c` covers the composition cases, and writing them turned up
|
||||
something worth recording: the obvious test does **not** catch this. For a
|
||||
bit that is *set*, `!AKGL_BITMASK_HAS(mask, bit)` misparses to
|
||||
`!(mask & bit) == bit`, which is `0 == bit` -- false, the same answer the
|
||||
correct parse gives. It only diverges for a bit that is *not* set and whose
|
||||
value is not 1: `!(0)` is 1, and `1 == 64` is false where the answer should
|
||||
be true. The suite now uses that shape, and fails against the old macros.
|
||||
|
||||
22. **The state and iterator bit macros mixed forms.**
|
||||
`AKGL_ITERATOR_OP_UPDATE` is `(1 << 0)` like its 31 siblings, and every
|
||||
`1 << n` in `iterator.h` and `actor.h` is parenthesized, with the
|
||||
hand-aligned value columns preserved.
|
||||
|
||||
The bit-pattern comments in `actor.h` are not wrong so much as unlabelled:
|
||||
each shows the pattern *within its own 16-bit half*, which is why bit 16
|
||||
looks like it restarts at bit 0. The section headings say so now. Rendering
|
||||
the full 32-bit value instead would push those lines past the 100-column
|
||||
fill.
|
||||
|
||||
**Newly recorded, and still open:** `1 << 31` is undefined behaviour on a
|
||||
signed `int`. It is `(1 << 31)` in both tables and wants to be an unsigned
|
||||
shift, but `akgl_Actor::state` is `int32_t` and `akgl_Iterator::flags` is
|
||||
`uint32_t`, so the two tables do not want the same answer. Worth deciding
|
||||
deliberately rather than sneaking a `u` in.
|
||||
|
||||
23. **`akgl_Frame` was defined and never used.** Deleted.
|
||||
|
||||
### 5. `AKGL_ACTOR_STATE_STRING_NAMES` disagrees with `actor.h`
|
||||
|
||||
24. **The array bound differs between declaration and definition.**
|
||||
`include/akgl/actor.h:57` declares `[AKGL_ACTOR_MAX_STATES+1]` (33);
|
||||
`src/actor_state_string_names.c:6` defines `[32]`. Any consumer that trusts
|
||||
the declared bound and reads index 32 reads past the object.
|
||||
**All three resolved in 0.5.0.**
|
||||
|
||||
25. **Two entries name the wrong bit.** `actor.h` assigns bit 11 to
|
||||
`AKGL_ACTOR_STATE_MOVING_IN` and bit 12 to `AKGL_ACTOR_STATE_MOVING_OUT`, but
|
||||
`actor_state_string_names.c:18-19` puts `"AKGL_ACTOR_STATE_UNDEFINED_11"` and
|
||||
`"AKGL_ACTOR_STATE_UNDEFINED_12"` at those indices — names for macros that do
|
||||
not exist. Since `akgl_registry_init_actor_state_strings`
|
||||
(`src/registry.c:79-94`) builds `AKGL_REGISTRY_ACTOR_STATE_STRINGS` from this
|
||||
array and `akgl_character_load_json_state_int_from_strings`
|
||||
(`src/character.c:101`) resolves character-JSON state names through it, a
|
||||
character JSON can never bind a sprite to `MOVING_IN` or `MOVING_OUT`.
|
||||
24. **The array bound differed between declaration and definition.** The
|
||||
header declared `[AKGL_ACTOR_MAX_STATES+1]` (33) and the definition was a
|
||||
literal `[32]`, so a consumer trusting the declared bound read past the
|
||||
object. Both are `[AKGL_ACTOR_MAX_STATES]` now, and the definition is sized
|
||||
by the macro rather than by a literal.
|
||||
|
||||
26. **The generation comment is stale.** `actor.h:53-55` says the file "is built
|
||||
by a utility script and not kept in git, see the Makefile for
|
||||
lib_src/actor_state_string_names.c". There is no Makefile (the project is
|
||||
CMake), no `lib_src/`, no such script under `scripts/` or `util/`, and the
|
||||
file *is* tracked in git at `src/actor_state_string_names.c`. Either restore
|
||||
the generator — which would fix items 24 and 25 by construction — or delete
|
||||
the comment and maintain the file by hand.
|
||||
25. **Two entries named the wrong bit.** Indices 11 and 12 said
|
||||
`AKGL_ACTOR_STATE_UNDEFINED_11` and `_12` where `actor.h` has `MOVING_IN`
|
||||
and `MOVING_OUT`, so no character JSON could ever bind a sprite to either
|
||||
state -- the name it would have to write was not in the registry.
|
||||
|
||||
`tests/registry.c` now walks the whole table: every entry non-`NULL`, every
|
||||
entry resolving to its own bit through
|
||||
`AKGL_REGISTRY_ACTOR_STATE_STRINGS`, no two entries sharing a name (a
|
||||
duplicate silently overwrites and makes one bit unreachable), and
|
||||
`MOVING_IN`/`MOVING_OUT` named explicitly so a regression reads as what it
|
||||
is.
|
||||
|
||||
26. **The generation comment was stale.** There is no generator, no Makefile
|
||||
and no `lib_src/`. The comment is gone and the file's own header now says it
|
||||
is maintained by hand and states the two invariants that keep breaking.
|
||||
|
||||
### 6. Doxygen drift
|
||||
|
||||
27. **Three struct doc comments in `tilemap.h` are rotated by one.**
|
||||
`akgl_TilemapObject` (line 31) is documented as "Stores tileset metadata,
|
||||
texture, and frame offsets" (that is `akgl_Tileset`); `akgl_TilemapLayer`
|
||||
(line 46) as "Represents an object embedded in a tilemap layer" (that is
|
||||
`akgl_TilemapObject`); `akgl_Tileset` (line 61) as "Stores tile, image, or
|
||||
object data for one map layer" (that is `akgl_TilemapLayer`).
|
||||
27. **Three struct doc comments in `tilemap.h` were rotated by one.** Already
|
||||
correct -- the doxygen rewrite fixed this before it was checked here.
|
||||
`akgl_TilemapObject`, `akgl_TilemapLayer` and `akgl_Tileset` each describe
|
||||
themselves.
|
||||
|
||||
28. **`point` is documented as "Represents a two-dimensional point"**
|
||||
(`util.h:12`) but has `x`, `y`, and `z`.
|
||||
28. **`point` documented as two-dimensional with an `x`, `y` and `z`.** Already
|
||||
correct, and the type is `akgl_Point` now.
|
||||
|
||||
29. **Doc comments live on the definition for public functions.** The convention
|
||||
is header-side documentation, but `akgl_path_relative_root` (`util.c:23`),
|
||||
`akgl_path_relative_from` (`util.c:97`), the four `gamepad_handle_*`
|
||||
(`controller.c:114+`), the `akgl_game_save_*` iterators and
|
||||
`akgl_game_load_*` helpers (`game.c:173+`), and the tilemap helpers
|
||||
(`tilemap.c:90+`) are documented only in the `.c`. This is the same set as
|
||||
item 7 — resolving that resolves this.
|
||||
29. **Doc comments on the definition rather than the header.** Resolved with
|
||||
item 7: the four controller handlers and the six tilemap loader helpers had
|
||||
their documentation moved to the header when they were declared there.
|
||||
|
||||
### 7. Formatting
|
||||
### 7. Formatting and hygiene
|
||||
|
||||
30. **A minority of files use a 2-column offset instead of the canonical
|
||||
4-column one.** The mix of tabs and spaces across most of `src/` is *not*
|
||||
disorder: it is exactly what Emacs `cc-mode` emits for the `stroustrup` style
|
||||
with `indent-tabs-mode t` and `tab-width 8` — depth 1 is four spaces, depth 2
|
||||
is one tab, depth 3 is a tab plus four spaces, and so on. Twelve of the
|
||||
seventeen `.c` files follow that ladder cleanly.
|
||||
**Items 31 through 36, 38 and 41 are resolved in 0.5.0. Item 37 is not; see
|
||||
below.**
|
||||
|
||||
The genuine outliers indent at 2 columns: `src/json_helpers.c` (82 lines,
|
||||
except `akgl_get_json_with_default` at line 149 which is 4), `src/util.c`
|
||||
(37 lines, from `akgl_rectangle_points` at line 118 onward), `src/assets.c`
|
||||
(9), `src/staticstring.c` (7), `src/actor.c` (8, in `akgl_actor_add_child`
|
||||
at line 272), plus `include/akgl/util.h` (7) and
|
||||
`include/akgl/staticstring.h` (2).
|
||||
31. **Leftover debug code.** The four `SDL_Log` lines in `src/controller.c`
|
||||
guarded by `event->type == 768 && event->key.which == 11 &&
|
||||
event->key.key == 13` -- decimal literals for one keyboard on one
|
||||
developer's machine, inside the per-event inner loop -- are gone.
|
||||
|
||||
**Resolved.** `AGENTS.md` now specifies the canonical style and the exact
|
||||
`cc-mode` settings; `.dir-locals.el` applies them in Emacs; and
|
||||
`scripts/reindent.sh` applies them in batch. The whole tree (32 files across
|
||||
`src/`, `include/`, `tests/`, and `util/`) has been reindented and is a fixed
|
||||
point of `scripts/reindent.sh --check`. Verified whitespace-only: apart from
|
||||
three trailing blank lines removed at EOF in `src/heap.c`, `src/registry.c`,
|
||||
and `tests/tilemap.c`, `git diff -w` over the reindent is empty, and the test
|
||||
results are unchanged (13/14, `character` still the intentional failure).
|
||||
`scripts/hooks/pre-commit` keeps it that way — enable with
|
||||
`git config core.hooksPath scripts/hooks`.
|
||||
32. **Large commented-out blocks.** The five abandoned `SDL_GetBasePath()`
|
||||
path-prefixing lines across `assets.c`, `sprite.c`, `character.c` and
|
||||
`tilemap.c` are gone. They were superseded by `akgl_path_relative`.
|
||||
|
||||
31. **Leftover debug code ships in the library.** `src/controller.c:91-97` logs
|
||||
four lines whenever `event->type == 768 && event->key.which == 11 &&
|
||||
event->key.key == 13` — hardcoded decimal values for a specific keyboard ID
|
||||
on a specific developer's machine, inside the per-event inner loop.
|
||||
33. **Unused locals.** `curTime` in `akgl_game_update` and in
|
||||
`akgl_render_2d_draw_world`, `j` in `akgl_render_2d_draw_world` (shadowed
|
||||
by its own inner loop), `target` in `akgl_character_sprite_get`, both
|
||||
`result` declarations in `util.c`, and `screenwidth`/`screenheight` in
|
||||
`akgl_game_init`. `opflags` in `akgl_heap_release_character` is no longer
|
||||
unused -- it is what drives the state-sprite walk added for **Defects**
|
||||
item 21.
|
||||
|
||||
32. **Large commented-out blocks.** `src/sprite.c:115-120,157,185-198,210`;
|
||||
`src/character.c:192-199,215`; `src/assets.c:18-27,50`;
|
||||
`src/tilemap.c:583,596-598,640`. All are the same abandoned
|
||||
`SDL_GetBasePath()` path-prefixing approach, superseded by
|
||||
`akgl_path_relative`. Delete them.
|
||||
34. **`akgl_game_update`'s default flags OR-ed the same bit twice.** It reads
|
||||
`AKGL_ITERATOR_OP_UPDATE | AKGL_ITERATOR_OP_LAYERMASK` now. This is a
|
||||
statement of intent rather than a behaviour change, and the reason is worth
|
||||
knowing: **nothing in that loop reads either bit**. It never compares
|
||||
`actor->layer` to the layer it is sweeping, which is exactly **Performance**
|
||||
item 32 -- every actor updated sixteen times a frame -- and that is still
|
||||
open.
|
||||
|
||||
33. **Unused locals.** `screenwidth`/`screenheight` (`game.c:53-54`), `curTime`
|
||||
(`game.c:499`), `curTime` and `j` (`renderer.c:114,116` — `j` is also shadowed
|
||||
by the inner loop at line 128), `target` (`character.c:59`), `result`
|
||||
(`util.c:37,73`), `opflags` (`heap.c:146`, declared and cleared but never
|
||||
read).
|
||||
35. **`akgl_draw_background` was the only public function outside the error
|
||||
protocol.** It now takes an `akgl_RenderBackend *` like every other entry
|
||||
point in `draw.h`, returns an error context, checks the backend and its
|
||||
`sdl_renderer`, and restores the draw colour it found instead of leaving it
|
||||
changed.
|
||||
|
||||
34. **`akgl_game_update`'s default flags OR the same bit twice.**
|
||||
`src/game.c:496` reads
|
||||
`(AKGL_ITERATOR_OP_LAYERMASK | AKGL_ITERATOR_OP_LAYERMASK)`. Given the loop
|
||||
that follows walks layers and calls `updatefunc`, the intent was almost
|
||||
certainly `AKGL_ITERATOR_OP_UPDATE | AKGL_ITERATOR_OP_LAYERMASK`.
|
||||
It was listed under **Remaining work** as needing the offscreen renderer
|
||||
harness. It does not, and never did -- what it needed was to stop reading
|
||||
the global. `tests/draw.c` covers the checkerboard pattern, the restored
|
||||
draw colour, zero and negative sizes, and a `NULL` backend.
|
||||
|
||||
35. **`akgl_draw_background` is the only public function outside the error
|
||||
protocol.** `include/akgl/draw.h:14` returns `void` and reports nothing;
|
||||
every other public entry point returns `akerr_ErrorContext *`. It also calls
|
||||
`SDL_SetRenderDrawColor` and `SDL_RenderFillRect` without checking `renderer`
|
||||
or `renderer->sdl_renderer`.
|
||||
36. **`akgl_registry_init_actor` was the only initializer that destroyed the
|
||||
registry it was replacing.** All eight go through one `registry_create()`
|
||||
helper now, so the other seven stop leaking an `SDL_PropertiesID` per call
|
||||
after the first -- which a game that resets between levels makes on every
|
||||
level.
|
||||
|
||||
36. **`akgl_registry_init_actor` is the only registry initializer that destroys
|
||||
an existing registry** before recreating it (`src/registry.c:47-49`). The
|
||||
other seven leak the old `SDL_PropertiesID` on a second call. Either all of
|
||||
them should do it or none should.
|
||||
Fixed alongside it, because it is the same function: `akgl_registry_init()`
|
||||
never called `akgl_registry_init_properties()`, which is **Defects → Known
|
||||
and still open** item 3. `AKGL_REGISTRY_PROPERTIES` stayed 0 for any caller
|
||||
that did not also go through `akgl_game_init`, making `akgl_set_property` a
|
||||
silent no-op and `akgl_get_property` always return the caller's default --
|
||||
so `akgl_physics_init_arcade` and `akgl_render_2d_init` quietly ignored
|
||||
their configuration. `tests/registry.c` sets a property and reads it back.
|
||||
|
||||
37. **Redundant casts obscure the code.** `(json_t *)json` where `json` is
|
||||
already `json_t *`, `(akgl_Tilemap *)dest`, `(akgl_Actor *)actorobj`,
|
||||
`(char *)&obj->name` on an array that already decays — roughly 180 pointer
|
||||
casts across `src/`, a large majority of them no-ops, densest in
|
||||
`src/tilemap.c:150-624` and `src/character.c:144-172`.
|
||||
They suppress exactly the conversion warnings that would catch a real
|
||||
mismatch.
|
||||
37. **Redundant casts obscure the code.** **Still open, and deliberately so.**
|
||||
Roughly 180 pointer casts across `src/`, a large majority no-ops --
|
||||
`(json_t *)json` where `json` is already `json_t *`, `(char *)&obj->name` on
|
||||
an array that decays anyway -- densest in `src/tilemap.c` and
|
||||
`src/character.c`.
|
||||
|
||||
38. **`struct`-qualified parameters in two definitions.**
|
||||
`akgl_physics_null_move` (`src/physics.c:29`) and `akgl_physics_arcade_move`
|
||||
(`src/physics.c:80`) are defined with `struct akgl_PhysicsBackend *self`
|
||||
while the header and the other eight physics functions use the typedef.
|
||||
The argument for fixing it is real: a no-op cast suppresses exactly the
|
||||
conversion warning that would catch a mismatch. The argument for not doing
|
||||
it *here* is that it is the largest and least mechanical change on this
|
||||
list, it touches almost every line of the two files with the most
|
||||
outstanding functional defects, and the benefit only arrives once the build
|
||||
actually turns those warnings on -- which it does not today. Doing this
|
||||
without `-Wall -Wextra` on the library target buys nothing but churn.
|
||||
|
||||
39. **`text.c:19` validates the wrong argument.**
|
||||
`FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null filepath")` checks
|
||||
`name` a second time; `filepath` is never checked and is passed straight to
|
||||
`TTF_OpenFont`. Copy-paste of line 18.
|
||||
**Do them together**, in that order: enable the warnings, see what they say,
|
||||
then remove the casts that were hiding something and the ones that were not.
|
||||
|
||||
**Resolved** alongside the text measurement work; `tests/text.c` asserts a
|
||||
NULL filepath reports `AKERR_NULLPOINTER`.
|
||||
38. **`struct`-qualified parameters in two definitions.** `akgl_physics_null_move`
|
||||
and `akgl_physics_arcade_move` use the typedef like the other eight.
|
||||
|
||||
40. **`akgl_path_relative` and `akgl_path_relative_from` disagree on the output
|
||||
parameter.** **Moot in 0.5.0**: `akgl_path_relative_from` is deleted (see
|
||||
**Defects → Known and still open** item 4) and `akgl_path_relative_root` is
|
||||
`static`, so `akgl_path_relative` is the only member of the family left and
|
||||
there is nothing left to disagree with. It still takes `akgl_String *dst`
|
||||
rather than the `**` form the rest of the library uses, which stays as
|
||||
**item 41**'s question of naming and this one's question of shape -- but
|
||||
with one function it is a decision to make when something needs it, not a
|
||||
drift between siblings.
|
||||
39. **`text.c` validated the wrong argument.** Resolved earlier, alongside the
|
||||
text measurement work.
|
||||
|
||||
40. **`akgl_path_relative_from` disagreed on the output parameter.** Moot: the
|
||||
function is deleted. See **Defects → Known and still open** item 4.
|
||||
|
||||
41. **`dst` vs `dest` for output parameters.** `akgl_string_copy` and
|
||||
`akgl_path_relative` take `dest` now, like everything else.
|
||||
|
||||
41. **`dst` vs `dest` for output parameters.** `akgl_string_copy` and the
|
||||
`akgl_path_relative*` family use `dst`; everything else uses `dest`.
|
||||
|
||||
## Test suites that could not fail
|
||||
|
||||
|
||||
Reference in New Issue
Block a user