/** * @file partition.c * @brief The broad phase, checked against brute force. * * A broad phase has one contract and it is asymmetric: it may report a pair that * does not overlap, and it may never miss one that does. Over-reporting costs a * narrowphase call; under-reporting is a wall an actor walks through, and it * fails silently and intermittently, only for the positions where the structure * happens to be wrong. * * So every query here is compared against a linear scan over the same proxies, * and the assertion is containment in one direction and not equality. That is * the only check that has any teeth: a broad phase can be tested against itself * all day and agree with itself all day. * * The suite is written as a table over partitioners so the same assertions run * against every implementation. A second implementation that is not held to the * first one's contract is not pluggable, it is just present. */ #include #include #include #include #include #include #include #include "testutil.h" /** @brief Every partitioner the factory knows, so the same tests run over each. */ static char *partitioners[] = { "grid" }; /** @brief What a visit callback accumulates. */ typedef struct { int16_t seen[AKGL_MAX_HEAP_COLLISION_PROXY]; int count; } visit_record; static akerr_ErrorContext *record_visit(akgl_CollisionProxy *proxy, void *data) { visit_record *rec = (visit_record *)data; PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, proxy, AKERR_NULLPOINTER, "NULL proxy in a visit"); FAIL_ZERO_RETURN(errctx, rec, AKERR_NULLPOINTER, "NULL record in a visit"); if ( rec->count < AKGL_MAX_HEAP_COLLISION_PROXY ) { rec->seen[rec->count] = (int16_t)(proxy - &akgl_heap_collision_proxies[0]); rec->count += 1; } SUCCEED_RETURN(errctx); } /** @brief How many cell entries are currently claimed from the pool. */ static int live_cells(void) { int live = 0; int i = 0; for ( i = 0; i < AKGL_MAX_HEAP_COLLISION_CELL; i++ ) { if ( akgl_heap_collision_cells[i].refcount != 0 ) { live += 1; } } return live; } /** @brief Was this proxy index reported? */ static bool sawit(visit_record *rec, int16_t idx) { int i = 0; for ( i = 0; i < rec->count; i++ ) { if ( rec->seen[i] == idx ) { return true; } } return false; } /** @brief Claim a proxy at a position and put it in the partitioner. */ static akerr_ErrorContext *spawn(akgl_CollisionWorld *world, akgl_CollisionShape *shape, float32_t x, float32_t y, akgl_CollisionProxy **dest) { PREPARE_ERROR(errctx); PASS(errctx, akgl_heap_next_collision_proxy(dest)); PASS(errctx, akgl_collision_proxy_initialize(*dest, NULL, shape, x, y, 0.0f)); PASS(errctx, world->partitioner.insert(&world->partitioner, *dest)); SUCCEED_RETURN(errctx); } /** * @brief A query must report every proxy a linear scan would, and may report more. * * Positions are chosen to land on cell boundaries, span several cells, and sit * outside the grid entirely, because those are the three places an index is * wrong and a linear scan is not. */ akerr_ErrorContext *test_query_never_misses(char *name) { akgl_CollisionWorld world; akgl_CollisionShape small; akgl_CollisionShape big; akgl_CollisionProxy *proxies[16]; visit_record rec; SDL_FRect area; SDL_FRect small_body = { .x = 0.0f, .y = 0.0f, .w = 16.0f, .h = 16.0f }; SDL_FRect big_body = { .x = 0.0f, .y = 0.0f, .w = 96.0f, .h = 96.0f }; float32_t spots[][2] = { { 0.0f, 0.0f }, { 16.0f, 0.0f }, { 32.0f, 32.0f }, { 8.0f, 8.0f }, { 100.0f, 100.0f }, { 1000.0f, 40.0f }, { -40.0f, -40.0f }, { 15.9f, 15.9f } }; int spotcount = (int)(sizeof(spots) / sizeof(spots[0])); bool missed = false; float32_t missx = 0.0f; float32_t missy = 0.0f; int i = 0; int q = 0; PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, akgl_heap_init()); CATCH(errctx, akgl_collision_world_init(&world, name, 16.0f, 16.0f)); CATCH(errctx, akgl_collision_shape_box(&small, &small_body, 0.0f)); CATCH(errctx, akgl_collision_shape_box(&big, &big_body, 0.0f)); for ( i = 0; i < spotcount; i++ ) { CATCH(errctx, spawn(&world, &small, spots[i][0], spots[i][1], &proxies[i])); } // One deliberately large enough to spill onto the oversized chain, which // is a separate code path a query must not forget to walk. CATCH(errctx, spawn(&world, &big, 200.0f, 200.0f, &proxies[spotcount])); /* * Sweep a query window across the world and compare against a linear * scan every time. Recorded into a flag and asserted after the loops: a * TEST_ASSERT in here reports by breaking, which would leave the loop * rather than the ATTEMPT block and could not fail the test. */ for ( q = 0; q < 40; q++ ) { area.x = (float32_t)((q * 13) % 300) - 50.0f; area.y = (float32_t)((q * 7) % 300) - 50.0f; area.w = 24.0f; area.h = 24.0f; memset(&rec, 0x00, sizeof(rec)); if ( world.partitioner.query(&world.partitioner, &area, AKGL_COLLISION_LAYER_ALL, &record_visit, &rec) != NULL ) { missed = true; break; } for ( i = 0; i <= spotcount; i++ ) { if ( !SDL_HasRectIntersectionFloat(&proxies[i]->bounds, &area) ) { continue; } if ( !sawit(&rec, (int16_t)(proxies[i] - &akgl_heap_collision_proxies[0])) ) { missed = true; missx = area.x; missy = area.y; } } } TEST_ASSERT(errctx, (missed == false), "%s missed a proxy that overlaps the query at (%f, %f); a broad phase may " "over-report and may never under-report", name, missx, missy); // A query is allowed to report a proxy more than the once, but must not: // a proxy spanning four cells appears on four chains. area.x = 0.0f; area.y = 0.0f; area.w = 300.0f; area.h = 300.0f; memset(&rec, 0x00, sizeof(rec)); CATCH(errctx, world.partitioner.query(&world.partitioner, &area, AKGL_COLLISION_LAYER_ALL, &record_visit, &rec)); TEST_ASSERT(errctx, (rec.count <= (spotcount + 1)), "%s reported %d proxies where at most %d exist; a proxy spanning several " "cells is being reported once per cell", name, rec.count, spotcount + 1); } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } /** * @brief The layer mask filters, and filters in the right direction. */ akerr_ErrorContext *test_query_respects_mask(char *name) { akgl_CollisionWorld world; akgl_CollisionShape actor; akgl_CollisionShape scenery; akgl_CollisionProxy *a = NULL; akgl_CollisionProxy *b = NULL; visit_record rec; SDL_FRect body = { .x = 0.0f, .y = 0.0f, .w = 16.0f, .h = 16.0f }; SDL_FRect area = { .x = -50.0f, .y = -50.0f, .w = 200.0f, .h = 200.0f }; PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, akgl_heap_init()); CATCH(errctx, akgl_collision_world_init(&world, name, 16.0f, 16.0f)); CATCH(errctx, akgl_collision_shape_box(&actor, &body, 0.0f)); CATCH(errctx, akgl_collision_shape_box(&scenery, &body, 0.0f)); scenery.layermask = AKGL_COLLISION_LAYER_STATIC; CATCH(errctx, spawn(&world, &actor, 0.0f, 0.0f, &a)); CATCH(errctx, spawn(&world, &scenery, 32.0f, 0.0f, &b)); memset(&rec, 0x00, sizeof(rec)); CATCH(errctx, world.partitioner.query(&world.partitioner, &area, AKGL_COLLISION_LAYER_STATIC, &record_visit, &rec)); TEST_ASSERT(errctx, (rec.count == 1), "%s reported %d proxies on the static layer, expected 1", name, rec.count); TEST_ASSERT(errctx, sawit(&rec, (int16_t)(b - &akgl_heap_collision_proxies[0])), "%s filtered out the static proxy and kept the actor", name); memset(&rec, 0x00, sizeof(rec)); CATCH(errctx, world.partitioner.query(&world.partitioner, &area, AKGL_COLLISION_LAYER_NONE, &record_visit, &rec)); TEST_ASSERT(errctx, (rec.count == 0), "%s reported %d proxies for an empty mask", name, rec.count); } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } /** * @brief Moving a proxy has to update the index, and moving it nowhere has not to. * * The first half is the correctness claim: a stale entry in the old cell is the * classic incremental-structure bug, and it shows up as a collision with where * something used to be. The second half is the *performance* claim the whole * structure was chosen for, and it is asserted rather than assumed. */ akerr_ErrorContext *test_move_is_incremental(char *name) { akgl_CollisionWorld world; akgl_CollisionShape shape; akgl_CollisionProxy *proxy = NULL; visit_record rec; SDL_FRect body = { .x = 0.0f, .y = 0.0f, .w = 8.0f, .h = 8.0f }; SDL_FRect here = { .x = 0.0f, .y = 0.0f, .w = 8.0f, .h = 8.0f }; SDL_FRect there = { .x = 200.0f, .y = 200.0f, .w = 8.0f, .h = 8.0f }; int16_t firstbefore = 0; int before = 0; int i = 0; PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, akgl_heap_init()); CATCH(errctx, akgl_collision_world_init(&world, name, 16.0f, 16.0f)); CATCH(errctx, akgl_collision_shape_box(&shape, &body, 0.0f)); CATCH(errctx, spawn(&world, &shape, 0.0f, 0.0f, &proxy)); // Move it a long way and re-query both ends. CATCH(errctx, akgl_collision_proxy_sync(proxy, &shape, 200.0f, 200.0f, 0.0f)); CATCH(errctx, world.partitioner.move(&world.partitioner, proxy)); memset(&rec, 0x00, sizeof(rec)); CATCH(errctx, world.partitioner.query(&world.partitioner, &here, AKGL_COLLISION_LAYER_ALL, &record_visit, &rec)); TEST_ASSERT(errctx, (rec.count == 0), "%s still reports a proxy where it used to be; the old cell entry was not " "removed, and a game would collide with a ghost", name); memset(&rec, 0x00, sizeof(rec)); CATCH(errctx, world.partitioner.query(&world.partitioner, &there, AKGL_COLLISION_LAYER_ALL, &record_visit, &rec)); TEST_ASSERT(errctx, (rec.count == 1), "%s does not report the proxy where it now is", name); /* * A move within the cells already occupied must touch nothing. That is * the property the uniform grid was chosen for over a tree, so it is * checked rather than trusted: the proxy's chain head is the cheapest * observable proof that no entry was released and re-claimed. */ firstbefore = proxy->first; CATCH(errctx, akgl_collision_proxy_sync(proxy, &shape, 201.0f, 201.0f, 0.0f)); CATCH(errctx, world.partitioner.move(&world.partitioner, proxy)); TEST_ASSERT(errctx, (proxy->first == firstbefore), "%s re-celled a proxy that did not leave its cells; the structure is not " "incremental and the argument for choosing it does not hold", name); /* * Moving repeatedly must not accumulate index entries. A move that links * into the new cells without unlinking the old ones answers *queries* * correctly -- the bounds re-check filters the stale entries out -- so * nothing above catches it. What it does instead is leak the cell pool * until the grid stops working, on a timescale no unit test reaches by * accident. Counting the pool is the only thing that sees it. */ before = live_cells(); for ( i = 0; i < 200; i++ ) { CATCH(errctx, akgl_collision_proxy_sync(proxy, &shape, (float32_t)((i * 37) % 400), (float32_t)((i * 53) % 400), 0.0f)); CATCH(errctx, world.partitioner.move(&world.partitioner, proxy)); } TEST_ASSERT(errctx, (live_cells() <= before), "%s holds %d cell entries for one proxy after 200 moves, up from %d; the " "old entries are not being unlinked and the pool is leaking", name, live_cells(), before); // Removing is idempotent, because a caller tearing down should not have // to remember what it already dropped. CATCH(errctx, world.partitioner.remove(&world.partitioner, proxy)); CATCH(errctx, world.partitioner.remove(&world.partitioner, proxy)); memset(&rec, 0x00, sizeof(rec)); CATCH(errctx, world.partitioner.query(&world.partitioner, &there, AKGL_COLLISION_LAYER_ALL, &record_visit, &rec)); TEST_ASSERT(errctx, (rec.count == 0), "%s still reports a removed proxy", name); } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } /** @brief What each_pair accumulates. */ typedef struct { int count; int selfpairs; } pair_record; static akerr_ErrorContext *record_pair(akgl_CollisionProxy *a, akgl_CollisionProxy *b, void *data) { pair_record *rec = (pair_record *)data; PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, a, AKERR_NULLPOINTER, "NULL first proxy in a pair"); FAIL_ZERO_RETURN(errctx, b, AKERR_NULLPOINTER, "NULL second proxy in a pair"); FAIL_ZERO_RETURN(errctx, rec, AKERR_NULLPOINTER, "NULL record in a pair"); if ( a == b ) { rec->selfpairs += 1; } rec->count += 1; SUCCEED_RETURN(errctx); } /** * @brief Every overlapping pair is reported, and none is reported twice. * * Two proxies sharing four cells is the case that produces four reports from a * naive implementation, and four reports means four impulses for one contact. */ akerr_ErrorContext *test_each_pair_is_once(char *name) { akgl_CollisionWorld world; akgl_CollisionShape shape; akgl_CollisionProxy *proxies[4]; pair_record rec; SDL_FRect body = { .x = 0.0f, .y = 0.0f, .w = 40.0f, .h = 40.0f }; int i = 0; PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, akgl_heap_init()); CATCH(errctx, akgl_collision_world_init(&world, name, 16.0f, 16.0f)); // 40 pixels on a 16 pixel grid: each of these covers nine cells, so every // pair among them shares several. CATCH(errctx, akgl_collision_shape_box(&shape, &body, 0.0f)); for ( i = 0; i < 4; i++ ) { CATCH(errctx, spawn(&world, &shape, (float32_t)(i * 4), 0.0f, &proxies[i])); } memset(&rec, 0x00, sizeof(rec)); CATCH(errctx, world.partitioner.each_pair(&world.partitioner, &record_pair, &rec)); // Four mutually overlapping proxies are six distinct pairs. TEST_ASSERT(errctx, (rec.selfpairs == 0), "%s paired a proxy with itself %d times", name, rec.selfpairs); TEST_ASSERT(errctx, (rec.count == 6), "%s reported %d pairs among four mutually overlapping proxies, expected 6; " "duplicates mean one contact resolved several times", name, rec.count); } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } /** @brief An empty world answers nothing, and reset really empties it. */ akerr_ErrorContext *test_reset_and_empty(char *name) { akgl_CollisionWorld world; akgl_CollisionShape shape; akgl_CollisionProxy *proxy = NULL; visit_record rec; SDL_FRect body = { .x = 0.0f, .y = 0.0f, .w = 16.0f, .h = 16.0f }; SDL_FRect area = { .x = -100.0f, .y = -100.0f, .w = 400.0f, .h = 400.0f }; PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, akgl_heap_init()); CATCH(errctx, akgl_collision_world_init(&world, name, 16.0f, 16.0f)); // The null hypothesis. A broken structure that reports nothing passes // every other test in this file, so this one is not a formality. memset(&rec, 0x00, sizeof(rec)); CATCH(errctx, world.partitioner.query(&world.partitioner, &area, AKGL_COLLISION_LAYER_ALL, &record_visit, &rec)); TEST_ASSERT(errctx, (rec.count == 0), "%s reported %d proxies in an empty world", name, rec.count); CATCH(errctx, spawn(&world, &shape, 0.0f, 0.0f, &proxy)); CATCH(errctx, akgl_collision_shape_box(&shape, &body, 0.0f)); CATCH(errctx, akgl_collision_proxy_sync(proxy, &shape, 0.0f, 0.0f, 0.0f)); CATCH(errctx, world.partitioner.move(&world.partitioner, proxy)); memset(&rec, 0x00, sizeof(rec)); CATCH(errctx, world.partitioner.query(&world.partitioner, &area, AKGL_COLLISION_LAYER_ALL, &record_visit, &rec)); TEST_ASSERT(errctx, (rec.count == 1), "%s lost the only proxy in the world", name); CATCH(errctx, world.partitioner.reset(&world.partitioner, &world)); memset(&rec, 0x00, sizeof(rec)); CATCH(errctx, world.partitioner.query(&world.partitioner, &area, AKGL_COLLISION_LAYER_ALL, &record_visit, &rec)); TEST_ASSERT(errctx, (rec.count == 0), "%s still reports %d proxies after a reset", name, rec.count); } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } /** * @brief A small map with a floor, a pillar, a gap and a one-tile corridor. * * Synthesized rather than loaded: collision reads `data[]`, `tilewidth`, * `tileheight`, `width` and `height` and nothing else, so a fixture needs no * image, no tileset and no loader. `tests/perf_render.c` builds its map the same * way for the same reason. * * Every shape here is one the two example games actually hit -- a floor to rest * on, a pillar to walk into, a gap to fall through, a corridor that blocks both * axes at once. */ /* * Static, not on the stack. sizeof(akgl_Tilemap) is about 26 MB -- the layer and * tileset arrays are sized for the worst case the format allows -- so a local * one overflows the stack before the first assertion runs. tilemap.h says so and * this fixture was written wrong once anyway. */ static akgl_Tilemap fixture_map; static void build_map(akgl_Tilemap *map) { int x = 0; memset(map, 0x00, sizeof(akgl_Tilemap)); map->tilewidth = 16; map->tileheight = 16; map->width = 20; map->height = 10; map->numlayers = 2; map->layers[0].type = AKGL_TILEMAP_LAYER_TYPE_TILES; map->layers[1].type = AKGL_TILEMAP_LAYER_TYPE_TILES; // Layer 0 is scenery and is not collidable. Layer 1 is terrain and is. If // the two were confused, an actor would collide with the background. for ( x = 0; x < map->width; x++ ) { map->layers[0].data[(2 * map->width) + x] = 99; } // A floor along row 9, with a two-tile gap at columns 5 and 6. for ( x = 0; x < map->width; x++ ) { if ( (x == 5) || (x == 6) ) { continue; } map->layers[1].data[(9 * map->width) + x] = 1; } // A one-tile pillar at column 12, rows 7 and 8. map->layers[1].data[(7 * map->width) + 12] = 1; map->layers[1].data[(8 * map->width) + 12] = 1; map->collidablelayers = (1u << 1); } /** * @brief Solid tiles block, non-collidable layers do not, and off-map is open. */ akerr_ErrorContext *test_tiles_are_geometry(char *name) { akgl_CollisionWorld world; akgl_Tilemap *map = &fixture_map; SDL_FRect box; bool solid = false; bool blocked = false; PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, akgl_heap_init()); CATCH(errctx, akgl_collision_world_init(&world, name, 16.0f, 16.0f)); build_map(map); CATCH(errctx, akgl_collision_bind_tilemap(&world, map)); TEST_ASSERT(errctx, (world.cellwidth == 16.0f), "binding a map left the cell width at %f rather than the tile width", world.cellwidth); // On the floor. CATCH(errctx, akgl_collision_solid_at(&world, 8.0f, 152.0f, &solid)); TEST_ASSERT(errctx, (solid == true), "%s does not see the floor tile at row 9", name); // In the gap in the floor. CATCH(errctx, akgl_collision_solid_at(&world, 88.0f, 152.0f, &solid)); TEST_ASSERT(errctx, (solid == false), "%s reports the gap in the floor as solid", name); // On the scenery layer, which is not collidable. Getting this wrong // means an actor colliding with the background. CATCH(errctx, akgl_collision_solid_at(&world, 8.0f, 40.0f, &solid)); TEST_ASSERT(errctx, (solid == false), "%s treats a non-collidable layer as solid; `collidable` is being ignored", name); // Off the map is open, not solid. A pit has to be a pit. CATCH(errctx, akgl_collision_solid_at(&world, -100.0f, -100.0f, &solid)); TEST_ASSERT(errctx, (solid == false), "%s reports off-map as solid", name); CATCH(errctx, akgl_collision_solid_at(&world, 8.0f, 10000.0f, &solid)); TEST_ASSERT(errctx, (solid == false), "%s reports below the map as solid", name); // A box resting exactly on the floor must not read as inside it. Without // the epsilon on the far edge, an actor standing flush against geometry // reads as overlapping it and every move it tries looks blocked. box.x = 0.0f; box.y = 128.0f; box.w = 16.0f; box.h = 16.0f; CATCH(errctx, akgl_collision_box_blocked(&world, &box, AKGL_COLLISION_LAYER_ALL, &blocked)); TEST_ASSERT(errctx, (blocked == false), "%s reports a box resting exactly on the floor as inside it; an actor " "standing on the ground would be unable to move", name); // One pixel lower and it is inside. box.y = 129.0f; CATCH(errctx, akgl_collision_box_blocked(&world, &box, AKGL_COLLISION_LAYER_ALL, &blocked)); TEST_ASSERT(errctx, (blocked == true), "%s does not see a box overlapping the floor", name); // The pillar blocks horizontally. box.x = 190.0f; box.y = 112.0f; CATCH(errctx, akgl_collision_box_blocked(&world, &box, AKGL_COLLISION_LAYER_ALL, &blocked)); TEST_ASSERT(errctx, (blocked == true), "%s does not see the pillar", name); // Detaching the map leaves nothing solid. CATCH(errctx, akgl_collision_bind_tilemap(&world, NULL)); CATCH(errctx, akgl_collision_solid_at(&world, 8.0f, 152.0f, &solid)); TEST_ASSERT(errctx, (solid == false), "%s still sees tiles after the map was detached", name); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_collision_solid_at(NULL, 0.0f, 0.0f, &solid), "a point query with no world"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_collision_box_blocked(&world, &box, 0, NULL), "a box query with no destination"); } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } /** * @brief Settling lifts a spawn out of the ground, and refuses when it cannot. */ akerr_ErrorContext *test_settle_lifts_a_spawn(char *name) { akgl_CollisionWorld world; akgl_Tilemap *map = &fixture_map; akgl_CollisionShape shape; SDL_FRect body = { .x = 0.0f, .y = 0.0f, .w = 16.0f, .h = 16.0f }; float32_t x = 0.0f; float32_t y = 0.0f; bool blocked = false; SDL_FRect box; PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, akgl_heap_init()); CATCH(errctx, akgl_collision_world_init(&world, name, 16.0f, 16.0f)); build_map(map); CATCH(errctx, akgl_collision_bind_tilemap(&world, map)); CATCH(errctx, akgl_collision_shape_box(&shape, &body, 0.0f)); // Placed straddling the floor, which is what an editor does when an // object is dropped near a step. x = 32.0f; y = 140.0f; CATCH(errctx, akgl_collision_settle(&world, &shape, &x, &y, 0)); TEST_ASSERT(errctx, (y < 140.0f), "%s did not lift a shape spawned inside the floor", name); CATCH(errctx, akgl_collision_shape_bounds(&shape, x, y, &box)); CATCH(errctx, akgl_collision_box_blocked(&world, &box, AKGL_COLLISION_LAYER_STATIC, &blocked)); TEST_ASSERT(errctx, (blocked == false), "%s settled a shape that is still inside geometry", name); // Already clear: settling must not move it at all. x = 32.0f; y = 40.0f; CATCH(errctx, akgl_collision_settle(&world, &shape, &x, &y, 0)); TEST_ASSERT_FEQ(errctx, y, 40.0f, "%s moved a shape that was already clear, to %f", name, y); } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *test_world_and_factory(void) { akgl_CollisionWorld world; akgl_Partitioner part; PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, akgl_collision_world_init(&world, NULL, 16.0f, 16.0f)); TEST_ASSERT(errctx, (strcmp(world.partitioner.name, "grid") == 0), "a NULL partitioner name installed \"%s\" rather than the default", world.partitioner.name); TEST_ASSERT(errctx, ((world.flags & AKGL_COLLISION_TEST_PLANAR) != 0), "a new world does not force planar normals; every game today is 2D"); CATCH(errctx, akgl_partitioner_factory(&part, "grid")); TEST_ASSERT(errctx, (part.query != NULL), "the grid installed no query"); TEST_ASSERT(errctx, (part.move != NULL), "the grid installed no move"); TEST_EXPECT_STATUS(errctx, AKERR_KEY, akgl_partitioner_factory(&part, "quadtree"), "a partitioner nobody wrote"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_partitioner_factory(NULL, "grid"), "the factory with no destination"); TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_collision_world_init(&world, NULL, 0.0f, 16.0f), "a world with no cell width"); TEST_EXPECT_STATUS(errctx, AKERR_VALUE, akgl_collision_world_init(&world, NULL, 16.0f, -1.0f), "a world with negative cell height"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_collision_world_init(NULL, NULL, 16.0f, 16.0f), "a world with no destination"); } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } int main(void) { akerr_ErrorContext *inner = NULL; int i = 0; PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, akgl_error_init()); CATCH(errctx, test_world_and_factory()); /* * Every partitioner is held to the same assertions. Collected rather * than CATCHed inside the loop, because CATCH reports by breaking and a * break here would leave the loop rather than the ATTEMPT block, so a * failing partitioner would be silently skipped. */ for ( i = 0; i < (int)(sizeof(partitioners) / sizeof(partitioners[0])); i++ ) { inner = test_query_never_misses(partitioners[i]); if ( inner != NULL ) { break; } inner = test_query_respects_mask(partitioners[i]); if ( inner != NULL ) { break; } inner = test_move_is_incremental(partitioners[i]); if ( inner != NULL ) { break; } inner = test_each_pair_is_once(partitioners[i]); if ( inner != NULL ) { break; } inner = test_reset_and_empty(partitioners[i]); if ( inner != NULL ) { break; } inner = test_tiles_are_geometry(partitioners[i]); if ( inner != NULL ) { break; } inner = test_settle_lifts_a_spawn(partitioners[i]); if ( inner != NULL ) { break; } } CATCH(errctx, inner); } CLEANUP { } PROCESS(errctx) { } FINISH_NORETURN(errctx); return 0; }