Document what the functions actually do instead of that they can fail

The Doxygen comments were generated from the declarations, so 217 @throws
lines across 21 headers read "When the corresponding validation or operation
fails" and told a caller nothing beyond the status name. The @param lines were
the same shape: every output was "Output destination populated by the
function", every instance "Object to initialize, inspect, or modify".

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 11:02:20 -04:00
parent 582008a411
commit f0858b0d38
28 changed files with 3090 additions and 1012 deletions

View File

@@ -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);

View File

@@ -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 <akerror.h>
/**
* @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);

View File

@@ -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);

View File

@@ -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": <name>, "state": [<state names>] }`
* objects), `speedtime` in seconds, `speed_x`, `speed_y`, `acceleration_x`, and
* `acceleration_y`. Each mapping's `state` array is OR-ed together into one
* bitmask by looking every name up in #AKGL_REGISTRY_ACTOR_STATE_STRINGS, so
* `["FACE_LEFT", "MOVING_LEFT"]` becomes a single key.
*
* Every referenced sprite must already be in #AKGL_REGISTRY_SPRITE: this loads
* characters, not sprites, so akgl_sprite_load_json runs first. On any failure
* the pooled character is released again.
*
* @param filename Path to the JSON document. Required. Used verbatim -- it is
* not resolved against `SDL_GetBasePath()`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER 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);

View File

@@ -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 <akerror.h>
#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_

View File

@@ -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);

View File

@@ -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

View File

@@ -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 <akgl/version.h>
/** @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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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 <akgl/error.h>
#include <akgl/staticstring.h>
/** @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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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_

View File

@@ -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 <akerror.h>
/**
* @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);

View File

@@ -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 <akgl/physics.h>
#include <jansson.h>
/** @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);

View File

@@ -1,6 +1,13 @@
/**
* @file types.h
* @brief Declares the public types API.
* @brief Width-named floating point aliases, to match the `<stdint.h>` 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 <stdint.h>
/** @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_

View File

@@ -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 <akerror.h>
#include <akgl/staticstring.h>
/** @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);