From f0858b0d38152404dc281344ec780e2c214f32e8 Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Fri, 31 Jul 2026 11:02:20 -0400 Subject: [PATCH] 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) --- include/akgl/actor.h | 425 ++++++++++++++++++++++-------- include/akgl/assets.h | 34 ++- include/akgl/audio.h | 123 ++++++--- include/akgl/character.h | 157 ++++++++--- include/akgl/controller.h | 222 +++++++++++----- include/akgl/draw.h | 177 +++++++++---- include/akgl/error.h | 10 +- include/akgl/game.h | 256 ++++++++++++++---- include/akgl/heap.h | 182 +++++++++---- include/akgl/iterator.h | 30 ++- include/akgl/json_helpers.h | 256 ++++++++++++------ include/akgl/physics.h | 236 ++++++++++++----- include/akgl/registry.h | 187 ++++++++++--- include/akgl/renderer.h | 166 +++++++++--- include/akgl/sprite.h | 161 +++++++++--- include/akgl/staticstring.h | 56 +++- include/akgl/text.h | 112 +++++--- include/akgl/tilemap.h | 505 ++++++++++++++++++++++++++---------- include/akgl/types.h | 11 +- include/akgl/util.h | 193 ++++++++++---- src/actor.c | 22 +- src/character.c | 50 +++- src/controller.c | 84 ++++-- src/draw.c | 47 +++- src/game.c | 156 ++++++++--- src/sprite.c | 29 ++- src/tilemap.c | 157 ++++++++--- src/util.c | 58 ++++- 28 files changed, 3090 insertions(+), 1012 deletions(-) diff --git a/include/akgl/actor.h b/include/akgl/actor.h index b3020cc..daaa83d 100644 --- a/include/akgl/actor.h +++ b/include/akgl/actor.h @@ -1,6 +1,32 @@ /** * @file actor.h - * @brief Declares the public actor API. + * @brief A live thing in the world: state bits, motion, hierarchy, and behaviour hooks. + * + * An actor is an instance; the akgl_Character it points at is its template. The + * actor carries only what is unique to it -- where it is, which way it is + * moving, which animation frame it is on -- and borrows speed, acceleration and + * the state-to-sprite map from the character. That is what makes a hundred of + * one kind of thing cheap. + * + * **State is a bitmask**, not an enum. An actor is facing left *and* moving left + * *and* alive at the same time, and the whole combination is the key that + * selects a sprite (see akgl_character_sprite_get). Adding a state means adding + * a bit here *and* a name in `src/actor_state_string_names.c`, or character JSON + * cannot refer to it. + * + * **Behaviour attaches as function pointers**, not by inheritance. Every actor + * gets the library's default `updatefunc`, `renderfunc`, `facefunc`, + * `movementlogicfunc`, `changeframefunc` and `addchild` from + * akgl_actor_initialize; a game that wants a different flavour of anything + * replaces the pointer on that one actor. + * + * **Motion is split four ways** -- thrust, environmental, acceleration, and the + * velocity that is their sum -- so that a character's own walking and the + * world's gravity and drag can be reasoned about separately. See physics.h for + * the model and which fields mean what. + * + * Actors are pool objects (akgl_heap_next_actor) published in + * #AKGL_REGISTRY_ACTOR under their name. */ #ifndef _AKGL_ACTOR_H_ @@ -48,214 +74,385 @@ #define AKGL_ACTOR_STATE_UNDEFINED_30 1 << 30 // 1073741824 0100 0000 0000 0000 #define AKGL_ACTOR_STATE_UNDEFINED_31 1 << 31 // 2147483648 1000 0000 0000 0000 +/** @brief Bits in an actor's state word. Fixed by the width of `int32_t state`. */ #define AKGL_ACTOR_MAX_STATES 32 // This is an array of strings equal to actor states from 1-32. // This is built by a utility script and not kept in git, see // the Makefile for lib_src/actor_state_string_names.c -/** @brief Maps actor-state bit positions to symbolic names. */ +/** + * @brief Bit position -> state name, as text. Index `i` names the bit `1 << i`. + * + * akgl_registry_init_actor_state_strings builds + * #AKGL_REGISTRY_ACTOR_STATE_STRINGS out of this, which is what lets character + * JSON write `"AKGL_ACTOR_STATE_FACE_LEFT"` instead of `2`. A bit whose name + * here does not match its `#define` above cannot be referred to from JSON at + * all. + * + * @warning Three known defects, all tracked in TODO.md items 24-26: the + * definition in `src/actor_state_string_names.c` is 32 entries while + * this declares 33, so reading index 32 reads past the object; bits 11 + * and 12 are named `UNDEFINED_11`/`UNDEFINED_12` rather than + * `MOVING_IN`/`MOVING_OUT`; and the comment above about a generator + * script is stale -- there is no such script, and the file is tracked + * in git and maintained by hand. + */ extern char *AKGL_ACTOR_STATE_STRING_NAMES[AKGL_ACTOR_MAX_STATES+1]; +/** @brief Every facing bit. Clear this before setting one, so an actor faces exactly one way. */ #define AKGL_ACTOR_STATE_FACE_ALL (AKGL_ACTOR_STATE_FACE_DOWN | AKGL_ACTOR_STATE_FACE_LEFT | AKGL_ACTOR_STATE_FACE_RIGHT | AKGL_ACTOR_STATE_FACE_UP) +/** @brief Every planar movement bit. The two depth bits, MOVING_IN and MOVING_OUT, are not in it. */ #define AKGL_ACTOR_STATE_MOVING_ALL (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_MOVING_UP | AKGL_ACTOR_STATE_MOVING_DOWN) +/** @brief Longest actor name, including the terminator. Names are truncated, not rejected. */ #define AKGL_ACTOR_MAX_NAME_LENGTH 128 +/** @brief Children one actor can carry. A child moves with its parent rather than simulating. */ #define AKGL_ACTOR_MAX_CHILDREN 8 +/** @brief Actors in the pool. Override before including heap.h to change it; see heap.h. */ #define AKGL_MAX_HEAP_ACTOR 64 /** @brief Represents a live actor, including state, motion, hierarchy, and behavior callbacks. */ typedef struct akgl_Actor { - uint8_t refcount; - char name[AKGL_ACTOR_MAX_NAME_LENGTH]; - akgl_Character *basechar; - uint8_t curSpriteFrameId; - SDL_Time curSpriteFrameTimer; - bool curSpriteReversing; - uint32_t layer; - int32_t state; - bool movement_controls_face; - void *actorData; - bool visible; - SDL_Time movetimer; + uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. One reference per parent, plus one for the registry. */ + char name[AKGL_ACTOR_MAX_NAME_LENGTH]; /**< Registry key. Truncated, not rejected, if the source name is longer. */ + akgl_Character *basechar; /**< The template this actor instantiates. Borrowed; no reference is taken. Required before the actor can update, render, or simulate. */ + uint8_t curSpriteFrameId; /**< Index into the current sprite's `frameids`, not a frame number on the sheet. */ + SDL_Time curSpriteFrameTimer; /**< When the current frame was shown. The next frame is due once the sprite's `speed` has elapsed. */ + bool curSpriteReversing; /**< Walking the animation backwards, for a sprite with `loopReverse` set. */ + uint32_t layer; /**< Which tilemap layer the actor is drawn and simulated on. Set from the object layer it was placed in. */ + int32_t state; /**< The `AKGL_ACTOR_STATE_*` bitmask. The whole value is the key that selects a sprite. */ + bool movement_controls_face; /**< When set, the default `facefunc` turns the actor to face whichever way it is moving. Clear it for an actor that aims independently. */ + void *actorData; /**< The game's own per-actor data. Never read or freed by the library. */ + bool visible; /**< Whether to draw at all. An actor off-camera is skipped separately, so this is for deliberate hiding. */ + SDL_Time movetimer; /**< Unused. Nothing in the library reads or writes it. */ // Velocity. Combined effect of all forces acting on the actor resulting // in energy along an axis. - float32_t vx; - float32_t vy; - float32_t vz; + float32_t vx; /**< Velocity along x, world units per second. Recomputed each step as `ex + tx`; writing it directly is overwritten. */ + float32_t vy; /**< Velocity along y. On a child actor this is read as an offset from the parent instead. */ + float32_t vz; /**< Velocity along z. */ // Environmental velocity. These are the forces acting on the actor by the // environment (such as gravity and atmospheric drag) - float32_t ex; - float32_t ey; - float32_t ez; + float32_t ex; /**< Environmental velocity along x: what gravity and drag have done. Accumulates across steps. */ + float32_t ey; /**< Environmental velocity along y. This is what carries a falling actor. */ + float32_t ez; /**< Environmental velocity along z. */ // Thrust. Energy originating only from the actor's own acceleration on a // given axis, before the effects of gravity and drag. - float32_t tx; - float32_t ty; - float32_t tz; + float32_t tx; /**< Thrust along x: the actor's own effort. Capped at `sx`, which is why gravity can outrun top speed and walking cannot. */ + float32_t ty; /**< Thrust along y, capped at `sy`. */ + float32_t tz; /**< Thrust along z, capped at `sz`. */ // Acceleration. These are borrowed from the base character object. // A given axis resets to 0 when the actor stops moving in a given axis. - float32_t ax; - float32_t ay; - float32_t az; + float32_t ax; /**< Acceleration along x, signed by the direction of travel. Copied from the character each step by the default movement logic. */ + float32_t ay; /**< Acceleration along y, signed the same way. */ + float32_t az; /**< Acceleration along z. Not set by the default movement logic. */ // Max speed. These are borrowed from the base character object. - float32_t sx; - float32_t sy; - float32_t sz; + float32_t sx; /**< Maximum thrust along x, copied from the character. */ + float32_t sy; /**< Maximum thrust along y. */ + float32_t sz; /**< Maximum thrust along z. */ // Position. - float32_t x; - float32_t y; - float32_t z; - float32_t scale; - struct akgl_Actor *children[AKGL_ACTOR_MAX_CHILDREN]; - struct akgl_Actor *parent; - akerr_ErrorContext AKERR_NOIGNORE *(*updatefunc)(struct akgl_Actor *obj); - akerr_ErrorContext AKERR_NOIGNORE *(*renderfunc)(struct akgl_Actor *obj); - akerr_ErrorContext AKERR_NOIGNORE *(*facefunc)(struct akgl_Actor *obj); - akerr_ErrorContext AKERR_NOIGNORE *(*movementlogicfunc)(struct akgl_Actor *obj, float32_t dt); - akerr_ErrorContext AKERR_NOIGNORE *(*changeframefunc)(struct akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtimems); - akerr_ErrorContext AKERR_NOIGNORE *(*addchild)(struct akgl_Actor *obj, struct akgl_Actor *child); + float32_t x; /**< Position in map pixels. For a child, an offset from the parent. */ + float32_t y; /**< Position in map pixels. Also what the tilemap's perspective band scales against. */ + float32_t z; /**< Depth. Carried through the simulation but not used at draw time. */ + float32_t scale; /**< Draw scale, 1.0 for natural size. Written by akgl_tilemap_scale_actor, or forced to 1.0 when tilemap scaling is off. */ + struct akgl_Actor *children[AKGL_ACTOR_MAX_CHILDREN]; /**< Attached actors, `NULL` in unused slots. Each holds a reference and is released with the parent. */ + struct akgl_Actor *parent; /**< The actor this one is attached to, or `NULL`. A child is positioned relative to it and does not simulate on its own. */ + akerr_ErrorContext AKERR_NOIGNORE *(*updatefunc)(struct akgl_Actor *obj); /**< Per-frame logic: facing, then animation frame. Defaults to akgl_actor_update. */ + akerr_ErrorContext AKERR_NOIGNORE *(*renderfunc)(struct akgl_Actor *obj); /**< Per-frame draw. Defaults to akgl_actor_render. */ + akerr_ErrorContext AKERR_NOIGNORE *(*facefunc)(struct akgl_Actor *obj); /**< Chooses the facing bits. Defaults to akgl_actor_automatic_face. */ + akerr_ErrorContext AKERR_NOIGNORE *(*movementlogicfunc)(struct akgl_Actor *obj, float32_t dt); /**< Called by the physics step before gravity. Defaults to akgl_actor_logic_movement. May raise AKGL_ERR_LOGICINTERRUPT to skip the rest of the step. */ + akerr_ErrorContext AKERR_NOIGNORE *(*changeframefunc)(struct akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtimems); /**< Advances the animation. Defaults to akgl_actor_logic_changeframe. */ + akerr_ErrorContext AKERR_NOIGNORE *(*addchild)(struct akgl_Actor *obj, struct akgl_Actor *child); /**< Attaches a child. Defaults to akgl_actor_add_child. */ } akgl_Actor; /** - * @brief Actor initialize. - * @param obj Object to initialize, inspect, or modify. - * @param name Registry key or human-readable object name. + * @brief Zero a pooled actor, name it, wire up its default behaviour, and register it. + * + * Sets `scale` to 1.0 and `movement_controls_face` to true, installs all six + * default function pointers, publishes the actor in #AKGL_REGISTRY_ACTOR, and + * takes the first reference. It does *not* set a character: the actor cannot + * update, render, or simulate until akgl_actor_set_character has run. + * + * @param obj Pooled actor to initialize, normally from akgl_heap_next_actor. + * Required. Any previous contents are discarded -- including its + * child list, so release an actor rather than reinitializing it. + * @param name Registry key, NUL-terminated. Required. Truncated at + * #AKGL_ACTOR_MAX_NAME_LENGTH. An existing entry with the same name + * is silently replaced, and the actor it displaced becomes + * unreachable rather than being released. * @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 @p name is `NULL`. + * @throws AKERR_KEY If the actor cannot be written into #AKGL_REGISTRY_ACTOR -- + * in practice, because akgl_registry_init has not run. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_initialize(akgl_Actor *obj, char *name); /** - * @brief Actor set character. - * @param obj Object to initialize, inspect, or modify. - * @param basecharname Registry name of the character to assign. + * @brief Bind an actor to a registered character, and copy its top speeds. + * + * Looks the character up by name and takes its `sx`/`sy` as this actor's speed + * limits, zeroing `ax` and `ay` so the actor starts from rest. The character is + * borrowed, not referenced: releasing it out from under a live actor leaves a + * dangling pointer. + * + * @param obj The actor to bind. Required. + * @param basecharname Registry name of the character. Required. Rebinding an + * actor mid-life is allowed, and is how a character swaps + * its whole sprite set at once. * @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 obj or @p basecharname is `NULL`, or if no + * character is registered under that name. The last case is a lookup + * miss reported with a pointer status rather than AKERR_KEY. + * + * @note `sz` is not copied and `az` is not zeroed, so an actor's depth speed + * keeps whatever it had. The default movement logic re-copies all three + * each step, which papers over it for anything that simulates. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_set_character(akgl_Actor *obj, char *basecharname); /** - * @brief Actor render. - * @param obj Object to initialize, inspect, or modify. - * @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_OUTOFBOUNDS When the corresponding validation or operation fails. + * @brief Draw an actor's current animation frame, if it is on camera. + * + * Selects the sprite for the actor's current state, checks it against the + * camera, and blits the frame at the actor's position translated into screen + * space. A child actor is drawn at its parent's position plus its own, which is + * what makes an offset mean an offset. + * + * Several things are skipped rather than reported, because a frame is not the + * place to fail: an actor with no sprite for its state, one off camera, one with + * `visible` clear, and one whose frame index has run past its current sprite + * are all simply not drawn. + * + * @param obj The actor to draw. Required, along with its `basechar`. + * @return `NULL` on success -- including every skipped case above -- otherwise + * an error context owned by the caller. + * @throws AKERR_NULLPOINTER If @p obj or `obj->basechar` is `NULL`. + * @throws AKERR_* Whatever akgl_sprite_sheet_coords_for_frame or the renderer's + * `draw_texture` raises. + * + * @note The destination height is computed from the sprite's *width*, so a + * non-square sprite is drawn square. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_render(akgl_Actor *obj); /** - * @brief Actor update. - * @param obj Object to initialize, inspect, or modify. + * @brief Per-frame logic: turn the actor to face its direction, then advance its animation. + * + * Calls the actor's `facefunc`, then selects the sprite for the resulting state + * and calls `changeframefunc` if that sprite's frame time has elapsed. An actor + * with no sprite registered for its current state is left alone and reported as + * success -- states change faster than art gets drawn, and a missing sprite + * should not stop the frame. + * + * @param obj The actor to update. Required, along with its `basechar`. * @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`. + * @throws AKERR_* Whatever `facefunc` or `changeframefunc` raises. Note that an + * AKERR_KEY from either is swallowed along with the missing-sprite case. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_update(akgl_Actor *obj); /** - * @brief Actor logic movement. - * @param obj Object to initialize, inspect, or modify. - * @param dt Elapsed simulation time in seconds. + * @brief The default `movementlogicfunc`: turn movement bits into signed acceleration. + * + * Re-copies the character's speed limits onto the actor -- so a change to the + * character takes effect next step -- and sets `ax`/`ay` to plus or minus the + * character's acceleration according to which movement bits are set. It does not + * integrate anything; the physics backend does that. + * + * Opposing bits do not cancel: `MOVING_LEFT` wins over `MOVING_RIGHT` because it + * is tested first. An actor with no movement bits keeps its previous + * acceleration rather than being zeroed, which is why the input handlers clear + * it themselves on key release. + * + * @param obj The actor to compute acceleration for. Required, along with its + * `basechar`. + * @param dt Seconds since the previous step. Accepted for the hook's signature; + * this implementation does not use it, since it sets an acceleration + * rather than integrating one. * @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 obj or `obj->basechar` is `NULL`. + * + * @note A replacement for this hook may raise AKGL_ERR_LOGICINTERRUPT to tell + * the physics step to skip the rest of this actor's tick. This + * implementation never does. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_logic_movement(akgl_Actor *obj, float32_t dt); /** - * @brief Actor logic changeframe. - * @param obj Object to initialize, inspect, or modify. - * @param curSprite Sprite currently selected for the actor. - * @param curtimems Current frame-clock value. + * @brief The default `changeframefunc`: step to the next animation frame. + * + * Walks forward through the sprite's frames; at the last one it either wraps to + * 0, or -- for a sprite with both `loop` and `loopReverse` -- turns round and + * walks back down, turning again at frame 0. A sprite with `loop` clear also + * wraps to 0 rather than holding the final frame. + * + * @param obj The actor whose frame index to advance. Required. + * @param curSprite The sprite currently selected for the actor, whose frame + * count and loop flags decide what happens at the end. + * Required in practice, and **not** checked -- a `NULL` here is + * a crash, not an error. + * @param curtimems The current clock reading. Accepted for the hook's signature; + * this implementation does not use it, because the caller has + * already decided the frame is due. * @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 obj is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_logic_changeframe(akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtimems); /** - * @brief Actor automatic face. - * @param obj Object to initialize, inspect, or modify. + * @brief The default `facefunc`: turn the actor to face whichever way it is moving. + * + * Clears every facing bit and sets the one matching the first movement bit it + * finds, in the order left, right, up, down -- so an actor moving diagonally + * faces the horizontal component. An actor with `movement_controls_face` clear + * is left alone entirely, which is the hook for something that aims + * independently of the way it walks. + * + * @param obj The actor to turn. 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 obj is `NULL`. + * + * @note An actor that has stopped moving is left with no facing bit at all, + * rather than keeping the way it was last facing -- so its state no longer + * matches any "standing still facing left" sprite. The implementation + * carries a TODO saying as much. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_automatic_face(akgl_Actor *obj); /** - * @brief Actor add child. - * @param obj Object to initialize, inspect, or modify. - * @param child Actor to attach as a child. + * @brief Attach one actor to another so it moves with it. + * + * A child is positioned relative to its parent rather than simulated: the + * physics step snaps it to the parent's position plus its own velocity fields, + * used here as a fixed offset. That is what a carried lantern or a turret on a + * tank wants. + * + * The parent takes a reference on the child, and releasing the parent releases + * every child with it. + * + * @param obj The parent. Required. + * @param child The actor to attach. Required. Must not already have a parent -- + * this builds a tree, not a graph. Its `x`/`y` become an offset + * from the parent's position rather than a world position. * @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_RELATIONSHIP When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p obj or @p child is `NULL`. + * @throws AKERR_RELATIONSHIP If @p child already has a parent. Detach it first -- + * though note there is no detach function, so in practice this means + * releasing and rebuilding it. + * @throws AKERR_OUTOFBOUNDS If @p obj already has #AKGL_ACTOR_MAX_CHILDREN + * children. + * + * @warning Nothing checks for a cycle. Making an actor its own ancestor makes + * akgl_heap_release_actor recurse until the stack runs out. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_add_child(akgl_Actor *obj, akgl_Actor *child); +/* + * The control-map handler functions ("cmhf"). These are what + * akgl_controller_default binds the arrow keys and the D-pad to, and what a + * game's own akgl_Control bindings can point at. They are called with the + * control map's target actor, not with a global "player", so they work for any + * number of locally controlled actors. + * + * Each pair is symmetric: the `_on` handler starts movement in a direction and + * turns the actor to face it, and the `_off` handler stops it dead -- zeroing + * acceleration, thrust, environmental velocity and velocity on that axis, so + * there is no coasting. That is an arcade feel rather than a physical one, and + * it is deliberate; a game wanting momentum binds its own handlers instead. + * + * An `_on` handler clears *every* facing and movement bit before setting its + * own, so holding two directions at once moves in whichever was pressed last + * rather than diagonally. + */ + /** - * @brief Actor cmhf left on. - * @param obj Object to initialize, inspect, or modify. - * @param event SDL input event to process. + * @brief Start the actor moving left, and turn it to face left. + * @param obj The actor to move, supplied by the control map. Required, along + * with its `basechar`, whose acceleration is negated onto `ax`. + * @param event The event that triggered this. Required, but not read -- the + * binding has already decided what the event means. * @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 obj, @p event, or `obj->basechar` is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_on(akgl_Actor *obj, SDL_Event *event); /** - * @brief Actor cmhf left off. - * @param obj Object to initialize, inspect, or modify. - * @param event SDL input event to process. + * @brief Stop the actor moving left, zeroing everything on the x axis. + * @param obj The actor to stop. Required. `basechar` is not needed here. + * @param event The event that triggered this. Required, but not read. * @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 obj or @p event is `NULL`. + * @note It clears the x axis whichever way the actor was going, so releasing + * left while holding right also stops the rightward movement. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_off(akgl_Actor *obj, SDL_Event *event); /** - * @brief Actor cmhf right on. - * @param obj Object to initialize, inspect, or modify. - * @param event SDL input event to process. + * @brief Start the actor moving right, and turn it to face right. + * @param obj The actor to move. Required, along with its `basechar`. + * @param event The event that triggered this. Required, but not read. * @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 obj, @p event, or `obj->basechar` is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_on(akgl_Actor *obj, SDL_Event *event); /** - * @brief Actor cmhf right off. - * @param obj Object to initialize, inspect, or modify. - * @param event SDL input event to process. + * @brief Stop the actor moving right, zeroing everything on the x axis. + * @param obj The actor to stop. Required. + * @param event The event that triggered this. Required, but not read. * @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 obj or @p event is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_off(akgl_Actor *obj, SDL_Event *event); /** - * @brief Actor cmhf up on. - * @param obj Object to initialize, inspect, or modify. - * @param event SDL input event to process. + * @brief Start the actor moving up the screen, and turn it to face up. + * @param obj The actor to move. Required, along with its `basechar`, whose y + * acceleration is negated -- y grows downward. + * @param event The event that triggered this. Required, but not read. * @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 obj, @p event, or `obj->basechar` is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_on(akgl_Actor *obj, SDL_Event *event); /** - * @brief Actor cmhf up off. - * @param obj Object to initialize, inspect, or modify. - * @param event SDL input event to process. + * @brief Stop the actor moving up, zeroing everything on the y axis. + * @param obj The actor to stop. Required. + * @param event The event that triggered this. Required, but not read. * @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 obj or @p event is `NULL`. + * @note This also zeroes `ey`, so an actor under gravity has its accumulated + * fall cancelled by releasing a movement key. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_off(akgl_Actor *obj, SDL_Event *event); /** - * @brief Actor cmhf down on. - * @param obj Object to initialize, inspect, or modify. - * @param event SDL input event to process. + * @brief Start the actor moving down the screen, and turn it to face down. + * @param obj The actor to move. Required, along with its `basechar`. + * @param event The event that triggered this. Required, but not read. * @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 obj, @p event, or `obj->basechar` is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_on(akgl_Actor *obj, SDL_Event *event); /** - * @brief Actor cmhf down off. - * @param obj Object to initialize, inspect, or modify. - * @param event SDL input event to process. + * @brief Stop the actor moving down, zeroing everything on the y axis. + * @param obj The actor to stop. Required. + * @param event The event that triggered this. Required, but not read. * @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 obj or @p event is `NULL`. + * @note Zeroes `ey` as well; see akgl_Actor_cmhf_up_off. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_off(akgl_Actor *obj, SDL_Event *event); /** - * @brief Registry iterate actor. - * @param userdata Caller data supplied to the SDL property iterator. - * @param registry SDL property registry being queried. - * @param name Registry key or human-readable object name. + * @brief `SDL_EnumerateProperties` callback that applies an akgl_Iterator to one registered actor. + * + * Runs the operations the iterator asks for, in a fixed order regardless of the + * order the bits were set: layer filter, update, tilemap scaling, render. With + * #AKGL_ITERATOR_OP_LAYERMASK set, an actor on any other layer is skipped + * entirely; with #AKGL_ITERATOR_OP_TILEMAPSCALE clear, `scale` is forced to 1.0 + * rather than left alone. + * + * @param userdata The akgl_Iterator carrying the operation flags, passed through + * by `SDL_EnumerateProperties`. Required despite the `void *`. + * @param registry #AKGL_REGISTRY_ACTOR, supplied by SDL. + * @param name The actor's registry key. 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. An actor whose `updatefunc` fails therefore terminates the + * game rather than skipping a frame. Install your own + * `akerr_handler_unhandled_error` if that is not what you want. */ void akgl_registry_iterate_actor(void *userdata, SDL_PropertiesID registry, const char *name); diff --git a/include/akgl/assets.h b/include/akgl/assets.h index c901045..0d9f119 100644 --- a/include/akgl/assets.h +++ b/include/akgl/assets.h @@ -1,6 +1,6 @@ /** * @file assets.h - * @brief Declares the public assets API. + * @brief Loads the game's startup assets into the global mixer and track table. */ #ifndef _ASSETS_H_ @@ -9,11 +9,35 @@ #include /** - * @brief Load start bgm. - * @param fname Path to the input or output file. + * @brief Load a music file, bind it to the background-music track, and start it playing. + * + * Loads @p fname through `akgl_mixer`, creates a track for it, stores that track + * in `akgl_tracks[AKGL_GAME_AUDIO_TRACK_BGM]`, and starts playback. The loaded + * audio is also published as the global `bgm`. This is a *startup* helper: it + * assumes the mixer already exists, so `akgl_game_init` (or a bare + * `akgl_audio_init`) has to have run first, and it overwrites whatever the BGM + * track slot held without releasing it. + * + * Loading is deferred inside SDL_mixer (`MIX_LoadAudio(..., true)` predecodes), + * so a large file costs its decode time here rather than at first play. + * + * On any failure after the audio loads, the audio is destroyed again before the + * error is returned; the track, if one was created, is not. + * + * @param fname Path to a music file in any format SDL_mixer can open. Required. + * Used verbatim -- it is not resolved against `SDL_GetBasePath()`, + * so a relative path is relative to the process working directory. * @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 fname is `NULL`, if SDL_mixer cannot load the + * file (missing, unreadable, or an unsupported format), or if it cannot + * allocate a track for it. The message carries `SDL_GetError()`. + * @throws AKGL_ERR_SDL If the loaded audio cannot be bound to the new track, or + * if playback fails to start. + * + * @note The infinite-loop request does not currently take effect: + * `MIX_PROP_PLAY_LOOPS_NUMBER` is set on property set 0, which is the + * "no properties" sentinel rather than a set this function owns, so the + * call is rejected and the music plays once. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_load_start_bgm(char *fname); diff --git a/include/akgl/audio.h b/include/akgl/audio.h index 58f35c0..27ce481 100644 --- a/include/akgl/audio.h +++ b/include/akgl/audio.h @@ -50,18 +50,18 @@ * uses for the same shape, for a caller translating one to the other. */ typedef enum { - AKGL_AUDIO_WAVE_TRIANGLE = 0, /** SOUND waveform 0. Soft, flute-like. */ - AKGL_AUDIO_WAVE_SAWTOOTH = 1, /** SOUND waveform 1. Bright, brassy. */ - AKGL_AUDIO_WAVE_SQUARE = 2, /** SOUND waveform 2. Hollow, reedy. The default. */ - AKGL_AUDIO_WAVE_NOISE = 3, /** SOUND waveform 3. Unpitched; percussion. */ - AKGL_AUDIO_WAVE_SINE = 4 /** No SOUND equivalent. A pure tone. */ + AKGL_AUDIO_WAVE_TRIANGLE = 0, /**< SOUND waveform 0. Soft, flute-like. */ + AKGL_AUDIO_WAVE_SAWTOOTH = 1, /**< SOUND waveform 1. Bright, brassy. */ + AKGL_AUDIO_WAVE_SQUARE = 2, /**< SOUND waveform 2. Hollow, reedy. The default. */ + AKGL_AUDIO_WAVE_NOISE = 3, /**< SOUND waveform 3. Unpitched; percussion. */ + AKGL_AUDIO_WAVE_SINE = 4 /**< No SOUND equivalent. A pure tone. */ } akgl_AudioWaveform; /** @brief Holds one voice's oscillator, envelope, and how far through it is. */ typedef struct { - bool active; - akgl_AudioWaveform waveform; - float32_t hz; + bool active; /**< Whether this voice is sounding. Cleared by the mixer once gate and release are both spent, so it goes quiet without being told to. */ + akgl_AudioWaveform waveform; /**< Oscillator shape. Persists across notes; set once with akgl_audio_waveform(). */ + float32_t hz; /**< Frequency of the current note. 0.0 when the voice has never sounded. */ /** * @brief Position through one cycle, 0.0 to 1.0. * @@ -74,8 +74,11 @@ typedef struct { uint32_t duration_frames; /** @brief Frames generated since the tone started, gate and release. */ uint32_t elapsed_frames; + /** @brief Frames to rise from silence to full level. 0 starts at full level. */ uint32_t attack_frames; + /** @brief Frames to fall from full level to `sustain`. 0 drops to it at once. */ uint32_t decay_frames; + /** @brief Frames to fall to silence after the gate closes. 0 cuts off at once. */ uint32_t release_frames; /** @brief Level the envelope decays to and holds, 0.0 to 1.0. */ float32_t sustain; @@ -92,17 +95,26 @@ extern akgl_AudioVoice akgl_audio_voices[AKGL_AUDIO_MAX_VOICES]; * it more than once. The voice table is reset only on the first call, so this * does not silence a voice that is already sounding. * + * The device is resumed immediately rather than left paused, which is SDL's + * default -- a caller who set up a voice and heard nothing would have no error + * to explain it. + * * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKGL_ERR_SDL When the corresponding validation or operation fails. + * @throws AKGL_ERR_SDL If no playback device can be opened -- none present, none + * permitted, or SDL's audio subsystem never initialized -- or if the + * device cannot be resumed. The message carries `SDL_GetError()`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_init(void); /** * @brief Close the audio device and silence every voice. * - * Safe to call when no device is open. + * Safe to call when no device is open, and safe to call twice. It also puts the + * voice table back to its defaults, so a subsequent akgl_audio_init() starts + * from a known state rather than from whatever was left sounding. * - * @return `NULL` on success, otherwise an error context owned by the caller. + * @return `NULL`. There is no failure path -- SDL's stream teardown reports + * nothing. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_shutdown(void); @@ -115,19 +127,30 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_shutdown(void); * slightly longer than @p ms. Sounding a voice that is already sounding * restarts it from the beginning of its envelope. * - * @param voice Zero-based voice index. - * @param hz Frequency in hertz. - * @param ms Gate length in milliseconds. + * @param voice Zero-based voice index, 0 to #AKGL_AUDIO_MAX_VOICES - 1. + * @param hz Frequency in hertz. Must be greater than 0. There is no upper + * bound check, so a frequency above half the sample rate aliases + * rather than being refused. + * @param ms Gate length in milliseconds. Must be non-zero -- a zero-length + * tone is refused rather than treated as "stop", because + * akgl_audio_stop() already means that. Rounded down to a whole + * number of frames, so a duration under ~0.023 ms rounds to + * nothing. * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails. + * @throws AKERR_OUTOFBOUNDS If @p voice is outside the table, if @p hz is not + * positive, or if @p ms is 0. Each message says which. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_tone(int voice, float32_t hz, uint32_t ms); /** * @brief Silence one voice immediately, skipping its release. - * @param voice Zero-based voice index. + * + * A hard cut, not a note-off: the release stage is not run, so the sound stops + * on the next sample. Stopping a voice that is already silent is a no-op. + * + * @param voice Zero-based voice index, 0 to #AKGL_AUDIO_MAX_VOICES - 1. * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails. + * @throws AKERR_OUTOFBOUNDS If @p voice is outside the table. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_stop(int voice); @@ -137,10 +160,12 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_stop(int voice); * Takes effect on the next akgl_audio_tone(); it does not reshape a note that * is already sounding. * - * @param voice Zero-based voice index. - * @param waveform Oscillator shape to use. + * @param voice Zero-based voice index, 0 to #AKGL_AUDIO_MAX_VOICES - 1. + * @param waveform One of the ::akgl_AudioWaveform shapes. The setting persists + * across notes until changed. * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails. + * @throws AKERR_OUTOFBOUNDS If @p voice is outside the table, or @p waveform is + * not one of the five defined shapes. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_waveform(int voice, akgl_AudioWaveform waveform); @@ -153,21 +178,34 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_waveform(int voice, akgl_AudioWave * * Takes effect on the next akgl_audio_tone(). * - * @param voice Zero-based voice index. - * @param attack Milliseconds to rise from silence to full level. - * @param decay Milliseconds to fall from full level to @p sustain. - * @param sustain Held level while the gate is open, 0.0 to 1.0. - * @param release Milliseconds to fall to silence once the gate closes. + * @param voice Zero-based voice index, 0 to #AKGL_AUDIO_MAX_VOICES - 1. + * @param attack Milliseconds to rise from silence to full level. 0 starts at + * full level. + * @param decay Milliseconds to fall from full level to @p sustain. 0 drops to + * it at once. + * @param sustain Held level while the gate is open, 0.0 to 1.0 inclusive. 0.0 is + * legal and means the note is audible only through its attack and + * decay -- a plucked sound. + * @param release Milliseconds to fall to silence once the gate closes. 0 cuts + * off at once. An attack plus decay longer than the note's gate + * is not an error: the release simply starts partway up the ramp. * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails. + * @throws AKERR_OUTOFBOUNDS If @p voice is outside the table, or @p sustain is + * outside 0.0 to 1.0. The three durations are unbounded. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_envelope(int voice, uint32_t attack, uint32_t decay, float32_t sustain, uint32_t release); /** * @brief Set the level every voice is scaled by. - * @param level Master level, 0.0 to 1.0. + * + * Applied after the voices are summed and before the mix is clamped, so it + * takes effect on notes already sounding. akgl_audio_shutdown() puts it back + * to 1.0. + * + * @param level Master level, 0.0 to 1.0 inclusive. 0.0 is silence. * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails. + * @throws AKERR_OUTOFBOUNDS If @p level is outside 0.0 to 1.0. The message + * reports it. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_volume(float32_t level); @@ -177,11 +215,16 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_volume(float32_t level); * A voice goes quiet on its own when its gate and release have both elapsed, so * this is how a caller waits out a note without keeping its own clock. * - * @param voice Zero-based voice index. - * @param active Output destination set to `true` while the voice is sounding. + * @param voice Zero-based voice index, 0 to #AKGL_AUDIO_MAX_VOICES - 1. + * @param active Receives `true` while the voice is sounding, including during + * its release. Required -- the return value is the error context. + * The flag is only cleared by the mixer, so a voice whose time is + * up still reads active until samples are next generated; with no + * device open and nobody calling akgl_audio_mix(), it never + * changes. * @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_OUTOFBOUNDS If @p voice is outside the table. + * @throws AKERR_NULLPOINTER If @p active is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_voice_active(int voice, bool *active); @@ -193,11 +236,19 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_voice_active(int voice, bool *acti * open a device here at all. Samples are single-precision, one channel, in the * range -1.0 to 1.0, and every active voice is advanced by @p frames. * - * @param dest Output destination populated with @p frames samples. - * @param frames Number of samples to generate. + * @param dest Receives @p frames samples. Required, and must have room for all + * of them -- the count is trusted, not checked against anything. + * Overwritten, not accumulated into. + * @param frames How many samples to generate. 0 is a no-op, not an error. * @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 dest is `NULL`. + * @throws AKERR_OUTOFBOUNDS If @p frames is negative. + * + * @warning This mutates the voice table -- it advances each voice's frame + * counter and clears `active` on voices that have finished. Calling it + * while a device opened by akgl_audio_init() is running means two + * threads advancing the same voices, and this path does not take the + * stream lock. Use one or the other, not both. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_mix(float32_t *dest, int frames); diff --git a/include/akgl/character.h b/include/akgl/character.h index 7e4952d..f5d4f47 100644 --- a/include/akgl/character.h +++ b/include/akgl/character.h @@ -1,6 +1,15 @@ /** * @file character.h - * @brief Declares the public character API. + * @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_ @@ -15,64 +24,140 @@ /** @brief Defines reusable movement parameters and actor-state sprite bindings. */ typedef struct akgl_Character { - uint8_t refcount; - char name[AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH]; - SDL_PropertiesID state_sprites; - uint64_t speedtime; - float32_t ax; - float32_t ay; - float32_t az; - float32_t sx; - float32_t sy; - float32_t sz; - akerr_ErrorContext AKERR_NOIGNORE *(*sprite_add)(struct akgl_Character *, akgl_Sprite *, int); - akerr_ErrorContext AKERR_NOIGNORE *(*sprite_get)(struct akgl_Character *, int, akgl_Sprite **); + 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 Character initialize. - * @param basechar Character whose state-to-sprite map is accessed. - * @param name Registry key or human-readable object name. + * @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_KEY When the corresponding validation or operation fails. - * @throws AKERR_NULLPOINTER When the corresponding validation or operation fails. + * @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 Character sprite add. - * @param basechar Character whose state-to-sprite map is accessed. - * @param ref Sprite reference to associate with the character. - * @param state Actor-state bit mask used as a sprite-map key. + * @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 When the corresponding validation or operation fails. + * @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 Character sprite get. - * @param basechar Character whose state-to-sprite map is accessed. - * @param state Actor-state bit mask used as a sprite-map key. - * @param dest Output destination populated by the function. + * @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_KEY When the corresponding validation or operation fails. - * @throws AKERR_NULLPOINTER When the corresponding validation or operation fails. + * @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 Character state sprites iterate. - * @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 `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 Character load json. - * @param filename Path to the source asset or JSON document. + * @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": , "state": [] }` + * 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 When the corresponding validation or operation fails. + * @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); diff --git a/include/akgl/controller.h b/include/akgl/controller.h index bb6d6c2..0efebbf 100644 --- a/include/akgl/controller.h +++ b/include/akgl/controller.h @@ -1,6 +1,20 @@ /** * @file controller.h - * @brief Declares the public controller API. + * @brief Input binding: SDL events in, actor state changes out. + * + * A control map ties one actor to one keyboard and one gamepad, and holds up to + * #AKGL_MAX_CONTROLS bindings. A binding says "when event X arrives from device + * Y carrying button or key Z, call this handler". Eight maps means up to eight + * locally controlled players, each on its own device. + * + * The host pumps SDL events into akgl_controller_handle_event(), which scans the + * maps in order and stops at the first binding that claims the event -- so a key + * bound in two maps only fires once, in the lower-numbered one. + * + * Alongside that, and independent of it, every key press is pushed into a small + * ring buffer that akgl_controller_poll_key() drains. That serves a caller that + * wants "is there a key waiting" without owning an event loop, and it sees + * keys whether or not a control map also claimed them. */ #ifndef _CONTROLLER_H_ @@ -10,7 +24,9 @@ #include #include "types.h" +/** @brief How many control maps exist -- effectively the local player limit. */ #define AKGL_MAX_CONTROL_MAPS 8 +/** @brief Bindings per control map. The default map installed by akgl_controller_default uses 8 of them. */ #define AKGL_MAX_CONTROLS 32 /** @@ -25,106 +41,188 @@ /** @brief Maps one SDL input to pressed and released callbacks. */ typedef struct { - uint32_t event_on; - uint32_t event_off; - uint8_t button; - SDL_Keycode key; - uint8_t axis; - uint8_t axis_range_min; - uint8_t axis_range_max; - akerr_ErrorContext AKERR_NOIGNORE *(*handler_on)(akgl_Actor *obj, SDL_Event *event); - akerr_ErrorContext AKERR_NOIGNORE *(*handler_off)(akgl_Actor *obj, SDL_Event *event); + uint32_t event_on; /**< SDL event type that fires `handler_on`, e.g. `SDL_EVENT_KEY_DOWN`. */ + uint32_t event_off; /**< SDL event type that fires `handler_off`, e.g. `SDL_EVENT_KEY_UP`. */ + uint8_t button; /**< Gamepad button (`SDL_GamepadButton`) this binding is for. Only consulted for gamepad events. */ + SDL_Keycode key; /**< Keycode this binding is for. Only consulted for keyboard events. */ + uint8_t axis; /**< Analogue axis. Declared but not yet consulted by akgl_controller_handle_event. */ + uint8_t axis_range_min; /**< Low end of the axis range that counts as "on". Not yet consulted. */ + uint8_t axis_range_max; /**< High end of that range. Not yet consulted. */ + akerr_ErrorContext AKERR_NOIGNORE *(*handler_on)(akgl_Actor *obj, SDL_Event *event); /**< Called with the map's target on `event_on`. Required if `event_on` can fire. */ + akerr_ErrorContext AKERR_NOIGNORE *(*handler_off)(akgl_Actor *obj, SDL_Event *event); /**< Called with the map's target on `event_off`. */ } akgl_Control; /** @brief Groups input bindings for one actor and its input devices. */ typedef struct { - akgl_Actor *target; - uint16_t nextMap; - akgl_Control controls[AKGL_MAX_CONTROLS]; - SDL_KeyboardID kbid; - SDL_JoystickID jsid; - SDL_MouseID mouseid; - SDL_PenID penid; + akgl_Actor *target; /**< The actor these bindings drive. A `NULL` target makes the whole map inert, which is how an unused slot is spelled. */ + uint16_t nextMap; /**< Number of bindings in use; the index akgl_controller_pushmap writes to next. */ + akgl_Control controls[AKGL_MAX_CONTROLS]; /**< The bindings, scanned in order. */ + SDL_KeyboardID kbid; /**< Keyboard this map listens to. A keyboard event from any other id is ignored, which is what keeps two players on two keyboards apart. */ + SDL_JoystickID jsid; /**< Gamepad this map listens to, matched the same way. */ + SDL_MouseID mouseid; /**< Mouse this map listens to. Declared but not yet consulted. */ + SDL_PenID penid; /**< Pen this map listens to. Declared but not yet consulted. */ } akgl_ControlMap; -/** @brief Stores all process-wide actor input maps. */ +/** @brief Every control map. Zeroed by akgl_game_init; index it by the same id the functions below take. */ extern akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS]; /** - * @brief Controller list keyboards. + * @brief Log every attached keyboard and its SDL id. + * + * A diagnostic, not a query: it writes to the SDL log rather than returning + * anything. Its use is finding the `kbid` to hand akgl_controller_default when + * more than one keyboard is attached. + * * @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 SDL cannot enumerate keyboards. The message + * carries `SDL_GetError()`; note that "no keyboards attached" is + * reported by SDL as an empty list, not as a failure. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void); /** - * @brief Controller handle event. - * @param appstate Application state supplied by SDL. - * @param event SDL input event to process. + * @brief Dispatch one SDL event to whichever control map binds it. + * + * The entry point for the whole subsystem -- call it for every event the host + * pumps. A key press is recorded in the poll buffer first, whether or not any + * map wants it; then the maps are scanned in index order and, within a map, + * bindings in the order they were pushed. The **first** binding whose event type + * and device id and button/key all match wins, and the scan stops there. + * + * An event nothing binds is not an error: it returns success having done + * nothing, which is what lets a host pass every event through unconditionally. + * + * @param appstate Passed through from SDL's callback. Required -- but only as a + * non-`NULL` token: nothing here reads it. Pass any non-`NULL` + * pointer if your program has no app state. + * @param event The event to dispatch. 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`. + * @throws AKERR_* Whatever the matched binding's handler raises. + * + * @warning A matched binding's handler pointer is not checked, so a control + * pushed with a `NULL` `handler_on` or `handler_off` crashes when its + * event arrives. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_event(void *appstate, SDL_Event *event); /** - * @brief Controller handle button down. + * @brief Declared but not defined under this name. Do not call. + * + * The implementation exists as `gamepad_handle_button_down` in `src/controller.c` + * and is `static`-in-spirit -- it is not declared anywhere -- so a caller that + * uses this declaration compiles and then fails to link. TODO.md, "Known and + * still open" item 10. + * * @param appstate Application state supplied by SDL. - * @param event SDL input event to process. - * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKERR_* Propagates an error reported by a delegated operation. + * @param event SDL input event to process. + * @return Nothing; it cannot be called. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_down(void *appstate, SDL_Event *event); /** - * @brief Controller handle button up. + * @brief Declared but not defined under this name. Do not call. + * + * See akgl_controller_handle_button_down. The implementation is + * `gamepad_handle_button_up`. + * * @param appstate Application state supplied by SDL. - * @param event SDL input event to process. - * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKERR_* Propagates an error reported by a delegated operation. + * @param event SDL input event to process. + * @return Nothing; it cannot be called. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_up(void *appstate, SDL_Event *event); /** - * @brief Controller handle added. + * @brief Declared but not defined under this name. Do not call. + * + * See akgl_controller_handle_button_down. The implementation is + * `gamepad_handle_added`, which opens a newly plugged-in gamepad. + * * @param appstate Application state supplied by SDL. - * @param event SDL input event to process. - * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKERR_* Propagates an error reported by a delegated operation. + * @param event SDL input event to process. + * @return Nothing; it cannot be called. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_added(void *appstate, SDL_Event *event); /** - * @brief Controller handle removed. + * @brief Declared but not defined under this name. Do not call. + * + * See akgl_controller_handle_button_down. The implementation is + * `gamepad_handle_removed`, which closes an unplugged gamepad. + * * @param appstate Application state supplied by SDL. - * @param event SDL input event to process. - * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKERR_* Propagates an error reported by a delegated operation. + * @param event SDL input event to process. + * @return Nothing; it cannot be called. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_removed(void *appstate, SDL_Event *event); /** - * @brief Controller pushmap. - * @param controlmapid Index of the control map to update. - * @param control Control binding to append to the selected map. + * @brief Append a binding to a control map. + * + * The binding is copied, so the caller's `akgl_Control` can be a stack local + * reused across pushes -- which is exactly what akgl_controller_default does. + * Bindings can only be appended; there is no remove, and no way to reset a map + * short of zeroing it in ::GAME_ControlMaps directly. + * + * @param controlmapid Which map to append to, 0 through + * #AKGL_MAX_CONTROL_MAPS - 1. + * @param control The binding to copy in. 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_OUTOFBOUNDS When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p control is `NULL`. + * @throws AKERR_OUTOFBOUNDS If @p controlmapid is at or above + * #AKGL_MAX_CONTROL_MAPS, or if the map already holds + * #AKGL_MAX_CONTROLS bindings. + * + * @warning A **negative** @p controlmapid is not rejected -- only the upper + * bound is checked -- and indexes before the start of + * ::GAME_ControlMaps. TODO.md, "Known and still open" item 11. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_pushmap(int controlmapid, akgl_Control *control); /** - * @brief Controller default. - * @param controlmapid Index of the control map to update. - * @param actorname Registry name of the controlled actor. - * @param kbid SDL keyboard identifier assigned to the map. - * @param jsid SDL joystick or gamepad identifier assigned to the map. + * @brief Bind an actor to the arrow keys and the D-pad, in one call. + * + * Points the map at the named actor and pushes eight bindings: the four arrow + * keys and the four D-pad directions, each wired to the matching + * `akgl_Actor_cmhf_*_on`/`_off` pair. It is the "just give me something that + * works" path -- a game wanting different keys builds its own bindings with + * akgl_controller_pushmap. + * + * @param controlmapid Which map to configure, 0 through + * #AKGL_MAX_CONTROL_MAPS - 1. + * @param actorname Registry name of the actor to drive. Required in practice, + * though a `NULL` is reported as "not found" rather than as + * a null pointer. + * @param kbid SDL keyboard id to listen to. Only events from this + * keyboard match; see akgl_controller_list_keyboards for + * how to find it. + * @param jsid SDL gamepad id to listen to, matched the same way. * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails. - * @throws AKGL_ERR_REGISTRY When the corresponding validation or operation fails. + * @throws AKERR_OUTOFBOUNDS If @p controlmapid is at or above + * #AKGL_MAX_CONTROL_MAPS, or if the map cannot hold eight more bindings. + * @throws AKGL_ERR_REGISTRY If @p actorname is not in #AKGL_REGISTRY_ACTOR -- + * usually because the actor has not been created yet. + * + * @warning A negative @p controlmapid is not rejected. See + * akgl_controller_pushmap. + * @note It appends rather than replaces, so calling it twice on the same map + * leaves sixteen bindings and the first eight are the ones that fire. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, char *actorname, int kbid, int jsid); /** - * @brief Controller open gamepads. + * @brief Open every gamepad currently attached. + * + * SDL will not deliver button events from a gamepad nobody has opened, so this + * runs once at startup -- akgl_game_init calls it. Devices plugged in later are + * SDL_EVENT_GAMEPAD_ADDED events, handled elsewhere. + * + * No gamepads attached is success, not an error. + * * @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 SDL reports gamepads present but cannot + * enumerate them, or if one of them cannot be opened. The message + * carries `SDL_GetError()`. + * + * @note The enumeration array is only freed on the success path, so a failure + * part-way through leaks it. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_open_gamepads(void); @@ -146,10 +244,15 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_open_gamepads(void); * was typed first is what is read first. This runs on whichever thread pumps * events; it is not synchronized. * - * @param keycode Output destination populated with the SDL keycode, or 0. - * @param available Output destination set to `true` when a keystroke was taken. + * @param keycode Receives the SDL keycode, or 0 when nothing was waiting. + * Required -- the return value is the error context. + * @param available Receives `true` when a keystroke was taken, `false` when the + * buffer was empty. Required. Check this rather than testing + * @p keycode against 0. * @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 keycode or @p available is `NULL`. Both are + * required; there is no "I only want to know whether one is waiting" + * form. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available); @@ -159,7 +262,10 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool * * For a caller that has been ignoring input and does not want a backlog acted * on the moment it starts polling again. * - * @return `NULL` on success, otherwise an error context owned by the caller. + * This empties only the polling buffer. Keys already dispatched to control maps + * have had their effect and cannot be taken back. + * + * @return `NULL`. There is no failure path -- it resets two counters. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_flush_keys(void); #endif // _CONTROLLER_H_ diff --git a/include/akgl/draw.h b/include/akgl/draw.h index 37454dc..c95e0da 100644 --- a/include/akgl/draw.h +++ b/include/akgl/draw.h @@ -37,57 +37,81 @@ #define AKGL_DRAW_MAX_FLOOD_SPANS 4096 /** - * @brief Draw background. - * @param w Destination width. - * @param h Destination height. + * @brief Paint an 8x8 grey checkerboard over a region, the way an image editor shows transparency. + * + * A diagnostic backdrop, not a general primitive -- `charviewer` uses it so a + * sprite's transparent pixels are visible rather than blending into black. It + * is the one function in this file that does not follow the file's conventions: + * it draws through the *global* `renderer` rather than a backend the caller + * passes in, it leaves the renderer's draw colour changed, and it reports + * nothing. + * + * @param w Width of the region to cover, in pixels, starting at x = 0. + * @param h Height of the region, starting at y = 0. Both round up to whole 8px + * cells, so a 12-pixel height paints 16. */ void akgl_draw_background(int w, int h); /** * @brief Plot a single pixel. - * @param self Backend or object instance to operate on. - * @param x Horizontal destination coordinate. - * @param y Vertical destination coordinate. - * @param color Color to draw with. + * @param self The backend to draw through. Required, along with its + * `sdl_renderer`. + * @param x Horizontal position in render-target pixels. Outside the target + * is clipped by SDL, not reported. + * @param y Vertical position. + * @param color Colour to plot in, including alpha. Blending follows the + * renderer's current blend mode, which this does not change. * @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 self or `self->sdl_renderer` is `NULL`. + * @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if the plot + * itself fails. The message carries `SDL_GetError()`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_point(akgl_RenderBackend *self, float32_t x, float32_t y, SDL_Color color); /** - * @brief Draw a line between two points. - * @param self Backend or object instance to operate on. - * @param x1 Horizontal coordinate of the first endpoint. - * @param y1 Vertical coordinate of the first endpoint. - * @param x2 Horizontal coordinate of the second endpoint. - * @param y2 Vertical coordinate of the second endpoint. - * @param color Color to draw with. + * @brief Draw a line between two points, endpoints included. + * @param self The backend to draw through. Required, along with its + * `sdl_renderer`. + * @param x1 Horizontal position of the first endpoint. + * @param y1 Vertical position of the first endpoint. + * @param x2 Horizontal position of the second endpoint. + * @param y2 Vertical position of the second endpoint. Coincident endpoints + * draw a single pixel rather than nothing. + * @param color Colour to draw in, including alpha. * @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 self or `self->sdl_renderer` is `NULL`. + * @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if the line + * cannot be drawn. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_line(akgl_RenderBackend *self, float32_t x1, float32_t y1, float32_t x2, float32_t y2, SDL_Color color); /** * @brief Draw the outline of a rectangle. - * @param self Backend or object instance to operate on. - * @param rect Rectangle to outline. - * @param color Color to draw with. + * @param self The backend to draw through. Required, along with its + * `sdl_renderer`. + * @param rect The rectangle, in render-target pixels. Required. A zero or + * negative width or height draws nothing and is not reported. + * @param color Colour to draw in, including alpha. * @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 self, `self->sdl_renderer`, or @p rect is + * `NULL`. + * @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if the + * outline cannot be drawn. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color); /** - * @brief Fill a rectangle. - * @param self Backend or object instance to operate on. - * @param rect Rectangle to fill. - * @param color Color to draw with. + * @brief Fill a rectangle, outline included. + * @param self The backend to draw through. Required, along with its + * `sdl_renderer`. + * @param rect The rectangle, in render-target pixels. Required. A zero or + * negative width or height fills nothing and is not reported. + * @param color Colour to fill with, including alpha. * @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 self, `self->sdl_renderer`, or @p rect is + * `NULL`. + * @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if the fill + * cannot be drawn. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_filled_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color); @@ -98,15 +122,22 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_filled_rect(akgl_RenderBackend *sel * algorithm -- integer arithmetic, eight-way symmetry, one pass per octant. A * radius of zero draws the center pixel and nothing else. * - * @param self Backend or object instance to operate on. - * @param x Horizontal coordinate of the center. - * @param y Vertical coordinate of the center. - * @param radius Circle radius in pixels. - * @param color Color to draw with. + * @param self The backend to draw through. Required, along with its + * `sdl_renderer`. + * @param x Horizontal position of the centre. Rounded to the nearest whole + * pixel -- the algorithm is integer-only. + * @param y Vertical position of the centre, rounded the same way. + * @param radius Radius in pixels, rounded to the nearest whole pixel. Must not + * be negative; 0 draws the centre pixel alone. + * @param color Colour to draw in, including alpha. The outline is one pixel + * wide and is not anti-aliased. * @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 AKGL_ERR_SDL When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p self or `self->sdl_renderer` is `NULL`. + * @throws AKERR_OUTOFBOUNDS If @p radius is negative. The message reports it. + * @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if any of + * the plotting passes fails. Failures inside the loop are recorded and + * reported once at the end rather than aborting mid-circle, so a partial + * arc may already be on the target. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_circle(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, SDL_Color color); @@ -121,14 +152,30 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_circle(akgl_RenderBackend *self, fl * Filling a region that already holds @p color is a no-op rather than an error. * A seed outside the render target reports AKERR_OUTOFBOUNDS. * - * @param self Backend or object instance to operate on. - * @param x Horizontal coordinate of the seed pixel. - * @param y Vertical coordinate of the seed pixel. - * @param color Color to fill with. + * The region is four-connected -- it spreads up, down, left and right, not + * diagonally -- and its boundary is any pixel whose colour differs from the + * seed's, exactly. There is no tolerance, so an anti-aliased edge stops the fill + * at its first blended pixel and leaves a fringe. + * + * @param self The backend to draw through. Required, along with its + * `sdl_renderer`. + * @param x Horizontal position of the seed pixel, in render-target pixels. + * Must be inside the target. + * @param y Vertical position of the seed pixel. + * @param color Colour to fill with. Written over the region rather than blended, + * since the pixels going back are the ones just read out. * @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 AKGL_ERR_SDL When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p self or `self->sdl_renderer` is `NULL`. + * @throws AKERR_OUTOFBOUNDS If the seed is outside the render target -- the + * message reports both the seed and the target size -- or if the region + * needs more than #AKGL_DRAW_MAX_FLOOD_SPANS pending spans, in which + * case **it is left partially filled**. + * @throws AKGL_ERR_SDL If the render target cannot be read back, converted, + * uploaded, or blitted. + * + * @warning Not reentrant, and not safe from two threads: the span stack is a + * single file-scope array. Nothing that touches an `SDL_Renderer` is + * thread-safe either way. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_flood_fill(akgl_RenderBackend *self, int x, int y, SDL_Color color); @@ -141,13 +188,23 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_flood_fill(akgl_RenderBackend *self * @p src's dimensions the pixels are copied into it instead, so a caller * saving the same region repeatedly does not churn allocations. * - * @param self Backend or object instance to operate on. - * @param src Rectangle of the render target to read. - * @param dest Output destination populated by the function. + * @param self The backend to read from. Required, along with its `sdl_renderer`. + * @param src Rectangle of the render target to read, in pixels. Required. It + * must fit *entirely* inside the target: SDL would otherwise clip + * the read and hand back a smaller surface than was asked for, which + * a caller pasting it back would not notice. + * @param dest Address of the destination surface. Required, and `*dest` must be + * initialized -- `NULL` to have one allocated (the caller then owns + * it and frees it with `SDL_DestroySurface`), or an existing surface + * of exactly @p src's dimensions to reuse. * @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 AKGL_ERR_SDL When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p self, `self->sdl_renderer`, @p src, or @p dest + * is `NULL`. + * @throws AKERR_OUTOFBOUNDS If @p src has no area, if it does not fit inside the + * render target, or if a supplied `*dest` is a different size from + * @p src. Each message reports both sets of dimensions. + * @throws AKGL_ERR_SDL If the target size cannot be queried, the pixels cannot + * be read, or the copy into a supplied `*dest` fails. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_copy_region(akgl_RenderBackend *self, SDL_Rect *src, SDL_Surface **dest); @@ -158,13 +215,21 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_copy_region(akgl_RenderBackend *sel * produced. The surface is not consumed and may be pasted as many times as the * caller likes. * - * @param self Backend or object instance to operate on. - * @param src Surface to draw. - * @param x Horizontal destination coordinate. - * @param y Vertical destination coordinate. + * @param self The backend to draw through. Required, along with its + * `sdl_renderer`. + * @param src The surface to paste. Required. Drawn at its own size -- there is + * no scaling -- and *replacing* what is on the target rather than + * blending with it, which is GSHAPE's default behaviour and means a + * saved region's transparent pixels come back as transparent rather + * than letting the background show through. + * @param x Horizontal position of the paste's left edge. + * @param y Vertical position of its top edge. Parts falling outside the + * target are clipped by SDL, not reported. * @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 self, `self->sdl_renderer`, or @p src is + * `NULL`. + * @throws AKGL_ERR_SDL If the surface cannot be uploaded as a texture, its blend + * mode cannot be set, or the draw fails. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y); diff --git a/include/akgl/error.h b/include/akgl/error.h index c2c081c..53bce9e 100644 --- a/include/akgl/error.h +++ b/include/akgl/error.h @@ -49,11 +49,11 @@ #define AKGL_ERR_OWNER "libakgl" #define AKGL_ERR_BASE AKERR_FIRST_CONSUMER_STATUS -#define AKGL_ERR_SDL (AKGL_ERR_BASE + 0) /** An SDL call failed; the message carries SDL_GetError() */ -#define AKGL_ERR_REGISTRY (AKGL_ERR_BASE + 1) /** A registry property or lookup operation failed */ -#define AKGL_ERR_HEAP (AKGL_ERR_BASE + 2) /** A heap pool has no free object left to hand out */ -#define AKGL_ERR_BEHAVIOR (AKGL_ERR_BASE + 3) /** A component did not behave the way its contract requires */ -#define AKGL_ERR_LOGICINTERRUPT (AKGL_ERR_BASE + 4) /** Actor logic is telling the physics simulator to skip it */ +#define AKGL_ERR_SDL (AKGL_ERR_BASE + 0) /**< An SDL call failed; the message carries SDL_GetError() */ +#define AKGL_ERR_REGISTRY (AKGL_ERR_BASE + 1) /**< A registry property or lookup operation failed */ +#define AKGL_ERR_HEAP (AKGL_ERR_BASE + 2) /**< A heap pool has no free object left to hand out */ +#define AKGL_ERR_BEHAVIOR (AKGL_ERR_BASE + 3) /**< A component did not behave the way its contract requires */ +#define AKGL_ERR_LOGICINTERRUPT (AKGL_ERR_BASE + 4) /**< Actor logic is telling the physics simulator to skip it */ // One past the last libakgl status. The reservation is all-or-nothing -- a // subset or superset of an existing one is refused -- so this must stay one diff --git a/include/akgl/game.h b/include/akgl/game.h index b21dd8c..54a7476 100644 --- a/include/akgl/game.h +++ b/include/akgl/game.h @@ -1,6 +1,30 @@ /** * @file game.h - * @brief Declares the public game API. + * @brief Process-wide game state, the startup sequence, and the per-frame tick. + * + * This is the top of the library. `akgl_game_init` brings up SDL, the pools, the + * registries and the audio and font engines; `akgl_game_update` is one frame -- + * update every actor, step the physics, draw the world. + * + * There is exactly one of everything. `renderer`, `physics`, `camera`, and + * `gamemap` are globals pointing at the `_akgl_*` storage below them, so a + * program can swap in its own instance by reassigning the pointer without the + * rest of the library knowing. That is the whole extent of the indirection: + * there is no notion of two worlds at once. + * + * The startup order that actually works: + * + * 1. fill in `game.name`, `game.version`, and `game.uri` -- akgl_game_init + * refuses to run without them; + * 2. akgl_game_init(); + * 3. akgl_registry_load_properties() or akgl_set_property(), to configure + * screen size, physics constants and so on; + * 4. akgl_render_init2d(renderer) and akgl_physics_factory(physics, ...), both + * of which read that configuration; + * 5. load assets, then loop on akgl_game_update(). + * + * @warning None of this is thread-safe beyond the `game.state` mutex, and that + * mutex protects the state flags, not the pools or the registries. */ #ifndef _AKGL_GAME_H_ @@ -16,59 +40,71 @@ // ship an empty Version field: nothing tied the two together. #include +/** @brief Slot in ::akgl_tracks reserved for background music. Note that slot 0 is unused. */ #define AKGL_GAME_AUDIO_TRACK_BGM 1 +/** @brief Size of the ::akgl_tracks table. Every simultaneous sound needs its own slot. */ #define AKGL_GAME_AUDIO_MAX_TRACKS 64 +/** @brief Nanoseconds in one second. The unit `SDL_GetTicksNS` reports in. */ #define AKGL_TIME_ONESEC_NS 1000000000 +/** + * @brief Misnamed: this is nanoseconds per **millisecond**, not milliseconds per second. + * + * One second in milliseconds is 1000. 1000000 is one millisecond in + * nanoseconds, which is how sprite and character load actually use it -- as a + * milliseconds-to-nanoseconds scale factor -- and is not how + * akgl_game_state_lock uses it. TODO.md item 6 proposes renaming it + * `AKGL_TIME_ONEMS_NS`. + */ #define AKGL_TIME_ONESEC_MS 1000000 /* ==================== GAME STATE VARIABLES =================== */ -/** @brief Describes a renderable frame. */ +/** @brief Describes a renderable frame. Declared but not used by anything in the library. */ typedef struct { - float32_t w; - float32_t h; - SDL_Texture *texture; + float32_t w; /**< Width in pixels. */ + float32_t h; /**< Height in pixels. */ + SDL_Texture *texture; /**< The frame's texture. */ } akgl_Frame; /** @brief Stores application-defined game-state flags. */ typedef struct { - int32_t flags; + int32_t flags; /**< Meaning is entirely the application's; the library never reads it. Guard changes with akgl_game_state_lock. */ } akgl_GameState; /** @brief Stores game metadata, timing, synchronization, and FPS accounting. */ typedef struct { - char libversion[32]; - char version[32]; - char name[256]; - char uri[256]; - akgl_GameState state; - SDL_Mutex *statelock; - int16_t fps; - SDL_Time gameStartTime; - SDL_Time lastIterTime; - SDL_Time lastFPSTime; - int16_t framesSinceUpdate; - void (*lowfpsfunc)(void); + char libversion[32]; /**< libakgl's version, stamped by akgl_game_init. Compared on load to refuse a save from another build. */ + char version[32]; /**< The *application's* version. Caller-supplied, required, and must be a semver string. */ + char name[256]; /**< Application name. Caller-supplied and required; also becomes SDL's app metadata. */ + char uri[256]; /**< Application URI, e.g. a reverse-DNS identifier. Caller-supplied, required, and used as the window title. */ + akgl_GameState state; /**< The application's own state flags. */ + SDL_Mutex *statelock; /**< Guards `state`. Created by akgl_game_init. */ + int16_t fps; /**< Frames drawn during the last completed second. Recomputed once per second, not per frame. */ + SDL_Time gameStartTime; /**< `SDL_GetTicksNS()` at akgl_game_init. */ + SDL_Time lastIterTime; /**< Timestamp of the most recent akgl_game_updateFPS call. */ + SDL_Time lastFPSTime; /**< When `fps` was last recomputed. */ + int16_t framesSinceUpdate; /**< Frames counted so far in the current second. */ + void (*lowfpsfunc)(void); /**< Called every frame while `fps` is under 30. Defaults to akgl_game_lowfps; replace it to do something more useful than log. */ } akgl_Game; -/** @brief SDL window used by the active renderer. */ +/** @brief The SDL window, created by akgl_render_init2d. `NULL` until then. */ extern SDL_Window *window; -/** @brief Loaded background-music resource. */ +/** @brief The background music, loaded by akgl_load_start_bgm. `NULL` until then. */ extern MIX_Audio *bgm; -/** @brief Mixer used for game audio playback. */ +/** @brief The mixer device, created by akgl_game_init. Everything audio goes through it. */ extern MIX_Mixer *akgl_mixer; -/** @brief Process-wide audio-track table. */ +/** @brief Playback tracks by slot. #AKGL_GAME_AUDIO_TRACK_BGM is the music track; the rest are the application's to assign. */ extern MIX_Track *akgl_tracks[AKGL_GAME_AUDIO_MAX_TRACKS]; -/** @brief Storage for the default camera. */ +/** @brief Storage behind the default `camera`. Point `camera` elsewhere rather than reaching for this. */ extern SDL_FRect _akgl_camera; -/** @brief Process-wide game metadata and timing state. */ +/** @brief The one game object: metadata, timing, and FPS accounting. */ extern akgl_Game game; -/** @brief Storage for the default renderer. */ +/** @brief Storage behind the default `renderer`. */ extern akgl_RenderBackend _akgl_renderer; -/** @brief Storage for the default physics backend. */ +/** @brief Storage behind the default `physics`. */ extern akgl_PhysicsBackend _akgl_physics; -/** @brief Storage for the default tilemap. */ +/** @brief Storage behind the default `gamemap`. */ extern akgl_Tilemap _akgl_gamemap; /** @brief Currently active tilemap. */ @@ -80,65 +116,183 @@ extern akgl_PhysicsBackend *physics; /** @brief Currently active camera. */ extern SDL_FRect *camera; +/** + * @brief True when every bit of `y` is set in `x`. Not "any of them" -- all of them. + * @warning Unparenthesized: it expands to `(x & y) == y`, so `!AKGL_BITMASK_HAS(a, b)` + * parses as `!(a & b) == b`. Use #AKGL_BITMASK_HASNOT rather than + * negating this. TODO.md item 21. + */ #define AKGL_BITMASK_HAS(x, y) (x & y) == y +/** @brief True when at least one bit of `y` is missing from `x`. Same parenthesization caveat as #AKGL_BITMASK_HAS. */ #define AKGL_BITMASK_HASNOT(x, y) (x & y) != y +/** @brief Set every bit of `y` in `x`. Modifies `x`. */ #define AKGL_BITMASK_ADD(x, y) x |= y +/** @brief Clear every bit of `y` in `x`. Modifies `x`. */ #define AKGL_BITMASK_DEL(x, y) x &= ~(y) +/** @brief Clear every bit of `x`. Carries its own trailing semicolon, so do not add another. */ #define AKGL_BITMASK_CLEAR(x) x = 0; /** - * @brief Game init. + * @brief Bring the whole library up: error codes, pools, registries, SDL, audio, fonts, gamepads. + * + * In order: claim the libakgl status band (so every later error has a name), + * stamp the library version, start the frame clock, create the state mutex, + * check that the caller filled in the three required `game` fields, zero the + * pools, create the registries, hand SDL the app metadata, clear the control + * maps, `SDL_Init` video/gamepad/audio, load the bundled controller database, + * open any attached gamepads, start SDL_mixer and SDL_ttf, and finally point + * `renderer`, `physics`, `camera`, and `gamemap` at their default storage. + * + * What it does *not* do: create the window, choose a physics backend, or load + * any configuration. Those read properties, so they come after the caller has + * set them. See the sequence at the top of this file. + * * @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 `game.name`, `game.version`, or `game.uri` is + * empty. All three are required and there are no defaults -- the + * window title, SDL's app metadata, and the savegame compatibility + * check are all built from them. + * @throws AKGL_ERR_SDL If the state mutex cannot be created, if `SDL_Init` + * fails, if a controller-database entry is rejected, if SDL_mixer + * cannot start or open the default playback device, or if SDL_ttf + * cannot start. Each message carries `SDL_GetError()`. + * @throws AKERR_STATUS_RANGE_OVERLAP If another component already owns part of + * the libakgl status band. See akgl_error_init. + * @throws AKERR_* Whatever the heap, registry, and controller initializers raise. + * + * @note It takes the state lock on the way in and releases it on the way out, so + * a failure part-way through leaves the mutex held. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init(); /** - * @brief Game init screen. - * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKERR_* Propagates an error reported by a delegated operation. + * @brief Declared but never defined. Do not call. + * + * There is no definition anywhere in the library, so a translation unit that + * calls this compiles and then fails to link. Screen setup is akgl_render_init2d. + * Tracked in TODO.md under header/implementation surface drift. + * + * @return Nothing; it cannot be called. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init_screen(); /** - * @brief Game updatefps. + * @brief Count this frame, and recompute the frame rate once a second has passed. + * + * Called at the top of akgl_game_update, so a caller running its own loop needs + * to call it itself. `game.fps` is only refreshed when a full second has + * elapsed, so it is a completed-second average rather than an instantaneous + * figure -- and it reads 0 for the first second of the process, which is under + * the low-FPS threshold and so fires `lowfpsfunc` on every frame until the first + * second is up. */ void akgl_game_updateFPS(); /** - * @brief Game save. - * @param fpath Path to the input or output file. + * @brief Write the game state and the name-to-pointer tables to a save file. + * + * Writes the `akgl_Game` struct verbatim, then four name tables -- actors, + * sprites, spritesheets, characters -- each mapping a registered name to the + * address the object had at save time, and each terminated by a zeroed name and + * a zeroed pointer. The tables are what let akgl_game_load reconnect pointers + * between objects that will be at different addresses next run. + * + * @param fpath Path to write. Required. Opened with `"wb"`; an existing file is + * truncated. * @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 fpath is `NULL`. + * @throws ENOENT, EACCES Or whatever else `fopen(3)` reports, with the path in + * the message. + * @throws AKERR_IO On a stream error or a short write. + * + * @note This is a partial implementation: the name tables are written but the + * objects themselves are not, so the file is not yet enough to restore a + * session. See also TODO.md, "Known and still open" item 7 -- the writer + * and the reader disagree on the name-field widths, so a save with any + * registered spritesheet cannot be read back. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath); /** - * @brief Game load. - * @param fpath Path to the input or output file. + * @brief Read a save file back, refusing one that does not match this build. + * + * Reads the saved `akgl_Game`, then rejects the file unless the library version, + * the game version, the game name, and the game URI all match the running + * program -- versions by exact semver equality, name and URI by string compare. + * Only then does it copy the saved state over `game` and rebuild the four + * old-address-to-current-object maps. + * + * @param fpath Path to read. Required. Opened with `"rb"`. * @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_NULLPOINTER If @p fpath is `NULL`. + * @throws ENOENT, EACCES Or whatever else `fopen(3)` reports. + * @throws AKERR_EOF If a read runs off the end of the file -- which is what a + * truncated or corrupt name table looks like. + * @throws AKERR_IO On a stream error. + * @throws AKERR_VALUE If either version string in the save file, or either in + * the running game, is not valid semver. + * @throws AKERR_API If the save file is from a different library version, game + * version, game name, or game URI. + * + * @note Like akgl_game_save, this is partial: it rebuilds the pointer maps but + * does not yet read back any objects. The four `SDL_CreateProperties` + * sets it builds are never destroyed, so each call leaks them. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath); /** - * @brief Game lowfps. + * @brief The default `game.lowfpsfunc`: log the current frame rate. + * + * Called from akgl_game_updateFPS on every frame where `game.fps` is under 30. + * It is a placeholder -- the point of the hook is that a game can replace it + * with something that actually sheds work. */ void akgl_game_lowfps(void); /** - * @brief Game state lock. - * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKGL_ERR_SDL When the corresponding validation or operation fails. + * @brief Take the game-state mutex, retrying rather than blocking. + * + * Polls with `SDL_TryLockMutex` on a 100 ms cadence instead of blocking + * outright, so a deadlock reports an error rather than hanging the process. + * + * @return `NULL` once the lock is held, otherwise an error context owned by the + * caller. + * @throws AKGL_ERR_SDL If the budget runs out with the lock still held + * elsewhere. The message carries `SDL_GetError()`, which after a failed + * `SDL_TryLockMutex` is usually stale or empty -- the status is the + * signal, not the text. + * + * @note The budget is meant to be one second but is not: the loop counts against + * #AKGL_TIME_ONESEC_MS, which is nanoseconds-per-millisecond (1000000) + * rather than milliseconds-per-second, so it retries 10,000 times at 100 ms + * and gives up after roughly 16 minutes. TODO.md item 6. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_lock(void); /** - * @brief Game state unlock. - * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKERR_* Propagates an error reported by a delegated operation. + * @brief Release the game-state mutex. + * + * @return `NULL`. `SDL_UnlockMutex` reports nothing, so there is no failure path + * -- including for the case that matters, unlocking a mutex this thread + * does not hold, which is undefined behaviour in SDL rather than an + * error here. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_unlock(void); /** - * @brief Game update. - * @param opflags Optional iterator operation flags; `NULL` selects defaults. + * @brief One frame: update every actor layer by layer, step the physics, draw the world. + * + * Takes the state lock, counts the frame, then walks layers 0 through + * #AKGL_TILEMAP_MAX_LAYERS calling each live actor's `updatefunc`, optionally + * rescaling it to the tilemap first. Then it steps `physics` and draws through + * `renderer`, and releases the lock. + * + * @param opflags Iterator flags. Optional -- `NULL` selects a default set that + * sweeps one layer at a time, in which case `layerid` is advanced + * by the loop. Only #AKGL_ITERATOR_OP_TILEMAPSCALE is read here; + * with it clear every actor's `scale` is forced to 1.0. Note that + * the flags are *not* forwarded to the physics or render calls, + * both of which are passed `NULL`. * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKERR_* Propagates an error reported by a delegated operation. + * @throws AKGL_ERR_SDL If the state lock cannot be taken. + * @throws AKERR_* Whatever an actor's `updatefunc`, akgl_tilemap_scale_actor, + * the physics backend, or the renderer raises. + * + * @warning Every failure path returns with the state lock still held, and a live + * actor's `updatefunc` is called without a `NULL` check -- a hand-built + * actor that never went through akgl_actor_initialize crashes here. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags); diff --git a/include/akgl/heap.h b/include/akgl/heap.h index 5858e79..4271e0d 100644 --- a/include/akgl/heap.h +++ b/include/akgl/heap.h @@ -1,6 +1,29 @@ /** * @file heap.h - * @brief Declares the public heap API. + * @brief The object pools. This library does not call `malloc`, it claims slots. + * + * Every runtime object -- actors, sprites, spritesheets, characters, strings -- + * comes out of a fixed, statically allocated array declared here. A "heap layer" + * is one such array plus its `next`/`release` pair. Allocation is a linear scan + * for a slot whose `refcount` is 0; release decrements, and the slot is zeroed + * and unregistered when the count reaches 0. + * + * The consequence to design around is that exhaustion is a *normal* error, not + * an out-of-memory catastrophe: AKGL_ERR_HEAP means the pool is full, and the + * fix is usually a missing release rather than a bigger pool. Every ceiling + * below is overridable at compile time, so a game that needs 256 actors defines + * `AKGL_MAX_HEAP_ACTOR` before including this -- but the arrays are sized at + * compile time, so the library and everything linking it must agree. + * + * If you need a new kind of runtime object, add a layer here. Do not reach for + * the allocator. + * + * @warning The acquire functions are asymmetric: akgl_heap_next_string takes the + * reference for you, and the other four do not -- their caller is + * expected to take it, which in practice the `*_initialize` function + * does. Until one is taken the slot is still free and the next + * allocation hands out the same pointer. TODO.md, "Known and still + * open" item 8. */ #ifndef _AKGL_HEAP_H_ @@ -28,102 +51,169 @@ #define AKGL_MAX_HEAP_STRING 256 #endif -/** @brief Fixed-capacity actor object pool. */ +/** @brief The actor pool. Public so the render and physics sweeps can walk it directly instead of going through the registry. */ extern akgl_Actor HEAP_ACTOR[AKGL_MAX_HEAP_ACTOR]; -/** @brief Fixed-capacity sprite object pool. */ +/** @brief The sprite pool. 16 per actor, on the assumption of one sprite per state combination. */ extern akgl_Sprite HEAP_SPRITE[AKGL_MAX_HEAP_SPRITE]; -/** @brief Fixed-capacity spritesheet object pool. */ +/** @brief The spritesheet pool. Sized like the sprite pool, though sharing means far fewer are used in practice. */ extern akgl_SpriteSheet HEAP_SPRITESHEET[AKGL_MAX_HEAP_SPRITESHEET]; -/** @brief Fixed-capacity character object pool. */ +/** @brief The character pool. */ extern akgl_Character HEAP_CHARACTER[AKGL_MAX_HEAP_CHARACTER]; -/** @brief Fixed-capacity static-string object pool. */ +/** @brief The string pool. Every entry is PATH_MAX bytes, so this is the largest of the five by a wide margin. */ extern akgl_String HEAP_STRING[AKGL_MAX_HEAP_STRING]; /** - * @brief Heap init. - * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKGL_ERR_BEHAVIOR When the corresponding validation or operation fails. - * @throws AKGL_ERR_HEAP When the corresponding validation or operation fails. - * @throws AKGL_ERR_LOGICINTERRUPT When the corresponding validation or operation fails. - * @throws AKGL_ERR_REGISTRY When the corresponding validation or operation fails. - * @throws AKGL_ERR_SDL When the corresponding validation or operation fails. + * @brief Zero every pool, marking every slot free. + * + * Call once at startup, before anything allocates. Calling it again is a reset, + * not a refresh: it does not release textures, clear registries, or consult + * reference counts, so every live object becomes a dangling pointer and every + * registry entry points at a zeroed slot. akgl_game_init calls it for you. + * + * @return `NULL`. There is no failure path today -- it is a series of `memset`s + * -- but check it anyway; the signature exists so a layer that needs + * real setup has somewhere to report from. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init(); /** - * @brief Heap init actor. - * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKERR_* Propagates an error reported by a delegated operation. + * @brief Zero the actor pool only. + * + * Split out from akgl_heap_init so a caller can reset the actors between levels + * while keeping the loaded sprites, sheets, and characters -- those are the + * expensive ones, since they own textures. Pair it with + * akgl_registry_init_actor, which clears the matching registry. + * + * @return `NULL`. No failure path today; see akgl_heap_init. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init_actor(); /** - * @brief Heap next actor. - * @param dest Output destination populated by the function. + * @brief Claim a free actor slot. + * @param dest Receives a pointer to the free slot. Required, and **not** + * checked -- a `NULL` here is a crash, not an error context. The + * slot is *not* zeroed and its `refcount` is not incremented; it + * stays free until akgl_actor_initialize takes the reference. * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKGL_ERR_HEAP When the corresponding validation or operation fails. + * @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_ACTOR slots is in use. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_actor(akgl_Actor **dest); /** - * @brief Heap next sprite. - * @param dest Output destination populated by the function. + * @brief Claim a free sprite slot. + * @param dest Receives a pointer to the free slot. Required and unchecked; not + * zeroed, and no reference taken. See akgl_heap_next_actor. * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKGL_ERR_HEAP When the corresponding validation or operation fails. + * @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_SPRITE slots is in use. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_sprite(akgl_Sprite **dest); /** - * @brief Heap next spritesheet. - * @param dest Output destination populated by the function. + * @brief Claim a free spritesheet slot. + * @param dest Receives a pointer to the free slot. Required and unchecked; not + * zeroed, and no reference taken. See akgl_heap_next_actor. * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKGL_ERR_HEAP When the corresponding validation or operation fails. + * @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_SPRITESHEET slots is + * in use. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_spritesheet(akgl_SpriteSheet **dest); /** - * @brief Heap next character. - * @param dest Output destination populated by the function. + * @brief Claim a free character slot. + * @param dest Receives a pointer to the free slot. Required and unchecked; not + * zeroed, and no reference taken. See akgl_heap_next_actor. * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKGL_ERR_HEAP When the corresponding validation or operation fails. + * @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_CHARACTER slots is in + * use. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_character(akgl_Character **dest); /** - * @brief Heap next string. - * @param dest Output destination populated by the function. + * @brief Claim a free string slot, and take the reference on it. + * + * The odd one out: this *does* increment `refcount`, so the slot is yours the + * moment it returns and stays yours until you call akgl_heap_release_string. + * That is what makes the scratch-buffer idiom -- claim, use, release in + * `CLEANUP` -- safe. The contents are whatever the last holder left; call + * akgl_string_initialize if you need it clean. + * + * @param dest Receives a pointer to the claimed slot. Required and **not** + * checked -- a `NULL` here is a crash, not an error context. * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKGL_ERR_HEAP When the corresponding validation or operation fails. + * @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_STRING slots is in + * use. In practice this means a missing release somewhere, not a pool + * that is genuinely too small. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_string(akgl_String **dest); /** - * @brief Heap release actor. - * @param ptr Heap object whose reference count is decremented. + * @brief Drop a reference to an actor, tearing it down when the last one goes. + * + * At zero it releases every child recursively, clears the actor's entry from + * #AKGL_REGISTRY_ACTOR, and zeroes the slot. + * + * @param ptr The actor to release. Required. A slot whose `refcount` is already + * 0 is re-torn-down rather than rejected, which is harmless on a + * zeroed slot and destructive on a live one that was never + * registered. * @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 ptr is `NULL`. + * + * @warning Children are released unconditionally, and the recursion has no cycle + * check: an actor reachable from its own child list recurses until the + * stack runs out. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_actor(akgl_Actor *ptr); /** - * @brief Heap release sprite. - * @param ptr Heap object whose reference count is decremented. + * @brief Drop a reference to a sprite, tearing it down when the last one goes. + * + * At zero it clears the sprite's entry from #AKGL_REGISTRY_SPRITE and zeroes the + * slot. The spritesheet it pointed at is *not* released -- the sprite only + * borrowed it. + * + * @param ptr The sprite to release. 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 ptr is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_sprite(akgl_Sprite *ptr); /** - * @brief Heap release spritesheet. - * @param ptr Heap object whose reference count is decremented. + * @brief Drop a reference to a spritesheet, destroying its texture when the last one goes. + * + * The only release that frees a resource outside the pool: at zero it clears the + * entry from #AKGL_REGISTRY_SPRITESHEET, destroys the `SDL_Texture`, and zeroes + * the slot. Any sprite still pointing at the sheet is left with a dangling + * pointer, since nothing takes a reference on a sheet on a sprite's behalf. + * + * @param ptr The spritesheet to release. 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 ptr is `NULL`. + * + * @note Destroying a texture is a main-thread operation in SDL, so this must be + * called from the thread that owns the renderer. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_spritesheet(akgl_SpriteSheet *ptr); /** - * @brief Heap release character. - * @param ptr Heap object whose reference count is decremented. + * @brief Drop a reference to a character, tearing it down when the last one goes. + * + * At zero it clears the entry from #AKGL_REGISTRY_CHARACTER and zeroes the slot. + * + * @param ptr The character to release. 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 ptr is `NULL`. + * + * @note It does not walk the state-to-sprite map, so the references the + * character took in akgl_character_sprite_add are never given back, and + * the SDL property set holding the map is leaked. Release the sprites + * first with akgl_character_state_sprites_iterate and + * #AKGL_ITERATOR_OP_RELEASE if you need them back. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_character(akgl_Character *ptr); /** - * @brief Heap release string. - * @param ptr Heap object whose reference count is decremented. + * @brief Drop a reference to a pooled string, zeroing it when the last one goes. + * + * Strings are not registered anywhere, so this is just the reference count and a + * wipe. It is safe to call on a string a function may or may not have claimed, + * which is why the `CLEANUP` blocks in this library call it unconditionally. + * + * @param ptr The string to release. Required -- unlike the pattern elsewhere, a + * `NULL` here is an error rather than a no-op, so `CLEANUP` blocks + * wrap it in `IGNORE()`. * @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 ptr is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_string(akgl_String *ptr); diff --git a/include/akgl/iterator.h b/include/akgl/iterator.h index 7824bb5..ec32991 100644 --- a/include/akgl/iterator.h +++ b/include/akgl/iterator.h @@ -1,6 +1,20 @@ /** * @file iterator.h - * @brief Declares the public iterator API. + * @brief The work order handed to a registry sweep. + * + * There is no iterator object with a `next()` on it. Traversal is SDL's -- + * `SDL_EnumerateProperties` over a registry -- and this struct is the `userdata` + * carried into each callback, telling it *which* entries to touch and *what* to + * do to each one. akgl_registry_iterate_actor and + * akgl_registry_iterate_character are the callbacks that read it; + * akgl_game_update, akgl_physics_simulate, and akgl_render_2d_draw_world are the + * entry points that take one. + * + * The operations are independent bits, not an enum: a single sweep can update, + * scale, and render, and they run in that fixed order regardless of how the bits + * were set. Passing `NULL` where an `akgl_Iterator *` is expected is not an + * error at the top-level entry points -- each substitutes its own default set -- + * but it *is* an error once inside a callback. */ #ifndef _AKGL_ITERATOR_H_ @@ -8,15 +22,15 @@ /** @brief Selects operations and an optional layer for actor traversal. */ typedef struct { - uint32_t flags; - uint8_t layerid; + uint32_t flags; /**< Bitwise OR of the `AKGL_ITERATOR_OP_*` values below. */ + uint8_t layerid; /**< Layer to restrict the sweep to. Read only when #AKGL_ITERATOR_OP_LAYERMASK is set. */ } akgl_Iterator; -#define AKGL_ITERATOR_OP_UPDATE 1 // 1 -#define AKGL_ITERATOR_OP_RENDER 1 << 1 // 2 -#define AKGL_ITERATOR_OP_RELEASE 1 << 2 // 4 -#define AKGL_ITERATOR_OP_LAYERMASK 1 << 3 // 8 -#define AKGL_ITERATOR_OP_TILEMAPSCALE 1 << 4 // 16 +#define AKGL_ITERATOR_OP_UPDATE 1 // 1 Call the actor's updatefunc +#define AKGL_ITERATOR_OP_RENDER 1 << 1 // 2 Call the actor's renderfunc +#define AKGL_ITERATOR_OP_RELEASE 1 << 2 // 4 Release the object back to its heap layer +#define AKGL_ITERATOR_OP_LAYERMASK 1 << 3 // 8 Skip anything whose layer != layerid +#define AKGL_ITERATOR_OP_TILEMAPSCALE 1 << 4 // 16 Scale actors to the tilemap; otherwise force scale 1.0 #define AKGL_ITERATOR_OP_UNDEFINED_5 1 << 5 // 32 #define AKGL_ITERATOR_OP_UNDEFINED_6 1 << 6 // 64 #define AKGL_ITERATOR_OP_UNDEFINED_7 1 << 7 // 128 diff --git a/include/akgl/json_helpers.h b/include/akgl/json_helpers.h index 8b8f1e9..fdc0f53 100644 --- a/include/akgl/json_helpers.h +++ b/include/akgl/json_helpers.h @@ -1,6 +1,29 @@ /** * @file json_helpers.h - * @brief Declares the public json helpers API. + * @brief Typed jansson accessors that report "missing" and "wrong type" as errors. + * + * Every asset in this library is described by a JSON document, and jansson's own + * accessors answer "missing key", "wrong type", and "index past the end" all + * with the same `NULL`. These wrappers split those apart -- AKERR_KEY, + * AKERR_TYPE, AKERR_OUTOFBOUNDS -- 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. + * + * Conventions that run through the whole set, so they need not be repeated per + * function: + * + * - **Absence is an error here.** Unlike the search functions in libakstdlib, + * these treat a missing key as AKERR_KEY. An *optional* key is expressed by + * passing the resulting error to akgl_get_json_with_default() rather than by + * the accessor staying quiet. + * - **The result comes back through @p dest**, because the return value is the + * error context. + * - **`json_t *` results are borrowed, not owned.** Objects and arrays are + * returned as pointers into the document; they are valid until the document is + * freed and must not be `json_decref`'d. + * - **@p dest is not `NULL`-checked** except where noted, so a `NULL` there is a + * crash rather than an error context. + * - **The document itself is never modified.** */ #ifndef _JSON_HELPERS_H_ @@ -10,126 +33,203 @@ #include "staticstring.h" /** - * @brief Get json object value. - * @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 nested object out of a JSON object. + * @param obj The object to read from. Required. + * @param key The member name to look up. A `NULL` key is reported as a missing + * key rather than as a `NULL` pointer. + * @param dest Receives a borrowed pointer to the nested object; not written on + * any failure path. Not checked for `NULL`. * @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_TYPE When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p obj is `NULL`. + * @throws AKERR_KEY If @p key is absent. The message names it. + * @throws AKERR_TYPE If @p key is present but is not an object. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_object_value(json_t *obj, char *key, json_t **dest); /** - * @brief Get json boolean value. - * @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 boolean out of a JSON object. + * @param obj The object to read from. Required. + * @param key The member name to look up. + * @param dest Receives the value; not written on any failure path. Not checked + * for `NULL`. * @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_TYPE When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p obj is `NULL`. + * @throws AKERR_KEY If @p key is absent. + * @throws AKERR_TYPE If @p key is present but is not `true` or `false`. A `0` + * or `1` is a number in JSON, and is refused here. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_boolean_value(json_t *obj, char *key, bool *dest); /** - * @brief Get json integer value. - * @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 an integer out of a JSON object. + * @param obj The object to read from. Required. + * @param key The member name to look up. + * @param dest Receives the value, narrowed to `int`. Not written on any failure + * path, and not checked for `NULL`. * @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_TYPE When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p obj is `NULL`. + * @throws AKERR_KEY If @p key is absent. + * @throws AKERR_TYPE If @p key is present but is not an integer. This is strict: + * `3.0` is a real in JSON and is refused, not truncated. Use + * akgl_get_json_number_value() where either spelling should be accepted. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_integer_value(json_t *obj, char *key, int *dest); /** - * @brief Get json number value. - * @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 number out of a JSON object as a `float`. + * @param obj The object to read from. Required. + * @param key The member name to look up. + * @param dest Receives the value, narrowed to `float`. Not written on any + * failure path, and not checked for `NULL`. * @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_TYPE When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p obj is `NULL`. + * @throws AKERR_KEY If @p key is absent. + * @throws AKERR_TYPE If @p key is present but is not a number. Integers and + * reals are both accepted, so `1` and `1.0` behave alike. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_number_value(json_t *obj, char *key, float *dest); /** - * @brief Get json double value. - * @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 number out of a JSON object as a `double`. + * + * The full-precision form of akgl_get_json_number_value(), for the physics + * constants, which are `double`. + * + * @param obj The object to read from. Required. + * @param key The member name to look up. + * @param dest Receives the value. Not written on any failure path, and not + * checked for `NULL`. * @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_TYPE When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p obj is `NULL`. + * @throws AKERR_KEY If @p key is absent. + * @throws AKERR_TYPE If @p key is present but is not a number. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_double_value(json_t *obj, char *key, double *dest); /** - * @brief Get json string value. - * @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 string out of a JSON object into a pooled akgl_String. + * + * Unlike the other accessors this copies, because the caller wants a buffer it + * can keep rather than a pointer into the document. `*dest` doubles as an input: + * `NULL` means "claim one for me", non-`NULL` means "write into this one". + * + * @param obj The object to read from. Required. + * @param key The member name to look up. Required -- checked here, unlike the + * other accessors in this file. + * @param dest Address of the destination string. Required, and it must be + * *initialized*: set `*dest` to `NULL` to have a pool string claimed + * for you, or to a claimed string to write in place. An + * indeterminate `*dest` is dereferenced. Either way the caller + * releases it with akgl_heap_release_string(). * @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_TYPE When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p obj, @p key, or @p dest is `NULL`. + * @throws AKERR_KEY If @p key is absent. + * @throws AKERR_TYPE If @p key is present but is not a string. + * @throws AKGL_ERR_HEAP If `*dest` was `NULL` and the string pool is exhausted. + * + * @note A value longer than #AKGL_MAX_STRING_LENGTH is truncated silently, and + * is left without a terminator -- this is `strncpy`, not `strlcpy`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_string_value(json_t *obj, char *key, akgl_String **dest); /** - * @brief Get json array value. - * @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 an array out of a JSON object. + * @param obj The object to read from. Required. + * @param key The member name to look up. + * @param dest Receives a borrowed pointer to the array; not written on any + * failure path. Not checked for `NULL`. Use `json_array_size()` on + * it and the `akgl_get_json_array_index_*` family to walk 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_NULLPOINTER When the corresponding validation or operation fails. - * @throws AKERR_TYPE When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p obj is `NULL`. + * @throws AKERR_KEY If @p key is absent. + * @throws AKERR_TYPE If @p key is present but is not an array. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_value(json_t *obj, char *key, json_t **dest); /** - * @brief Get json array index object. - * @param array JSON array to read. - * @param index Zero-based element index. - * @param dest Output destination populated by the function. + * @brief Read one element of a JSON array as an object. + * @param array The array to read from. Required. + * @param index Zero-based element index. A negative index is reported the same + * way as one past the end. + * @param dest Receives a borrowed pointer to the element; not written on any + * failure path. Not checked for `NULL`. * @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_TYPE When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p array is `NULL`. + * @throws AKERR_OUTOFBOUNDS If @p index is outside the array. The message + * reports the index. + * @throws AKERR_TYPE If the element is not an object. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_object(json_t *array, int index, json_t **dest); /** - * @brief Get json array index integer. - * @param array JSON array to read. + * @brief Read one element of a JSON array as an integer. + * @param array The array to read from. Required. * @param index Zero-based element index. - * @param dest Output destination populated by the function. + * @param dest Receives the value, narrowed to `int`; not written on any failure + * path. Not checked for `NULL`. * @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_TYPE When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p array is `NULL`. + * @throws AKERR_OUTOFBOUNDS If @p index is outside the array. + * @throws AKERR_TYPE If the element is not an integer. Strict, as in + * akgl_get_json_integer_value(). */ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_integer(json_t *array, int index, int *dest); /** - * @brief Get json array index string. - * @param array JSON array to read. + * @brief Read one element of a JSON array into a pooled akgl_String. + * @param array The array to read from. Required. * @param index Zero-based element index. - * @param dest Output destination populated by the function. + * @param dest Address of the destination string, with the same claim-or-reuse + * contract as akgl_get_json_string_value(): required, must be + * initialized, and released by the caller. * @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_TYPE When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p array or @p dest is `NULL`. + * @throws AKERR_OUTOFBOUNDS If @p index is outside the array. + * @throws AKERR_TYPE If the element is not a string. + * @throws AKGL_ERR_HEAP If `*dest` was `NULL` and the string pool is exhausted. + * + * @note Truncates silently at #AKGL_MAX_STRING_LENGTH, as + * akgl_get_json_string_value() does. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_string(json_t *array, int index, akgl_String **dest); /** - * @brief Get json with default. - * @param e Error context to inspect and optionally consume. - * @param defval Fallback value copied when the supplied error is eligible. - * @param dest Output destination populated by the function. - * @param defsize Size in bytes of the fallback value. - * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKERR_INDEX When the corresponding validation or operation fails. - * @throws AKERR_KEY When the corresponding validation or operation fails. - * @throws AKERR_NULLPOINTER When the corresponding validation or operation fails. + * @brief Turn a "key not found" error from one of the accessors above into a default value. + * + * This is how an optional key is spelled. Run the accessor, hand its error + * context here along with the fallback, and a missing key becomes @p dest + * holding @p defval and a `NULL` return; anything else propagates untouched: + * + * ```c + * int width = 0; + * int width_default = 32; + * PASS(errctx, + * akgl_get_json_with_default( + * akgl_get_json_integer_value(json, "width", &width), + * &width_default, + * &width, + * sizeof(int) + * )); + * ``` + * + * A `NULL` @p e -- the accessor succeeded -- is the ordinary case and returns at + * once without touching @p dest. When the error *is* consumed it is also + * released, so the caller must not release it again. + * + * @param e The error context to inspect, straight from an accessor. + * `NULL` means "no error", which is not itself an error. + * @param defval The fallback value to copy. Required when @p e is non-`NULL`. + * @param dest Where to copy it. Required when @p e is non-`NULL`. Must be the + * same destination the accessor was given, and at least + * @p defsize bytes. + * @param defsize Bytes to copy out of @p defval. Trusted, not derived -- it must + * match the type both sides actually are, since this is a + * `memcpy` through `void *` with no type information. + * @return `NULL` when the error was consumed or there was none, otherwise an + * error context owned by the caller. + * @throws AKERR_NULLPOINTER If @p e is non-`NULL` and @p defval or @p dest is + * `NULL`. + * @throws AKERR_* Whatever @p e carried, if it is not one of the statuses this + * defaults on -- an AKERR_TYPE from a key that exists but is the wrong + * type propagates, which is right: that is a malformed document, not an + * omitted setting. + * + * @note It defaults on AKERR_KEY and AKERR_INDEX, but *not* on + * AKERR_OUTOFBOUNDS -- which is the status the + * `akgl_get_json_array_index_*` family actually raises for a short array. + * So this pairs with the object accessors and does not currently give an + * array index a default. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext *e, void *defval, void *dest, uint32_t defsize); diff --git a/include/akgl/physics.h b/include/akgl/physics.h index faf51c4..45e3617 100644 --- a/include/akgl/physics.h +++ b/include/akgl/physics.h @@ -1,6 +1,28 @@ /** * @file physics.h - * @brief Declares the public physics API. + * @brief The pluggable physics backend, and the two implementations that ship. + * + * Same shape as the renderer: a record of function pointers plus an initializer + * that fills it in. `null` accepts every call and changes nothing, which is what + * a menu screen or a test harness wants; `arcade` applies gravity, drag, thrust + * and a speed cap. Selecting between them is akgl_physics_factory, driven by the + * `physics.engine` configuration property -- not a branch in the simulation. + * + * The model an actor is simulated under, and the fields on akgl_Actor that carry + * it: + * + * | Term | Fields | Comes from | + * |-----------------------|--------------|-----------------------------------------------| + * | thrust | `tx, ty, tz` | the actor's own acceleration while it is moving | + * | environmental | `ex, ey, ez` | gravity, less drag | + * | velocity | `vx, vy, vz` | thrust + environmental | + * | max speed | `sx, sy, sz` | the character; caps thrust, not velocity | + * | acceleration | `ax, ay, az` | the character | + * + * Each step: movement logic, then gravity, then drag, then velocity, then move. + * Because the cap is applied to thrust rather than to velocity, gravity can + * carry an actor faster than its stated top speed -- which is the point, for a + * falling one. */ #ifndef _PHYSICS_H_ @@ -14,110 +36,202 @@ /** @brief Defines a pluggable physics backend and its environmental parameters. */ typedef struct akgl_PhysicsBackend { - akerr_ErrorContext AKERR_NOIGNORE *(*simulate)(struct akgl_PhysicsBackend *self, akgl_Iterator *opflags); - akerr_ErrorContext AKERR_NOIGNORE *(*gravity)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt); - akerr_ErrorContext AKERR_NOIGNORE *(*collide)(struct akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2); - akerr_ErrorContext AKERR_NOIGNORE *(*move)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt); + akerr_ErrorContext AKERR_NOIGNORE *(*simulate)(struct akgl_PhysicsBackend *self, akgl_Iterator *opflags); /**< Step the whole world. Both backends point this at akgl_physics_simulate. */ + akerr_ErrorContext AKERR_NOIGNORE *(*gravity)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt); /**< Apply environmental acceleration to one actor. */ + akerr_ErrorContext AKERR_NOIGNORE *(*collide)(struct akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2); /**< Resolve a collision between two actors. Not called by the simulation loop yet. */ + akerr_ErrorContext AKERR_NOIGNORE *(*move)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt); /**< Commit one actor's velocity to its position. */ - double drag_x; - double drag_y; - double drag_z; - double gravity_x; - double gravity_y; - double gravity_z; - SDL_Time gravity_time; - SDL_Time timer_gravity; + double drag_x; /**< Fraction of environmental velocity shed per second along x. 0 disables. From `physics.drag.x`. */ + double drag_y; /**< Drag along y. From `physics.drag.y`. */ + double drag_z; /**< Drag along z. From `physics.drag.z`. */ + double gravity_x; /**< Acceleration along x, world units per second squared. Applied toward screen left. From `physics.gravity.x`. */ + double gravity_y; /**< Acceleration along y. Applied down the screen, since y grows downward. From `physics.gravity.y`. */ + double gravity_z; /**< Acceleration along z. Applied away from the camera. From `physics.gravity.z`. */ + SDL_Time gravity_time; /**< Timestamp of the previous step, in nanoseconds. The simulation's `dt` is measured from this. */ + SDL_Time timer_gravity; /**< Unused. Nothing in the library reads or writes it. */ } akgl_PhysicsBackend; /** - * @brief Physics null gravity. - * @param self Backend or object instance to operate on. - * @param actor Actor to inspect or modify. - * @param dt Elapsed simulation time in seconds. + * @brief Gravity for the null backend: accept the call and change nothing. + * @param self The backend. Required -- the one thing this does check. + * @param actor The actor that would have been accelerated. Ignored; `NULL` is + * accepted. + * @param dt Seconds since the previous step. Ignored. * @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 self is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt); /** - * @brief Physics null collide. - * @param self Backend or object instance to operate on. - * @param a1 First actor supplied to collision handling. - * @param a2 Second actor supplied to collision handling. + * @brief Collision for the null backend: accept the call and change nothing. + * + * Note that this *succeeds* where the arcade backend's collide refuses -- the + * null backend's contract is "nothing collides", which is an answer, not a gap. + * + * @param self The backend. Required. + * @param a1 First actor. Ignored; `NULL` is accepted. + * @param a2 Second actor. Ignored; `NULL` is accepted. * @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 self is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2); /** - * @brief Physics null move. - * @param self Backend or object instance to operate on. - * @param actor Actor to inspect or modify. - * @param dt Elapsed simulation time in seconds. + * @brief Movement for the null backend: accept the call and change nothing. + * + * Actors under the null backend do not move on their own. Anything that sets an + * actor's `x`/`y` directly still works -- this only declines to integrate + * velocity. + * + * @param self The backend. Required. + * @param actor The actor that would have moved. Ignored; `NULL` is accepted. + * @param dt Seconds since the previous step. Ignored. * @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 self is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt); /** - * @brief Physics init null. - * @param self Backend or object instance to operate on. + * @brief Install the null backend's methods. + * + * Reads no configuration and touches no gravity or drag fields, so whatever was + * in them stays -- harmlessly, since none of the null methods look. + * + * @param self The backend to initialize. 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 self is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_null(akgl_PhysicsBackend *self); /** - * @brief Physics arcade gravity. - * @param self Backend or object instance to operate on. - * @param actor Actor to inspect or modify. - * @param dt Elapsed simulation time in seconds. + * @brief Accelerate one actor's environmental velocity by gravity. + * + * Adds `gravity * dt` to the actor's `ex`/`ey`/`ez` on each axis whose gravity + * is non-zero, with the sign chosen for screen space: x pulls left, y pulls + * down, z pulls away from the camera. It touches only the environmental term, so + * an actor's own thrust is unaffected and the speed cap does not apply -- which + * is why a falling actor keeps accelerating past its character's top speed. + * + * @param self The backend supplying the gravity constants. Required. + * @param actor The actor to accelerate. Required. + * @param dt Seconds since the previous step. A `dt` of 0 is a no-op; a + * negative one accelerates backwards. * @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 self or @p actor is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt); /** - * @brief Physics arcade collide. - * @param self Backend or object instance to operate on. - * @param a1 First actor supplied to collision handling. - * @param a2 Second actor supplied to collision handling. - * @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. + * @brief Collision for the arcade backend. Not implemented. + * + * Deliberately loud rather than silently permissive: unlike + * akgl_physics_null_collide, which means "nothing collides", this means "nobody + * has written this yet", and a caller that reaches it should find out. + * + * @param self The backend. Required, and checked before the refusal. + * @param a1 First actor. Never examined. + * @param a2 Second actor. Never examined. + * @return Never `NULL`. + * @throws AKERR_NULLPOINTER If @p self is `NULL`. + * @throws AKERR_API Otherwise, always, with the message "Not implemented". */ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2); /** - * @brief Physics arcade move. - * @param self Backend or object instance to operate on. - * @param actor Actor to inspect or modify. - * @param dt Elapsed simulation time in seconds. + * @brief Commit an actor's velocity to its position. + * + * The last step of a simulation tick: `position += velocity * dt` on all three + * axes. It does not clamp to the map, consult the tilemap, or test for + * collisions -- an actor will walk straight through a wall and off the edge of + * the world. + * + * @param self The backend. Required, though nothing on it is read. + * @param actor The actor to move. Required. Its `vx`/`vy`/`vz` must already have + * been computed; akgl_physics_simulate does that immediately + * before calling this. + * @param dt Seconds since the previous step. * @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 self or @p actor is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt); /** - * @brief Physics init arcade. - * @param self Backend or object instance to operate on. + * @brief Install the arcade backend's methods and read its constants from configuration. + * + * Reads `physics.gravity.x`, `.y`, `.z` and `physics.drag.x`, `.y`, `.z` from + * the property registry, all defaulting to `"0.0"` -- so an arcade backend with + * no configuration behaves like the null one until something is set. + * + * @param self The backend to initialize. Required. Its method pointers are + * installed before the properties are read, so a failure part-way + * leaves a usable backend with some constants still at their + * previous values. * @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 self is `NULL`. + * @throws AKERR_VALUE If one of the six properties is set to something that is + * not a number. + * @throws ERANGE If one of them does not fit a `double`. + * @throws AKGL_ERR_HEAP If the string pool is exhausted. + * + * @note With #AKGL_REGISTRY_PROPERTIES uninitialized every property reads back + * as its default, so this silently configures zero gravity and zero drag + * rather than reporting anything. See the warning in registry.h. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_arcade(akgl_PhysicsBackend *self); /** - * @brief Physics factory. - * @param self Backend or object instance to operate on. - * @param type Expected JSON property type or backend type name. + * @brief Select and initialize a physics backend by name. + * + * The dispatch point for the whole subsystem: `"null"` and `"arcade"` are the + * two names, and akgl_game_init passes whatever the `physics.engine` property + * holds. Adding a backend means adding a name here, not a branch in the + * simulation. + * + * @param self The backend to initialize. Required. + * @param type The backend name. Required. Matched on its leading characters, so + * `"nullify"` selects `null` and `"arcadia"` selects `arcade`. * @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 self or @p type is `NULL`. + * @throws AKERR_KEY If @p type matches neither name. The message quotes what was + * asked for. + * @throws AKERR_* Whatever the selected initializer raises. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_factory(akgl_PhysicsBackend *self, akgl_String *type); /** - * @brief Physics simulate. - * @param self Backend or object instance to operate on. - * @param opflags Optional iterator operation flags; `NULL` selects defaults. + * @brief Step every live actor forward by the time elapsed since the previous call. + * + * Shared by both backends -- what differs is the `gravity` and `move` they point + * at. For each live actor, in pool order: + * + * - a child actor is snapped to its parent's position plus its own `v` as an + * offset, and skipped entirely -- children do not simulate; + * - an actor with no character is skipped, since the speed and acceleration + * constants live there; + * - thrust accumulates along an axis the actor is moving on, then is clamped to + * the character's maximum speed; + * - the backend's `gravity` runs, drag is applied to the environmental term, + * velocity becomes environmental plus thrust, and the backend's `move` + * commits it. + * + * `dt` is measured from `self->gravity_time`, which is stamped at the end. The + * first call after initialization therefore measures from 0 and produces an + * enormous `dt` -- initialize `gravity_time` from `SDL_GetTicksNS()` before the + * first step if that matters. + * + * @param self The backend. Required, along with its `move` pointer. + * @param opflags Iterator flags. Optional -- `NULL` means "simulate everything". + * Only #AKGL_ITERATOR_OP_LAYERMASK is honoured, restricting the + * step to actors on `layerid`; the update, render, and release + * bits are ignored here. * @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_LOGICINTERRUPT When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p self or `self->move` is `NULL`. + * @throws AKERR_* Whatever an actor's `movementlogicfunc`, or the backend's + * `gravity` or `move`, raises. The first failure aborts the whole step, + * leaving the actors already processed advanced and the rest not. + * + * @note An actor's `movementlogicfunc` can raise AKGL_ERR_LOGICINTERRUPT to say + * "skip the rest of the simulation for me this tick". It is handled here + * and does not propagate -- it is a control signal wearing an error's + * clothes, not a failure. Only `movementlogicfunc` gets that treatment: + * the `gravity` and `move` calls return straight out of the loop on any + * status, LOGICINTERRUPT included, so a backend must not use it to opt an + * actor out from there. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *self, akgl_Iterator *opflags); diff --git a/include/akgl/registry.h b/include/akgl/registry.h index 7a6a490..71f0869 100644 --- a/include/akgl/registry.h +++ b/include/akgl/registry.h @@ -1,6 +1,29 @@ /** * @file registry.h - * @brief Declares the public registry API. + * @brief The name-to-object lookup tables, and the string configuration store. + * + * Nothing in this library is passed around by pointer where a name will do. An + * actor names the character it instantiates, a character names the sprites it + * draws, a sprite names the sheet it cuts frames from -- all resolved at load + * time through these eight registries. That is what lets the whole asset graph + * be described in JSON files that reference each other by name. + * + * They are SDL property sets, not akgl types, so a caller can enumerate one with + * `SDL_EnumerateProperties` -- which is exactly what akgl_registry_iterate_actor + * and akgl_character_state_sprites_iterate are for. + * + * Every id starts at 0, which SDL treats as "no such property set": reads return + * the default and writes are silently dropped. So an uninitialized registry does + * not fail loudly, it fails quietly, and akgl_registry_init has to run first. + * + * @warning akgl_registry_init does *not* initialize + * #AKGL_REGISTRY_PROPERTIES -- akgl_registry_init_properties is a + * separate call, made by akgl_game_init but by nothing else. A program + * that builds its own startup path and skips it gets a silently + * no-op akgl_set_property and an akgl_get_property that always hands + * back the caller's default, which in turn means akgl_render_init2d + * and akgl_physics_init_arcade quietly ignore their configuration. + * TODO.md, "Known and still open" item 3. */ #ifndef _REGISTRY_H_ @@ -9,99 +32,185 @@ #include #include -/** @brief Registry of actors keyed by name. */ +/** @brief Actor name -> `akgl_Actor *`. Written by akgl_actor_initialize, cleared when an actor's last reference goes. */ extern SDL_PropertiesID AKGL_REGISTRY_ACTOR; -/** @brief Registry of sprites keyed by name. */ +/** @brief Sprite name -> `akgl_Sprite *`. The `name` field from a sprite JSON. */ extern SDL_PropertiesID AKGL_REGISTRY_SPRITE; -/** @brief Registry of spritesheets keyed by asset path. */ +/** @brief Resolved image path -> `akgl_SpriteSheet *`. Keyed by path so two sprites sharing an image share the texture. */ extern SDL_PropertiesID AKGL_REGISTRY_SPRITESHEET; -/** @brief Registry of characters keyed by name. */ +/** @brief Character name -> `akgl_Character *`. What akgl_actor_set_character resolves against. */ extern SDL_PropertiesID AKGL_REGISTRY_CHARACTER; -/** @brief Registry mapping actor-state names to bit values. */ +/** @brief Actor-state name -> its bit value, as a number. Lets character JSON say "AKGL_ACTOR_STATE_FACE_LEFT" instead of 2. */ extern SDL_PropertiesID AKGL_REGISTRY_ACTOR_STATE_STRINGS; -/** @brief Registry of fonts keyed by name. */ +/** @brief Font name -> `TTF_Font *`. Names are chosen by the caller of akgl_text_loadfont, not derived from the file. */ extern SDL_PropertiesID AKGL_REGISTRY_FONT; -/** @brief Registry of music assets keyed by name. */ +/** @brief Music name -> audio handle. Created but not yet populated by anything in the library. */ extern SDL_PropertiesID AKGL_REGISTRY_MUSIC; -/** @brief Registry of string configuration properties. */ +/** @brief Configuration key -> string value. Read through akgl_get_property; everything is a string, including numbers. */ extern SDL_PropertiesID AKGL_REGISTRY_PROPERTIES; /** - * @brief Registry init. + * @brief Create the seven asset registries, in dependency order. + * + * Spritesheet, sprite, character, actor, actor-state-strings, font, music. Call + * it once at startup, before loading anything. + * * @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 SDL cannot allocate any one of the property sets; + * the message names which registry failed. The registries created before + * the failure are left in place rather than torn down. + * + * @warning This does not create #AKGL_REGISTRY_PROPERTIES -- see the warning on + * this file. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init(); /** - * @brief Registry init music. + * @brief Create the music registry. * @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 SDL cannot allocate the property set. + * @note Overwrites the existing id without destroying the old set, so calling it + * twice leaks the first one along with everything registered in it. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_music(); /** - * @brief Registry init font. + * @brief Create the font registry. * @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 SDL cannot allocate the property set. + * @note Overwrites the existing id without destroying the old set. See + * akgl_registry_init_music. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_font(); /** - * @brief Registry init actor. + * @brief Create the actor registry, destroying any previous one first. + * + * The one initializer here that cleans up after itself, which is what makes it + * safe to call between levels: it destroys the old property set before creating + * the replacement, so the actors from the previous map are unregistered in one + * step. Note that it destroys the *registry*, not the actors -- releasing those + * is akgl_heap_release_actor's job. + * * @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 SDL cannot allocate the property set. The old one + * has already been destroyed at that point, so the registry is left at 0. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor(); /** - * @brief Registry init sprite. + * @brief Create the sprite registry. * @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 SDL cannot allocate the property set. + * @note Overwrites the existing id without destroying the old set. See + * akgl_registry_init_music. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_sprite(); /** - * @brief Registry init spritesheet. + * @brief Create the spritesheet registry. * @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 SDL cannot allocate the property set. + * @note Overwrites the existing id without destroying the old set. See + * akgl_registry_init_music. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_spritesheet(); /** - * @brief Registry init character. + * @brief Create the character registry. * @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 SDL cannot allocate the property set. + * @note Overwrites the existing id without destroying the old set. See + * akgl_registry_init_music. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_character(); /** - * @brief Registry init properties. + * @brief Create the configuration-property registry. + * + * Deliberately separate from akgl_registry_init, because configuration has to be + * in place before the subsystems that read it start up. akgl_game_init creates + * it; filling it in -- with akgl_registry_load_properties or akgl_set_property -- + * is the caller's job, and has to happen before akgl_render_init2d or + * akgl_physics_init_arcade run. + * * @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 SDL cannot allocate the property set. + * @note Overwrites the existing id without destroying the old set, so every + * property set before a second call is lost. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_properties(); /** - * @brief Registry load properties. - * @param fname Path to the input or output file. + * @brief Load a JSON configuration file into the property registry. + * + * Expects a document with a top-level `properties` object whose members are all + * strings; each becomes one entry. Numbers are configured as strings here and + * parsed by whoever reads them -- `game.screenwidth` is `"800"`, not `800`. + * + * @param fname Path to the JSON document. Required. Used verbatim. * @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 fname is `NULL`, or if the file cannot be + * opened or does not parse. The message carries jansson's line number + * and text. + * @throws AKERR_KEY If the document has no top-level `properties` member. + * @throws AKERR_TYPE If `properties` is not an object, or if one of its members + * is not a string. + * @throws AKGL_ERR_HEAP If the string pool is exhausted. + * + * @note Writes into #AKGL_REGISTRY_PROPERTIES, so akgl_registry_init_properties + * has to have run -- otherwise every entry is silently dropped and this + * still returns success. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_load_properties(char *fname); /** - * @brief Registry init actor state strings. + * @brief Create the actor-state-name registry and fill it from the name table. + * + * Walks `AKGL_ACTOR_STATE_STRING_NAMES` and maps entry `i` to the value `1 << i`, + * which is what lets a character JSON write `"AKGL_ACTOR_STATE_FACE_LEFT"` and + * have akgl_character_load_json turn it into a bit. + * * @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 SDL cannot allocate the property set. The + * individual name registrations are not checked. + * + * @note The name table it reads disagrees with actor.h in two places: bits 11 + * and 12 are named `UNDEFINED_11`/`UNDEFINED_12` rather than `MOVING_IN` + * and `MOVING_OUT`, so those two states cannot be named from JSON at all. + * TODO.md items 24-26. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor_state_strings(); /** - * @brief Set property. - * @param name Registry key or human-readable object name. - * @param value Property value to store. + * @brief Set one configuration property. + * + * @param name Property key. Required. + * @param value Property value. Required. SDL copies it, so the caller's buffer + * can go away afterwards. * @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 name or @p value is `NULL`. + * + * @note The write itself is unchecked. If #AKGL_REGISTRY_PROPERTIES is still 0 -- + * akgl_registry_init_properties never having run -- this discards the + * value and returns success. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_set_property(char *name, char *value); /** - * @brief Get property. - * @param name Registry key or human-readable object name. - * @param dest Output destination populated by the function. - * @param def Fallback string returned when the property is absent. + * @brief Read one configuration property into a pooled string, with a fallback. + * + * Absence is not an error: an unset property yields @p def, which is how every + * caller in the library gets a working default without checking first. + * + * @param name Property key. Required. + * @param dest Receives the value. Required. If `*dest` is `NULL` a string is + * claimed from the pool for you, so initialize it to `NULL` on the + * first call; either way the caller releases it with + * akgl_heap_release_string. If `*dest` is non-`NULL` it is written + * in place. + * @param def Value to use when @p name is not set. Effectively required -- + * a `NULL` here on an unset property is an AKERR_NULLPOINTER rather + * than an empty result. * @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 name or @p dest is `NULL`, or if the property + * is unset and @p def is `NULL`. + * @throws AKERR_VALUE If the value and the destination overlap in memory. + * @throws AKGL_ERR_HEAP If `*dest` was `NULL` and the string pool is exhausted. + * + * @note The copy is a fixed #AKGL_MAX_STRING_LENGTH bytes rather than the length + * of the value, so a short property value is read past its end. It works + * because the destination is a full-sized pool string; it is still an + * overread of the source. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_property(char *name, akgl_String **dest, char *def); diff --git a/include/akgl/renderer.h b/include/akgl/renderer.h index 6b3d2bc..32858aa 100644 --- a/include/akgl/renderer.h +++ b/include/akgl/renderer.h @@ -1,6 +1,19 @@ /** * @file renderer.h - * @brief Declares the public renderer API. + * @brief The pluggable rendering backend: a record of function pointers plus an initializer. + * + * There is one backend shipped, the 2D SDL one, and `akgl_render_init2d` is its + * initializer -- it creates the window and the `SDL_Renderer` and then fills in + * the six `akgl_render_2d_*` entry points. A different renderer is a different + * initializer populating the same struct, not a branch inside these functions. + * + * Callers do not normally name the `akgl_render_2d_*` functions directly; they + * go through the pointers on the backend (`renderer->frame_start(renderer)`), + * which is what makes the swap possible. The global `renderer` in game.h is the + * instance the rest of the library draws through. + * + * A frame is `frame_start` (clear), any number of `draw_*` calls, then + * `frame_end` (present). */ #ifndef _RENDERER_H_ @@ -14,71 +27,146 @@ /** @brief Defines a pluggable renderer backend and drawing callbacks. */ typedef struct akgl_RenderBackend { - SDL_Renderer *sdl_renderer; - akerr_ErrorContext AKERR_NOIGNORE *(*shutdown)(struct akgl_RenderBackend *self); - akerr_ErrorContext AKERR_NOIGNORE *(*frame_start)(struct akgl_RenderBackend *self); - akerr_ErrorContext AKERR_NOIGNORE *(*frame_end)(struct akgl_RenderBackend *self); - akerr_ErrorContext AKERR_NOIGNORE *(*draw_texture)(struct akgl_RenderBackend *self, SDL_Texture *texture, SDL_FRect *src, SDL_FRect *dest, double angle, SDL_FPoint *center, SDL_FlipMode flip); - akerr_ErrorContext AKERR_NOIGNORE *(*draw_mesh)(struct akgl_RenderBackend *self); - akerr_ErrorContext AKERR_NOIGNORE *(*draw_world)(struct akgl_RenderBackend *self, akgl_Iterator *opflags); + SDL_Renderer *sdl_renderer; /**< The SDL renderer, created by the initializer. Owned by SDL, not freed here. */ + akerr_ErrorContext AKERR_NOIGNORE *(*shutdown)(struct akgl_RenderBackend *self); /**< Tear the backend down. */ + akerr_ErrorContext AKERR_NOIGNORE *(*frame_start)(struct akgl_RenderBackend *self); /**< Begin a frame: clear the target. */ + akerr_ErrorContext AKERR_NOIGNORE *(*frame_end)(struct akgl_RenderBackend *self); /**< End a frame: present it. */ + akerr_ErrorContext AKERR_NOIGNORE *(*draw_texture)(struct akgl_RenderBackend *self, SDL_Texture *texture, SDL_FRect *src, SDL_FRect *dest, double angle, SDL_FPoint *center, SDL_FlipMode flip); /**< Blit one texture. */ + akerr_ErrorContext AKERR_NOIGNORE *(*draw_mesh)(struct akgl_RenderBackend *self); /**< Reserved for a 3D backend; the 2D one refuses. */ + akerr_ErrorContext AKERR_NOIGNORE *(*draw_world)(struct akgl_RenderBackend *self, akgl_Iterator *opflags); /**< Draw the whole scene, layer by layer. */ } akgl_RenderBackend; /** - * @brief Render 2d shutdown. - * @param self Backend or object instance to operate on. - * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKERR_* Propagates an error reported by a delegated operation. + * @brief Tear the 2D backend down. + * + * A no-op placeholder that always succeeds. The window and `SDL_Renderer` this + * backend created are still owned by SDL and are reclaimed by `SDL_Quit`, so + * there is nothing here to release yet; the hook exists so a backend that *does* + * own resources has somewhere to free them. + * + * @param self The backend to shut down. Ignored -- not even dereferenced, so + * `NULL` is currently harmless. + * @return `NULL` always. It has no failure path today; check it anyway, because + * a backend that acquires anything will grow one. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_shutdown(akgl_RenderBackend *self); /** - * @brief Render 2d frame start. - * @param self Backend or object instance to operate on. + * @brief Begin a frame: clear the render target to opaque black. + * @param self The backend to draw through. Required, and dereferenced *before* + * it is checked -- a `NULL` @p self is a crash, not an error. * @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 `self->sdl_renderer` is `NULL`, which means the + * backend was never run through akgl_render_init2d. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_start(akgl_RenderBackend *self); /** - * @brief Render 2d frame end. - * @param self Backend or object instance to operate on. + * @brief End a frame: present everything drawn since akgl_render_2d_frame_start. + * @param self The backend to draw through. Required, and dereferenced *before* + * it is checked -- a `NULL` @p self is a crash, not an error. * @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 `self->sdl_renderer` is `NULL`, which means the + * backend was never run through akgl_render_init2d. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_end(akgl_RenderBackend *self); /** - * @brief Render 2d draw texture. - * @param self Backend or object instance to operate on. - * @param texture SDL texture to draw. - * @param src Source value to copy or inspect. - * @param dest Output destination populated by the function. - * @param angle Clockwise rotation angle in degrees. - * @param center Optional rotation center. - * @param flip SDL texture flip mode. + * @brief Blit a texture, rotating it only when asked to. + * + * A rotation of exactly 0 takes the plain `SDL_RenderTexture` path, which is why + * @p center and @p flip are only consulted when @p angle is non-zero. + * + * @param self The backend to draw through. Required. + * @param texture The texture to blit. Required. + * @param src The rectangle of @p texture to take, in pixels. Optional -- + * `NULL` means the whole texture. + * @param dest Where to put it on the render target, in pixels. Optional -- + * `NULL` means stretch to fill the whole target. Not an output + * parameter despite the name. + * @param angle Clockwise rotation in degrees. Exactly 0 skips rotation + * entirely, along with @p center and @p flip. + * @param center Pivot for the rotation, relative to @p dest. Required *when* + * @p angle is non-zero, ignored otherwise. SDL's own "NULL means + * the centre of dest" convention is not available here. + * @param flip Horizontal/vertical mirroring. Applied only on the rotated + * path; ignored when @p angle is 0. * @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 self or @p texture is `NULL`; if @p angle is + * non-zero and @p center is `NULL`; or if the SDL draw call itself + * fails, in which case the message carries `SDL_GetError()`. That last + * case is a reused status rather than a pointer problem. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_texture(akgl_RenderBackend *self, SDL_Texture *texture, SDL_FRect *src, SDL_FRect *dest, double angle, SDL_FPoint *center, SDL_FlipMode flip); /** - * @brief Render 2d draw mesh. - * @param self Backend or object instance to operate on. - * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKERR_API When the corresponding validation or operation fails. + * @brief Draw geometry. Not implemented by the 2D backend. + * + * The hook exists so a 3D backend has a slot to fill; this one refuses every + * call. It is not a stub that quietly does nothing -- it fails loudly, so a + * caller that reaches it finds out at once. + * + * @param self The backend. Ignored. + * @return Never `NULL`. + * @throws AKERR_API Always, with the message "Not implemented". */ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_mesh(akgl_RenderBackend *self); /** - * @brief Render 2d draw world. - * @param self Backend or object instance to operate on. - * @param opflags Optional iterator operation flags; `NULL` selects defaults. + * @brief Draw the whole scene: every tilemap layer, and the actors standing on it. + * + * Walks layers from 0 to #AKGL_TILEMAP_MAX_LAYERS. For each, it draws that layer + * of the global `gamemap` through `camera` (if the map has that many layers), + * then sweeps the actor pool and calls `renderfunc` on every live actor whose + * `layer` matches. Sweeping in layer order is what puts actors in front of the + * scenery they stand on and behind the scenery they walk under. + * + * It reads the globals `gamemap`, `camera`, and `HEAP_ACTOR` directly rather + * than taking them as arguments, so there is exactly one world to draw. + * + * @param self The backend to draw through. Required. + * @param opflags Iterator flags. Optional -- `NULL` substitutes a zeroed set. + * Currently ignored either way: this function sweeps the actor + * pool itself instead of going through + * akgl_registry_iterate_actor, so no `AKGL_ITERATOR_OP_*` bit + * reaches anything. The parameter is here for the backend + * signature and for the layer-mask support that has yet to be + * wired up. * @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 self is `NULL`. + * @throws AKERR_* Whatever akgl_tilemap_draw or an actor's own `renderfunc` + * raises; the first failure aborts the frame and propagates unchanged. + * + * @warning Neither the global `gamemap` nor a live actor's `renderfunc` is + * checked before use, so drawing before akgl_tilemap_load, or with a + * hand-built actor that was never run through akgl_actor_initialize, + * is a crash rather than an error context. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_world(akgl_RenderBackend *self, akgl_Iterator *opflags); /** - * @brief Render init2d. - * @param self Backend or object instance to operate on. + * @brief Create the window and SDL renderer, and bind the 2D backend's methods. + * + * Reads `game.screenwidth` and `game.screenheight` from the property registry + * (both defaulting to the string "0", which asks SDL for a zero-sized window), + * creates the window and renderer with `game.uri` as the title, points `camera` + * at the full screen rectangle, and installs the six `akgl_render_2d_*` function + * pointers on @p self. + * + * Because the dimensions come from the registry, akgl_registry_init_properties + * and the property writes have to happen first -- see the note on + * akgl_registry_init about the properties registry not being initialized for + * callers that skip akgl_game_init. + * + * @param self The backend to initialize. Required. Its `sdl_renderer` and method + * pointers are overwritten; anything it held before is not released. * @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 self is `NULL`. + * @throws AKERR_VALUE If `game.screenwidth` or `game.screenheight` is set to + * something that is not a base-10 integer. + * @throws ERANGE If either dimension does not fit in an `int`. + * @throws AKGL_ERR_SDL If the window and renderer cannot be created. The message + * carries `SDL_GetError()`. + * @throws AKGL_ERR_HEAP If the string pool is exhausted while reading the two + * properties. + * + * @note The two pooled strings holding the dimensions are only released on the + * success path, so each failed initialization leaks two string slots. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_init2d(akgl_RenderBackend *self); diff --git a/include/akgl/sprite.h b/include/akgl/sprite.h index 64c4975..3e9c12b 100644 --- a/include/akgl/sprite.h +++ b/include/akgl/sprite.h @@ -1,6 +1,18 @@ /** * @file sprite.h - * @brief Declares the public sprite API. + * @brief Spritesheets (one texture, many frames) and the animations cut out of them. + * + * The split is deliberate: an akgl_SpriteSheet owns the `SDL_Texture` and the + * frame grid, and any number of akgl_Sprite animations point at the same sheet + * and name the frame indices they use. Loading two sprites from the same image + * loads the image once -- akgl_sprite_load_json looks the sheet up in + * #AKGL_REGISTRY_SPRITESHEET by resolved path before creating one. + * + * Both are pool objects (akgl_heap_next_sprite, akgl_heap_next_spritesheet) and + * both publish themselves in a registry under their name, which is how + * akgl_character_sprite_add and the tilemap loader find them. Because loading a + * sheet uploads a texture, the renderer has to exist first -- so + * akgl_render_init2d, or akgl_game_init, before any of this. */ #ifndef _AKGL_SPRITE_H_ @@ -21,67 +33,138 @@ /** @brief Stores a loaded spritesheet texture and frame geometry. */ typedef struct { - uint8_t refcount; - SDL_Texture *texture; - char name[AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH]; - uint16_t sprite_w; - uint16_t sprite_h; + uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. Destroys the texture when it reaches 0. */ + SDL_Texture *texture; /**< The whole sheet as one GPU texture. Owned by this struct. */ + char name[AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH]; /**< Registry key: the resolved path the image was loaded from. */ + uint16_t sprite_w; /**< Frame width. Vestigial: nothing in the library writes or reads it; the grid comes from akgl_Sprite::width. */ + uint16_t sprite_h; /**< Frame height. Vestigial, same as sprite_w. */ } akgl_SpriteSheet; /** @brief Describes an animated sprite and its spritesheet reference. */ typedef struct { - uint8_t refcount; - uint8_t frameids[AKGL_SPRITE_MAX_FRAMES]; // which IDs on the spritesheet belong to our frames - uint32_t frames; // how many frames are in this animation - uint32_t width; - uint32_t height; - uint32_t speed; // how many milliseconds a given sprite frame should be visible before cycling - bool loop; // when this sprite is done playing, it should immediately start again - bool loopReverse; // when this sprite is done playing, it should go in reverse order through its frames - char name[AKGL_SPRITE_MAX_NAME_LENGTH]; - akgl_SpriteSheet *sheet; + uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. One reference per character that maps it. */ + uint8_t frameids[AKGL_SPRITE_MAX_FRAMES]; /**< Frame numbers on the sheet, in playback order. Counted left to right, then wrapping to the next row. */ + uint32_t frames; /**< How many entries of frameids are in use. */ + uint32_t width; /**< Frame width in pixels; also the horizontal stride used to find a frame on the sheet. */ + uint32_t height; /**< Frame height in pixels; the vertical stride once frames wrap to the next row. */ + uint32_t speed; /**< Nanoseconds one frame is held. Read from JSON in milliseconds and scaled by #AKGL_TIME_ONESEC_MS, which despite its name is nanoseconds-per-millisecond. TODO.md item 6. */ + bool loop; /**< Restart from frame 0 when the last frame is reached, instead of holding it. */ + bool loopReverse; /**< Play back down to frame 0 instead of jumping to it -- a ping-pong loop. */ + char name[AKGL_SPRITE_MAX_NAME_LENGTH]; /**< Registry key, from the JSON `name` field. */ + akgl_SpriteSheet *sheet; /**< The sheet these frames are cut from. Borrowed, not owned. */ } akgl_Sprite; // initializes a new sprite to use the given sheet and otherwise sets to zero /** - * @brief Sprite initialize. - * @param spr Sprite object to initialize. - * @param name Registry key or human-readable object name. - * @param sheet Spritesheet used by the sprite. + * @brief Zero a pooled sprite, bind it to a sheet, and publish it in the sprite registry. + * + * Sets the name and the sheet and takes the first reference; everything else -- + * frame list, geometry, speed, loop flags -- is left at zero for the caller, or + * akgl_sprite_load_json, to fill in. The sheet is borrowed, so this does *not* + * take a reference on it: releasing the sheet out from under a live sprite + * leaves a dangling pointer. + * + * @param spr Pooled sprite to initialize, normally from akgl_heap_next_sprite. + * Required. Any previous contents are discarded. + * @param name Registry key, NUL-terminated. Required. Copied at a fixed + * #AKGL_SPRITE_MAX_NAME_LENGTH bytes, so a shorter string reads + * past its end -- pass a name from an akgl_String or another + * buffer of at least that size. An existing entry with the same + * name is silently replaced. + * @param sheet The spritesheet this sprite's frames come from. Required. * @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 spr, @p name, or @p sheet is `NULL`. + * @throws AKERR_KEY If the sprite cannot be written into #AKGL_REGISTRY_SPRITE + * -- in practice, because akgl_registry_init has not run. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_sprite_initialize(akgl_Sprite *spr, char *name, akgl_SpriteSheet *sheet); // loads a given image file into a new spritesheet /** - * @brief Spritesheet initialize. - * @param sheet Spritesheet used by the sprite. - * @param sprite_w Width of one sprite frame in pixels. - * @param sprite_h Height of one sprite frame in pixels. - * @param filename Path to the source asset or JSON document. + * @brief Load an image file as a texture and publish it as a spritesheet. + * + * Uploads @p filename through SDL_image using the global `renderer`, so the + * renderer must already be initialized. The sheet is registered in + * #AKGL_REGISTRY_SPRITESHEET under @p filename, which is the key + * akgl_sprite_load_json looks it up by to avoid loading the same image twice. + * + * @param sheet Pooled spritesheet to initialize, normally from + * akgl_heap_next_spritesheet. Required. Zeroed first, so a + * texture it already held is leaked rather than destroyed. + * @param sprite_w Frame width in pixels. Accepted and currently discarded -- + * the frame grid actually used at draw time comes from the + * akgl_Sprite's `width`/`height`, not from here. + * @param sprite_h Frame height in pixels. Same caveat as @p sprite_w. + * @param filename Path to the image, in any format SDL_image can decode. + * Required. Doubles as the registry key, so callers should pass + * an already-resolved path; two spellings of the same file are + * two sheets. Truncated at + * #AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH. * @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 AKGL_ERR_SDL When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p sheet or @p filename is `NULL`. + * @throws AKGL_ERR_SDL If the image cannot be loaded -- missing, unreadable, or + * a format SDL_image was not built with. The message carries + * `SDL_GetError()`. + * @throws AKERR_KEY If the sheet cannot be written into + * #AKGL_REGISTRY_SPRITESHEET. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_spritesheet_initialize(akgl_SpriteSheet *sheet, int sprite_w, int sprite_h, char *filename); /** - * @brief Sprite load json. - * @param filename Path to the source asset or JSON document. + * @brief Build a sprite -- and, if needed, its spritesheet -- from a JSON definition file. + * + * Reads `name`, `width`, `height`, `speed` (seconds, scaled to milliseconds), + * `loop`, `loopReverse`, a `frames` array of frame indices, and a `spritesheet` + * object holding `filename`, `frame_width`, and `frame_height`. The sheet's + * `filename` is resolved *relative to the directory of @p filename*, so a sprite + * definition can sit next to its image and move with it. + * + * If a spritesheet with that resolved path is already registered it is reused; + * otherwise one is claimed from the pool and loaded. On failure both the sprite + * and any sheet loaded for it are released again. + * + * @param filename Path to the JSON document. Required. Must be shorter than + * #AKGL_MAX_STRING_LENGTH -- it is copied into a pooled string + * so `dirname` can be taken without modifying the caller's + * buffer. * @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 filename is `NULL`, or if the file cannot be + * opened or does not parse. The message carries jansson's line number + * and text. + * @throws AKERR_OUTOFBOUNDS If @p filename is at least #AKGL_MAX_STRING_LENGTH + * bytes long, or if the `frames` array is indexed past its end. + * @throws AKERR_KEY If a required key is absent, or if the sprite or sheet + * cannot be added to its registry. The message names the key. + * @throws AKERR_TYPE If a key is present with the wrong JSON type -- `speed` as + * a string, `frames` as an object. + * @throws AKGL_ERR_SDL If the spritesheet image fails to load. + * @throws AKGL_ERR_HEAP If the sprite, spritesheet, or string pool is exhausted. + * + * @note The `frames` array is not bounded against #AKGL_SPRITE_MAX_FRAMES. A + * definition with more than 16 frames writes past `frameids` into the + * rest of the struct. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_sprite_load_json(char *filename); /** - * @brief Sprite sheet coords for frame. - * @param self Backend or object instance to operate on. - * @param srccoords Output source rectangle for the selected frame. - * @param frameid Sprite frame index to resolve. + * @brief Work out where one animation frame sits on its spritesheet. + * + * Frames are numbered in reading order across the sheet: multiply the frame + * number by the sprite width, then wrap whole rows off the right-hand edge of + * the texture and step down by the sprite height for each. Callers use the + * result as the source rectangle for `SDL_RenderTexture`. + * + * @param self The sprite whose sheet and frame geometry to use. Required, + * along with its `sheet`. + * @param srccoords Receives the frame's rectangle on the sheet, in pixels. + * Required -- the return value is the error context. + * @param frameid Index into the sprite's `frameids` array, not a frame number + * on the sheet. Not bounds-checked against `frames` or + * #AKGL_SPRITE_MAX_FRAMES; an out-of-range index reads a + * neighbouring struct member and yields a nonsense rectangle + * rather than an error. * @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 self, @p srccoords, or `self->sheet` is + * `NULL`. Note that `sheet->texture` is dereferenced without a check, + * so a registered-but-unloaded sheet is a crash rather than an error. */ akerr_ErrorContext *akgl_sprite_sheet_coords_for_frame(akgl_Sprite *self, SDL_FRect *srccoords, uint8_t frameid); diff --git a/include/akgl/staticstring.h b/include/akgl/staticstring.h index d8033c3..fa508c9 100644 --- a/include/akgl/staticstring.h +++ b/include/akgl/staticstring.h @@ -1,6 +1,11 @@ /** * @file staticstring.h - * @brief Declares the public staticstring API. + * @brief A fixed-capacity string object handed out by the akgl string heap layer. + * + * The library allocates nothing at runtime, so a "string" here is a + * PATH_MAX-sized buffer claimed from the pool with akgl_heap_next_string and + * given back with akgl_heap_release_string. Capacity is fixed at compile time: + * these functions truncate rather than grow, and truncation is silent. */ #ifndef _STRING_H_ @@ -15,25 +20,52 @@ /** @brief Provides a fixed-capacity, heap-managed string buffer. */ typedef struct { - int refcount; - char data[AKGL_MAX_STRING_LENGTH]; + 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; /** - * @brief String initialize. - * @param obj Object to initialize, inspect, or modify. - * @param init Optional initial string contents; `NULL` clears the buffer. + * @brief Set a pooled string's contents and mark the slot in use. + * + * Copies at most #AKGL_MAX_STRING_LENGTH bytes out of @p init, or zeroes the + * buffer when @p init is `NULL`, then sets `refcount` to 1. Callers normally + * reach this through akgl_heap_next_string rather than calling it directly. + * + * @param obj The pooled string to (re)initialize. Required. Its previous + * contents are discarded without inspection. + * @param init Initial contents, NUL-terminated. Optional -- `NULL` zero-fills + * the buffer instead. An @p init longer than + * #AKGL_MAX_STRING_LENGTH is truncated *and left unterminated*, + * because this is `strncpy` semantics, not `strlcpy`. * @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 obj is `NULL`. + * + * @note Known defect: the `NULL` @p init path zeroes `sizeof(akgl_String)` + * bytes starting at `data`, which is four bytes past the end of the + * buffer -- `refcount` sits in front of it. TODO.md, "Known and still + * open" item 6. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_string_initialize(akgl_String *obj, char *init); /** - * @brief String copy. - * @param src Source value to copy or inspect. - * @param dst Output destination populated by the function. - * @param count Maximum number of elements or bytes to process; zero selects the default. + * @brief Copy the contents of one pooled string into another. + * + * A bounded `strncpy` between two already-claimed pool slots. It copies bytes + * only: `refcount` is left alone, so @p dst keeps whatever pool state it had. + * + * @param src Source string. Required. Read up to @p count bytes. + * @param dst Destination string. Required. Overwritten in place; the pool + * slot must already have been claimed. + * @param count Maximum bytes to copy. 0 selects #AKGL_MAX_STRING_LENGTH, the + * whole buffer. A @p count shorter than the source truncates + * without writing a terminator; a @p count longer than the source + * zero-pads the remainder, per `strncpy`. Values above + * #AKGL_MAX_STRING_LENGTH overrun both buffers and are not + * rejected. * @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 src or @p dst is `NULL`. + * @throws errno Whatever `errno` holds if `strncpy` returns something other + * than @p dst. In practice `strncpy` always returns its destination, so + * this path is unreachable rather than merely rare. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_string_copy(akgl_String *src, akgl_String *dst, int count); #endif //_STRING_H_ diff --git a/include/akgl/text.h b/include/akgl/text.h index f5bbd4a..ea4d574 100644 --- a/include/akgl/text.h +++ b/include/akgl/text.h @@ -1,6 +1,16 @@ /** * @file text.h - * @brief Declares the public text API. + * @brief Loading fonts and drawing or measuring strings with them. + * + * Fonts are `TTF_Font *` handles kept in the #AKGL_REGISTRY_FONT property + * registry under a caller-chosen name; there is no akgl font type wrapping them. + * SDL_ttf must be initialized (akgl_game_init does it) before any of this. + * + * The two measure functions do not touch the renderer, so they are usable + * before -- or entirely without -- a window. Drawing is immediate mode: each + * akgl_text_rendertextat() call rasterizes, uploads, blits, and throws the + * texture away, which is fine for a HUD line and wrong for a large body of + * static text redrawn every frame. */ #ifndef _TEXT_H_ @@ -11,26 +21,58 @@ #include /** - * @brief Text loadfont. - * @param name Registry key or human-readable object name. - * @param filepath Path to the requested file. - * @param size Requested font size. + * @brief Open a TrueType font at one size and publish it in the font registry. + * + * A size is baked into the handle, so the same file at two sizes is two calls + * under two names. Nothing releases these: the handles live until the process + * ends. + * + * @param name Registry key to publish the font under. Required. An existing + * entry with the same name is replaced, and the font it + * displaced is leaked rather than closed. + * @param filepath Path to a `.ttf`/`.otf` file. Required. Used verbatim -- not + * resolved against `SDL_GetBasePath()`. + * @param size Point size to rasterize at. Passed straight to SDL_ttf, which + * rejects anything that is not positive. * @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 AKGL_ERR_SDL When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p name or @p filepath is `NULL`. + * @throws AKGL_ERR_SDL If the font cannot be opened -- missing, unreadable, not + * a font, or a @p size SDL_ttf refuses. The message carries + * `SDL_GetError()`. + * @throws AKERR_KEY If the font cannot be written into #AKGL_REGISTRY_FONT -- + * in practice, because akgl_registry_init has not run. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath, int size); /** - * @brief Text rendertextat. - * @param font Font used to render the text. - * @param text UTF-8 text to render. - * @param color Text color. - * @param wraplength Maximum rendered line width; zero disables wrapping. - * @param x Horizontal destination coordinate. - * @param y Vertical destination coordinate. + * @brief Rasterize a string and blit it at a screen position, in one call. + * + * Renders blended (anti-aliased, alpha-blended) through SDL_ttf, uploads the + * result to a texture, draws it through the global `renderer`, and destroys both + * the texture and the surface before returning. The text is drawn at its natural + * size -- @p x and @p y are the top-left corner, not a centre. + * + * Coordinates are screen coordinates, not world ones: this does not go through + * the camera, so a HUD stays put while the world scrolls under it. + * + * @param font Font to render with, from akgl_text_loadfont. Required. + * @param text UTF-8 text. Required. May contain newlines, which break + * lines on either path. + * @param color Text colour, including alpha. + * @param wraplength Wrap width in pixels. Greater than 0 wraps on word + * boundaries at that width; 0 or less draws a single line and + * breaks only on newlines in @p text. + * @param x Left edge of the text, in screen pixels. + * @param y Top edge of the text, in screen pixels. * @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 font or @p text is `NULL`; if SDL_ttf cannot + * rasterize the string; or if the surface cannot be uploaded as a + * texture. The last two carry `SDL_GetError()` and are a reused status + * rather than a pointer problem. + * @throws AKERR_* Whatever the backend's `draw_texture` raises. + * + * @note On a failure after rasterizing -- the texture upload, or the draw -- the + * surface and texture are not destroyed, because the error returns before + * the cleanup. Repeated failures leak. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color color, int wraplength, int x, int y); /** @@ -40,13 +82,16 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char * * grid measures one cell with this -- the advance width of a single glyph in a * monospaced font -- and derives the rest of the grid from it. * - * @param font Font used to render the text. - * @param text UTF-8 text to measure. - * @param w Output destination populated with the rendered width in pixels. - * @param h Output destination populated with the rendered height in pixels. + * @param font Font to measure with, from akgl_text_loadfont. Required. + * @param text UTF-8 text to measure. Required. The empty string is legal and + * measures 0 wide by one line high. + * @param w Receives the width in pixels. Required. + * @param h Receives the height in pixels -- one line, whatever @p text + * contains, since this form does not wrap. Required. * @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 font, @p text, @p w, or @p h is `NULL`. + * @throws AKGL_ERR_SDL If SDL_ttf cannot measure the string -- a corrupt font, + * or text that is not valid UTF-8. The message carries `SDL_GetError()`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure(TTF_Font *font, char *text, int *w, int *h); /** @@ -57,15 +102,22 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure(TTF_Font *font, char *text, * longer than @p wraplength reports the height of every line it breaks onto. * A @p wraplength of zero wraps only on newlines in @p text. * - * @param font Font used to render the text. - * @param text UTF-8 text to measure. - * @param wraplength Maximum rendered line width; zero wraps on newlines only. - * @param w Output destination populated with the rendered width in pixels. - * @param h Output destination populated with the rendered height in pixels. + * @param font Font to measure with, from akgl_text_loadfont. Required. + * @param text UTF-8 text to measure. Required. + * @param wraplength Wrap width in pixels. 0 wraps on newlines only. Negative is + * refused rather than passed through: SDL_ttf reads a negative + * width as a very large unsigned one and silently stops + * wrapping, which would return a measurement that is wrong + * rather than an error. + * @param w Receives the width in pixels: the longest line, not + * @p wraplength. Required. + * @param h Receives the height in pixels, covering every line the text + * wraps onto. 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_OUTOFBOUNDS When the corresponding validation or operation fails. - * @throws AKGL_ERR_SDL When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p font, @p text, @p w, or @p h is `NULL`. + * @throws AKERR_OUTOFBOUNDS If @p wraplength is negative. + * @throws AKGL_ERR_SDL If SDL_ttf cannot measure the string -- a corrupt font, + * or text that is not valid UTF-8. The message carries `SDL_GetError()`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure_wrapped(TTF_Font *font, char *text, int wraplength, int *w, int *h); diff --git a/include/akgl/tilemap.h b/include/akgl/tilemap.h index 1bed423..0317542 100644 --- a/include/akgl/tilemap.h +++ b/include/akgl/tilemap.h @@ -1,6 +1,34 @@ /** * @file tilemap.h - * @brief Declares the public tilemap API. + * @brief Loading and drawing Tiled maps: tilesets, layers, embedded actors, per-map physics. + * + * The map format is the JSON export from the Tiled editor, so the level design + * tool is somebody else's and the loader is the only thing that has to know + * about it. A map is a stack of layers -- tile grids, images, and object groups + * -- drawn over a set of tilesets, each tileset being one image cut into a grid. + * + * Three things beyond ordinary Tiled semantics are worth knowing: + * + * - **Object layers can spawn actors.** An object of type `actor` names a + * character in its custom properties, and loading the map creates and + * registers the actor rather than just recording a rectangle. + * - **A map can carry its own physics.** A `physics.model` custom property on + * the map selects a backend, and `physics.gravity.*` / `physics.drag.*` set + * its constants, so a swimming level and a walking level differ by data + * rather than by code. + * - **A map can carry a pseudo-3D perspective.** Two objects named + * `p_foreground` and `p_vanishing` in an object layer, each with a `scale` + * property, define a band down the screen over which actors are scaled -- + * which is what makes a character look further away as they walk up the + * screen. akgl_tilemap_scale_actor applies it. + * + * All paths inside a map -- tileset images, layer images -- are resolved + * relative to the map file, so a map and its art move together. + * + * Note the size of akgl_Tilemap: a layer's tile grid alone is 512x512 ints, and + * a tileset's offset table is 65536 pairs. It is a megabytes-large object and + * belongs in static storage, which is where `_akgl_gamemap` puts it. Do not put + * one on the stack. */ #ifndef _TILEMAP_H_ @@ -12,61 +40,74 @@ #include #include +/** @brief Widest map, in tiles. Width times height is what is actually bounded. */ #define AKGL_TILEMAP_MAX_WIDTH 512 +/** @brief Tallest map, in tiles. */ #define AKGL_TILEMAP_MAX_HEIGHT 512 +/** @brief Layers per map. Also the number of draw passes akgl_render_2d_draw_world makes. */ #define AKGL_TILEMAP_MAX_LAYERS 16 +/** @brief Tilesets per map. */ #define AKGL_TILEMAP_MAX_TILESETS 16 +/** @brief Entries in a tileset's offset table. Indexed by *local* tile id, so a tileset with a high `firstgid` still starts at 0. */ #define AKGL_TILEMAP_MAX_TILES_PER_IMAGE 65536 +/** @brief Longest tileset name. */ #define AKGL_TILEMAP_MAX_TILESET_NAME_SIZE 512 +/** @brief Longest resolved tileset image path. */ #define AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE PATH_MAX +/** @brief Longest object name. Note that an object naming an actor is truncated at #AKGL_ACTOR_MAX_NAME_LENGTH (128) instead. */ #define AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE 512 +/** @brief Objects in one object layer. Not enforced by the loader -- a longer group writes past the array. */ #define AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER 128 +/** @brief Object `type` for a Tiled object whose `type` field is the string "actor". */ #define AKGL_TILEMAP_OBJECT_TYPE_ACTOR 1 +/** @brief Layer `type` for a Tiled `tilelayer`: a grid of tile ids. */ #define AKGL_TILEMAP_LAYER_TYPE_TILES 1 +/** @brief Layer `type` for a Tiled `objectgroup`: actors and perspective markers. */ #define AKGL_TILEMAP_LAYER_TYPE_OBJECTS 2 +/** @brief Layer `type` for a Tiled `imagelayer`: one image drawn at the origin. */ #define AKGL_TILEMAP_LAYER_TYPE_IMAGE 3 -/** @brief Stores tileset metadata, texture, and frame offsets. */ +/** @brief One object placed in a Tiled object layer. */ typedef struct { - float x; - float y; - int gid; - int id; - int height; - int width; - int rotation; - int type; - bool visible; - akgl_Actor *actorptr; - char name[AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE]; + float x; /**< Position in map pixels, from the Tiled object. Copied onto the actor it spawns. */ + float y; /**< Position in map pixels. */ + int gid; /**< Global tile id, for a tile object. Not read by the loader. */ + int id; /**< Tiled's object id. Not read by the loader. */ + int height; /**< Height in map pixels. Read into the map's perspective bands, not into this field. */ + int width; /**< Width in map pixels. Not read by the loader. */ + int rotation; /**< Rotation in degrees. Not read by the loader. */ + int type; /**< #AKGL_TILEMAP_OBJECT_TYPE_ACTOR, or 0 for anything else. */ + bool visible; /**< Copied onto the actor. Forced to `false` for perspective markers, which are geometry rather than scenery. */ + akgl_Actor *actorptr; /**< The actor this object spawned or attached to, for an actor object. `NULL` otherwise. Borrowed. */ + char name[AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE]; /**< Object name. For an actor object this is the registry key; for a perspective marker it is `p_foreground` or `p_vanishing`. */ } akgl_TilemapObject; -/** @brief Represents an object embedded in a tilemap layer. */ +/** @brief One layer of a tilemap: a tile grid, an image, or a group of objects. */ typedef struct { - short type; - float opacity; - bool visible; - int height; - int width; - int x; - int y; - int id; - SDL_Texture *texture; - int data[AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT]; - akgl_TilemapObject objects[AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER]; + short type; /**< Which of the `AKGL_TILEMAP_LAYER_TYPE_*` kinds this is; decides which of the members below mean anything. */ + float opacity; /**< 0.0 to 1.0, from Tiled. Recorded but not yet applied at draw time. */ + bool visible; /**< From Tiled. Recorded but not yet consulted at draw time. */ + int height; /**< Tile layer: height in tiles. Image layer: the texture's height in pixels. */ + int width; /**< Tile layer: width in tiles. Image layer: the texture's width in pixels. */ + int x; /**< Layer offset from Tiled. Recorded but not applied at draw time. */ + int y; /**< Layer offset from Tiled. */ + int id; /**< Tiled's layer id. Not the same as the index into akgl_Tilemap::layers. */ + SDL_Texture *texture; /**< Image layers only: the whole layer as one texture. `NULL` otherwise. Owned by this struct. */ + int data[AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT]; /**< Tile layers only: global tile ids in row-major order. 0 means an empty cell. */ + akgl_TilemapObject objects[AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER]; /**< Object layers only. */ } akgl_TilemapLayer; -/** @brief Stores tile, image, or object data for one map layer. */ +/** @brief One tileset: an image cut into a grid, plus the lookup table that finds a tile in it. */ typedef struct { - int columns; - int firstgid; - char imagefilename[AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE]; - int imageheight; - int imagewidth; - char name[AKGL_TILEMAP_MAX_TILESET_NAME_SIZE]; - SDL_Texture *texture; + int columns; /**< Tiles per row in the image. What turns a linear tile id into a row and column. */ + int firstgid; /**< Global id of this tileset's first tile. Subtracting it from a map cell gives the local tile id. */ + char imagefilename[AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE]; /**< Resolved absolute path to the image, from the map file's directory. */ + int imageheight; /**< Image height in pixels, as declared by Tiled. */ + int imagewidth; /**< Image width in pixels, as declared. */ + char name[AKGL_TILEMAP_MAX_TILESET_NAME_SIZE]; /**< Tileset name from Tiled. Diagnostic only; lookups go by `firstgid`. */ + SDL_Texture *texture; /**< The image as one texture. Owned by this struct. */ // Use this as a lookup table instead of storing tiles // in individual textures to blit them from a single // texture at runtime @@ -81,64 +122,123 @@ typedef struct { // lead to premature exhaustion of AKGL_TILEMAP_MAX_TILES_PER_IMAGE // because set D or E may only have 64 tiles but they may be // at the upper end of the array bound already because of this. - int tile_offsets[AKGL_TILEMAP_MAX_TILES_PER_IMAGE][2]; - int tilecount; - int tileheight; - int tilewidth; - int spacing; - int margin; + int tile_offsets[AKGL_TILEMAP_MAX_TILES_PER_IMAGE][2]; /**< Local tile id -> {x, y} pixel offset into the image. Computed once at load by akgl_tilemap_compute_tileset_offsets. */ + int tilecount; /**< How many tiles the image holds. */ + int tileheight; /**< Height of one tile in pixels. */ + int tilewidth; /**< Width of one tile in pixels. */ + int spacing; /**< Pixels between adjacent tiles in the image. */ + int margin; /**< Pixels of border around the whole grid. Recorded but **not** accounted for by the offset computation. */ } akgl_Tileset; /** @brief Represents a complete tilemap and its optional physics backend. */ typedef struct { - int tilewidth; - int tileheight; - int width; - int height; - int numlayers; - int orientation; // 0 = orthogonal, 1 = isometric - int numtilesets; - int p_foreground_y; - int p_vanishing_y; - int p_foreground_h; - int p_vanishing_h; - float p_foreground_scale; - float p_vanishing_scale; - float p_scale; - float p_rate; - akgl_Tileset tilesets[AKGL_TILEMAP_MAX_TILESETS]; - akgl_TilemapLayer layers[AKGL_TILEMAP_MAX_LAYERS]; + int tilewidth; /**< Width of one map cell in pixels. Tiles from a tileset with a different tile size are not rescaled. */ + int tileheight; /**< Height of one map cell in pixels. */ + int width; /**< Map width in tiles. */ + int height; /**< Map height in tiles. */ + int numlayers; /**< Layers actually loaded, at most #AKGL_TILEMAP_MAX_LAYERS. */ + int orientation; /**< 0 = orthogonal, 1 = isometric. Always set to 0 by the loader; isometric is not implemented. */ + int numtilesets; /**< Tilesets actually loaded. */ + int p_foreground_y; /**< Y of the `p_foreground` marker: the row at which actors are at full size. 0 disables perspective. */ + int p_vanishing_y; /**< Y of the `p_vanishing` marker: the row at which actors are smallest. 0 disables perspective. */ + int p_foreground_h; /**< Height of the `p_foreground` marker object. Recorded; not used in the current rate calculation. */ + int p_vanishing_h; /**< Height of the `p_vanishing` marker object. Recorded, likewise. */ + float p_foreground_scale; /**< Actor scale at `p_foreground_y`. Defaults to 1.0. */ + float p_vanishing_scale; /**< Actor scale at `p_vanishing_y`. Defaults to 1.0. */ + float p_scale; /**< Unused. Left from an earlier formulation of the perspective maths. */ + float p_rate; /**< Scale change per pixel of y between the two markers. Derived at load; what akgl_tilemap_scale_actor interpolates with. */ + akgl_Tileset tilesets[AKGL_TILEMAP_MAX_TILESETS]; /**< The tilesets, in the order Tiled listed them. */ + akgl_TilemapLayer layers[AKGL_TILEMAP_MAX_LAYERS]; /**< The layers, in draw order: index 0 is furthest back. */ // Different levels may have different physics. - bool use_own_physics; - akgl_PhysicsBackend physics; + bool use_own_physics; /**< Set when the map declared a `physics.model` property. A caller that honours it simulates through `physics` instead of the global backend. */ + akgl_PhysicsBackend physics; /**< This map's own backend, valid only when `use_own_physics` is set. */ } akgl_Tilemap; /** - * @brief Tilemap load. - * @param fname Path to the input or output file. - * @param dest Output destination populated by the function. + * @brief Load a Tiled JSON map: physics, geometry, layers, tilesets, and any actors it spawns. + * + * Zeroes @p dest, then reads the map's own physics properties, its tile + * dimensions, its size in tiles, its layers, and its tilesets -- in that order, + * because layers reference tilesets by global id and objects reference + * characters by name. Finally, if the map carried both perspective markers, it + * works out the per-pixel scaling rate between them. + * + * Loading a map has side effects beyond @p dest: tileset and layer images are + * uploaded as textures, and every `actor` object in an object layer is created + * in the actor pool and published in #AKGL_REGISTRY_ACTOR. So the renderer, the + * pools, the registries, and the characters the map names all have to be in + * place first. + * + * @param fname Path to the map JSON. Required. Its directory becomes the root + * that every tileset and layer image inside is resolved against. + * @param dest The tilemap to fill in. Required. Zeroed first, so a map already + * loaded into it has its textures leaked rather than destroyed -- + * call akgl_tilemap_release first if you are reusing one. Do not + * put one of these on the stack; see the note on this file. * @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 fname or @p dest is `NULL`; if the map file + * cannot be opened or does not parse, with jansson's line and text; or + * if a tileset image cannot be loaded. + * @throws AKERR_OUTOFBOUNDS If the map is larger than + * #AKGL_TILEMAP_MAX_WIDTH x #AKGL_TILEMAP_MAX_HEIGHT tiles, if it has + * more than #AKGL_TILEMAP_MAX_LAYERS layers, or if a layer's own + * declared size exceeds the same bound. + * @throws AKERR_KEY If a required key is absent, if an `actor` object has an + * empty name, or if the map names a physics model that does not exist. + * @throws AKERR_TYPE If a key is present with the wrong JSON type. + * @throws ENOENT If @p fname or a path referenced inside it does not exist. + * @throws AKGL_ERR_SDL If a layer image cannot be loaded. + * @throws AKGL_ERR_HEAP If the string or actor pool is exhausted. + * + * @note Nothing is released on a failure part-way through: textures already + * uploaded and actors already created stay. Treat a failed load as + * needing akgl_tilemap_release and a fresh actor registry. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load(char *fname, akgl_Tilemap *dest); /** - * @brief Tilemap draw. - * @param dest Output destination populated by the function. - * @param viewport World-space rectangle visible to the renderer. - * @param layeridx Zero-based tilemap layer index. + * @brief Draw one layer of a map, clipped to a viewport. + * + * Only the tiles that intersect @p viewport are drawn, and the tiles at its + * edges are drawn partially, so scrolling is smooth rather than snapping to the + * tile grid. An image layer ignores the viewport entirely and is drawn once at + * the origin. + * + * Drawing goes through the global `renderer`, not through a backend passed in. + * + * @param dest The map to draw from. Required. Not an output parameter + * despite the name. + * @param viewport The rectangle of the map, in map pixels, that is on screen. + * Required. Usually the global `camera`. + * @param layeridx Which layer to draw, 0 to `numlayers - 1`. **Not** + * bounds-checked: an index past the end reads a neighbouring + * layer, or past the array. * @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 dest or @p viewport is `NULL`. + * @throws AKERR_* Whatever the renderer's `draw_texture` raises. The first + * failure abandons the rest of the layer. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw(akgl_Tilemap *dest, SDL_FRect *viewport, int layeridx); /** - * @brief Tilemap draw tileset. - * @param dest Output destination populated by the function. - * @param tilesetidx Zero-based tileset index. + * @brief Draw a whole tileset to the screen, tile by tile, from its offset table. + * + * A debugging tool, not a rendering path. It reconstructs the original tileset + * image out of the offsets computed at load time, so if the picture that appears + * matches the source image the offset table is right -- and if tiles are shifted + * or duplicated, it is not. The `charviewer` utility uses it. + * + * @param dest The map holding the tileset. Required. Not an output + * parameter despite the name. + * @param tilesetidx Which tileset to draw, 0 to `numtilesets - 1`. * @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 dest is `NULL`. + * @throws AKERR_OUTOFBOUNDS If @p tilesetidx is at or above `numtilesets`. A + * negative index is not rejected. + * @throws AKERR_* Whatever the renderer's `draw_texture` raises. + * + * @note Tiles are laid out using the *map's* tile size rather than the + * tileset's, so a tileset whose tiles are a different size from the map's + * is reconstructed at the wrong pitch. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw_tileset(akgl_Tilemap *dest, int tilesetidx); @@ -148,106 +248,245 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw_tileset(akgl_Tilemap *dest, */ /** - * @brief Get json tilemap property. - * @param obj Object to initialize, inspect, or modify. - * @param key JSON object key or property name to locate. - * @param type Expected JSON property type or backend type name. - * @param dest Output destination populated by the function. + * @brief Find one entry in a Tiled `properties` array by name, checking its declared type. + * + * Tiled does not store custom properties as JSON object members. It stores them + * as an array of `{"name": ..., "type": ..., "value": ...}` objects, so getting + * one means a linear scan and a type-string comparison rather than a lookup. + * That is what this does; the `akgl_get_json_properties_*` wrappers put a typed + * face on it. + * + * @param obj The Tiled object -- map, layer, or object -- whose `properties` + * array to search. Required. + * @param key The property name to find. Required. + * @param type The type string Tiled wrote, e.g. `"string"`, `"int"`, `"float"`. + * A property found under the right name but the wrong type is an + * error, not a miss. Not `NULL`-checked. + * @param dest Receives a borrowed pointer to the whole property object -- the + * one with `name`, `type`, and `value` -- not to its value. Not + * `NULL`-checked. * @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_TYPE When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`. + * @throws AKERR_KEY If @p obj has no `properties` array, or if no entry in it + * has that name. + * @throws AKERR_TYPE If `properties` is not an array, an entry is malformed, or + * the named property's declared type is not @p type. That last message + * reports both the expected and the actual type. + * @throws AKGL_ERR_HEAP If the string pool is exhausted. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_tilemap_property(json_t *obj, char *key, char *type, json_t **dest); /** - * @brief Get json properties string. - * @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 `string`. + * @param obj The Tiled object whose `properties` array to search. Required. + * @param key The property name. Required. + * @param dest Receives the value in a *newly claimed* pool string. Required. Note + * that this always claims -- unlike akgl_get_json_string_value it + * does not reuse a string already in `*dest`, so passing one leaks + * it. The caller releases the result with akgl_heap_release_string. * @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 `string`. + * @throws AKGL_ERR_HEAP If the string pool is exhausted. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_string(json_t *obj, char *key, akgl_String **dest); /** - * @brief Get json properties integer. - * @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 `int`. + * @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 `int`, or its `value` is + * not an integer. + * @throws AKGL_ERR_HEAP If the string pool is exhausted. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_integer(json_t *obj, char *key, int *dest); /** - * @brief Tilemap compute tileset offsets. - * @param dest Output destination populated by the function. - * @param tilesetidx Zero-based tileset index. - * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKERR_* Propagates an error reported by a delegated operation. + * @brief Build a tileset's local-id-to-pixel-offset table. + * + * Run once per tileset at load time so drawing a tile is an array lookup rather + * than a division. Walks the grid left to right and top to bottom, stepping by + * tile size plus `spacing`. + * + * @param dest The map holding the tileset. Required in practice, though + * not checked -- it is dereferenced immediately. + * @param tilesetidx Which tileset, 0 to `numtilesets - 1`. Not bounds-checked. + * @return `NULL`. There is no failure path: it is arithmetic into an already + * allocated table. + * + * @note Two known limits. `margin` is not accounted for, so a tileset image with + * a border produces offsets shifted by it. And the table is indexed by + * local id from 0, while akgl_tilemap_draw indexes it by + * `tilenum - firstgid`, so the entries a second tileset needs sit at the + * front of its own table -- see the FIXME on akgl_Tileset::tile_offsets + * for why that wastes space. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int tilesetidx); /** - * @brief Tilemap load layer objects. - * @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 object layer: spawn its actors and record its perspective markers. + * + * Two kinds of object are understood. `actor` objects name a registered actor, + * or create one -- claiming it from the pool, initializing it, and binding the + * character named in its `character` property -- and place it at the object's + * coordinates on this layer. `perspective` objects named `p_foreground` or + * `p_vanishing` set the map's scaling band and are themselves invisible. + * Anything else is recorded and otherwise ignored. + * + * An actor named by two objects is not duplicated: the second takes another + * reference on the first. + * + * @param dest The map to load into. Required. + * @param root The layer's JSON object -- not the map root. Required. + * @param layerid Index of the layer being loaded, which becomes each spawned + * actor's `layer`. Not bounds-checked. + * @param dirname Directory to resolve paths against. Accepted for signature + * consistency with the other layer loaders; nothing here uses 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 @p dest or @p root is `NULL`. + * @throws AKERR_KEY If the layer has no `objects` array, if a required key is + * absent, or if an `actor` object has an empty name. + * @throws AKERR_TYPE If a key is present with the wrong JSON type. + * @throws AKERR_OUTOFBOUNDS If the array is indexed past its end. + * @throws AKGL_ERR_HEAP If the actor or string pool is exhausted. + * @throws AKERR_* Whatever akgl_actor_initialize or akgl_actor_set_character + * raises -- notably AKERR_KEY if the named character is not registered. + * + * @warning The object count is not checked against + * #AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER, so a layer with more than 128 + * objects writes past the array. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname); /** - * @brief Tilemap load layer tile. - * @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 tile layer's grid of global tile ids. + * + * @param dest The map to load into. Required. + * @param root The layer's JSON object -- not the map root. Required. + * @param layerid Index of the layer being loaded. Not bounds-checked. + * @param dirname Directory to resolve paths against. Required, though a tile + * layer references no files and nothing here uses 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_OUTOFBOUNDS When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p dest, @p root, or @p dirname is `NULL`. + * @throws AKERR_OUTOFBOUNDS If the layer's declared `width` times `height` + * reaches #AKGL_TILEMAP_MAX_WIDTH x #AKGL_TILEMAP_MAX_HEIGHT, or if the + * `data` array is shorter than that product. + * @throws AKERR_KEY If `height`, `width`, or `data` is absent. + * @throws AKERR_TYPE If one of them has the wrong JSON type, or a cell is not an + * integer. + * + * @note The declared `width` and `height` are trusted over the actual length of + * `data`: a layer declaring more cells than it lists fails on the read + * past the end rather than on a length check. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_tile(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname); /** - * @brief Tilemap load layers. - * @param dest Output destination populated by the function. - * @param root Base directory used to resolve the path. - * @param dirname Directory containing paths referenced by the document. + * @brief Load every layer of a map, dispatching on each layer's declared type. + * + * Reads the common fields -- id, opacity, visibility, offset -- then hands the + * layer to the loader for its kind: `objectgroup`, `tilelayer`, or `imagelayer`. + * A layer of any other type keeps its common fields and is otherwise skipped, + * with `type` left at 0. + * + * @param dest The map to load into. Required. `numlayers` is set from the + * array's length before any bound is checked, so it may briefly + * exceed #AKGL_TILEMAP_MAX_LAYERS on the failure path. + * @param root The map's root JSON object. Required. + * @param dirname Directory to resolve layer image paths against. 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_OUTOFBOUNDS When the corresponding validation or operation fails. + * @throws AKERR_NULLPOINTER If @p dest, @p root, or @p dirname is `NULL`. + * @throws AKERR_OUTOFBOUNDS If the map has more than #AKGL_TILEMAP_MAX_LAYERS + * layers. + * @throws AKERR_KEY If the map has no `layers` array, or a layer is missing a + * common field. + * @throws AKERR_TYPE If a field has the wrong JSON type. + * @throws AKERR_* Whatever the per-kind loader raises. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, akgl_String *dirname); /** - * @brief Tilemap load tilesets each. - * @param tileset JSON tileset object to load. - * @param dest Output destination populated by the function. - * @param tsidx Zero-based destination tileset index. - * @param dirname Directory containing paths referenced by the document. + * @brief Load one tileset's metadata and upload its image. + * + * @param tileset The tileset's JSON object. Required in practice, though not + * checked -- it is passed straight to the accessors, which + * report it. + * @param dest The map to load into. Required, unchecked, dereferenced at once. + * @param tsidx Which slot to fill, 0 to #AKGL_TILEMAP_MAX_TILESETS - 1. Not + * bounds-checked. + * @param dirname Directory to resolve the tileset's `image` path against -- + * the map file's own directory. 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 a required key is missing its object, or if the + * tileset image fails to load. The image message carries + * `SDL_GetError()`. + * @throws AKERR_KEY If one of `columns`, `firstgid`, `imageheight`, + * `imagewidth`, `margin`, `spacing`, `tilecount`, `tileheight`, + * `tilewidth`, `name`, or `image` is absent. + * @throws AKERR_TYPE If one of them has the wrong JSON type. + * @throws AKERR_OUTOFBOUNDS If the resolved image path is too long for a pooled + * string. + * @throws ENOENT If the image path does not exist. + * @throws AKGL_ERR_HEAP If the string pool is exhausted. + * + * @note This does not go through the spritesheet registry, so a tileset image + * shared between two maps is loaded twice. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilemap *dest, int tsidx, akgl_String *dirname); /** - * @brief Tilemap load tilesets. - * @param dest Output destination populated by the function. - * @param root Base directory used to resolve the path. - * @param dirname Directory containing paths referenced by the document. + * @brief Load every tileset of a map and compute each one's offset table. + * + * @param dest The map to load into. Required. `numtilesets` is reset to 0 and + * incremented as each tileset succeeds, so it always reflects + * what actually loaded. + * @param root The map's root JSON object. Required. + * @param dirname Directory to resolve tileset image paths against. Required in + * practice; not checked here. * @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 dest or @p root is `NULL`. + * @throws AKERR_KEY If the map has no `tilesets` array. + * @throws AKERR_TYPE If `tilesets` is not an array of objects. + * @throws AKERR_* Whatever akgl_tilemap_load_tilesets_each raises. + * + * @warning The tileset count is not checked against + * #AKGL_TILEMAP_MAX_TILESETS, so a map with more than 16 tilesets + * writes past the array. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root, akgl_String *dirname); /** - * @brief Tilemap release. - * @param dest Output destination populated by the function. + * @brief Destroy the textures a map owns. + * + * Call before reusing a tilemap struct for a different map; akgl_tilemap_load + * zeroes rather than releases, so without this the previous map's textures are + * leaked. + * + * @param dest The map to release. 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 dest is `NULL`. + * + * @warning Known defect: the layer loop destroys `tilesets[i].texture` rather + * than `layers[i].texture`, so tileset textures are destroyed twice and + * image-layer textures never. Nothing is set to `NULL` either, so a + * second call is a use-after-free. It also does not release the actors + * the map's object layers created. TODO.md, "Known and still open" + * item 2. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_release(akgl_Tilemap *dest); /** - * @brief Tilemap scale actor. - * @param map Tilemap to inspect, draw, or modify. - * @param actor Actor to inspect or modify. + * @brief Scale an actor for its distance up the screen, using the map's perspective band. + * + * The pseudo-3D trick: an actor at or below `p_foreground_y` is drawn at + * `p_foreground_scale`, one at or above `p_vanishing_y` at `p_vanishing_scale`, + * and one in between is interpolated linearly. A map with no perspective markers + * has both scales at 1.0 and a rate of 0, so this is a no-op rather than a + * special case. + * + * Only the actor's `scale` is written; nothing is drawn. akgl_game_update calls + * it before each actor's update when #AKGL_ITERATOR_OP_TILEMAPSCALE is set. + * + * @param map The map supplying the perspective band. Required. + * @param actor The actor to scale. Required. Its `y` is read and its `scale` + * written. * @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 map or @p actor is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_scale_actor(akgl_Tilemap *map, akgl_Actor *actor); diff --git a/include/akgl/types.h b/include/akgl/types.h index 62cad49..0e3956f 100644 --- a/include/akgl/types.h +++ b/include/akgl/types.h @@ -1,6 +1,13 @@ /** * @file types.h - * @brief Declares the public types API. + * @brief Width-named floating point aliases, to match the `` spelling. + * + * C names its integers by width and its floats by rank, so a struct full of + * `uint32_t` and `float` reads inconsistently. These are the float spellings the + * rest of the library uses. They are plain aliases -- no guarantee beyond what + * the platform's `float` and `double` already provide -- and are deliberately + * *not* C23's `_Float32`/`_Float64`, which carry IEEE-754 requirements this + * library does not need. */ #ifndef _AKGL_TYPES_H_ @@ -8,7 +15,9 @@ #include +/** @brief Single-precision float. Positions, velocities, and scale factors. */ typedef float float32_t; +/** @brief Double-precision float. Declared for symmetry; unused so far. */ typedef double float64_t; #endif // _AKGL_TYPES_H_ diff --git a/include/akgl/util.h b/include/akgl/util.h index e8e6f24..ecfe1c9 100644 --- a/include/akgl/util.h +++ b/include/akgl/util.h @@ -1,6 +1,15 @@ /** * @file util.h - * @brief Declares the public util API. + * @brief Axis-aligned collision tests, path resolution, and two test-only image helpers. + * + * The grab bag. Three unrelated groups live here: rectangle/point overlap for + * the physics backend, path resolution for the asset loaders, and a pair of + * pixel-comparison routines that exist only so tests can assert on what was + * actually drawn. + * + * All the geometry here is axis-aligned and treats edges as touching: a point + * exactly on a boundary is inside. There is no rotation support and no + * separating-axis test. */ #ifndef _UTIL_H_ @@ -9,83 +18,173 @@ #include #include -/** @brief Represents a two-dimensional point. */ +/** @brief An integer point. Carries a `z` the collision routines do not use. */ typedef struct point { - int x; - int y; - int z; + int x; /**< Horizontal position, in whatever space the caller is working in. */ + int y; /**< Vertical position. */ + int z; /**< Depth. Never written by akgl_rectangle_points and never read by the collision tests. */ } point; -/** @brief Stores the corners of an axis-aligned rectangle. */ +/** + * @brief The four corners of an axis-aligned rectangle, precomputed. + * + * akgl_collide_rectangles works corner by corner rather than by comparing edge + * spans, so it wants the corners as points. akgl_rectangle_points derives one of + * these from an `SDL_FRect`. + */ typedef struct RectanglePoints { - point topleft; - point topright; - point bottomleft; - point bottomright; + point topleft; /**< (x, y). */ + point topright; /**< (x + w, y). */ + point bottomleft; /**< (x, y + h). */ + point bottomright; /**< (x + w, y + h). */ } RectanglePoints; +/** + * @brief Do not use. Three open parentheses, two closes -- any expansion is a + * syntax error. It duplicates akgl_collide_rectangles(), has no callers, + * and is slated for deletion. TODO.md item 20. + */ #define AKGL_COLLIDE_RECTANGLES(r1x, r1y, r1w, r1h, r2x, r2y, r2w, r2h) ((r1x < (r2x + r2w)) || ((r1x + r1w) > r2x) /** - * @brief Rectangle points. - * @param dest Output destination populated by the function. - * @param rect Rectangle whose corner points are calculated. + * @brief Expand a rectangle into its four corner points. + * + * Coordinates are truncated from `float` to `int` on the way in, so a rectangle + * at x = 10.9 has its corners at 10. That is deliberate for tile-grid work and + * wrong for sub-pixel work; callers needing the latter should not round-trip + * through this. + * + * @param dest Receives the corners. Required. + * @param rect The rectangle, in any coordinate space. Required. `w` and `h` are + * taken as extents from `x`/`y`, so a negative one produces a + * rectangle whose "bottom right" is above and left of its "top + * left" -- which every test here then reports as empty. * @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 dest or @p rect is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_rectangle_points(RectanglePoints *dest, SDL_FRect *rect); /** - * @brief Collide point rectangle. - * @param p Point to test for containment. - * @param r Rectangle corner data used for collision testing. - * @param collide Output set to whether the tested geometry intersects. + * @brief Test whether a point falls inside a rectangle, edges included. + * + * Compares against `topleft` and `bottomright` only, so it assumes @p r is + * well-formed -- the two corners actually being the minimum and maximum. `z` is + * ignored on both sides: this is a 2D test. + * + * @param p The point to test. Required. + * @param r The rectangle, as corners from akgl_rectangle_points. Required. + * @param collide Receives `true` when the point is inside or exactly on an edge, + * `false` otherwise. Required -- the return value is the error + * context. Not written on any failure path. * @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 p, @p r, or @p collide is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_point_rectangle(point *p, RectanglePoints *r, bool *collide); /** - * @brief Collide rectangles. - * @param r1 First rectangle to compare. - * @param r2 Second rectangle to compare. - * @param collide Output set to whether the tested geometry intersects. + * @brief Test whether two rectangles overlap, edges included. + * + * Tests all eight corners -- each rectangle's four against the other -- and + * stops at the first hit. Checking both directions is what catches the case + * where one rectangle is entirely inside the other and so has no corner within + * its neighbour. + * + * @param r1 First rectangle. Required. + * @param r2 Second rectangle. Required. Order does not matter. + * @param collide Receives `true` on any overlap or shared edge, `false` + * otherwise. 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 r1, @p r2, or @p collide is `NULL`. + * + * @note A corner-containment test misses the one arrangement where two + * rectangles overlap in a cross without either enclosing a corner of the + * other -- a tall thin rectangle crossing a short wide one. Both are + * reported as not colliding. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide); /** - * @brief Path relative. - * @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 an asset path, trying the working directory before the given root. + * + * Asset files name their neighbours relatively -- a sprite definition names its + * spritesheet, a tilemap names its tilesets -- and "relative" has to mean + * relative to the file doing the naming, not to wherever the game was launched + * from. So this tries @p path against the process working directory first, and + * only if that does not exist joins it onto @p root and resolves that. Either + * way the result is absolute, with symlinks and `..` folded out. + * + * @param root Directory to fall back to, normally `dirname` of the file that + * contained @p path. Required, even when unused. + * @param path The path to resolve, relative or absolute. Required. + * @param dst Receives the resolved absolute path. Required, and must already be + * a claimed pool string -- this writes into it, it does not claim + * one for you. * @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 root, @p path, or @p dst is `NULL`. + * @throws AKERR_OUTOFBOUNDS If `root + "/" + path` would not fit in + * #AKGL_MAX_STRING_LENGTH. + * @throws ENOENT If neither spelling names an existing file. Any other `errno` + * `realpath(3)` can raise -- EACCES on an unsearchable directory, + * ELOOP, ENOTDIR -- propagates the same way. + * @throws AKGL_ERR_HEAP If the string pool is exhausted. + * + * @note The fallback path -- the common one, since most asset references are + * relative to their own file rather than to the working directory -- + * returns straight out of the ENOENT handler and so never releases the + * error context it was handling. Each such call consumes one slot of + * libakerror's fixed 128-entry context array for the life of the process. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_path_relative(char *root, char *path, akgl_String *dst); // These are REALLY slow routines that are only useful in testing harnesses /** - * @brief Compare sdl surfaces. - * @param s1 First SDL surface to compare. - * @param s2 Second SDL surface to compare. - * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKERR_NULLPOINTER When the corresponding validation or operation fails. - * @throws AKERR_VALUE When the corresponding validation or operation fails. + * @brief Assert that two surfaces hold byte-identical pixels. + * + * A `memcmp` over the raw pixel buffer, so it is exact: one differing byte in + * one pixel is a failure. Meant for test harnesses asserting on rendered output, + * not for anything on a frame path. + * + * @param s1 First surface. Required. Its `pitch * h` is what determines how many + * bytes are compared. + * @param s2 Second surface. Required. + * @return `NULL` when the pixels match, otherwise an error context owned by the + * caller. "Not equal" is reported as an error, not as an out-param. + * @throws AKERR_NULLPOINTER If @p s1 or @p s2 is `NULL`. + * @throws AKERR_VALUE If the pixels differ. + * + * @warning The surfaces' dimensions, pitch, and format are not compared, so a + * smaller @p s2 is read past its end rather than reported as a + * mismatch. TODO.md, "Known and still open" item 5. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2); /** - * @brief Render and compare. - * @param t1 First texture to render for comparison. - * @param t2 Second texture to render for comparison. - * @param x Horizontal destination coordinate. - * @param y Vertical destination coordinate. - * @param w Destination width. - * @param h Destination height. - * @param writeout Optional filename for the rendered diagnostic image. - * @return `NULL` on success, otherwise an error context owned by the caller. - * @throws AKERR_IO When the corresponding validation or operation fails. - * @throws AKERR_NULLPOINTER When the corresponding validation or operation fails. - * @throws AKGL_ERR_SDL When the corresponding validation or operation fails. + * @brief Draw two textures in turn, read the framebuffer back after each, and compare. + * + * The test-harness counterpart to akgl_compare_sdl_surfaces: it answers "do + * these two textures *render* the same", which is not the same question as "are + * these two textures identical", because the renderer's scaling and blending sit + * in between. Both are drawn into the same rectangle against a cleared target. + * + * @param t1 First texture. Required. + * @param t2 Second texture. Required. + * @param x Left edge of the region, in pixels. Used for the source + * rectangle, the destination, and the readback alike. + * @param y Top edge of the region. + * @param w Width of the region. + * @param h Height of the region. + * @param writeout Optional filename for a PNG of the *first* render, written + * under `SDL_GetBasePath()`. `NULL` skips it. This is a + * debugging aid -- when an image assertion fails, this is how + * you see what was actually drawn. + * @return `NULL` when the two renders match, otherwise an error context owned by + * the caller. + * @throws AKERR_NULLPOINTER If @p t1 or @p t2 is `NULL`. + * @throws AKGL_ERR_SDL If the framebuffer cannot be read back. + * @throws AKERR_IO If @p writeout is given and the PNG cannot be written. + * @throws AKERR_VALUE If the two renders differ. + * @throws AKGL_ERR_HEAP If the string pool is exhausted. + * + * @warning Known defect: both passes draw @p t1 -- @p t2 is never rendered -- so + * this currently always reports a match. TODO.md, "Known and still + * open" item 1. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout); diff --git a/src/actor.c b/src/actor.c index 3acc71c..ef71410 100644 --- a/src/actor.c +++ b/src/actor.c @@ -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) { diff --git a/src/character.c b/src/character.c index 3f9ea54..549effc 100644 --- a/src/character.c +++ b/src/character.c @@ -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) { diff --git a/src/controller.c b/src/controller.c index e05ae22..61d0652 100644 --- a/src/controller.c +++ b/src/controller.c @@ -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) { diff --git a/src/draw.c b/src/draw.c index efc8fce..94bed47 100644 --- a/src/draw.c +++ b/src/draw.c @@ -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) { diff --git a/src/game.c b/src/game.c index 331e155..cf3418c 100644 --- a/src/game.c +++ b/src/game.c @@ -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) { diff --git a/src/sprite.c b/src/sprite.c index 01bfa59..1dea358 100644 --- a/src/sprite.c +++ b/src/sprite.c @@ -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) { diff --git a/src/tilemap.c b/src/tilemap.c index 888450a..e4018f3 100644 --- a/src/tilemap.c +++ b/src/tilemap.c @@ -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) { diff --git a/src/util.c b/src/util.c index 181b57d..2e3ef2a 100644 --- a/src/util.c +++ b/src/util.c @@ -21,13 +21,34 @@ #include /** - * @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) {