Files
libakgl/include/akgl/character.h
Andrew Kesterson f0858b0d38 Document what the functions actually do instead of that they can fail
The Doxygen comments were generated from the declarations, so 217 @throws
lines across 21 headers read "When the corresponding validation or operation
fails" and told a caller nothing beyond the status name. The @param lines were
the same shape: every output was "Output destination populated by the
function", every instance "Object to initialize, inspect, or modify".

Rewritten against the implementations, following the pattern libakstdlib
already uses:

- @throws names the condition. akgl_sprite_load_json separated AKERR_KEY
  (absent) from AKERR_TYPE (present, wrong type) from AKERR_OUTOFBOUNDS
  (filename too long, or array indexed past its end), and gained AKGL_ERR_SDL
  and AKGL_ERR_HEAP, which it raises and never declared.
- Parameters say whether they are required, what a NULL means, and what is
  written on a failure path. Where an argument is not checked, the doc says so:
  akgl_heap_next_actor's dest is a crash on NULL, not an error, and
  akgl_render_2d_frame_start dereferences self before testing it.
- The conventions move up to the file blocks so the per-function docs stay
  short. json_helpers.h states once that absence is an error here and that
  json_t * results are borrowed; heap.h explains the pool model and the
  acquire asymmetry; physics.h carries the thrust/environmental/velocity table.
- Struct fields, enum values, macros and exported globals are documented,
  including the dead ones - sprite_w/sprite_h, movetimer, p_scale and
  timer_gravity are read by nothing, and say so.

