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>
This commit is contained in:
2026-07-31 11:02:20 -04:00
parent 582008a411
commit f0858b0d38
28 changed files with 3090 additions and 1012 deletions

View File

@@ -174,13 +174,23 @@ akerr_ErrorContext *akgl_actor_update(akgl_Actor *obj)
}
/**
* @brief Actor visible.
* @param obj Object to initialize, inspect, or modify.
* @param camera Camera rectangle used for visibility testing.
* @param visible Output set to whether the actor intersects the camera.
* @brief Decide whether an actor is worth drawing this frame.
*
* Two questions at once: is the actor on camera, and does it want to be drawn.
* The camera test allows a sprite's own width and height on the near edges, so
* an actor partly on screen still counts as visible rather than popping in once
* its origin crosses the boundary.
*
* An actor with no sprite for its current state is reported as not visible
* rather than as an error -- there is nothing to draw, which is an answer.
*
* @param obj The actor to test. Required, along with its `basechar`.
* @param camera The visible rectangle in map coordinates. Required in practice;
* not checked, and dereferenced once a sprite has been found.
* @param visible Receives the verdict. Required; not checked. Written on the
* no-sprite path as well as the ordinary ones.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_KEY When the corresponding validation or operation fails.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKERR_NULLPOINTER If @p obj or `obj->basechar` is `NULL`.
*/
static akerr_ErrorContext *actor_visible(akgl_Actor *obj, SDL_FRect *camera, bool *visible)
{

View File

@@ -91,12 +91,28 @@ void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID regis
}
/**
* @brief Character load json state int from strings.
* @param states JSON array of actor-state names.
* @param dest Output destination populated by the function.
* @brief OR a JSON array of actor-state names together into one bitmask.
*
* This is what lets a character JSON say `["FACE_LEFT", "MOVING_LEFT"]` rather
* than a number: each name is looked up in #AKGL_REGISTRY_ACTOR_STATE_STRINGS
* and OR-ed in. A name that is not a known state is an error rather than being
* skipped -- a typo in a state name would otherwise bind a sprite to the wrong
* combination and show up as art that never appears.
*
* @param states JSON array of state-name strings. Required. An empty array is
* legal and leaves @p dest as it was.
* @param dest Receives the OR of every named bit. Required in practice, and
* **not** checked -- the guard that should test it tests @p states
* a second time instead, so a `NULL` here is a crash. It is OR-ed
* into rather than assigned, so the caller must zero it first.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_KEY When the corresponding validation or operation fails.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKERR_NULLPOINTER If @p states is `NULL`.
* @throws AKERR_KEY If a name is not in the actor-state registry. The message
* quotes it. Note that bits 11 and 12 are unreachable by name; see
* akgl_registry_init_actor_state_strings.
* @throws AKERR_TYPE If an array element is not a string.
* @throws AKERR_OUTOFBOUNDS If the array is indexed past its end.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
static akerr_ErrorContext *akgl_character_load_json_state_int_from_strings(json_t *states, int *dest)
{
@@ -123,11 +139,27 @@ static akerr_ErrorContext *akgl_character_load_json_state_int_from_strings(json_
}
/**
* @brief Character load json inner.
* @param json JSON object to parse.
* @param obj Object to initialize, inspect, or modify.
* @brief Name a character and build its state-to-sprite map from a JSON document.
*
* The half of akgl_character_load_json that deals with identity and sprites; the
* caller handles the numeric fields afterwards. Reads `name`, initializes the
* character with it, then walks `sprite_mappings` binding each named sprite to
* the bitmask its `state` array adds up to.
*
* Every sprite named must already be in #AKGL_REGISTRY_SPRITE.
*
* @param json The parsed character document. Required in practice; not checked,
* and passed straight to accessors that report it.
* @param obj The pooled character to fill in. Required, unchecked -- it is
* handed to akgl_character_initialize, which does check it.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKERR_NULLPOINTER If a mapping names a sprite that is not registered.
* The message names the character, the state, and the sprite.
* @throws AKERR_KEY If `name`, `sprite_mappings`, or a mapping's `sprite` or
* `state` is absent, or if a state name is unknown.
* @throws AKERR_TYPE If one of those keys has the wrong JSON type.
* @throws AKERR_OUTOFBOUNDS If an array is indexed past its end.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
static akerr_ErrorContext *akgl_character_load_json_inner(json_t *json, akgl_Character *obj)
{

View File

@@ -152,11 +152,29 @@ akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *even
}
/**
* @brief Gamepad handle button down.
* @param appstate Application state supplied by SDL.
* @param event SDL input event to process.
* @brief Set the movement and facing bits on the "player" actor for a D-pad or arrow press.
*
* A whole-application shortcut rather than a control-map binding: it looks the
* actor up by the literal name `"player"` in #AKGL_REGISTRY_ACTOR instead of
* being told which actor to act on, so it only ever drives one. The
* akgl_Actor_cmhf_* handlers are the general form.
*
* Facing follows movement unless the actor sets `movement_controls_face`, which
* is how an actor that aims independently of the direction it walks opts out.
*
* Declared nowhere -- `controller.h` advertises this as
* `akgl_controller_handle_button_down`, which does not exist. TODO.md, "Known
* and still open" item 10.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only;
* never read.
* @param event The button or key event. Required. Both the gamepad and
* keyboard unions are read on every call, so the arm that does
* not correspond to the event type is read as garbage -- which
* works only because no real button and keycode pair collides.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`, or if there is
* no actor registered under the name "player".
*/
akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
{
@@ -210,11 +228,20 @@ akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
}
/**
* @brief Gamepad handle button up.
* @param appstate Application state supplied by SDL.
* @param event SDL input event to process.
* @brief Clear the movement bit on the "player" actor for a D-pad or arrow release, and reset its animation.
*
* The counterpart to gamepad_handle_button_down. It clears only the movement
* bit, leaving the facing bits alone so the actor keeps looking the way it was
* walking, and rewinds `curSpriteFrameId` to 0 so the next step starts from the
* first frame of the walk cycle rather than wherever it stopped.
*
* Declared nowhere; see gamepad_handle_button_down.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
* @param event The button or key release event. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`, or if there is
* no actor registered under the name "player".
*/
akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
{
@@ -256,11 +283,28 @@ akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
}
/**
* @brief Gamepad handle added.
* @param appstate Application state supplied by SDL.
* @param event SDL input event to process.
* @brief Open a gamepad that has just been plugged in, and log its mapping.
*
* SDL delivers no button events from an unopened gamepad, so a device that
* appears mid-session has to be opened here the way akgl_controller_open_gamepads
* opens the ones present at startup. A gamepad SDL has already opened is logged
* and left alone.
*
* The mapping is logged because a controller with no entry in the database
* produces no button events at all, and that is otherwise indistinguishable
* from a broken binding.
*
* Declared nowhere; see gamepad_handle_button_down.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
* @param event The SDL_EVENT_GAMEPAD_ADDED event. Required. The joystick id
* is read out of the `gbutton` arm rather than `gdevice`, which
* works because the two share their `which` field.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
*
* @note A failed `SDL_OpenGamepad` is logged rather than reported: this returns
* success and the device stays silent.
*/
akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
{
@@ -293,11 +337,19 @@ akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
}
/**
* @brief Gamepad handle removed.
* @param appstate Application state supplied by SDL.
* @param event SDL input event to process.
* @brief Close a gamepad that has been unplugged.
*
* An unplugged device SDL still has open is a leaked handle, so this closes it.
* A removal for a device that was never opened is logged and otherwise ignored.
* Any control map still holding that `jsid` simply stops matching -- the map is
* not torn down.
*
* Declared nowhere; see gamepad_handle_button_down.
*
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
* @param event The SDL_EVENT_GAMEPAD_REMOVED event. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
*/
akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event)
{

View File

@@ -13,9 +13,9 @@
/** @brief One horizontal run of pixels the flood fill has still to examine. */
typedef struct {
int x1;
int x2;
int y;
int x1; /**< Leftmost column of the run, inclusive. */
int x2; /**< Rightmost column of the run, inclusive. */
int y; /**< The row the run is on. */
} FloodSpan;
/*
@@ -59,6 +59,16 @@ void akgl_draw_background(int w, int h)
*
* @p previous is written before anything that can fail, so a caller may restore
* it unconditionally from a CLEANUP block.
*
* @param self The backend. Assumed non-`NULL` with a live `sdl_renderer`;
* every caller has already checked both.
* @param color The colour to install.
* @param previous Receives the colour that was in place. Assumed non-`NULL`.
* Pre-filled with opaque black, so it is safe to restore from
* even if the query below fails.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_SDL If the current draw colour cannot be read or the new one
* cannot be set. The message carries `SDL_GetError()`.
*/
static akerr_ErrorContext *push_draw_color(akgl_RenderBackend *self, SDL_Color color, SDL_Color *previous)
{
@@ -82,7 +92,18 @@ static akerr_ErrorContext *push_draw_color(akgl_RenderBackend *self, SDL_Color c
SUCCEED_RETURN(errctx);
}
/** @brief Put back the draw color push_draw_color() recorded. */
/**
* @brief Put back the draw color push_draw_color() recorded.
*
* Called from `CLEANUP` blocks under `IGNORE()`, so its error is logged rather
* than propagated -- failing to restore a colour must not mask the failure the
* cleanup is unwinding from.
*
* @param self The backend. Assumed non-`NULL` with a live `sdl_renderer`.
* @param previous The colour push_draw_color() recorded. Assumed non-`NULL`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_SDL If the draw colour cannot be set.
*/
static akerr_ErrorContext *pop_draw_color(akgl_RenderBackend *self, SDL_Color *previous)
{
PREPARE_ERROR(errctx);
@@ -112,6 +133,24 @@ static akerr_ErrorContext *pop_draw_color(akgl_RenderBackend *self, SDL_Color *p
* Running out of stack leaves the region partially filled and reports
* AKERR_OUTOFBOUNDS. There is no way to unwind a partial fill short of keeping
* a copy of the whole surface, and the caller asked for a bounded operation.
*
* @param surface The pixels to fill, in SDL_PIXELFORMAT_RGBA32. Assumed
* non-`NULL` and locked-or-lockless; the caller converts.
* @param x Seed column. Assumed inside the surface -- akgl_draw_flood_fill
* has already range-checked it.
* @param y Seed row, likewise.
* @param oldpixel The packed pixel value the region is made of. Everything else
* is a boundary.
* @param newpixel The packed pixel value to write. Must differ from @p oldpixel:
* if they are equal the fill never terminates making progress,
* which is why the caller checks that case first.
* @param dirty Receives the bounding box of everything written, so the caller
* can put back only the pixels that changed. Assumed non-`NULL`.
* Left describing an empty box if nothing matched.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_OUTOFBOUNDS If the region needs more than
* #AKGL_DRAW_MAX_FLOOD_SPANS pending spans. @p dirty is then not written
* and the surface is partially filled.
*/
static akerr_ErrorContext *flood_region(SDL_Surface *surface, int x, int y, uint32_t oldpixel, uint32_t newpixel, SDL_Rect *dirty)
{

View File

@@ -190,10 +190,24 @@ void akgl_game_updateFPS()
*/
/**
* @brief Serializes an actor name-to-pointer entry.
* @param userdata Open output stream supplied by the caller.
* @param props Actor registry being iterated.
* @param name Actor registry key to serialize.
* @brief Write one actor's name and save-time address to the name table.
*
* An `SDL_EnumerateProperties` callback. The name is written at a fixed
* #AKGL_ACTOR_MAX_NAME_LENGTH bytes so the reader can walk the table without a
* length prefix, and the pointer is written raw -- it is not a pointer the
* loader will ever dereference, only a key it maps back to the object that has
* the same name next run.
*
* @param userdata The open output stream, as a `FILE *`. Required.
* @param props #AKGL_REGISTRY_ACTOR, supplied by SDL.
* @param name The actor's registry key. Read for
* #AKGL_ACTOR_MAX_NAME_LENGTH bytes regardless of its actual
* length, so a shorter key is read past its end.
*
* @warning Ends in `FINISH_NORETURN`: an unhandled error here logs a stack trace
* and then calls libakerror's unhandled-error handler, which by default
* **exits the process**. A failed write during a save terminates the
* game rather than returning an error.
*/
void akgl_game_save_actorname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
{
@@ -211,10 +225,17 @@ void akgl_game_save_actorname_iterator(void *userdata, SDL_PropertiesID props, c
}
/**
* @brief Game save spritename iterator.
* @param userdata Caller data supplied to the SDL property iterator.
* @param props SDL property collection being iterated.
* @param name Registry key or human-readable object name.
* @brief Write one sprite's name and save-time address to the name table.
*
* As akgl_game_save_actorname_iterator, but the name field is
* #AKGL_SPRITE_MAX_NAME_LENGTH wide.
*
* @param userdata The open output stream, as a `FILE *`. Required.
* @param props #AKGL_REGISTRY_SPRITE, supplied by SDL.
* @param name The sprite's registry key, written at fixed width.
*
* @warning Terminates the process on an unhandled error. See
* akgl_game_save_actorname_iterator.
*/
void akgl_game_save_spritename_iterator(void *userdata, SDL_PropertiesID props, const char *name)
{
@@ -232,10 +253,21 @@ void akgl_game_save_spritename_iterator(void *userdata, SDL_PropertiesID props,
}
/**
* @brief Game save spritesheetname iterator.
* @param userdata Caller data supplied to the SDL property iterator.
* @param props SDL property collection being iterated.
* @param name Registry key or human-readable object name.
* @brief Write one spritesheet's name and save-time address to the name table.
*
* As akgl_game_save_actorname_iterator, but the name field is
* #AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH wide -- the widest of the four, since
* a spritesheet is keyed by its resolved path.
*
* @param userdata The open output stream, as a `FILE *`. Required.
* @param props #AKGL_REGISTRY_SPRITESHEET, supplied by SDL.
* @param name The spritesheet's registry key, written at fixed width.
*
* @warning Terminates the process on an unhandled error. See
* akgl_game_save_actorname_iterator.
* @note This is the table the loader disagrees with: it reads every table at
* #AKGL_ACTOR_MAX_NAME_LENGTH, so a save containing any spritesheet cannot
* be read back. TODO.md, "Known and still open" item 7.
*/
void akgl_game_save_spritesheetname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
{
@@ -253,10 +285,17 @@ void akgl_game_save_spritesheetname_iterator(void *userdata, SDL_PropertiesID pr
}
/**
* @brief Game save charactername iterator.
* @param userdata Caller data supplied to the SDL property iterator.
* @param props SDL property collection being iterated.
* @param name Registry key or human-readable object name.
* @brief Write one character's name and save-time address to the name table.
*
* As akgl_game_save_actorname_iterator, but the name field is
* #AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH wide.
*
* @param userdata The open output stream, as a `FILE *`. Required.
* @param props #AKGL_REGISTRY_CHARACTER, supplied by SDL.
* @param name The character's registry key, written at fixed width.
*
* @warning Terminates the process on an unhandled error. See
* akgl_game_save_actorname_iterator.
*/
void akgl_game_save_charactername_iterator(void *userdata, SDL_PropertiesID props, const char *name)
{
@@ -274,10 +313,23 @@ void akgl_game_save_charactername_iterator(void *userdata, SDL_PropertiesID prop
}
/**
* @brief Game save actors.
* @param fp Open save-game stream.
* @brief Write all four name-to-address tables, each with its terminator.
*
* Actors, sprites, spritesheets, characters, in that order -- which is the order
* akgl_game_load reads them back in. Each table is the registry enumerated one
* entry at a time, followed by a zeroed name field and a zeroed pointer that the
* reader stops on.
*
* @param fp The open save-game stream. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKERR_NULLPOINTER If @p fp is `NULL`.
* @throws AKERR_IO On a stream error or a short write while emitting a
* terminator.
*
* @note Only the terminators are written through the error-reporting path. The
* entries themselves go through `SDL_EnumerateProperties`, whose callbacks
* cannot report failure upward and instead terminate the process -- so a
* write error mid-table never reaches this function's return value.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save_actors(FILE *fp)
{
@@ -352,14 +404,40 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath)
}
/**
* @brief Game load objectnamemap.
* @param fp Open save-game stream.
* @param map Tilemap to inspect, draw, or modify.
* @param namelength Fixed serialized name-field length.
* @param ptrlength Serialized pointer-field length.
* @param registry SDL property registry being queried.
* @brief Read one name table back, building an old-address to current-object map.
*
* This is the mechanism that makes a save file portable across runs. Objects are
* serialized carrying the addresses they had at save time; those addresses mean
* nothing now, but the table says which *name* lived at each one, and the name
* still resolves through the registry. So each entry becomes
* `"0x7f..." -> current object`, and a deserialized pointer can be translated by
* looking up its printed form.
*
* The keys are the old pointers rendered with `%p`, because SDL property sets
* take string keys only.
*
* Reading stops at the terminator -- a zeroed name and a zeroed pointer.
*
* @param fp The open save-game stream, positioned at the start of a
* table. Required.
* @param map The property set to fill in. Required, and created by the
* caller.
* @param namelength Width of the table's name field, which must match what the
* corresponding save iterator wrote. It also sizes a
* variable-length array on the stack.
* @param ptrlength Width of the table's pointer field, i.e. `sizeof` the
* pointer type that table holds.
* @param registry The registry to resolve names against -- #AKGL_REGISTRY_ACTOR
* for the actor table, and so on.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_* Propagates an error reported by a delegated operation.
* @throws AKERR_EOF If the stream ends before a terminator is found, which is
* what a truncated save file looks like.
* @throws AKERR_IO On a stream error.
* @throws AKERR_NULLPOINTER If @p fp is `NULL`, by way of the read wrapper.
*
* @note A name in the table that is no longer in @p registry maps to `NULL`
* rather than being reported: the entry is written with whatever
* `SDL_GetPointerProperty` returned.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_objectnamemap(FILE *fp, SDL_PropertiesID map, int namelength, int ptrlength, SDL_PropertiesID registry)
{
@@ -406,14 +484,24 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_objectnamemap(FILE *fp, SDL_Pr
}
/**
* @brief Game load versioncmp.
* @param versiontype Description of the version being compared.
* @param newversion Version read from the save data.
* @param curversion Version supported by the running library.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_API When the corresponding validation or operation fails.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKERR_VALUE When the corresponding validation or operation fails.
* @brief Refuse a save file unless its version matches the running one exactly.
*
* Both strings are parsed as semver and compared with `"="` -- exact equality,
* not compatibility. That is deliberate and strict: a save file is a raw memory
* image of `akgl_Game` and the object tables, so any change to a struct layout
* invalidates it, and semver has no way to say "the layout did not move".
*
* @param versiontype What is being compared -- `"library"` or `"game"`. Used
* only to build the message, but required, since a bare
* "incompatible version" error would not say which.
* @param newversion The version read out of the save file. Required.
* @param curversion The version of the running program or library. Required.
* @return `NULL` when the two match, otherwise an error context owned by the
* caller.
* @throws AKERR_NULLPOINTER If any of the three arguments is `NULL`.
* @throws AKERR_VALUE If either string is not valid semver. The message says
* which side it came from.
* @throws AKERR_API If both parse but are not equal. The message quotes both.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_versioncmp(char *versiontype, char *newversion, char *curversion)
{

View File

@@ -41,12 +41,31 @@ akerr_ErrorContext *akgl_sprite_sheet_coords_for_frame(akgl_Sprite *self, SDL_FR
}
/**
* @brief Sprite load json spritesheet.
* @param json JSON object to parse.
* @param sheet Spritesheet used by the sprite.
* @param relative_path Base directory for relative asset references.
* @brief Find or load the spritesheet a sprite definition names.
*
* Resolves the `spritesheet.filename` path relative to the sprite definition's
* own directory, then looks that resolved path up in #AKGL_REGISTRY_SPRITESHEET.
* A hit is reused as is -- which is what keeps ten sprites cut from one image
* down to one texture -- and a miss claims a sheet from the pool and loads it,
* reading `frame_width` and `frame_height` only in that case.
*
* @param json The parsed sprite document, whose `spritesheet` object
* this reads. Required in practice; not checked.
* @param sheet Receives the sheet, whether found or freshly loaded.
* Required; not checked. On the reuse path the sheet's
* reference count is *not* incremented, so the sprite
* borrows it.
* @param relative_path Directory to resolve the image path against -- the
* directory holding the sprite JSON. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_* Propagates an error reported by a delegated operation.
* @throws AKERR_KEY If the document has no `spritesheet` object, or that object
* has no `filename` -- or, on the load path, no `frame_width` or
* `frame_height`.
* @throws AKERR_TYPE If one of those has the wrong JSON type.
* @throws AKERR_OUTOFBOUNDS If the joined path is too long for a pooled string.
* @throws ENOENT If the image path does not exist.
* @throws AKGL_ERR_SDL If the image cannot be decoded.
* @throws AKGL_ERR_HEAP If the spritesheet or string pool is exhausted.
*/
static akerr_ErrorContext *akgl_sprite_load_json_spritesheet(json_t *json, akgl_SpriteSheet **sheet, char *relative_path)
{

View File

@@ -88,12 +88,21 @@ akerr_ErrorContext *akgl_get_json_properties_integer(json_t *obj, char *key, int
}
/**
* @brief Get json properties number.
* @param obj Object to initialize, inspect, or modify.
* @param key JSON object key or property name to locate.
* @param dest Output destination populated by the function.
* @brief Read a Tiled custom property declared as `number`.
*
* Tiled writes `float` for a floating-point property, so this matches nothing
* Tiled produces today -- akgl_get_json_properties_float is the one the loader
* uses. Kept because a hand-written or older map may still say `number`.
*
* @param obj The Tiled object whose `properties` array to search. Required.
* @param key The property name. Required.
* @param dest Receives the value as a `float`. Not written on any failure path.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_* Propagates an error reported by a delegated operation.
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
* @throws AKERR_TYPE If the property is not declared `number`, or its `value` is
* not numeric.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext *akgl_get_json_properties_number(json_t *obj, char *key, float *dest)
{
@@ -106,12 +115,20 @@ akerr_ErrorContext *akgl_get_json_properties_number(json_t *obj, char *key, floa
}
/**
* @brief Get json properties float.
* @param obj Object to initialize, inspect, or modify.
* @param key JSON object key or property name to locate.
* @param dest Output destination populated by the function.
* @brief Read a Tiled custom property declared as `float`.
*
* The spelling Tiled actually writes for a floating-point property. This is how
* the `scale` on a perspective marker is read.
*
* @param obj The Tiled object whose `properties` array to search. Required.
* @param key The property name. Required.
* @param dest Receives the value. Not written on any failure path.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_* Propagates an error reported by a delegated operation.
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
* @throws AKERR_TYPE If the property is not declared `float`, or its `value` is
* not numeric.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext *akgl_get_json_properties_float(json_t *obj, char *key, float *dest)
{
@@ -124,12 +141,22 @@ akerr_ErrorContext *akgl_get_json_properties_float(json_t *obj, char *key, float
}
/**
* @brief Get json properties double.
* @param obj Object to initialize, inspect, or modify.
* @param key JSON object key or property name to locate.
* @param dest Output destination populated by the function.
* @brief Read a Tiled custom property declared as `float`, at full precision.
*
* Same declared type as akgl_get_json_properties_float, but writing a `double`.
* The map's physics constants are `double`, which is the reason both exist.
*
* @param obj The Tiled object whose `properties` array to search. Required.
* @param key The property name. Required.
* @param dest Receives the value. Not written on any failure path.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_* Propagates an error reported by a delegated operation.
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
* akgl_tilemap_load_physics passes this status to
* akgl_get_json_with_default, which turns it into a default of 0.0.
* @throws AKERR_TYPE If the property is not declared `float`, or its `value` is
* not numeric.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext *akgl_get_json_properties_double(json_t *obj, char *key, double *dest)
{
@@ -262,13 +289,39 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root,
}
/**
* @brief Tilemap load layer object actor.
* @param curobj Tilemap object being populated.
* @param layerdatavalue JSON object describing the layer entry.
* @param layerid Zero-based tilemap layer index.
* @param dirname Directory containing paths referenced by the document.
* @brief Turn one Tiled object into a live actor, creating it if it is not already registered.
*
* The object's name is the actor's registry key, which is what lets the same
* actor be placed by more than one object: the first placement creates it and
* binds the character named in the object's `character` property, and every
* later one just takes another reference. Either way the object's position,
* visibility, and layer are copied onto the actor and the object keeps a
* pointer to it.
*
* Not declared in tilemap.h -- reachable only through
* akgl_tilemap_load_layer_objects.
*
* @param curobj The tilemap object, with its `name`, `x`, `y`, and
* `visible` already read from JSON. Required, unchecked,
* dereferenced at once.
* @param layerdatavalue The object's JSON, for the custom properties --
* `character` and `state` -- that are not part of Tiled's
* own object fields. Required.
* @param layerid The layer this object sits on, copied onto the actor.
* @param dirname Directory to resolve paths against. Accepted for
* signature consistency; nothing here uses it.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_KEY When the corresponding validation or operation fails.
* @throws AKERR_KEY If the object's name is empty -- an actor has to be
* addressable -- if the `character` or `state` property is absent, or if
* the named character is not in #AKGL_REGISTRY_CHARACTER.
* @throws AKERR_TYPE If `character` is not declared `string` or `state` not
* declared `int`.
* @throws AKERR_NULLPOINTER If akgl_actor_initialize refuses the name.
* @throws AKGL_ERR_HEAP If the actor or string pool is exhausted.
*
* @note An existing actor's `character` and position are overwritten by each
* later placement, so two objects naming one actor leave it wherever the
* last one put it rather than producing two.
*/
akerr_ErrorContext *akgl_tilemap_load_layer_object_actor(akgl_TilemapObject *curobj, json_t *layerdatavalue, int layerid, akgl_String *dirname)
{
@@ -392,14 +445,32 @@ akerr_ErrorContext *akgl_tilemap_load_layer_tile(akgl_Tilemap *dest, json_t *roo
}
/**
* @brief Tilemap load layer image.
* @param dest Output destination populated by the function.
* @param root Base directory used to resolve the path.
* @param layerid Zero-based tilemap layer index.
* @param dirname Directory containing paths referenced by the document.
* @brief Load one image layer: upload its image as a single texture.
*
* An image layer is one picture rather than a grid, which is what a parallax
* backdrop wants. The layer's `width` and `height` are taken from the loaded
* texture rather than from the JSON, so they are always the true pixel size.
*
* Not declared in tilemap.h -- reachable only through akgl_tilemap_load_layers.
*
* @param dest The map to load into. Required.
* @param root The layer's JSON object. Required.
* @param layerid Which layer slot to fill. Not bounds-checked.
* @param dirname Directory to resolve the layer's `image` path against.
* Required. Joined with a `/` rather than run through
* akgl_path_relative, so unlike a tileset image this path is not
* canonicalized and an absolute one will not work.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
* @throws AKERR_NULLPOINTER If @p dest, @p root, or @p dirname is `NULL`.
* @throws AKERR_KEY If the layer has no `image`.
* @throws AKERR_TYPE If `image` is not a string.
* @throws AKGL_ERR_SDL If the image cannot be loaded. The message carries
* `SDL_GetError()`.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*
* @note The joined path is truncated at
* #AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE without complaint, so an
* over-long path fails as a missing file rather than as a length error.
*/
akerr_ErrorContext *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname)
{
@@ -488,11 +559,31 @@ akerr_ErrorContext *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, a
}
/**
* @brief Tilemap load physics.
* @param dest Output destination populated by the function.
* @param root Base directory used to resolve the path.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_KEY When the corresponding validation or operation fails.
* @brief Give the map its own physics backend, if it asked for one.
*
* A map with a `physics.model` custom property gets its own backend, configured
* from `physics.gravity.x`/`.y`/`.z` and `physics.drag.x`/`.y`/`.z`, and
* `use_own_physics` is set so a caller knows to simulate through it. Each
* constant defaults to 0.0 when absent, so a map can name a model and override
* only what it cares about.
*
* Absence is the normal case at every level: a map with no properties at all, or
* with properties but no `physics.model`, uses the game's global backend and
* this returns success. That is why the AKERR_KEY handlers here are not error
* paths -- they are the "map did not ask" path.
*
* Not declared in tilemap.h -- reachable only through akgl_tilemap_load.
*
* @param dest The map to configure. Required in practice; not checked here,
* since akgl_tilemap_load has already done so.
* @param root The map's root JSON object. Required, likewise.
* @return `NULL` on success -- including when the map declared no physics at
* all -- otherwise an error context owned by the caller.
* @throws AKERR_KEY If `physics.model` names a backend that does not exist.
* @throws AKERR_TYPE If `physics.model` is not declared `string`, or one of the
* six constants is not declared `float`.
* @throws AKERR_NULLPOINTER If akgl_physics_factory refuses its arguments.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*/
akerr_ErrorContext *akgl_tilemap_load_physics(akgl_Tilemap *dest, json_t *root)
{

View File

@@ -21,13 +21,34 @@
#include <akstdlib.h>
/**
* @brief Path relative root.
* @param root Base directory used to resolve the path.
* @param path Path to resolve or transform.
* @param dst Output destination populated by the function.
* @brief Resolve @p path against @p root and write the absolute result to @p dst.
*
* The second half of akgl_path_relative: joins the two with a `/` and resolves
* the join, so symlinks and `..` are folded out and the result is absolute. It
* is the fallback, reached only once resolving @p path on its own has failed.
*
* Not declared in util.h -- it is reachable only through akgl_path_relative.
*
* @param root Directory to resolve against, normally `dirname` of the file that
* named @p path. Required. A trailing `/` is harmless; the join adds
* one unconditionally and `realpath` collapses the double.
* @param path Relative path to resolve. Required. An absolute @p path still gets
* @p root pasted in front of it, which will not exist -- so callers
* must not send absolute paths down this branch.
* @param dst Receives the resolved absolute path. Required, and must already be
* a claimed pool string.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
* @throws AKERR_NULLPOINTER If @p root, @p path, or @p dst is `NULL`.
* @throws AKERR_OUTOFBOUNDS If `root + path` is at least #AKGL_MAX_STRING_LENGTH
* bytes. The message reports both the combined length and the limit.
* @throws ENOENT If the joined path does not exist. Any other `errno`
* `realpath(3)` raises -- EACCES, ELOOP, ENOTDIR -- propagates likewise.
* @throws AKGL_ERR_HEAP If the string pool cannot supply the two scratch buffers.
*
* @note The AKERR_OUTOFBOUNDS check uses `FAIL_RETURN` from inside the `ATTEMPT`
* block, which returns past `CLEANUP` -- so on that one path the two
* scratch strings are never released. This is exactly the hazard AGENTS.md
* warns about; it wants a `FAIL_BREAK`.
*/
akerr_ErrorContext *akgl_path_relative_root(char *root, char *path, akgl_String *dst)
{
@@ -102,12 +123,27 @@ akerr_ErrorContext *akgl_path_relative(char *root, char *path, akgl_String *dst)
}
/**
* @brief Path relative from.
* @param path Path to resolve or transform.
* @param from Base path from which the result is made relative.
* @param dst Output destination populated by the function.
* @brief Intended to resolve @p path against the directory holding @p from. Unfinished.
*
* The shape is there -- resolve @p from, take its `dirname` -- but the body
* stops before it does anything with @p path, and `*dst` is never written. Not
* declared in util.h, and nothing in the tree calls it.
*
* @param path The path to resolve. Required, and currently ignored past the
* `NULL` check.
* @param from A file whose directory @p path should be taken as relative to.
* Required. Must exist -- it is resolved with `realpath(3)`.
* @param dst Intended to receive the resolved path. Never written. Note the
* double indirection, which disagrees with the rest of the
* `akgl_path_relative*` family; see TODO.md item 40.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
* @throws AKERR_NULLPOINTER If @p path or @p from is `NULL`.
* @throws ENOENT If @p from does not exist, along with any other `errno`
* `realpath(3)` raises.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*
* @warning Known defect: the scratch string it claims is never released, so 256
* calls exhaust the string pool. TODO.md, "Known and still open" item 4.
*/
akerr_ErrorContext *akgl_path_relative_from(char *path, char *from, akgl_String **dst)
{