Files
libakgl/include/akgl/collision.h
Andrew Kesterson cf930f68bb Add a second partitioner, so the vtable is a seam and not a decoration
A binary space partition on libakstdlib's tree links and lists, registered in
the factory as "bsp". It runs the same assertions the grid does, because
tests/partition.c is a table over partitioners and adding a row is all it takes
to be held to the contract.

**Use the grid.** This is here to prove the vtable works and to have something
to measure the grid against, and the file says so at the top. It rebuilds
whenever the proxy set changes, which is the shape PERFORMANCE.md records Phaser
using and capping out around five thousand bodies. It would earn its place in a
world with wildly non-uniform object sizes, or one larger than the grid's fixed
cell array covers.

The difference is visible in the code rather than buried in a benchmark: the
grid's `move` compares four integers and returns when a proxy has not left its
cells, and this one marks the whole partition stale. The incremental-move
assertion in the suite is therefore grid-only, and says why.

aksl_tree_iterate is not used, and the file carries the three reasons so the
next reader does not rediscover them: it is a complete traversal whose only
control signal stops the entire walk, so there is no way to prune a subtree --
which is the only operation a spatial query is made of; it carries no per-node
context, and a pruning descent needs each node's bounds and plane; and its
breadth-first modes allocate, while only the depth-first ones are malloc-free
and those are the ones without pruning. aksl_tree_insert is unusable for a
different reason again: it is a comparator-ordered BST, and a spatial insert has
to compare a leaf against a plane, which aksl_TreeCompareFunc cannot express.

What is used is the link structure and the lists, neither of which allocates,
which is why they fit here at all. The descent is an explicit stack rather than
recursion, so the depth bound is an array bound the compiler can see -- this
library already has one documented way to blow the C stack and does not need a
second.

Split planes are the median of the item centres on the longer axis, not the
spatial midpoint: actors in a tile game cluster on the floor, and a midpoint
split gives one empty child and one full one for several levels running. A split
that separates nothing degrades the node to a leaf rather than recursing forever
on the same set.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 07:08:54 -04:00

705 lines
38 KiB
C

