# 18. Utilities `util.h`, `json_helpers.h` and `staticstring.h` hold the pieces that belong to no subsystem: rectangle overlap, asset path resolution, typed JSON accessors, and the pooled string type everything else passes around. The JSON accessors are the valuable part of this chapter — their status semantics are what every asset loader in the library is written against, and they are documented nowhere else. ## Collision helpers Three functions, all axis-aligned, all in `util.h`: | Function | Question it answers | |---|---| | `akgl_rectangle_points(dest, rect)` | What are the four corners of this `SDL_FRect`? | | `akgl_collide_point_rectangle(p, r, collide)` | Is this point inside those corners? | | `akgl_collide_rectangles(r1, r2, collide)` | Do these two rectangles overlap? | ```c #include #include #include /* Do two actor boxes touch? Edges count as touching. */ akerr_ErrorContext *boxes_touch(SDL_FRect *a, SDL_FRect *b, bool *hit) { PREPARE_ERROR(errctx); PASS(errctx, akgl_collide_rectangles(a, b, hit)); SUCCEED_RETURN(errctx); } /* Is the mouse over a button? Corners first, then the point test. */ akerr_ErrorContext *point_in_box(int x, int y, SDL_FRect *box, bool *hit) { akgl_RectanglePoints corners; akgl_Point p; PREPARE_ERROR(errctx); p.x = x; p.y = y; p.z = 0; PASS(errctx, akgl_rectangle_points(&corners, box)); PASS(errctx, akgl_collide_point_rectangle(&p, &corners, hit)); SUCCEED_RETURN(errctx); } ``` Four properties, all deliberate: **Axis-aligned only.** There is no rotation support and no separating-axis test. An actor drawn with a non-zero `angle` still collides as its unrotated box. **Edges count as touching.** A point exactly on a boundary is inside; two rectangles sharing an edge and no area overlap. Both comparisons are `>=` and `<=`. **Coordinates are truncated from `float` to `int` on the way in.** A rectangle at `x = 10.9` has its corners at 10. That is right for tile-grid work and wrong for sub-pixel work; if you need the latter, do not round-trip through `akgl_rectangle_points`. **`z` is carried and never used.** `akgl_Point` has one, `akgl_rectangle_points` never writes it, and neither collision test reads it. These are 2D tests. ### The arrangement it misses `akgl_collide_rectangles` works corner by corner: it tests each rectangle's four corners against the other, eight tests, stopping at the first hit. Testing both directions is what catches one rectangle wholly inside the other, which has no corner in its neighbour. **It still misses the cross.** A tall thin rectangle crossing a short wide one overlaps without either enclosing a corner of the other, and both are reported as not colliding: ```text +---+ +---+ | | | | +---|---|---+ +---+---+---+ <- overlaps, but | | | | is | | | | no corner of +---|---|---+ reported +---+---+---+ either is inside | | as | | the other +---+ "no hit" +---+ ``` The fix is an edge-span comparison instead of a corner-containment one — `r1.x < r2.x + r2.w && r2.x < r1.x + r1.w` on both axes — and it is not made here. If your game can produce that shape, and a long thin platform crossing a tall thin character is exactly that shape, test for it yourself. Recorded as an `@note` on the function. This is separate from the fact that **nothing in the library calls these during a frame.** `akgl_physics_simulate` never calls `collide`, and `akgl_physics_arcade_collide` raises `AKERR_API` — see [Chapter 14](14-physics.md). Collision detection is yours to run, and these are the helpers for running it. ## Path resolution Asset files name their neighbours relatively: a sprite definition names its spritesheet, a tilemap names its tilesets. "Relative" has to mean *relative to the file doing the naming*, not to wherever the game was launched from — and `akgl_path_relative` is what makes that true. ```c #include #include #include #include /* Resolve one asset's reference to a neighbour. dest must already be a claimed pool string -- this writes into it, it does not claim one. */ akerr_ErrorContext *resolve_neighbour(char *mapdir, char *reference) { akgl_String *resolved = NULL; PREPARE_ERROR(errctx); PASS(errctx, akgl_heap_next_string(&resolved)); ATTEMPT { CATCH(errctx, akgl_path_relative(mapdir, reference, resolved)); SDL_Log("%s -> %s", reference, resolved->data); } CLEANUP { IGNORE(akgl_heap_release_string(resolved)); } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } ``` The order it tries things in: ```text akgl_path_relative(root, path, dest) | 1. realpath(path) <- relative to the process CWD | | | +-- succeeds -> absolute result into dest, done | | | +-- ENOENT ---+ | | 2. +-> realpath(root + "/" + path) | +-- succeeds -> absolute result, done | +-- fails -> the errno propagates ``` Either way the result is **absolute**, with symlinks and `..` folded out, because both branches end in `realpath(3)`. Three things to know: - **`dest` must already be a claimed pool string.** This writes into it; it does not claim one for you. `root` is required even on the path where it goes unused. - **The fallback pastes `root` in front of `path` unconditionally**, so an absolute `path` that does not exist gets `root` prepended and fails on a nonsense spelling. Do not send absolute paths you expect to fall through. - **`AKERR_OUTOFBOUNDS`** if `root + "/" + path` would not fit `AKGL_MAX_STRING_LENGTH` (`PATH_MAX`); **`ENOENT`**, or whatever else `realpath(3)` reports — `EACCES`, `ELOOP`, `ENOTDIR` — if neither spelling names an existing file; **`AKGL_ERR_HEAP`** if the string pool cannot supply the scratch buffers. > **Stale defect note.** `util.h:124-128` warns that the fallback path "returns straight out > of the ENOENT handler and so never releases the error context it was handling", costing one > of libakerror's 128 context slots per call for the life of the process, and `TODO.md` item > 15 records the same thing as open. **Both are out of date.** `src/util.c:115-129` sets a > flag in the `HANDLE(errctx, ENOENT)` block and calls `path_relative_root` *after* `FINISH`, > which is exactly the fix that entry proposes, and the code carries a comment explaining > why. The leak is gone; the two notes describing it are not. ## The JSON accessors Every asset format in this library is JSON, parsed by [jansson](https://jansson.readthedocs.io/), and jansson's own accessors answer "missing key", "wrong type" and "index past the end" all with the same `NULL`. `json_helpers.h` splits them apart so a malformed asset file produces a message naming the key and what was wrong with it, instead of a `NULL` dereference three frames later. **jansson owns `json_t`, `json_decref`, the parser and the reference-counting rules.** What this section documents is which status means what, and who owns the results. ### Five conventions that run through the whole set **Absence is an error.** A missing key is `AKERR_KEY` — not a quiet `NULL`, not a zero. That is the opposite of the search functions in libakstdlib, and it is deliberate: an optional key is expressed by handing the resulting error to `akgl_get_json_with_default`, not by the accessor staying quiet. So `AKERR_KEY` in a loader is frequently a `HANDLE` block rather than a failure. **Results come back through `dest`,** because the return value is the error context. Every argument is `NULL`-checked — the document or array, the key where there is one, and `dest` — and each raises `AKERR_NULLPOINTER` with a message saying which. `dest` is not written on any failure path. **`json_t *` results are borrowed, not owned.** `akgl_get_json_object_value`, `akgl_get_json_array_value` and `akgl_get_json_array_index_object` hand back pointers *into* the document. They are valid until the document is freed and **must never be `json_decref`'d.** Doing so drops a reference the accessor never took and takes the sub-object out from under the document that owns it. **Strings are the exception: they are copied.** `akgl_get_json_string_value` and `akgl_get_json_array_index_string` write into a pooled `akgl_String` you release yourself. `*dest` doubles as an input — `NULL` means "claim one for me", non-`NULL` means "write into this one" — and it must be *initialized* either way, because an indeterminate `*dest` is dereferenced. A value longer than `AKGL_MAX_STRING_LENGTH` is truncated silently. **The document is never modified.** No accessor writes to it, adds to it, or reorders it. ### The status table These are the four statuses this family raises. [Chapter 4](04-errors.md) carries the full tables for the library; this one says what each means *here*. | Status | Means | Raised by | |---|---|---| | `AKERR_NULLPOINTER` | Any argument was `NULL` — the document, the key, or `dest` | All | | `AKERR_KEY` | **The key is absent.** The idiomatic "optional thing was not there" | The object accessors | | `AKERR_TYPE` | The key is present but is the wrong JSON type | All | | `AKERR_OUTOFBOUNDS` | The index is outside the array. A negative index reports the same way as one past the end | The `akgl_get_json_array_index_*` family | | `AKGL_ERR_HEAP` | `*dest` was `NULL` and the string pool is exhausted | The two string accessors | **`AKERR_INDEX` is not in that list, and libakgl never raises it anywhere.** It appears in this subsystem only as a `HANDLE_GROUP` arm inside `akgl_get_json_with_default`, defending against a status no accessor produces. Harmless, and worth knowing before you write a `HANDLE` block for it. > **Stale header prose.** The conventions block at the top of `json_helpers.h` says "**@p > dest is not `NULL`-checked** except where noted, so a `NULL` there is a crash rather than an > error context", and `akgl_get_json_string_value`'s `@param key` says it is "checked here, > unlike the other accessors in this file". **Neither is true.** All eleven accessors check > `dest`, and all seven that take a `key` check that too — `src/json_helpers.c` raises > `AKERR_NULLPOINTER` with "NULL destination pointer" or "NULL key string" at every one. The > per-function claim that a `NULL` key "is reported as a missing key rather than as a `NULL` > pointer" is wrong the same way. ### Integer is strict; number is not This is the distinction that catches people: | Accessor | `"w": 3` | `"w": 3.0` | `"w": "3"` | |---|---|---|---| | `akgl_get_json_integer_value` | 3 | **`AKERR_TYPE`** | `AKERR_TYPE` | | `akgl_get_json_number_value` | 3.0f | 3.0f | `AKERR_TYPE` | | `akgl_get_json_double_value` | 3.0 | 3.0 | `AKERR_TYPE` | `3.0` is a *real* in JSON, and `akgl_get_json_integer_value` refuses it rather than truncating it. Use one of the number accessors where either spelling should be accepted; `_double_value` exists because the physics constants are `double`. `akgl_get_json_boolean_value` is strict in the same direction: `0` and `1` are numbers in JSON and are refused. It wants `true` or `false`. ### Reading a document ```c #include #include #include #include #include /* A required key, an optional one with a default, and a borrowed sub-object. */ akerr_ErrorContext *read_sprite_header(json_t *doc) { json_t *frames = NULL; /* BORROWED. Never json_decref this. */ akgl_String *name = NULL; /* Copied, and pooled. Release it. */ int width = 0; int width_default = 32; float32_t scale = 0.0f; PREPARE_ERROR(errctx); ATTEMPT { /* Required: an absent "name" is AKERR_KEY and propagates. */ CATCH(errctx, akgl_get_json_string_value(doc, "name", &name)); /* Optional: AKERR_KEY becomes width_default and a NULL return. */ CATCH(errctx, akgl_get_json_with_default( akgl_get_json_integer_value(doc, "width", &width), &width_default, &width, sizeof(int))); /* Strict: "scale": 2 and "scale": 2.0 are both accepted here. akgl_get_json_integer_value would refuse the second. */ CATCH(errctx, akgl_get_json_number_value(doc, "scale", &scale)); /* Borrowed: valid until the document is freed. */ CATCH(errctx, akgl_get_json_array_value(doc, "frames", &frames)); SDL_Log("%s: %d wide, %.1f scale, %d frames", name->data, width, scale, (int)json_array_size(frames)); } CLEANUP { IGNORE(akgl_heap_release_string(name)); } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } ``` ### `akgl_get_json_with_default` — how optional is spelled Run the accessor, hand its error context here along with the fallback. A status it handles becomes `dest` holding the default and a `NULL` return; anything else propagates untouched. When the error *is* consumed it is also released, so you must not release it again. A `NULL` `e` — the accessor succeeded — returns at once without touching `dest`. **It defaults on three statuses**, and this is the current behaviour, checked against `src/json_helpers.c:207-211`: | Status | Defaulted? | Why | |---|---|---| | `AKERR_KEY` | Yes | The key is absent. That is what "optional" means for an object member | | `AKERR_OUTOFBOUNDS` | **Yes** | The index is past the end. That is what "optional" means for an array element | | `AKERR_INDEX` | Yes | Defensive only — nothing in libakgl raises it | | `AKERR_TYPE` | **No** | Propagates, and that is the correct line to draw | **The `AKERR_TYPE` exclusion is the interesting one.** "The key is missing" and "the key is present but holds a string where a number belongs" are not the same situation. The first is an omitted setting and a default is exactly right for it. The second is a malformed document, and quietly substituting a default there would hide the defect and hand the game a value the author never wrote. So it propagates. `AKERR_OUTOFBOUNDS` is the one that changed, because it is what the `akgl_get_json_array_index_*` family actually raises for a short array. **An array element does get a default now**, and "this element is optional" works for an array index as well as for an object member. > **Stale header prose.** `include/akgl/json_helpers.h:230-234` still says the opposite — > that it defaults on `AKERR_KEY` and `AKERR_INDEX` "but *not* on `AKERR_OUTOFBOUNDS` … so > this pairs with the object accessors and does not currently give an array index a default". > That was true before 0.5.0. `src/json_helpers.c` now carries a third > `HANDLE_GROUP(e, AKERR_OUTOFBOUNDS)` arm, placed *above* the arm holding the `memcpy` > because `HANDLE_GROUP` emits no `break` and every arm falls into that body, and > `tests/json_helpers.c` covers it through both the integer and object index accessors. > `TODO.md` item 18 records it as fixed. The header comment wants correcting in its own > commit. `defsize` is trusted, not derived: it is a `memcpy` through `void *` with no type information, and it must match the type both sides actually are. **`defsize` is not the trap, though. Ownership is.** ```c #include #include #include #include /* akgl_get_json_with_default hands BACK the context it was given when it does not handle the status. Take the result into a local, drop your own pointer, and release it yourself -- a CLEANUP block that also releases it double-releases, and a double-released context corrupts the failure instead of reporting it. */ akerr_ErrorContext *optional_int(json_t *doc, char *key, int *dest, int fallback) { akerr_ErrorContext *keyerr = NULL; akerr_ErrorContext *defaulted = NULL; PREPARE_ERROR(errctx); keyerr = akgl_get_json_integer_value(doc, key, dest); defaulted = akgl_get_json_with_default(keyerr, &fallback, dest, sizeof(int)); keyerr = NULL; /* ownership passed either way */ if ( defaulted != NULL ) { defaulted->handled = true; defaulted = akerr_release_error(defaulted); FAIL_RETURN(errctx, AKGL_ERR_BEHAVIOR, "%s was present but not an integer", key); } SUCCEED_RETURN(errctx); } ``` The inline form in the previous example — passing the accessor's return straight into `akgl_get_json_with_default` inside a `CATCH` — is safe, because you never hold a second pointer to the context. It is when you *keep* one that this matters, and a test written the naive way passes against broken code: the first draft of the `AKERR_OUTOFBOUNDS` test in `tests/json_helpers.c` did exactly that. See [Chapter 4](04-errors.md) and `AGENTS.md` under "Testing Guidelines". ### The Tiled property helpers are elsewhere `akgl_get_json_properties_integer`, `_number`, `_float`, `_double`, `_string` and `akgl_get_json_tilemap_property` are declared in `tilemap.h`, not here. They read Tiled's custom-property array — a list of `{"name": …, "type": …, "value": …}` objects — rather than a plain JSON object, which is a different shape and belongs to a different chapter. See [Chapter 13](13-tilemaps.md). ## `akgl_String` The library allocates nothing at runtime, so a string here is a fixed-capacity buffer claimed from the pool: ```c excerpt=include/akgl/staticstring.h #define AKGL_MAX_STRING_LENGTH PATH_MAX /** @brief Provides a fixed-capacity, heap-managed string buffer. */ typedef struct { int refcount; /**< Pool bookkeeping; 0 means the slot is free. Owned by the heap layer. */ char data[AKGL_MAX_STRING_LENGTH]; /**< The characters. Not guaranteed NUL-terminated when filled to capacity. */ } akgl_String; ``` `data` is a plain `char[]`, so `str->data` goes anywhere a `char *` does. `refcount` belongs to the heap layer — [Chapter 5](05-the-heap.md) — and **`akgl_heap_next_string` is the one acquire function that takes the reference for you**, unlike the other four pools. Two functions operate on the contents: | Function | What it does | |---|---| | `akgl_string_initialize(obj, init)` | Copies `init` in, or zero-fills when `init` is `NULL`, and sets `refcount` to 1. Always NUL-terminates; truncates silently | | `akgl_string_copy(src, dest, count)` | Bounded copy between two already-claimed slots. `count` of 0 selects the whole buffer. Always NUL-terminates. Leaves `refcount` alone | **Capacity is `PATH_MAX`**, which is what makes the string pool by far the largest of the five — 256 slots is about a megabyte — and why claiming the *last* free slot from it costs 250 ns against 4 ns from the actor pool. Same algorithm, different cache behaviour; the scan touches one reference count every 4 KiB. `PERFORMANCE.md` has the measurement. `akgl_string_copy` refuses a negative `count` or one above `AKGL_MAX_STRING_LENGTH` with `AKERR_OUTOFBOUNDS`, because both buffers are exactly that long and a larger count walked off the end of two pool slots at once. ## The two test-only helpers `akgl_compare_sdl_surfaces` and `akgl_render_and_compare` are in `util.h` and are labelled in the header as "REALLY slow routines that are only useful in testing harnesses". They are not frame-path functions and this manual does not otherwise cover them: - `akgl_compare_sdl_surfaces(s1, s2)` — dimensions, pitch and format first, then a `memcmp` over the raw pixels. A mismatch is reported as `AKERR_VALUE`, an error rather than an out-parameter. - `akgl_render_and_compare(t1, t2, x, y, w, h, writeout)` — draws each texture into the same rectangle, reads the framebuffer back after each, and compares. Answers "do these render the same", which is not the same question as "are these identical", because the renderer's scaling and blending sit in between. `writeout` names a PNG of the first render, under `SDL_GetBasePath()`, which is how you see what was actually drawn when an assertion fails. Both have a scar worth repeating. Until 0.5.0 `akgl_render_and_compare` drew `t1` on **both** passes, so it always reported a match and every image assertion built on it asserted nothing.