A seventh behaviour hook, collidefunc, defaulting to akgl_actor_collide_block. Contacts are delivered one actor at a time with the normal already pointing the way that actor has to move, which removes the "which one is a1" ambiguity that made the old physics `collide` slot unimplementable -- and means a game overriding one actor's response never has to reason about the other's. The default moves the actor out along the normal by the penetration depth and removes the component of its motion that was going into the surface. Three things about that are easy to write wrongly, and each has a test that names the symptom rather than the assertion: - **It writes `e` and `t`, never `v`.** akgl_physics_simulate recomputes velocity as `e + t` at the top of every step, so a write to `v` is discarded before anything reads it. Worse than useless: an actor holding a direction into a wall would keep accumulating thrust while standing still and leave at speed the instant the wall ended. Breaking this leaves the fall running at 300 px/s through the floor. - **It removes the component, not the axis.** Zeroing the whole thrust vector costs the actor its motion *along* the surface as well, which is a character scraping a ceiling stopping dead and a character landing on a floor losing its run. Sliding along a wall is the difference between a wall and glue. - **A separating contact is left alone.** An actor already moving out of a surface that gets its velocity zeroed is an actor stuck to a wall it is walking away from, and it reads to a player as the collision grabbing them. `e` and `t` are treated separately rather than as their sum, so an actor pressing into a floor loses its downward gravity without losing the sideways run it is also doing. A sensor is reported and never pushed -- walking through a coin is not being blocked by it. The default deliberately does not pre-load the environmental term with a step of gravity. A resolver running *before* the step has to, or the step it is correcting for re-adds gravity and commits a quarter pixel of penetration, which is invisible and still fatal because every horizontal move then reads as blocked. This library resolves after the move, so the case does not arise; the sidescroller example carries that workaround because it had no choice. Also asserted: the other six hooks survive. A seventh that displaced one of them would be a silent regression somewhere unrelated. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
846 lines
34 KiB
C
846 lines
34 KiB
C
/**
|
|
* @file collision.c
|
|
* @brief Collision shapes: construction, defaults, and the extrusion invariant.
|
|
*
|
|
* The invariant is the one worth staring at. A 2D shape handed to a 3D
|
|
* narrowphase has to be thick enough that separating two of them along z is
|
|
* never cheaper than separating them in the plane -- otherwise the narrowphase
|
|
* answers "push it into the screen", the resolver does, and the actor sinks
|
|
* through the floor while every test that only asks "did they collide" passes.
|
|
* test_shape_extrusion_beats_planar_penetration checks the inequality directly
|
|
* rather than checking that the number is 2.
|
|
*/
|
|
|
|
#include <math.h>
|
|
#include <string.h>
|
|
|
|
#include <akerror.h>
|
|
|
|
#include <akgl/actor.h>
|
|
#include <akgl/collision.h>
|
|
#include <akgl/error.h>
|
|
#include <akgl/heap.h>
|
|
#include <akgl/registry.h>
|
|
|
|
#include "testutil.h"
|
|
|
|
akerr_ErrorContext *test_shape_box(void)
|
|
{
|
|
akgl_CollisionShape shape;
|
|
SDL_FRect body = { .x = 8.0f, .y = 0.0f, .w = 16.0f, .h = 32.0f };
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_collision_shape_box(&shape, &body, 0.0f));
|
|
|
|
TEST_ASSERT(errctx, (shape.kind == AKGL_COLLISION_SHAPE_BOX), "a box is not a box");
|
|
// Top-left-and-size in, centre-and-half-extent out.
|
|
TEST_ASSERT_FEQ(errctx, shape.hx, 8.0f, "half width is %f, expected 8", shape.hx);
|
|
TEST_ASSERT_FEQ(errctx, shape.hy, 16.0f, "half height is %f, expected 16", shape.hy);
|
|
TEST_ASSERT_FEQ(errctx, shape.ox, 16.0f, "centre x is %f, expected 16", shape.ox);
|
|
TEST_ASSERT_FEQ(errctx, shape.oy, 16.0f, "centre y is %f, expected 16", shape.oy);
|
|
|
|
// The defaults exist so that giving an actor a hitbox does not silently
|
|
// make it shove every other actor on the map.
|
|
TEST_ASSERT(errctx, (shape.layermask == AKGL_COLLISION_LAYER_ACTOR),
|
|
"a new shape is not on the actor layer");
|
|
TEST_ASSERT(errctx, (shape.collidemask == AKGL_COLLISION_LAYER_STATIC),
|
|
"a new shape responds to something other than map geometry");
|
|
TEST_ASSERT(errctx, (shape.flags == AKGL_COLLISION_FLAG_NONE),
|
|
"a new shape carries flags nobody set");
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_collision_shape_box(NULL, &body, 0.0f),
|
|
"a box with no destination");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_collision_shape_box(&shape, NULL, 0.0f),
|
|
"a box with no rectangle");
|
|
|
|
body.w = 0.0f;
|
|
TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_collision_shape_box(&shape, &body, 0.0f),
|
|
"a box with zero width");
|
|
body.w = -4.0f;
|
|
TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_collision_shape_box(&shape, &body, 0.0f),
|
|
"a box with negative width");
|
|
body.w = 16.0f;
|
|
body.h = 0.0f;
|
|
TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_collision_shape_box(&shape, &body, 0.0f),
|
|
"a box with zero height");
|
|
body.h = 32.0f;
|
|
TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_collision_shape_box(&shape, &body, -1.0f),
|
|
"a box with negative depth");
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_shape_circle_and_capsule(void)
|
|
{
|
|
akgl_CollisionShape shape;
|
|
SDL_FRect wide = { .x = 0.0f, .y = 0.0f, .w = 40.0f, .h = 10.0f };
|
|
SDL_FRect tall = { .x = 0.0f, .y = 0.0f, .w = 10.0f, .h = 40.0f };
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_collision_shape_circle(&shape, 16.0f, 16.0f, 8.0f, 0.0f));
|
|
TEST_ASSERT(errctx, (shape.kind == AKGL_COLLISION_SHAPE_CIRCLE), "a circle is not a circle");
|
|
TEST_ASSERT_FEQ(errctx, shape.hx, 8.0f, "circle radius is %f, expected 8", shape.hx);
|
|
TEST_ASSERT_FEQ(errctx, shape.hy, 8.0f, "a circle's hy should mirror its radius, got %f", shape.hy);
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_VALUE,
|
|
akgl_collision_shape_circle(&shape, 0.0f, 0.0f, 0.0f, 0.0f),
|
|
"a circle of zero radius");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_collision_shape_circle(NULL, 0.0f, 0.0f, 8.0f, 0.0f),
|
|
"a circle with no destination");
|
|
|
|
CATCH(errctx, akgl_collision_shape_capsule(&shape, &wide, AKGL_COLLISION_SHAPE_CAPSULE_X, 0.0f));
|
|
TEST_ASSERT(errctx, (shape.kind == AKGL_COLLISION_SHAPE_CAPSULE_X), "an x capsule is not one");
|
|
CATCH(errctx, akgl_collision_shape_capsule(&shape, &tall, AKGL_COLLISION_SHAPE_CAPSULE_Y, 0.0f));
|
|
TEST_ASSERT(errctx, (shape.kind == AKGL_COLLISION_SHAPE_CAPSULE_Y), "a y capsule is not one");
|
|
|
|
/*
|
|
* A capsule capped on its *short* axis is a circle described
|
|
* confusingly. Refusing it is the difference between a caller getting an
|
|
* error and a caller getting a shape that is not the one they drew.
|
|
*/
|
|
TEST_EXPECT_STATUS(errctx, AKERR_VALUE,
|
|
akgl_collision_shape_capsule(&shape, &tall, AKGL_COLLISION_SHAPE_CAPSULE_X, 0.0f),
|
|
"a tall rectangle capped on x");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_VALUE,
|
|
akgl_collision_shape_capsule(&shape, &wide, AKGL_COLLISION_SHAPE_CAPSULE_Y, 0.0f),
|
|
"a wide rectangle capped on y");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_VALUE,
|
|
akgl_collision_shape_capsule(&shape, &wide, AKGL_COLLISION_SHAPE_BOX, 0.0f),
|
|
"a capsule on an axis that is not an axis");
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief The inequality that keeps a 2D game flat, checked rather than assumed.
|
|
*
|
|
* For any two shapes the setters produce, the overlap along z when they are
|
|
* concentric must exceed the largest penetration they can reach in the plane.
|
|
* If it does not, the narrowphase is entitled to answer "separate along z", and
|
|
* a resolver acting on that pushes the actor into the screen -- invisible, and
|
|
* fatal, because the actor is still inside the floor.
|
|
*
|
|
* This tests the property over a spread of sizes rather than testing that the
|
|
* ratio constant equals 2, so that changing the constant to something that does
|
|
* not work fails here instead of failing in a game.
|
|
*/
|
|
akerr_ErrorContext *test_shape_extrusion_beats_planar_penetration(void)
|
|
{
|
|
akgl_CollisionShape a;
|
|
akgl_CollisionShape b;
|
|
SDL_FRect body;
|
|
float32_t sizes[] = { 1.0f, 4.0f, 16.0f, 32.0f, 512.0f };
|
|
float32_t zoverlap = 0.0f;
|
|
float32_t planarmax = 0.0f;
|
|
float32_t worstz = 0.0f;
|
|
float32_t worstplanar = 0.0f;
|
|
float32_t worsta = 0.0f;
|
|
float32_t worstb = 0.0f;
|
|
bool violated = false;
|
|
int i = 0;
|
|
int j = 0;
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
ATTEMPT {
|
|
/*
|
|
* The check is recorded into a flag and asserted after the loops rather
|
|
* than asserted inside them. TEST_ASSERT expands to FAIL_BREAK, which
|
|
* reports by `break`ing, and a `break` inside a `for` leaves the loop
|
|
* rather than the ATTEMPT block -- so an assertion written in here
|
|
* cannot fail the test. AGENTS.md documents that hazard for CATCH; it
|
|
* applies to every macro in the family, and this test was written with
|
|
* the bug before it was written without it.
|
|
*/
|
|
for ( i = 0; i < (int)(sizeof(sizes) / sizeof(sizes[0])); i++ ) {
|
|
for ( j = 0; j < (int)(sizeof(sizes) / sizeof(sizes[0])); j++ ) {
|
|
body.x = 0.0f;
|
|
body.y = 0.0f;
|
|
body.w = sizes[i];
|
|
body.h = sizes[i];
|
|
if ( akgl_collision_shape_box(&a, &body, 0.0f) != NULL ) {
|
|
violated = true;
|
|
break;
|
|
}
|
|
body.w = sizes[j];
|
|
body.h = sizes[j];
|
|
if ( akgl_collision_shape_box(&b, &body, 0.0f) != NULL ) {
|
|
violated = true;
|
|
break;
|
|
}
|
|
|
|
// Concentric is the worst case: every axis is fully overlapped.
|
|
zoverlap = 2.0f * ((a.hz < b.hz) ? a.hz : b.hz);
|
|
planarmax = 2.0f * ((a.hx < b.hx) ? a.hx : b.hx);
|
|
if ( zoverlap <= planarmax ) {
|
|
violated = true;
|
|
worstz = zoverlap;
|
|
worstplanar = planarmax;
|
|
worsta = sizes[i];
|
|
worstb = sizes[j];
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_ASSERT(errctx, (violated == false),
|
|
"a %fx%f box against a %fx%f one overlaps %f on z and up to %f in the "
|
|
"plane; z is the cheaper separation, so the narrowphase will answer "
|
|
"\"push it into the screen\" and the actor will sink through the floor",
|
|
worsta, worsta, worstb, worstb, worstz, worstplanar);
|
|
|
|
// A depth given by hand is taken at face value. A caller who does that
|
|
// has opted out of the guarantee above, which is why the setters choose
|
|
// for you unless you say otherwise.
|
|
body.w = 16.0f;
|
|
body.h = 16.0f;
|
|
CATCH(errctx, akgl_collision_shape_box(&a, &body, 3.0f));
|
|
TEST_ASSERT_FEQ(errctx, a.hz, 3.0f, "an explicit depth of 3 became %f", a.hz);
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *test_shape_bounds(void)
|
|
{
|
|
akgl_CollisionShape shape;
|
|
SDL_FRect body = { .x = 8.0f, .y = 4.0f, .w = 16.0f, .h = 32.0f };
|
|
SDL_FRect bounds;
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_collision_shape_box(&shape, &body, 0.0f));
|
|
|
|
// At the origin the bounds are the rectangle it was built from. That
|
|
// round trip is the whole contract, and it is easy to get wrong by half
|
|
// an extent in either direction.
|
|
CATCH(errctx, akgl_collision_shape_bounds(&shape, 0.0f, 0.0f, &bounds));
|
|
TEST_ASSERT_FEQ(errctx, bounds.x, 8.0f, "bounds x is %f, expected 8", bounds.x);
|
|
TEST_ASSERT_FEQ(errctx, bounds.y, 4.0f, "bounds y is %f, expected 4", bounds.y);
|
|
TEST_ASSERT_FEQ(errctx, bounds.w, 16.0f, "bounds w is %f, expected 16", bounds.w);
|
|
TEST_ASSERT_FEQ(errctx, bounds.h, 32.0f, "bounds h is %f, expected 32", bounds.h);
|
|
|
|
// And they translate with the owner rather than staying put.
|
|
CATCH(errctx, akgl_collision_shape_bounds(&shape, 100.0f, -50.0f, &bounds));
|
|
TEST_ASSERT_FEQ(errctx, bounds.x, 108.0f, "bounds x is %f, expected 108", bounds.x);
|
|
TEST_ASSERT_FEQ(errctx, bounds.y, -46.0f, "bounds y is %f, expected -46", bounds.y);
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_collision_shape_bounds(NULL, 0.0f, 0.0f, &bounds),
|
|
"bounds of no shape");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_collision_shape_bounds(&shape, 0.0f, 0.0f, NULL),
|
|
"bounds with no destination");
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief The mask test, and the asymmetry that is the reason it exists.
|
|
*/
|
|
akerr_ErrorContext *test_shape_interacts(void)
|
|
{
|
|
akgl_CollisionShape player;
|
|
akgl_CollisionShape npc;
|
|
akgl_CollisionShape none;
|
|
SDL_FRect body = { .x = 0.0f, .y = 0.0f, .w = 16.0f, .h = 16.0f };
|
|
bool interacts = false;
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_collision_shape_box(&player, &body, 0.0f));
|
|
CATCH(errctx, akgl_collision_shape_box(&npc, &body, 0.0f));
|
|
memset(&none, 0x00, sizeof(akgl_CollisionShape));
|
|
|
|
// Out of the box, two actors ignore each other. This is the default that
|
|
// keeps a town full of NPCs from shoving itself apart.
|
|
CATCH(errctx, akgl_collision_shape_interacts(&player, &npc, &interacts));
|
|
TEST_ASSERT(errctx, (interacts == false),
|
|
"two default shapes collide with each other; the default masks are wrong");
|
|
|
|
// The player opts in to blocking against actors. The NPC does not opt in
|
|
// to blocking against the player, and must not be dragged along by it --
|
|
// that asymmetry is the entire point of two masks instead of one.
|
|
player.collidemask |= AKGL_COLLISION_LAYER_ACTOR;
|
|
CATCH(errctx, akgl_collision_shape_interacts(&player, &npc, &interacts));
|
|
TEST_ASSERT(errctx, (interacts == true), "the player does not respond to an actor it opted in to");
|
|
CATCH(errctx, akgl_collision_shape_interacts(&npc, &player, &interacts));
|
|
TEST_ASSERT(errctx, (interacts == false),
|
|
"the NPC responded to the player because the player responded to it");
|
|
|
|
// A shape with no kind takes no part, in either direction.
|
|
CATCH(errctx, akgl_collision_shape_interacts(&player, &none, &interacts));
|
|
TEST_ASSERT(errctx, (interacts == false), "a shape of no kind was collided with");
|
|
CATCH(errctx, akgl_collision_shape_interacts(&none, &player, &interacts));
|
|
TEST_ASSERT(errctx, (interacts == false), "a shape of no kind collided with something");
|
|
|
|
// And neither does a disabled one, which is the cheap way to switch an
|
|
// actor out without losing its shape.
|
|
npc.layermask = AKGL_COLLISION_LAYER_ACTOR;
|
|
npc.flags |= AKGL_COLLISION_FLAG_DISABLED;
|
|
CATCH(errctx, akgl_collision_shape_interacts(&player, &npc, &interacts));
|
|
TEST_ASSERT(errctx, (interacts == false), "a disabled shape was collided with");
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_collision_shape_interacts(NULL, &npc, &interacts),
|
|
"interacts with no self");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_collision_shape_interacts(&player, NULL, &interacts),
|
|
"interacts with no other");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_collision_shape_interacts(&player, &npc, NULL),
|
|
"interacts with no destination");
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief The pool convention, and the window it deliberately leaves open.
|
|
*/
|
|
akerr_ErrorContext *test_proxy_pool(void)
|
|
{
|
|
akgl_CollisionProxy *first = NULL;
|
|
akgl_CollisionProxy *second = NULL;
|
|
akgl_CollisionShape shape;
|
|
SDL_FRect body = { .x = 0.0f, .y = 0.0f, .w = 16.0f, .h = 16.0f };
|
|
akerr_ErrorContext *inner = NULL;
|
|
int claimed = 0;
|
|
int i = 0;
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_heap_init());
|
|
CATCH(errctx, akgl_collision_shape_box(&shape, &body, 0.0f));
|
|
|
|
/*
|
|
* Acquiring does not claim the slot, and that is the convention rather
|
|
* than an oversight: an acquire abandoned before initialization leaks
|
|
* nothing, because the slot still reads as free. The cost is this window,
|
|
* where two acquires in a row return the same pointer -- which is why the
|
|
* library writes the acquire and the initialize adjacent.
|
|
*/
|
|
CATCH(errctx, akgl_heap_next_collision_proxy(&first));
|
|
TEST_ASSERT(errctx, (first->refcount == 0),
|
|
"acquiring a proxy claimed the slot; that is not this pool's convention");
|
|
CATCH(errctx, akgl_heap_next_collision_proxy(&second));
|
|
TEST_ASSERT(errctx, (first == second),
|
|
"two acquires with no initialize between them returned different slots");
|
|
|
|
// Initializing is what closes it.
|
|
CATCH(errctx, akgl_collision_proxy_initialize(first, NULL, &shape, 10.0f, 20.0f, 0.0f));
|
|
TEST_ASSERT(errctx, (first->refcount == 1), "initializing did not take the reference");
|
|
CATCH(errctx, akgl_heap_next_collision_proxy(&second));
|
|
TEST_ASSERT(errctx, (first != second), "an initialized slot was handed out again");
|
|
|
|
// The proxy carries a copy of the shape, not a pointer to it, so a later
|
|
// edit to the caller's shape cannot change what the broad phase indexed.
|
|
shape.hx = 999.0f;
|
|
TEST_ASSERT_FEQ(errctx, first->shape.hx, 8.0f,
|
|
"the proxy tracked an edit to the caller's shape; it holds %f", first->shape.hx);
|
|
// Body {0,0,16,16} centres at (8,8) with half-extents 8, so at owner x=10
|
|
// the bounds start at 10 and are 16 wide.
|
|
TEST_ASSERT_FEQ(errctx, first->bounds.x, 10.0f,
|
|
"proxy bounds x is %f, expected 10", first->bounds.x);
|
|
TEST_ASSERT_FEQ(errctx, first->bounds.y, 20.0f,
|
|
"proxy bounds y is %f, expected 20", first->bounds.y);
|
|
TEST_ASSERT_FEQ(errctx, first->bounds.w, 16.0f,
|
|
"proxy bounds w is %f, expected 16", first->bounds.w);
|
|
|
|
CATCH(errctx, akgl_heap_release_collision_proxy(first));
|
|
TEST_ASSERT(errctx, (first->refcount == 0), "releasing did not free the slot");
|
|
|
|
// Exhaustion is an ordinary reported error, not a crash and not a silent
|
|
// hand-out of a slot somebody else is using.
|
|
CATCH(errctx, akgl_heap_init());
|
|
for ( i = 0; i < (AKGL_MAX_HEAP_COLLISION_PROXY + 4); i++ ) {
|
|
inner = akgl_heap_next_collision_proxy(&first);
|
|
if ( inner != NULL ) {
|
|
break;
|
|
}
|
|
inner = akgl_collision_proxy_initialize(first, NULL, &shape, 0.0f, 0.0f, 0.0f);
|
|
if ( inner != NULL ) {
|
|
break;
|
|
}
|
|
claimed += 1;
|
|
}
|
|
TEST_ASSERT(errctx, (inner != NULL), "the proxy pool never ran out");
|
|
TEST_EXPECT_STATUS(errctx, AKGL_ERR_HEAP, inner, "exhausting the proxy pool");
|
|
inner = NULL;
|
|
TEST_ASSERT(errctx, (claimed == AKGL_MAX_HEAP_COLLISION_PROXY),
|
|
"the pool handed out %d of %d slots before refusing",
|
|
claimed, AKGL_MAX_HEAP_COLLISION_PROXY);
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_heap_next_collision_proxy(NULL),
|
|
"acquiring into no destination");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_heap_release_collision_proxy(NULL),
|
|
"releasing no proxy");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_collision_proxy_initialize(NULL, NULL, &shape, 0.0f, 0.0f, 0.0f),
|
|
"initializing no proxy");
|
|
} CLEANUP {
|
|
if ( inner != NULL ) {
|
|
inner->handled = true;
|
|
inner = akerr_release_error(inner);
|
|
}
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief A proxy must not outlive the actor it points at.
|
|
*
|
|
* akgl_heap_release_actor zeroes the actor slot. A proxy left registered would
|
|
* hold a borrowed `owner` into a slot that now reads as free, and the next
|
|
* contact against it would be a collision with nothing -- so the release has to
|
|
* reach through and take the proxy with it.
|
|
*/
|
|
akerr_ErrorContext *test_proxy_dies_with_its_actor(void)
|
|
{
|
|
akgl_Actor *actor = NULL;
|
|
akgl_CollisionProxy *proxy = NULL;
|
|
akgl_CollisionShape shape;
|
|
SDL_FRect body = { .x = 0.0f, .y = 0.0f, .w = 16.0f, .h = 16.0f };
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_heap_init());
|
|
CATCH(errctx, akgl_registry_init_actor());
|
|
CATCH(errctx, akgl_collision_shape_box(&shape, &body, 0.0f));
|
|
|
|
CATCH(errctx, akgl_heap_next_actor(&actor));
|
|
CATCH(errctx, akgl_actor_initialize(actor, "collider"));
|
|
CATCH(errctx, akgl_heap_next_collision_proxy(&proxy));
|
|
CATCH(errctx, akgl_collision_proxy_initialize(proxy, actor, &shape, 0.0f, 0.0f, 0.0f));
|
|
actor->proxy = proxy;
|
|
|
|
TEST_ASSERT(errctx, (proxy->refcount == 1), "the proxy was not claimed");
|
|
CATCH(errctx, akgl_heap_release_actor(actor));
|
|
TEST_ASSERT(errctx, (proxy->refcount == 0),
|
|
"releasing the actor left its proxy registered, pointing at a freed slot");
|
|
TEST_ASSERT(errctx, (proxy->owner == NULL), "the released proxy still names an owner");
|
|
|
|
// An actor with no proxy releases without complaint, which is every
|
|
// actor a game written before this existed.
|
|
CATCH(errctx, akgl_heap_next_actor(&actor));
|
|
CATCH(errctx, akgl_actor_initialize(actor, "plain"));
|
|
CATCH(errctx, akgl_heap_release_actor(actor));
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief Build a stack proxy around a shape at a position, for a test. */
|
|
static akerr_ErrorContext *at(akgl_CollisionProxy *dest, akgl_CollisionShape *shape, float32_t x, float32_t y)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
PASS(errctx, akgl_collision_proxy_initialize(dest, NULL, shape, x, y, 0.0f));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief The normal points the way a resolver needs it to, and the depth undoes the overlap.
|
|
*
|
|
* The sign convention is the thing every caller would otherwise have to
|
|
* rediscover: the normal points **out of the second shape and toward the
|
|
* first**, so `a` moving along it by `depth` separates them. Getting it
|
|
* backwards does not fail to compile and does not fail a "do these collide"
|
|
* test -- it drags actors *into* walls, which is why it is asserted directly.
|
|
*/
|
|
akerr_ErrorContext *test_narrowphase_box_normal_and_depth(void)
|
|
{
|
|
akgl_CollisionShape shape;
|
|
akgl_CollisionProxy a;
|
|
akgl_CollisionProxy b;
|
|
akgl_Contact contact;
|
|
SDL_FRect body = { .x = 0.0f, .y = 0.0f, .w = 16.0f, .h = 16.0f };
|
|
bool hit = false;
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_collision_shape_box(&shape, &body, 0.0f));
|
|
|
|
// b is 4 to the right of a and overlapping by 12.
|
|
CATCH(errctx, at(&a, &shape, 0.0f, 0.0f));
|
|
CATCH(errctx, at(&b, &shape, 4.0f, 0.0f));
|
|
CATCH(errctx, akgl_collision_test(&a, &b, AKGL_COLLISION_TEST_PLANAR, &contact, &hit));
|
|
TEST_ASSERT(errctx, (hit == true), "two overlapping boxes did not collide");
|
|
TEST_ASSERT_FEQ(errctx, contact.nx, -1.0f,
|
|
"the normal is %f on x; a is left of b so it must leave to the left",
|
|
contact.nx);
|
|
TEST_ASSERT_FEQ(errctx, contact.ny, 0.0f, "a purely horizontal overlap produced ny %f", contact.ny);
|
|
TEST_ASSERT_FEQ(errctx, contact.depth, 12.0f, "depth is %f, expected 12", contact.depth);
|
|
|
|
// Mirrored: the answer must mirror with it.
|
|
CATCH(errctx, at(&b, &shape, -4.0f, 0.0f));
|
|
CATCH(errctx, akgl_collision_test(&a, &b, AKGL_COLLISION_TEST_PLANAR, &contact, &hit));
|
|
TEST_ASSERT(errctx, (hit == true), "the mirrored pair did not collide");
|
|
TEST_ASSERT_FEQ(errctx, contact.nx, 1.0f, "the mirrored normal is %f, expected 1", contact.nx);
|
|
|
|
// Standing on a floor: the smallest overlap is vertical, so the normal is
|
|
// vertical, and it is exactly vertical rather than nearly so.
|
|
CATCH(errctx, at(&b, &shape, 0.0f, 14.0f));
|
|
CATCH(errctx, akgl_collision_test(&a, &b, AKGL_COLLISION_TEST_PLANAR, &contact, &hit));
|
|
TEST_ASSERT(errctx, (hit == true), "an actor resting on a box did not collide");
|
|
TEST_ASSERT_FEQ(errctx, contact.ny, -1.0f, "resting normal is %f on y, expected -1", contact.ny);
|
|
TEST_ASSERT_FEQ(errctx, contact.nx, 0.0f,
|
|
"resting normal has %f on x; a converged normal creeps along a floor",
|
|
contact.nx);
|
|
TEST_ASSERT_FEQ(errctx, contact.depth, 2.0f, "resting depth is %f, expected 2", contact.depth);
|
|
|
|
// Separated, and touching exactly, are both "no push".
|
|
CATCH(errctx, at(&b, &shape, 100.0f, 0.0f));
|
|
CATCH(errctx, akgl_collision_test(&a, &b, AKGL_COLLISION_TEST_PLANAR, &contact, &hit));
|
|
TEST_ASSERT(errctx, (hit == false), "boxes 100 apart collided");
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_collision_test(NULL, &b, 0, &contact, &hit), "a test with no first proxy");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_collision_test(&a, &b, 0, NULL, &hit), "a test with no contact");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
|
akgl_collision_test(&a, &b, 0, &contact, NULL), "a test with no hit flag");
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief A shape whose depth was set badly must still not resolve along z.
|
|
*
|
|
* This is the failure the planar flag exists for, and it is invisible without
|
|
* it: the narrowphase reports a contact, the resolver pushes the actor into the
|
|
* screen, the actor does not move on screen, and it stays inside the floor. The
|
|
* shape here is deliberately built with a depth that defeats the extrusion
|
|
* invariant, which is the only way a caller can reach the case.
|
|
*/
|
|
akerr_ErrorContext *test_narrowphase_planar_guard(void)
|
|
{
|
|
akgl_CollisionShape thin;
|
|
akgl_CollisionShape thincircle;
|
|
akgl_CollisionProxy a;
|
|
akgl_CollisionProxy b;
|
|
akgl_Contact contact;
|
|
SDL_FRect body = { .x = 0.0f, .y = 0.0f, .w = 16.0f, .h = 16.0f };
|
|
bool hit = false;
|
|
float32_t len = 0.0f;
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
ATTEMPT {
|
|
// Depth 0.5 against half-extents of 8: z is now by far the cheapest axis.
|
|
CATCH(errctx, akgl_collision_shape_box(&thin, &body, 0.5f));
|
|
CATCH(errctx, at(&a, &thin, 0.0f, 0.0f));
|
|
CATCH(errctx, at(&b, &thin, 2.0f, 0.0f));
|
|
|
|
// Without the flag the narrowphase is free to answer along z, and does.
|
|
CATCH(errctx, akgl_collision_test(&a, &b, 0, &contact, &hit));
|
|
TEST_ASSERT(errctx, (hit == true), "two thin overlapping boxes did not collide");
|
|
// Either sign is correct -- the pair is concentric on z, so both
|
|
// directions separate it equally. That z was chosen at all is the point.
|
|
TEST_ASSERT_FEQ(errctx, fabsf(contact.nz), 1.0f,
|
|
"a badly extruded pair resolved along %f on z; if this is not +/-1 the "
|
|
"fixture no longer reproduces the case the guard exists for", contact.nz);
|
|
|
|
// With it, the answer is planar and still a unit vector.
|
|
CATCH(errctx, akgl_collision_test(&a, &b, AKGL_COLLISION_TEST_PLANAR, &contact, &hit));
|
|
TEST_ASSERT(errctx, (hit == true), "the guarded test lost the collision");
|
|
TEST_ASSERT_FEQ(errctx, contact.nz, 0.0f, "the guard left %f on z", contact.nz);
|
|
len = sqrtf((contact.nx * contact.nx) + (contact.ny * contact.ny));
|
|
TEST_ASSERT_FEQ(errctx, len, 1.0f, "the flattened normal has length %f, expected 1", len);
|
|
TEST_ASSERT_FEQ(errctx, contact.nx, -1.0f, "the flattened normal is %f on x, expected -1", contact.nx);
|
|
|
|
/*
|
|
* Exactly concentric. There is no planar direction to separate along, and
|
|
* a zero-length normal would be a wall that moves nothing -- so the guard
|
|
* has to invent one rather than pass the degenerate answer through. Level
|
|
* authors put things on top of each other constantly.
|
|
*/
|
|
CATCH(errctx, at(&b, &thin, 0.0f, 0.0f));
|
|
CATCH(errctx, akgl_collision_test(&a, &b, AKGL_COLLISION_TEST_PLANAR, &contact, &hit));
|
|
TEST_ASSERT(errctx, (hit == true), "two concentric shapes did not collide");
|
|
len = sqrtf((contact.nx * contact.nx) + (contact.ny * contact.ny));
|
|
TEST_ASSERT_FEQ(errctx, len, 1.0f,
|
|
"concentric shapes produced a normal of length %f; a zero normal is a "
|
|
"wall that does not stop anything", len);
|
|
TEST_ASSERT(errctx, (contact.depth > 0.0f), "concentric shapes produced depth %f", contact.depth);
|
|
|
|
/*
|
|
* And again through the iterative solver, which is a different branch
|
|
* with its own copy of the guard. Circles cannot take the box fast path,
|
|
* so this is the only way to reach it -- the box version above proves
|
|
* nothing about this code.
|
|
*/
|
|
CATCH(errctx, akgl_collision_shape_circle(&thincircle, 0.0f, 0.0f, 8.0f, 0.5f));
|
|
CATCH(errctx, at(&a, &thincircle, 0.0f, 0.0f));
|
|
CATCH(errctx, at(&b, &thincircle, 2.0f, 0.0f));
|
|
|
|
/*
|
|
* Unlike the closed-form box path, the iterative solver is seeded from
|
|
* the line between the two centres, so for a planar offset it converges
|
|
* to a planar answer and picks z only rarely -- measured, not assumed:
|
|
* this pair comes back with nz of about 0 even unguarded. The guard on
|
|
* this branch is therefore a net rather than a routine correction, and
|
|
* what is asserted below is that it produces a well-formed planar normal,
|
|
* not that it rescues one. The box path above is where the guard earns
|
|
* its place, and that is the assertion that fails if it is removed.
|
|
*/
|
|
CATCH(errctx, akgl_collision_test(&a, &b, AKGL_COLLISION_TEST_PLANAR, &contact, &hit));
|
|
TEST_ASSERT(errctx, (hit == true), "the guarded circle test lost the collision");
|
|
TEST_ASSERT_FEQ(errctx, contact.nz, 0.0f,
|
|
"the iterative solver's guard left %f on z", contact.nz);
|
|
len = sqrtf((contact.nx * contact.nx) + (contact.ny * contact.ny));
|
|
TEST_ASSERT_FEQ(errctx, len, 1.0f,
|
|
"the flattened circle normal has length %f, expected 1", len);
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief The box fast path and the general solver must agree.
|
|
*
|
|
* Two implementations of one answer are two chances to be wrong, and the fast
|
|
* path exists only because it is cheaper and exact. Driving the same
|
|
* arrangements through both -- by asking a box pair, then asking the same
|
|
* geometry as capsules, which cannot take the fast path -- is what keeps the
|
|
* shortcut honest. The agreement is on the *decision* and the *axis*; the
|
|
* iterative solver's depth converges rather than being exact, so it is compared
|
|
* with tolerance.
|
|
*/
|
|
akerr_ErrorContext *test_narrowphase_fast_path_agrees(void)
|
|
{
|
|
akgl_CollisionShape box;
|
|
akgl_CollisionShape circle;
|
|
akgl_CollisionProxy a;
|
|
akgl_CollisionProxy b;
|
|
akgl_Contact boxcontact;
|
|
akgl_Contact mprcontact;
|
|
SDL_FRect body = { .x = -8.0f, .y = -8.0f, .w = 16.0f, .h = 16.0f };
|
|
float32_t offsets[] = { 0.0f, 4.0f, 12.0f, 15.9f, 16.0f, 24.0f };
|
|
bool boxhit = false;
|
|
bool mprhit = false;
|
|
bool disagreed = false;
|
|
float32_t worst = 0.0f;
|
|
int i = 0;
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_collision_shape_box(&box, &body, 0.0f));
|
|
// A circle inscribed in the same box: it cannot take the fast path.
|
|
CATCH(errctx, akgl_collision_shape_circle(&circle, 0.0f, 0.0f, 8.0f, 0.0f));
|
|
|
|
/*
|
|
* Recorded and asserted after the loop, not inside it. TEST_ASSERT
|
|
* reports by breaking, and a break inside a `for` leaves the loop rather
|
|
* than the ATTEMPT block -- an assertion written in here cannot fail.
|
|
*/
|
|
for ( i = 0; i < (int)(sizeof(offsets) / sizeof(offsets[0])); i++ ) {
|
|
if ( at(&a, &box, 0.0f, 0.0f) != NULL ) { disagreed = true; break; }
|
|
if ( at(&b, &box, offsets[i], 0.0f) != NULL ) { disagreed = true; break; }
|
|
if ( akgl_collision_test(&a, &b, AKGL_COLLISION_TEST_PLANAR, &boxcontact, &boxhit) != NULL ) {
|
|
disagreed = true;
|
|
break;
|
|
}
|
|
|
|
if ( at(&a, &circle, 0.0f, 0.0f) != NULL ) { disagreed = true; break; }
|
|
if ( at(&b, &circle, offsets[i], 0.0f) != NULL ) { disagreed = true; break; }
|
|
if ( akgl_collision_test(&a, &b, AKGL_COLLISION_TEST_PLANAR, &mprcontact, &mprhit) != NULL ) {
|
|
disagreed = true;
|
|
break;
|
|
}
|
|
|
|
// Two circles of radius 8 and two boxes of half-extent 8 overlap on
|
|
// this axis over exactly the same range, so the decision must match.
|
|
if ( boxhit != mprhit ) {
|
|
disagreed = true;
|
|
worst = offsets[i];
|
|
break;
|
|
}
|
|
if ( (boxhit == true) && (mprhit == true) ) {
|
|
if ( (boxcontact.nx * mprcontact.nx) < 0.0f ) {
|
|
disagreed = true;
|
|
worst = offsets[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_ASSERT(errctx, (disagreed == false),
|
|
"the box fast path and the iterative solver disagree at an offset of %f; "
|
|
"two implementations of one answer are two chances to be wrong", worst);
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief Build a contact by hand, so a response can be tested without a step. */
|
|
static void contact_at(akgl_Contact *dest, float32_t nx, float32_t ny, float32_t depth, bool sensor)
|
|
{
|
|
memset(dest, 0x00, sizeof(akgl_Contact));
|
|
dest->tilex = -1;
|
|
dest->tiley = -1;
|
|
dest->tilelayer = -1;
|
|
dest->nx = nx;
|
|
dest->ny = ny;
|
|
dest->depth = depth;
|
|
dest->sensor = sensor;
|
|
}
|
|
|
|
/**
|
|
* @brief The blocking response, and the four ways it is easy to write wrongly.
|
|
*/
|
|
akerr_ErrorContext *test_collide_block(void)
|
|
{
|
|
akgl_Actor actor;
|
|
akgl_Contact contact;
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
ATTEMPT {
|
|
// Landing on a floor: the normal points up, out of the surface.
|
|
memset(&actor, 0x00, sizeof(akgl_Actor));
|
|
actor.y = 100.0f;
|
|
actor.ey = 300.0f; // falling
|
|
actor.tx = 90.0f; // and running right
|
|
contact_at(&contact, 0.0f, -1.0f, 4.0f, false);
|
|
|
|
CATCH(errctx, akgl_actor_collide_block(&actor, &contact));
|
|
TEST_ASSERT_FEQ(errctx, actor.y, 96.0f,
|
|
"the actor was not pushed out of the floor; y is %f, expected 96", actor.y);
|
|
TEST_ASSERT_FEQ(errctx, actor.ey, 0.0f,
|
|
"the fall was not stopped; ey is %f", actor.ey);
|
|
|
|
/*
|
|
* The run survives. Removing the whole thrust vector rather than its
|
|
* component into the surface is what makes a character scraping a
|
|
* ceiling stop dead, and it is the bug the sidescroller example has to
|
|
* work around by hand.
|
|
*/
|
|
TEST_ASSERT_FEQ(errctx, actor.tx, 90.0f,
|
|
"landing on a floor cost the actor its horizontal run; tx is %f", actor.tx);
|
|
|
|
// v is recomputed from e + t, because that is what simulate reads.
|
|
TEST_ASSERT_FEQ(errctx, actor.vx, 90.0f, "vx is %f, expected e + t", actor.vx);
|
|
TEST_ASSERT_FEQ(errctx, actor.vy, 0.0f, "vy is %f, expected e + t", actor.vy);
|
|
|
|
/*
|
|
* A separating contact changes nothing but the position. An actor
|
|
* already moving out of a surface that has its velocity zeroed is an
|
|
* actor stuck to a wall it is walking away from.
|
|
*/
|
|
memset(&actor, 0x00, sizeof(akgl_Actor));
|
|
actor.ey = -200.0f; // rising
|
|
contact_at(&contact, 0.0f, -1.0f, 2.0f, false);
|
|
CATCH(errctx, akgl_actor_collide_block(&actor, &contact));
|
|
TEST_ASSERT_FEQ(errctx, actor.ey, -200.0f,
|
|
"a separating velocity was removed; ey is %f, expected -200. An actor "
|
|
"moving away from a surface must not be stopped by it", actor.ey);
|
|
|
|
// The thrust into a wall goes, and the motion along it stays.
|
|
memset(&actor, 0x00, sizeof(akgl_Actor));
|
|
actor.tx = 120.0f; // running right
|
|
actor.ty = 40.0f; // and drifting down
|
|
contact_at(&contact, -1.0f, 0.0f, 3.0f, false); // wall on the right
|
|
CATCH(errctx, akgl_actor_collide_block(&actor, &contact));
|
|
TEST_ASSERT_FEQ(errctx, actor.tx, 0.0f, "the run into the wall survived; tx is %f", actor.tx);
|
|
TEST_ASSERT_FEQ(errctx, actor.ty, 40.0f,
|
|
"hitting a wall cost the actor its vertical motion; ty is %f. Sliding "
|
|
"along a wall is the difference between a wall and glue", actor.ty);
|
|
|
|
// A sensor reports and never pushes.
|
|
memset(&actor, 0x00, sizeof(akgl_Actor));
|
|
actor.x = 50.0f;
|
|
actor.ey = 300.0f;
|
|
contact_at(&contact, 0.0f, -1.0f, 8.0f, true);
|
|
CATCH(errctx, akgl_actor_collide_block(&actor, &contact));
|
|
TEST_ASSERT_FEQ(errctx, actor.x, 50.0f, "a sensor moved the actor; x is %f", actor.x);
|
|
TEST_ASSERT_FEQ(errctx, actor.ey, 300.0f, "a sensor stopped the actor; ey is %f", actor.ey);
|
|
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_actor_collide_block(NULL, &contact),
|
|
"a response with no actor");
|
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_actor_collide_block(&actor, NULL),
|
|
"a response with no contact");
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief Every actor gets the default, and a game can replace it. */
|
|
akerr_ErrorContext *test_collidefunc_is_installed(void)
|
|
{
|
|
akgl_Actor *actor = NULL;
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_heap_init());
|
|
CATCH(errctx, akgl_registry_init_actor());
|
|
CATCH(errctx, akgl_heap_next_actor(&actor));
|
|
CATCH(errctx, akgl_actor_initialize(actor, "hooked"));
|
|
|
|
TEST_ASSERT(errctx, (actor->collidefunc == &akgl_actor_collide_block),
|
|
"akgl_actor_initialize did not install the default collision response");
|
|
|
|
// The other six are still there. A seventh hook that displaced one of
|
|
// them would be a silent regression in something unrelated.
|
|
TEST_ASSERT(errctx, (actor->updatefunc != NULL), "updatefunc was lost");
|
|
TEST_ASSERT(errctx, (actor->renderfunc != NULL), "renderfunc was lost");
|
|
TEST_ASSERT(errctx, (actor->facefunc != NULL), "facefunc was lost");
|
|
TEST_ASSERT(errctx, (actor->movementlogicfunc != NULL), "movementlogicfunc was lost");
|
|
TEST_ASSERT(errctx, (actor->changeframefunc != NULL), "changeframefunc was lost");
|
|
TEST_ASSERT(errctx, (actor->addchild != NULL), "addchild was lost");
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_error_init());
|
|
CATCH(errctx, test_shape_box());
|
|
CATCH(errctx, test_shape_circle_and_capsule());
|
|
CATCH(errctx, test_shape_extrusion_beats_planar_penetration());
|
|
CATCH(errctx, test_shape_bounds());
|
|
CATCH(errctx, test_shape_interacts());
|
|
CATCH(errctx, test_proxy_pool());
|
|
CATCH(errctx, test_proxy_dies_with_its_actor());
|
|
CATCH(errctx, test_narrowphase_box_normal_and_depth());
|
|
CATCH(errctx, test_narrowphase_planar_guard());
|
|
CATCH(errctx, test_narrowphase_fast_path_agrees());
|
|
CATCH(errctx, test_collide_block());
|
|
CATCH(errctx, test_collidefunc_is_installed());
|
|
} CLEANUP {
|
|
} PROCESS(errctx) {
|
|
} FINISH_NORETURN(errctx);
|
|
|
|
return 0;
|
|
}
|