/**
* @file collision.h
* @brief Collision shapes: what an actor occupies, and what it will collide with.
*
* A shape is a convex volume positioned relative to an actor's origin. It lives
* on the akgl_Character, so every goblin sharing a character shares one shape
* definition, exactly as they share speeds and the state-to-sprite map -- and an
* individual actor may override it when it needs to.
*
* @section collision_leaf This header includes nothing of ours but types.h
*
* `actor.h` and `character.h` both need akgl_CollisionShape by value, so this
* header cannot include either of them without a cycle. It names actors and
* tilemaps as incomplete struct pointers instead, which is why they need struct
* tags and why `akgl_Tilemap` grew one. The `headers` suite compiles this file
* as the first include of a translation unit and would fail immediately if that
* ever stopped being true.
*
* @section collision_extrusion Why a 2D shape has a depth
*
* The narrowphase behind this is three-dimensional, which is what lets the same
* shapes carry into a 3D game later. It answers with the **minimum** translation
* that separates two volumes -- and if a 2D shape were extruded into a thin
* slab, the cheapest way to separate two of them would be along z. The
* narrowphase would report a contact, the resolver would push the actor into the
* screen, and on screen nothing would happen at all while the actor sank through
* the floor.
*
* So the setters give every shape a depth of #AKGL_COLLISION_DEPTH_RATIO times
* its largest planar half-extent unless told otherwise. That is not a large
* number chosen for comfort; it is the smallest ratio for which the z overlap of
* any two shapes built this way is provably larger than any planar penetration
* they can reach, so z can never be the minimum axis. Pass a depth by hand only
* if you know what that costs.
*/
#ifndef _AKGL_COLLISION_H_
#define _AKGL_COLLISION_H_
#include <stdbool.h>
#include <stdint.h>
#include <SDL3/SDL_rect.h>
#include <akerror.h>
#include <akstdlib.h>
#include <akgl/types.h>
struct akgl_Actor;
struct akgl_Tilemap;
struct akgl_CollisionWorld;
/** @brief No shape. The actor takes no part in collision. */
#define AKGL_COLLISION_SHAPE_NONE 0
/** @brief An axis-aligned box. The common case, and the one with a closed-form answer. */
#define AKGL_COLLISION_SHAPE_BOX 1
/** @brief A circle in the xy plane, extruded along z. `hx` is the radius and `hy` is ignored. */
#define AKGL_COLLISION_SHAPE_CIRCLE 2
/** @brief A capsule whose long axis is x: a box with semicircular caps left and right. */
#define AKGL_COLLISION_SHAPE_CAPSULE_X 3
/** @brief A capsule whose long axis is y: a box with semicircular caps top and bottom. */
#define AKGL_COLLISION_SHAPE_CAPSULE_Y 4
/** @brief No flags. */
#define AKGL_COLLISION_FLAG_NONE 0x00000000u
/** @brief Never moved by the resolver and never re-inserted into the broad phase. */
#define AKGL_COLLISION_FLAG_STATIC 0x00000001u
/** @brief Reports a contact and never pushes. Pickups, trigger volumes, damage zones. */
#define AKGL_COLLISION_FLAG_SENSOR 0x00000002u
/** @brief Present but ignored this step. Cheaper than dropping the shape and putting it back. */
#define AKGL_COLLISION_FLAG_DISABLED 0x00000004u
/**
* @brief Reserved for a swept narrowphase. **Not implemented; setting it does nothing.**
*
* Sub-stepping bounds how far an actor moves between collision tests, which
* stops anything moving at ordinary speeds from passing through a wall. A
* projectile is not moving at ordinary speeds. See `TODO.md`.
*/
#define AKGL_COLLISION_FLAG_BULLET 0x00000008u
/** @brief On no layer. A shape with this `layermask` collides with nothing. */
#define AKGL_COLLISION_LAYER_NONE 0x00000000u
/** @brief Map geometry: solid tiles and static proxies. The default `collidemask`. */
#define AKGL_COLLISION_LAYER_STATIC (1u << 0)
/** @brief Ordinary actors. The default `layermask`. */
#define AKGL_COLLISION_LAYER_ACTOR (1u << 1)
/** @brief Suggested layer for the player. Nothing in the library treats it specially. */
#define AKGL_COLLISION_LAYER_PLAYER (1u << 2)
/** @brief Suggested layer for hostiles. */
#define AKGL_COLLISION_LAYER_ENEMY (1u << 3)
/** @brief Suggested layer for things picked up rather than bumped into. */
#define AKGL_COLLISION_LAYER_PICKUP (1u << 4)
/** @brief Every layer, including the ones a game defines for itself in bits 5 and above. */
#define AKGL_COLLISION_LAYER_ALL 0xFFFFFFFFu
/**
* @brief Depth given to a 2D shape, as a multiple of its largest planar half-extent.
*
* 2 and not 1. At 1, two identical squares fully overlapped have the same
* overlap on z as in the plane, and which axis the narrowphase calls "minimum"
* comes down to floating-point luck. At 2 the z overlap of any pair is at least
* twice the largest planar half-extent either can reach, and the planar
* penetration is at most that -- so z loses every time, by construction rather
* than by margin.
*/
#define AKGL_COLLISION_DEPTH_RATIO 2.0f
/**
* @brief A convex volume, positioned relative to an actor's origin.
*
* Stored as a centre and half-extents rather than as an `SDL_FRect`, because
* that is the form both the overlap test and the narrowphase want and this way
* the conversion happens once, in a setter, instead of on every one of the tens
* of thousands of tests a frame. The setters take the rectangle form a game
* already has.
*/
typedef struct akgl_CollisionShape {
uint8_t kind; /**< One of the `AKGL_COLLISION_SHAPE_*` values. #AKGL_COLLISION_SHAPE_NONE means this actor does not collide, and is what a zeroed shape is. */
uint32_t flags; /**< Bitwise OR of the `AKGL_COLLISION_FLAG_*` values. */
uint32_t layermask; /**< Which layers this shape is *on*. What other shapes test against. */
uint32_t collidemask; /**< Which layers this shape *responds to*. Asymmetric on purpose: a player can block against an NPC while the NPC ignores the player. */
float32_t ox; /**< Centre offset from the actor's `x`, in map pixels. */
float32_t oy; /**< Centre offset from the actor's `y`. Positive is down, matching screen space. */
float32_t oz; /**< Centre offset from the actor's `z`. */
float32_t hx; /**< Half-extent along x. The radius, for a circle or a capsule. */
float32_t hy; /**< Half-extent along y. Ignored for a circle. */
float32_t hz; /**< Half-extent along z. Never 0 after a setter has run; see the extrusion note on this file. */
} akgl_CollisionShape;
/**
* @brief One shape's registration in the broad phase.
*
* A proxy is what the partitioner indexes. It carries a **copy** of the shape
* rather than a pointer to it, which is deliberate: an actor's own logic may
* legitimately rewrite its shape mid-step -- a character crouching, a projectile
* arming -- and a broad phase whose bounds were computed from a shape that has
* since changed is a source of bugs that only appear when two things happen in
* the same frame. The copy is refreshed once per step, from one place.
*
* Proxies are pool objects. The library creates and destroys them; a game does
* not, and a game that releases an actor gets its proxy released with it.
*/
typedef struct akgl_CollisionProxy {
uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. First member, as in every pooled type here. */
struct akgl_Actor *owner; /**< The actor this proxy tracks, or `NULL` for static geometry with no actor behind it. Borrowed; no reference is taken. */
akgl_CollisionShape shape; /**< A copy of the owner's shape as of the last refresh. */
SDL_FRect bounds; /**< Planar world bounds in map pixels, computed from #shape at (#x, #y). */
float32_t x; /**< World position #bounds was computed from. */
float32_t y; /**< World position #bounds was computed from. */
float32_t z; /**< World position #bounds was computed from. Not represented in #bounds. */
uint32_t stamp; /**< Sweep serial this proxy was last visited on, so a proxy spanning several cells is reported once. Written by the partitioner. */
int32_t cx0; /**< Grid cell rectangle currently occupied, inclusive. Written by the uniform grid only, and the thing its `move` compares against. */
int32_t cy0; /**< Grid cell rectangle currently occupied. */
int32_t cx1; /**< Grid cell rectangle currently occupied. */
int32_t cy1; /**< Grid cell rectangle currently occupied. */
int16_t first; /**< Head of this proxy's chain of cell entries, or -1 when it is not in the grid. Written by the uniform grid only. */
aksl_ListNode node; /**< Membership of one BSP node's item list. Written by the BSP partitioner only. A proxy is in exactly one node at a time, so one link suffices and the BSP needs no pool of its own for these. */
} akgl_CollisionProxy;
/**
* @brief One node of the BSP partitioner. Pool object.
*
* The tree links are libakstdlib's, but only the links: the traversal is written
* by hand. See `src/collision_bsp.c` for why aksl_tree_iterate cannot be used
* for a spatial query.
*/
typedef struct akgl_BspNode {
uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. */
aksl_TreeNode node; /**< left/right/parent. `leaf` points back at this struct so a walk can recover it. */
aksl_List items; /**< Proxies stored here, threaded through akgl_CollisionProxy::node. */
SDL_FRect bounds; /**< The region this node covers. aksl_TreeNode does not carry it and a pruning descent cannot work without it. */
uint8_t axis; /**< 0 splits on x, 1 splits on y, 2 is a leaf. */
float32_t split; /**< World coordinate of the plane on #axis. Meaningless in a leaf. */
uint8_t depth; /**< Distance from the root, so the build terminates. */
} akgl_BspNode;
/**
* @brief One proxy's membership of one grid cell.
*
* Two intrusive chains: one through the cell, so a query can walk what is in it,
* and one through the proxy, so removing a proxy unlinks its whole span without
* searching. Everything is an index rather than a pointer, which keeps the
* structure relocatable and makes clearing it a `memset`.
*/
typedef struct akgl_CollisionCell {
uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. */
int16_t proxy; /**< Index into the proxy pool. */
int32_t cell; /**< Which cell, as `(row * columns) + column`. */
int16_t next; /**< Next entry in this cell's chain, or -1. */
int16_t prev; /**< Previous entry in this cell's chain, or -1, so removal is O(1). */
int16_t ownernext; /**< Next entry belonging to the same proxy, or -1. */
} akgl_CollisionCell;
/**
* @brief Force every contact normal into the xy plane.
*
* A safety net rather than a mode. The extrusion the setters apply already makes
* z the most expensive axis to separate on, so a normal along z should be
* unreachable -- but a caller who set a depth by hand has opted out of that
* guarantee, and the failure it produces is silent: the resolver pushes the
* actor into the screen, which moves it nowhere a player can see while leaving
* it inside whatever it hit.
*
* Pass it for a 2D game, which is every game today. When there is a real third
* axis to resolve on, leave it off.
*/
#define AKGL_COLLISION_TEST_PLANAR 0x00000001u
/**
* @brief One overlap, described well enough to undo it.
*
* The narrowphase fills in the geometry. Which actor, which tile, and whether
* either side was a sensor are filled in by the layer above, which is the only
* one that knows.
*/
typedef struct akgl_Contact {
struct akgl_Actor *self; /**< The actor being told about this contact. Filled in by the resolver, not the narrowphase. */
struct akgl_Actor *other; /**< The other actor, or `NULL` when the hit was map geometry with no actor behind it. */
int32_t tilex; /**< Tile column that was hit, or -1 when the other side was not a tile. */
int32_t tiley; /**< Tile row, or -1. */
int32_t tilelayer; /**< Index into the tilemap's layers, or -1. */
int32_t tilegid; /**< Global tile id that was hit, or 0. This is what tells a spike from a floor without a second lookup. */
float32_t nx; /**< Contact normal, unit length. **Points out of the other shape and toward this one**, so moving along it by #depth separates them. */
float32_t ny; /**< Normal along y. Negative means the surface is *below*, since y grows downward. */
float32_t nz; /**< Normal along z. Always 0 when #AKGL_COLLISION_TEST_PLANAR was used. */
float32_t depth; /**< How far along the normal to move to stop overlapping, in map pixels. Never negative. */
float32_t px; /**< A point on the overlap, in map pixels. Approximate for a non-box pair; see below. */
float32_t py; /**< Contact point along y. */
float32_t pz; /**< Contact point along z. */
float32_t dt; /**< Length of the sub-step this contact was found in, in seconds. Filled in by the resolver. */
bool sensor; /**< Either side carries #AKGL_COLLISION_FLAG_SENSOR: report, do not push. Filled in by the resolver. */
bool statichit; /**< The other side does not move -- a tile, or a proxy flagged #AKGL_COLLISION_FLAG_STATIC. */
} akgl_Contact;
/**
* @brief Test two positioned shapes, and describe the overlap if there is one.
*
* Three paths, cheapest first. The proxies' bounds reject most pairs outright.
* A box against a box is answered in closed form -- exact depth, exactly
* axis-aligned normal, no iteration -- which matters because a tile game is
* almost entirely boxes, and because a resting actor wants a normal that is
* precisely `(0, -1, 0)` rather than one converged to within a tolerance, or it
* creeps. Everything else goes to the iterative narrowphase.
*
* @note **The contact point is approximate for anything but a box pair.** The
* iterative solver returns a point on the portal it converged to, which
* for a deep off-centre overlap can sit noticeably away from the deepest
* point. The depth and the normal are not approximate, and the blocking
* resolver uses only those. Do not build a damage falloff on the point.
*
* @param a First proxy. Required.
* @param b Second proxy. Required.
* @param flags Bitwise OR of the `AKGL_COLLISION_TEST_*` values.
* @param dest Receives the contact geometry when @p hit comes back `true`.
* Required. The normal points out of @p b and toward @p a.
* @param hit Receives whether the two overlap. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If any pointer argument is `NULL`.
* @throws AKGL_ERR_COLLISION If the solver could not characterise an
* intersection it found -- a degenerate shape, or the arena running out.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_test(akgl_CollisionProxy *a, akgl_CollisionProxy *b, uint32_t flags, akgl_Contact *dest, bool *hit);
/** @brief Longest partitioner name, including the terminator. */
#define AKGL_PARTITIONER_MAX_NAME_LENGTH 32
/**
* @brief Visits one proxy a query found. Raise `AKERR_ITERATOR_BREAK` to stop early.
*
* A query is allowed to over-report and forbidden to under-report, so a visitor
* will see proxies that turn out not to overlap. Rejecting those is the
* narrowphase's job and it is cheap; missing one is a wall an actor walks
* through.
*/
typedef akerr_ErrorContext AKERR_NOIGNORE *(*akgl_CollisionVisitFunc)(akgl_CollisionProxy *proxy, void *data);
/** @brief Visits one candidate pair. Raise `AKERR_ITERATOR_BREAK` to stop early. */
typedef akerr_ErrorContext AKERR_NOIGNORE *(*akgl_CollisionPairFunc)(akgl_CollisionProxy *a, akgl_CollisionProxy *b, void *data);
/**
* @brief A pluggable broad phase: a record of function pointers and an initializer.
*
* The same shape as akgl_RenderBackend and akgl_PhysicsBackend, for the same
* reason -- behaviour that varies is a slot to fill, not a branch to add.
*
* @section partitioner_move Why `move` is its own slot
*
* It could be spelled remove-then-insert, and that would throw away the entire
* argument for the structure that is installed by default. A uniform grid's
* `move` is a comparison and a return when the proxy has not left the cells it
* was in, which is the steady state for a walking actor and forever for a static
* one. Collapsing it into remove-and-insert turns the incremental grid into the
* rebuild-every-frame tree that was measured and rejected.
*/
typedef struct akgl_Partitioner {
char name[AKGL_PARTITIONER_MAX_NAME_LENGTH]; /**< Which implementation is installed. Diagnostic, and what the tests use to report which row failed. */
akerr_ErrorContext AKERR_NOIGNORE *(*reset)(struct akgl_Partitioner *self, struct akgl_CollisionWorld *world); /**< Drop every proxy and re-derive the structure's geometry from the world. */
akerr_ErrorContext AKERR_NOIGNORE *(*insert)(struct akgl_Partitioner *self, akgl_CollisionProxy *proxy); /**< Add a proxy at its current bounds. */
akerr_ErrorContext AKERR_NOIGNORE *(*remove)(struct akgl_Partitioner *self, akgl_CollisionProxy *proxy); /**< Take a proxy out. Removing one that is not in is success, not an error. */
akerr_ErrorContext AKERR_NOIGNORE *(*move)(struct akgl_Partitioner *self, akgl_CollisionProxy *proxy); /**< The proxy's bounds changed. See the note above on why this is not remove-then-insert. */
akerr_ErrorContext AKERR_NOIGNORE *(*query)(struct akgl_Partitioner *self, SDL_FRect *area, uint32_t mask, akgl_CollisionVisitFunc visit, void *data); /**< Visit every proxy that may overlap @p area and whose `layermask` intersects @p mask. Each proxy is visited at most once. */
akerr_ErrorContext AKERR_NOIGNORE *(*each_pair)(struct akgl_Partitioner *self, akgl_CollisionPairFunc visit, void *data); /**< Visit every candidate pair exactly once. */
void *state; /**< The implementation's private storage. Never dereferenced outside the file that set it. */
} akgl_Partitioner;
/**
* @brief Broad-phase geometry and the partitioner that indexes it.
*
* One of these exists per world. It is reached by pointer and never embedded by
* value in anything, because the structures behind it are measured in tens of
* kilobytes and `akgl_Tilemap` is already large enough to matter.
*/
typedef struct akgl_CollisionWorld {
akgl_Partitioner partitioner; /**< The broad phase. By value: it is a vtable and a `void *`, not storage. */
float32_t cellwidth; /**< Broad-phase cell width in map pixels. */
float32_t cellheight; /**< Broad-phase cell height. */
float32_t originx; /**< World x the cell grid is measured from. 0 for a Tiled map. */
float32_t originy; /**< World y the cell grid is measured from. */
uint32_t flags; /**< `AKGL_COLLISION_TEST_*` bits applied to every test this world runs. */
uint32_t sweep; /**< Serial of the current pass. Stamped onto proxies so one spanning several cells is reported once. */
uint32_t tests; /**< Narrowphase calls during the last pass. Diagnostic. */
struct akgl_Tilemap *tilesource;/**< The map whose solid tile layers are static geometry, or `NULL` for none. Borrowed. */
uint32_t tilelayers; /**< Which of that map's layers are solid, copied from its `collidablelayers`. */
uint32_t tilelayermask; /**< The collision layer solid tiles are treated as being on. #AKGL_COLLISION_LAYER_STATIC by default. */
} akgl_CollisionWorld;
/**
* @brief Bring up a collision world with a named partitioner.
*
* @param self The world. Required. Zeroed first.
* @param type `"grid"` for the incremental uniform grid, which is the default
* and what a game should use, or `NULL` for the same. Matched on
* leading characters, like akgl_physics_factory.
* @param cellwidth Cell width in map pixels. Pass the map's tile width. Must be positive.
* @param cellheight Cell height in map pixels. Must be positive.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
* @throws AKERR_VALUE If either cell dimension is not positive.
* @throws AKERR_KEY If @p type names no partitioner. The message quotes it.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_world_init(akgl_CollisionWorld *self, char *type, float32_t cellwidth, float32_t cellheight);
/**
* @brief Install a partitioner by name.
*
* Adding an implementation means adding a name here, not a branch anywhere else.
*
* @param self The partitioner to fill in. Required.
* @param type `"grid"`, or `NULL` for the default. Matched on leading characters.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
* @throws AKERR_KEY If @p type matches nothing.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_partitioner_factory(akgl_Partitioner *self, char *type);
/**
* @brief Install the incremental uniform grid.
*
* The default, and the structure the performance record argues for: cells keyed
* on tile size, static storage, and insert and remove that happen only when a
* proxy crosses a cell boundary. A proxy that has not left its cells costs
* nothing to update, and a static one costs nothing ever.
*
* @param self The partitioner to fill in. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_partitioner_init_grid(akgl_Partitioner *self);
/**
* @brief Install the binary space partition.
*
* **Use the grid.** This ships so that "pluggable" means something -- a vtable
* with one implementation is a vtable nobody has tested -- and so the two can be
* measured against each other rather than argued about. It rebuilds from scratch
* whenever the proxy set changes, which is the shape `PERFORMANCE.md` records
* Phaser using and capping out around five thousand bodies.
*
* It earns its place where a uniform grid degenerates: a world with wildly
* non-uniform object sizes, or one far larger than the fixed cell array covers.
*
* @param self The partitioner to fill in. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_partitioner_init_bsp(akgl_Partitioner *self);
/**
* @brief Make a map's solid layers count as collision geometry.
*
* @section collision_tiles Tiles are read, not registered
*
* A map's solid tiles are **not** given proxies. At the maximum map size that
* would be a quarter of a million of them per layer -- tens of megabytes of
* index, to describe data that is already a dense grid sitting in the tilemap.
* Instead the world keeps a borrowed pointer to the map and reads
* `layers[i].data[]` directly over whatever cell range a query covers, which is
* nine array reads for a 32-pixel actor on 16-pixel tiles and costs nothing to
* maintain when a level loads.
*
* Static geometry that is *not* tile-aligned -- a slope, a Tiled object
* rectangle, a platform that only moves between levels -- is still an ordinary
* proxy carrying #AKGL_COLLISION_FLAG_STATIC. Both mechanisms exist; tiles use
* the free one because there are a hundred thousand of them.
*
* @param self The world. Required.
* @param map The map, or `NULL` to detach the one already bound.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
* @throws AKERR_VALUE If the map's tile dimensions are not positive.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_bind_tilemap(akgl_CollisionWorld *self, struct akgl_Tilemap *map);
/**
* @brief Is the tile under this point solid?
*
* A point query against the bound map's collidable layers, and nothing else --
* it does not consult proxies. Off the map is **not** solid: a game that wants
* an edge of the world puts one there, and a pit that kills the player has to be
* a pit rather than a wall.
*
* @param self The world. Required.
* @param x World x in map pixels.
* @param y World y in map pixels.
* @param dest Receives whether a solid tile covers that point. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self or @p dest is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_solid_at(akgl_CollisionWorld *self, float32_t x, float32_t y, bool *dest);
/**
* @brief Would this rectangle overlap anything solid?
*
* Tiles and proxies both. This is the query a game reaches for when it wants an
* answer without a response: a ledge probe ahead of a walking enemy, a check
* that a spawn point is clear, a door that only opens when nothing is standing
* in it.
*
* @param self The world. Required.
* @param box The rectangle, in map pixels. Required.
* @param mask Which collision layers count. #AKGL_COLLISION_LAYER_ALL for
* everything, #AKGL_COLLISION_LAYER_STATIC for map geometry only.
* @param dest Receives the answer. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If any pointer argument is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_box_blocked(akgl_CollisionWorld *self, SDL_FRect *box, uint32_t mask, bool *dest);
/**
* @brief Visit every proxy that may overlap a rectangle.
*
* The broad phase, exposed. Tiles are not proxies and so are not visited; use
* akgl_collision_box_blocked when the question includes map geometry.
*
* @param self The world. Required.
* @param box The rectangle, in map pixels. Required.
* @param mask Which collision layers to report.
* @param visit Called once per candidate. Required.
* @param data Passed through to @p visit.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If any pointer argument is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_query_box(akgl_CollisionWorld *self, SDL_FRect *box, uint32_t mask, akgl_CollisionVisitFunc visit, void *data);
/**
* @brief Lift a shape out of the geometry it was placed inside.
*
* Resolution stops a shape *entering* geometry and has nothing to say about one
* that started inside it -- what it does instead is refuse every move, so an
* actor spawned in a wall is simply stuck. That is not a hypothetical: level
* authors place a character on a step and Tiled rounds the object to a position
* that overlaps the tile below it.
*
* This walks the shape up a tile at a time until it is clear, and refuses rather
* than searching forever if it cannot be. Call it once, after a map load, for
* anything the map placed.
*
* @param self The world. Required.
* @param shape The shape being placed. Required.
* @param x Position in map pixels. Read and written.
* @param y Position in map pixels. Read and written; this is what moves.
* @param maxsteps How many tiles to try before giving up. 0 uses a sensible default.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If any pointer argument is `NULL`.
* @throws AKERR_VALUE If the shape is still inside geometry after @p maxsteps.
* The message carries the position, because the answer is almost always
* to move the object in the level rather than to raise the limit.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_settle(akgl_CollisionWorld *self, akgl_CollisionShape *shape, float32_t *x, float32_t *y, int maxsteps);
/**
* @brief Resolve one actor against everything it currently overlaps.
*
* Called by the physics step after each sub-move, so it looks at where the actor
* *is* rather than predicting where it will be. That is what lets a game delete
* the arithmetic it would otherwise have to duplicate from the integrator.
*
* Tiles first, then proxies. Each contact goes to the actor's `collidefunc`, and
* the index entry is re-synced after every response, because a response that
* moves the actor invalidates everything computed before it.
*
* @param self The world. Required.
* @param actor The actor. Required. Returns success unchanged if it has no
* shape, no proxy or no response hook.
* @param dt Length of the sub-step, in seconds. Carried on each contact.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self or @p actor is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_resolve(akgl_CollisionWorld *self, struct akgl_Actor *actor, float32_t dt);
/**
* @brief How many pieces this actor's move has to be taken in.
*
* Bounds how far an actor travels between collision tests to less than the
* thinnest thing it could pass through -- a cell, or its own extent, whichever
* is smaller, because a small fast actor is the one that tunnels.
*
* Answers 1 when there is no world or the actor has no shape, and the caller
* then takes exactly the step it always did: `dt / 1.0f` is `dt` exactly, so a
* non-colliding actor follows a bit-identical arithmetic path.
*
* @param self The world, or `NULL` for no collision.
* @param actor The actor. Required.
* @param dt Length of the whole step, in seconds.
* @param dest Receives the count, never below 1. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p actor or @p dest is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_substeps(akgl_CollisionWorld *self, struct akgl_Actor *actor, float32_t dt, int *dest);
/**
* @brief Bring every live actor's proxy into agreement with the actor.
*
* One pass over the actor pool at the top of a step: create a proxy for anything
* that has gained a shape, release one from anything that has lost its shape,
* and refresh the rest. The only place a proxy is created or destroyed during a
* step, so nothing downstream has to reason about lifetimes.
*
* @param self The world. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
* @throws AKGL_ERR_HEAP If the proxy pool is exhausted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_sync_actors(akgl_CollisionWorld *self);
/*
* The following is part of the internal API. Proxies are created and destroyed
* by the library, not by a game.
*/
/**
* @brief Claim a pooled proxy for an actor's shape and take the reference on it.
*
* Follows the convention every pool here uses: akgl_heap_next_collision_proxy
* finds a free slot and does **not** claim it, and this takes the reference.
* Write the two adjacent, because until the reference is taken the slot is still
* free and the next acquire hands out the same pointer. That is `TODO.md` "Known
* and still open" item 8, it applies to four of the five existing pools, and
* this one does not depart from it -- a sixth convention would be worse than the
* defect.
*
* @param obj The proxy to initialize. Required.
* @param owner The actor it tracks, or `NULL` for static geometry.
* @param shape The shape to copy into it. Required.
* @param x World position of the owner.
* @param y World position of the owner.
* @param z World position of the owner.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p shape is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_proxy_initialize(akgl_CollisionProxy *obj, struct akgl_Actor *owner, akgl_CollisionShape *shape, float32_t x, float32_t y, float32_t z);
/**
* @brief Recompute a proxy's bounds from its owner's current shape and position.
*
* Called once per step per live proxy, before the broad phase is asked anything.
* This is the one place the shape copy is refreshed.
*
* @param obj The proxy. Required.
* @param shape The current shape to copy. Required.
* @param x Current world position of the owner.
* @param y Current world position of the owner.
* @param z Current world position of the owner.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p shape is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_proxy_sync(akgl_CollisionProxy *obj, akgl_CollisionShape *shape, float32_t x, float32_t y, float32_t z);
/**
* @brief Claim a pooled grid cell entry, pointing it at a proxy and a cell.
*
* The other half of the acquire-then-initialize pair, as for proxies.
*
* @param obj The entry. Required.
* @param proxyidx Index of the proxy in the proxy pool.
* @param cell Cell number, or -1 for the oversized chain.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_cell_initialize(akgl_CollisionCell *obj, int16_t proxyidx, int32_t cell);
/**
* @brief Build an axis-aligned box from the frame-relative rectangle a game has.
*
* @p body is an offset and a size measured from the actor's position -- the same
* form a game already writes a hitbox in, inset into its sprite frame because
* sprite art does not reach the edges of its cell. It is converted here to the
* centre-and-half-extent form everything downstream uses.
*
* The shape is zeroed first, so every field not named by the arguments ends at a
* known value: no flags, and the default masks of #AKGL_COLLISION_LAYER_ACTOR on
* and #AKGL_COLLISION_LAYER_STATIC responded to. That default is chosen so an
* actor given a shape and nothing else collides with the map and with nothing
* else -- a town full of NPCs does not start shoving itself around because
* somebody gave the townsfolk hitboxes.
*
* @param dest Receives the shape. Required.
* @param body Offset and size in map pixels, relative to the actor's position.
* Required. Both `w` and `h` must be positive.
* @param depth Half-extent along z. Pass 0 for a 2D game and the extrusion is
* chosen for you; see the note on this file for why that matters.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest or @p body is `NULL`.
* @throws AKERR_VALUE If `body->w` or `body->h` is not positive, or @p depth is
* negative.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_shape_box(akgl_CollisionShape *dest, SDL_FRect *body, float32_t depth);
/**
* @brief Build a circle, extruded along z into a cylinder.
*
* A cylinder and not a sphere: a sphere's caps would round away from the plane
* and let a shape slip past a corner in a way a 2D game never expects.
*
* @param dest Receives the shape. Required.
* @param ox Centre offset from the actor's `x`, in map pixels.
* @param oy Centre offset from the actor's `y`.
* @param radius Radius in map pixels. Must be positive.
* @param depth Half-extent along z, or 0 to have it chosen.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest is `NULL`.
* @throws AKERR_VALUE If @p radius is not positive, or @p depth is negative.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_shape_circle(akgl_CollisionShape *dest, float32_t ox, float32_t oy, float32_t radius, float32_t depth);
/**
* @brief Build a capsule: a box with semicircular caps on one axis.
*
* Useful for a character that should slide off a corner rather than catch on it,
* which a box does and a capsule does not.
*
* @param dest Receives the shape. Required.
* @param body Offset and size in map pixels, as for akgl_collision_shape_box.
* Required.
* @param axis #AKGL_COLLISION_SHAPE_CAPSULE_X or #AKGL_COLLISION_SHAPE_CAPSULE_Y.
* @param depth Half-extent along z, or 0 to have it chosen.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest or @p body is `NULL`.
* @throws AKERR_VALUE If either side is not positive, @p depth is negative, @p
* axis is neither capsule kind, or the capped axis is not the longer one
* -- a capsule whose caps are wider than it is long is a circle written
* confusingly, and is refused rather than silently reinterpreted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_shape_capsule(akgl_CollisionShape *dest, SDL_FRect *body, uint8_t axis, float32_t depth);
/**
* @brief The planar bounds a shape occupies with its owner at a given position.
*
* This is what the broad phase indexes on and what a cheap overlap test uses
* before anything more expensive runs. `z` is not represented: the extrusion
* exists to keep the narrowphase honest, not to be searched.
*
* @param shape The shape. Required.
* @param x The owner's `x`.
* @param y The owner's `y`.
* @param dest Receives the bounds in map pixels. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p shape or @p dest is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_shape_bounds(akgl_CollisionShape *shape, float32_t x, float32_t y, SDL_FRect *dest);
/**
* @brief Whether two shapes are allowed to interact, in the given direction.
*
* Asymmetric, and that is the point: @p self responds to @p other when @p
* other's `layermask` intersects @p self's `collidemask`. The reverse is a
* separate question with its own answer, which is how a player blocks against a
* pushable crate while the crate ignores everything.
*
* A shape of kind #AKGL_COLLISION_SHAPE_NONE, or one carrying
* #AKGL_COLLISION_FLAG_DISABLED, interacts with nothing in either direction.
*
* @param self The shape doing the responding. Required.
* @param other The shape being responded to. Required.
* @param dest Receives whether the pair is worth testing. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If any argument is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_shape_interacts(akgl_CollisionShape *self, akgl_CollisionShape *other, bool *dest);
#endif