Also fixes ten comments in error.h and audio.h that opened with /** rather
than /**<, so Doxygen attached them to the following entity and rendered the
text as part of the macro's value. Verified against the generated HTML.

Comments only - no declaration changed. Doxygen builds clean under
WARN_AS_ERROR, scripts/reindent.sh --check passes, 19/19 suites pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:23:15 -04:00

165 lines
9.8 KiB
C

/**
* @file character.h
* @brief The reusable half of an actor: movement constants and a state-to-sprite map.
*
* A character is the template; an akgl_Actor is the instance. Everything that is
* the same for every goblin on the map -- top speed, acceleration, frame dwell
* time, and which sprite to draw for which combination of actor-state bits --
* lives here and is shared by pointer, so a hundred actors cost one character.
*
* Characters are pool objects (akgl_heap_next_character) and are published in
* the #AKGL_REGISTRY_CHARACTER property registry under their name, which is how
* akgl_actor_set_character finds them. akgl_registry_init must have run first.
*/
#ifndef _AKGL_CHARACTER_H_
#define _AKGL_CHARACTER_H_
#include <SDL3/SDL_properties.h>
#include "types.h"
#include "sprite.h"
#define AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH 128
#define AKGL_MAX_HEAP_CHARACTER 256
/** @brief Defines reusable movement parameters and actor-state sprite bindings. */
typedef struct akgl_Character {
uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. */
char name[AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH]; /**< Registry key. Truncated, not rejected, if the source name is longer. */
SDL_PropertiesID state_sprites; /**< State bitmask (decimal, as a string) -> akgl_Sprite *. */
uint64_t speedtime; /**< Nanoseconds one sprite frame is held before advancing. Read from JSON in milliseconds and scaled by #AKGL_TIME_ONESEC_MS, which despite its name is nanoseconds-per-millisecond. TODO.md item 6. */
float32_t ax; /**< Acceleration along x, world units per second squared. Copied into an actor by akgl_actor_set_character. */
float32_t ay; /**< Acceleration along y. */
float32_t az; /**< Acceleration along z. Not read from JSON; stays 0 unless set by hand. */
float32_t sx; /**< Maximum speed along x, world units per second. */
float32_t sy; /**< Maximum speed along y. */
float32_t sz; /**< Maximum speed along z. Not read from JSON; stays 0 unless set by hand. */
akerr_ErrorContext AKERR_NOIGNORE *(*sprite_add)(struct akgl_Character *, akgl_Sprite *, int); /**< Bound to akgl_character_sprite_add by akgl_character_initialize. */
akerr_ErrorContext AKERR_NOIGNORE *(*sprite_get)(struct akgl_Character *, int, akgl_Sprite **); /**< Bound to akgl_character_sprite_get by akgl_character_initialize. */
} akgl_Character;
/**
* @brief Zero a pooled character, name it, and publish it in the character registry.
*
* Wipes the struct, copies @p name into it, creates the empty state-to-sprite
* property set, binds the `sprite_add`/`sprite_get` function pointers, inserts
* the character into #AKGL_REGISTRY_CHARACTER under @p name, and takes the first
* reference. Everything numeric (speeds, accelerations, `speedtime`) is left at
* zero for the caller -- or akgl_character_load_json -- to fill in.
*
* @param basechar Pooled character to initialize, normally straight from
* akgl_heap_next_character. Required. Any previous contents are
* discarded without releasing the sprites they referenced.
* @param name Registry key, NUL-terminated. Required. Truncated at
* #AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH. A name already in the
* registry is silently replaced, and the character it displaced
* becomes unreachable rather than being released.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p basechar or @p name is `NULL`, or if SDL
* cannot create the state-to-sprite property set (out of memory).
* @throws AKERR_KEY If the character cannot be written into
* #AKGL_REGISTRY_CHARACTER -- in practice, because akgl_registry_init
* has not run and the registry id is still 0.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_initialize(akgl_Character *basechar, char *name);
/**
* @brief Bind a sprite to one exact combination of actor-state bits.
*
* The map key is the decimal spelling of @p state, so lookups match the *whole*
* value: a sprite added for `FACE_LEFT|MOVING_LEFT` is not found by a lookup for
* `FACE_LEFT` alone. Callers are expected to register every combination they
* intend to draw. Adding a sprite takes a reference on it.
*
* @param basechar Character to add the mapping to. Required. Must already have
* been through akgl_character_initialize.
* @param ref Sprite to draw for @p state. Required. Its `refcount` is
* incremented, so the character keeps it alive.
* @param state The exact actor-state bitmask (`AKGL_ACTOR_STATE_*`) this
* sprite is for. 0 is accepted and is a usable key.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p basechar or @p ref is `NULL`.
*
* @note Re-adding a different sprite for a @p state that is already mapped
* replaces the entry without releasing the sprite it displaced, so the
* displaced sprite's reference is never given back.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_sprite_add(akgl_Character *basechar, akgl_Sprite *ref, int state);
/**
* @brief Look up the sprite bound to one exact combination of actor-state bits.
*
* The counterpart to akgl_character_sprite_add, and matched the same way: an
* exact match on the whole @p state value, with no fallback to a subset of the
* bits and no default sprite. Unlike most lookups in this codebase, *not finding
* one is an error* rather than a successful "nothing here" -- an actor with no
* sprite for its current state cannot be drawn.
*
* @param basechar Character to search. Required.
* @param state The exact actor-state bitmask to look up.
* @param dest Receives the mapped sprite. Required -- the return value is
* spoken for by the error context. Set to `NULL` when there is
* no mapping, alongside the AKERR_KEY error.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p basechar or @p dest is `NULL`.
* @throws AKERR_KEY If no sprite is bound to exactly that @p state. The message
* carries the state both as a decimal and as a bit pattern.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_sprite_get(akgl_Character *basechar, int state, akgl_Sprite **dest);
// This is an SDL iterator so we can't return our error state from it.
/**
* @brief `SDL_EnumerateProperties` callback that applies an akgl_Iterator to one state-to-sprite entry.
*
* Currently implements exactly one operation: with #AKGL_ITERATOR_OP_RELEASE
* set, it hands the mapped sprite back to its heap layer. Every other flag is
* ignored here. Used to tear a character's sprite map down entry by entry.
*
* @param userdata The akgl_Iterator carrying the operation flags, passed through
* by `SDL_EnumerateProperties`. Required despite the `void *` --
* a `NULL` here is an error, not "no operations".
* @param props The character's `state_sprites` set, supplied by SDL.
* @param name The property key: one state bitmask in decimal. Required.
*
* @warning This is an SDL callback, so it returns `void` and has nowhere to
* propagate an error to. It ends in `FINISH_NORETURN`, which logs the
* stack trace and then calls libakerror's unhandled-error handler --
* whose default implementation **exits the process** with the error
* status. A missing sprite or a `NULL` @p userdata therefore terminates
* the game rather than skipping an entry. Install your own
* `akerr_handler_unhandled_error` if that is not what you want.
*/
void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID props, const char *name);
/**
* @brief Build a character from a JSON definition file and register it.
*
* Claims a character from the pool, then reads: `name` (the registry key),
* `sprite_mappings` (an array of `{ "sprite": <name>, "state": [<state names>] }`
* objects), `speedtime` in seconds, `speed_x`, `speed_y`, `acceleration_x`, and
* `acceleration_y`. Each mapping's `state` array is OR-ed together into one
* bitmask by looking every name up in #AKGL_REGISTRY_ACTOR_STATE_STRINGS, so
* `["FACE_LEFT", "MOVING_LEFT"]` becomes a single key.
*
* Every referenced sprite must already be in #AKGL_REGISTRY_SPRITE: this loads
* characters, not sprites, so akgl_sprite_load_json runs first. On any failure
* the pooled character is released again.
*
* @param filename Path to the JSON document. Required. Used verbatim -- it is
* not resolved against `SDL_GetBasePath()`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p filename is `NULL`; if the file cannot be
* opened or does not parse (the message carries jansson's line number
* and text); or if a mapping names a sprite that is not in the sprite
* registry.
* @throws AKERR_KEY If a `state` array contains a name that is not a known
* actor state, if a required top-level key is absent, or if the
* character cannot be added to the registry.
* @throws AKERR_TYPE If a key is present but holds the wrong JSON type -- for
* example `speedtime` as a string. The message names the key.
* @throws AKERR_OUTOFBOUNDS If a `state` array is indexed past its end.
* @throws AKGL_ERR_HEAP If the character pool or the string pool is exhausted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_load_json(char *filename);
#endif // _AKGL_CHARACTER_H_