Restore ECS

This commit is contained in:
2026-07-18 19:05:51 -05:00
parent 11e5d37563
commit 1fa5cd316e
37 changed files with 3130 additions and 1033 deletions
+2 -1
View File
@@ -9,7 +9,8 @@ add_subdirectory(error)
add_subdirectory(network)
add_subdirectory(thread)
add_subdirectory(display)
add_subdirectory(rpg)
add_subdirectory(entity)
add_subdirectory(scene)
# add_subdirectory(item)
add_subdirectory(time)
add_subdirectory(util)
@@ -6,7 +6,5 @@
include(dusktest)
# Tests
dusktest(test_maparea.c)
dusktest(test_tileshape.c)
# Subdirs
dusktest(test_entitymanager.c)
dusktest(test_entityposition.c)
+97
View File
@@ -0,0 +1,97 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "util/memory.h"
#include "entity/entitymanager.h"
#include "entity/component/display/entityposition.h"
static void test_entityManagerAddIsolatesEntities(void **state) {
entitymanager_t mgr;
entityManagerInit(&mgr);
entityid_t a = entityManagerAdd(&mgr);
entityid_t b = entityManagerAdd(&mgr);
assert_true(a != ENTITY_ID_INVALID);
assert_true(b != ENTITY_ID_INVALID);
assert_true(a != b);
entityManagerDispose(&mgr);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityManagerFullAsserts(void **state) {
entitymanager_t mgr;
entityManagerInit(&mgr);
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
assert_true(entityManagerAdd(&mgr) != ENTITY_ID_INVALID);
}
expect_assert_failure(entityManagerAdd(&mgr));
entityManagerDispose(&mgr);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityAddGetDisposeComponent(void **state) {
entitymanager_t mgr;
entityManagerInit(&mgr);
entityid_t entity = entityManagerAdd(&mgr);
assert_int_equal(
entityGetComponent(&mgr, entity, COMPONENT_TYPE_POSITION),
COMPONENT_ID_INVALID
);
componentid_t posComp = entityAddComponent(
&mgr, entity, COMPONENT_TYPE_POSITION
);
assert_true(posComp != COMPONENT_ID_INVALID);
assert_int_equal(
entityGetComponent(&mgr, entity, COMPONENT_TYPE_POSITION), posComp
);
entityposition_t *pos = entityPositionGet(&mgr, entity, posComp);
assert_non_null(pos);
entityDispose(&mgr, entity);
assert_int_equal(
entityGetComponent(&mgr, entity, COMPONENT_TYPE_POSITION),
COMPONENT_ID_INVALID
);
assert_true((mgr.entities[entity].state & ENTITY_STATE_ACTIVE) == 0);
entityManagerDispose(&mgr);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityAddDuplicateComponentAsserts(void **state) {
entitymanager_t mgr;
entityManagerInit(&mgr);
entityid_t entity = entityManagerAdd(&mgr);
entityAddComponent(&mgr, entity, COMPONENT_TYPE_POSITION);
expect_assert_failure(
entityAddComponent(&mgr, entity, COMPONENT_TYPE_POSITION)
);
entityManagerDispose(&mgr);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(int argc, char **argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_entityManagerAddIsolatesEntities),
cmocka_unit_test(test_entityManagerFullAsserts),
cmocka_unit_test(test_entityAddGetDisposeComponent),
cmocka_unit_test(test_entityAddDuplicateComponentAsserts),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+130
View File
@@ -0,0 +1,130 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "util/memory.h"
#include "entity/entitymanager.h"
#include "entity/component/display/entityposition.h"
static void test_entityPositionLocalGetSet(void **state) {
entitymanager_t mgr;
entityManagerInit(&mgr);
entityid_t entity = entityManagerAdd(&mgr);
componentid_t comp = entityAddComponent(
&mgr, entity, COMPONENT_TYPE_POSITION
);
vec3 setPos = { 1.0f, 2.0f, 3.0f };
entityPositionSetLocalPosition(&mgr, entity, comp, setPos);
vec3 getPos;
entityPositionGetLocalPosition(&mgr, entity, comp, getPos);
assert_float_equal(getPos[0], 1.0f, 0.0001f);
assert_float_equal(getPos[1], 2.0f, 0.0001f);
assert_float_equal(getPos[2], 3.0f, 0.0001f);
// World position of a parentless entity matches local position.
vec3 worldPos;
entityPositionGetWorldPosition(&mgr, entity, comp, worldPos);
assert_float_equal(worldPos[0], 1.0f, 0.0001f);
assert_float_equal(worldPos[1], 2.0f, 0.0001f);
assert_float_equal(worldPos[2], 3.0f, 0.0001f);
entityManagerDispose(&mgr);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityPositionParentChildWorldTransform(void **state) {
entitymanager_t mgr;
entityManagerInit(&mgr);
entityid_t parent = entityManagerAdd(&mgr);
componentid_t parentComp = entityAddComponent(
&mgr, parent, COMPONENT_TYPE_POSITION
);
entityid_t child = entityManagerAdd(&mgr);
componentid_t childComp = entityAddComponent(
&mgr, child, COMPONENT_TYPE_POSITION
);
vec3 parentPos = { 10.0f, 0.0f, 0.0f };
entityPositionSetLocalPosition(&mgr, parent, parentComp, parentPos);
vec3 childLocalPos = { 1.0f, 0.0f, 0.0f };
entityPositionSetLocalPosition(&mgr, child, childComp, childLocalPos);
entityPositionSetParent(&mgr, child, childComp, parent, parentComp);
vec3 childWorldPos;
entityPositionGetWorldPosition(&mgr, child, childComp, childWorldPos);
assert_float_equal(childWorldPos[0], 11.0f, 0.0001f);
assert_float_equal(childWorldPos[1], 0.0f, 0.0001f);
assert_float_equal(childWorldPos[2], 0.0f, 0.0001f);
// Moving the parent should lazily update the child's world position too.
vec3 parentPosMoved = { 20.0f, 5.0f, 0.0f };
entityPositionSetLocalPosition(&mgr, parent, parentComp, parentPosMoved);
entityPositionGetWorldPosition(&mgr, child, childComp, childWorldPos);
assert_float_equal(childWorldPos[0], 21.0f, 0.0001f);
assert_float_equal(childWorldPos[1], 5.0f, 0.0001f);
assert_float_equal(childWorldPos[2], 0.0f, 0.0001f);
// Detaching does not preserve world position -- the child's local
// position (never touched since entityPositionSetLocalPosition above) is
// now its world position too, since it no longer has a parent.
entityPositionSetParent(
&mgr, child, childComp, ENTITY_ID_INVALID, COMPONENT_ID_INVALID
);
vec3 parentPosMovedAgain = { 100.0f, 100.0f, 100.0f };
entityPositionSetLocalPosition(
&mgr, parent, parentComp, parentPosMovedAgain
);
entityPositionGetWorldPosition(&mgr, child, childComp, childWorldPos);
assert_float_equal(childWorldPos[0], childLocalPos[0], 0.0001f);
assert_float_equal(childWorldPos[1], childLocalPos[1], 0.0001f);
assert_float_equal(childWorldPos[2], childLocalPos[2], 0.0001f);
entityManagerDispose(&mgr);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityPositionDisposeDeep(void **state) {
entitymanager_t mgr;
entityManagerInit(&mgr);
entityid_t parent = entityManagerAdd(&mgr);
componentid_t parentComp = entityAddComponent(
&mgr, parent, COMPONENT_TYPE_POSITION
);
entityid_t child = entityManagerAdd(&mgr);
componentid_t childComp = entityAddComponent(
&mgr, child, COMPONENT_TYPE_POSITION
);
entityPositionSetParent(&mgr, child, childComp, parent, parentComp);
entityPositionDisposeDeep(&mgr, parent, parentComp);
assert_true((mgr.entities[parent].state & ENTITY_STATE_ACTIVE) == 0);
assert_true((mgr.entities[child].state & ENTITY_STATE_ACTIVE) == 0);
entityManagerDispose(&mgr);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(int argc, char **argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_entityPositionLocalGetSet),
cmocka_unit_test(test_entityPositionParentChildWorldTransform),
cmocka_unit_test(test_entityPositionDisposeDeep),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-14
View File
@@ -1,14 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(dusktest)
# Tests
dusktest(test_rpg.c)
# Subdirs
add_subdirectory(overworld)
add_subdirectory(physics)
add_subdirectory(entity)
-105
View File
@@ -1,105 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "util/memory.h"
#include "rpg/entity/entity.h"
static void testEntitiesReset(void) {
memoryZero(ENTITIES, sizeof(ENTITIES));
}
static void testEntityPlace(
const uint8_t index,
const entitytype_t type,
const vec3 position,
const entitydir_t direction
) {
entityInit(&ENTITIES[index], type);
ENTITIES[index].direction = direction;
const vec3 extents = ENTITY_PHYSICS_EXTENTS_DEFAULT;
physicsBodyInit(&ENTITIES[index].body, position, extents);
}
static void test_entityGetFacingFindsEntityAhead(void **state) {
testEntitiesReset();
testEntityPlace(0, ENTITY_TYPE_PLAYER, (vec3){ 0.0f, 0.0f, 0.0f },
ENTITY_DIR_EAST);
testEntityPlace(1, ENTITY_TYPE_NPC, (vec3){ 1.0f, 0.0f, 0.0f },
ENTITY_DIR_NORTH);
entity_t *result = entityGetFacing(&ENTITIES[0], ENTITY_INTERACT_RANGE);
assert_ptr_equal(result, &ENTITIES[1]);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityGetFacingIgnoresEntityBehind(void **state) {
testEntitiesReset();
testEntityPlace(0, ENTITY_TYPE_PLAYER, (vec3){ 0.0f, 0.0f, 0.0f },
ENTITY_DIR_EAST);
testEntityPlace(1, ENTITY_TYPE_NPC, (vec3){ -2.0f, 0.0f, 0.0f },
ENTITY_DIR_NORTH);
entity_t *result = entityGetFacing(&ENTITIES[0], ENTITY_INTERACT_RANGE);
assert_null(result);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityGetFacingIgnoresEntityToSide(void **state) {
testEntitiesReset();
testEntityPlace(0, ENTITY_TYPE_PLAYER, (vec3){ 0.0f, 0.0f, 0.0f },
ENTITY_DIR_EAST);
testEntityPlace(1, ENTITY_TYPE_NPC, (vec3){ 0.0f, 3.0f, 0.0f },
ENTITY_DIR_NORTH);
entity_t *result = entityGetFacing(&ENTITIES[0], ENTITY_INTERACT_RANGE);
assert_null(result);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityGetFacingReturnsNullWhenNothingInRange(void **state) {
testEntitiesReset();
testEntityPlace(0, ENTITY_TYPE_PLAYER, (vec3){ 0.0f, 0.0f, 0.0f },
ENTITY_DIR_EAST);
entity_t *result = entityGetFacing(&ENTITIES[0], ENTITY_INTERACT_RANGE);
assert_null(result);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityGetFacingPicksNearest(void **state) {
testEntitiesReset();
testEntityPlace(0, ENTITY_TYPE_PLAYER, (vec3){ 0.0f, 0.0f, 0.0f },
ENTITY_DIR_EAST);
// Farther candidate placed at the lower array index, to prove the
// result is chosen by distance, not array/insertion order.
testEntityPlace(1, ENTITY_TYPE_NPC, (vec3){ 1.5f, 0.0f, 0.0f },
ENTITY_DIR_NORTH);
testEntityPlace(2, ENTITY_TYPE_NPC, (vec3){ 1.0f, 0.0f, 0.0f },
ENTITY_DIR_NORTH);
entity_t *result = entityGetFacing(&ENTITIES[0], ENTITY_INTERACT_RANGE);
assert_ptr_equal(result, &ENTITIES[2]);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_entityGetFacingFindsEntityAhead),
cmocka_unit_test(test_entityGetFacingIgnoresEntityBehind),
cmocka_unit_test(test_entityGetFacingIgnoresEntityToSide),
cmocka_unit_test(test_entityGetFacingReturnsNullWhenNothingInRange),
cmocka_unit_test(test_entityGetFacingPicksNearest),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-84
View File
@@ -1,84 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "util/memory.h"
#include "rpg/entity/entitydir.h"
static void test_entityDirToVec2(void **state) {
vec2 out;
entityDirToVec2(ENTITY_DIR_NORTH, out);
assert_float_equal(out[0], 0.0f, 0.0001f);
assert_float_equal(out[1], 1.0f, 0.0001f);
entityDirToVec2(ENTITY_DIR_EAST, out);
assert_float_equal(out[0], 1.0f, 0.0001f);
assert_float_equal(out[1], 0.0f, 0.0001f);
entityDirToVec2(ENTITY_DIR_SOUTH, out);
assert_float_equal(out[0], 0.0f, 0.0001f);
assert_float_equal(out[1], -1.0f, 0.0001f);
entityDirToVec2(ENTITY_DIR_WEST, out);
assert_float_equal(out[0], -1.0f, 0.0001f);
assert_float_equal(out[1], 0.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityDirFromVec2Cardinal(void **state) {
assert_int_equal(
entityDirFromVec2((vec2){ 0.0f, 1.0f }), ENTITY_DIR_NORTH
);
assert_int_equal(
entityDirFromVec2((vec2){ 1.0f, 0.0f }), ENTITY_DIR_EAST
);
assert_int_equal(
entityDirFromVec2((vec2){ 0.0f, -1.0f }), ENTITY_DIR_SOUTH
);
assert_int_equal(
entityDirFromVec2((vec2){ -1.0f, 0.0f }), ENTITY_DIR_WEST
);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityDirFromVec2DominantAxis(void **state) {
// |x| > |y| picks east/west regardless of y's sign.
assert_int_equal(
entityDirFromVec2((vec2){ 2.0f, 1.0f }), ENTITY_DIR_EAST
);
assert_int_equal(
entityDirFromVec2((vec2){ -2.0f, -1.0f }), ENTITY_DIR_WEST
);
// |y| >= |x| picks north/south.
assert_int_equal(
entityDirFromVec2((vec2){ 1.0f, 2.0f }), ENTITY_DIR_NORTH
);
assert_int_equal(
entityDirFromVec2((vec2){ -1.0f, -3.0f }), ENTITY_DIR_SOUTH
);
// Exact diagonal ties break toward north/south.
assert_int_equal(
entityDirFromVec2((vec2){ 0.7f, 0.7f }), ENTITY_DIR_NORTH
);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_entityDirToVec2),
cmocka_unit_test(test_entityDirFromVec2Cardinal),
cmocka_unit_test(test_entityDirFromVec2DominantAxis),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-126
View File
@@ -1,126 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "util/memory.h"
#include "rpg/overworld/maparea.h"
static uint32_t TEST_ENTER_COUNT;
static uint32_t TEST_STEP_COUNT;
static uint32_t TEST_EXIT_COUNT;
static void testAreaCallback(entity_t *entity, const uint8_t trigger) {
if(trigger == MAP_TRIGGER_ENTER) TEST_ENTER_COUNT++;
else if(trigger == MAP_TRIGGER_STEP) TEST_STEP_COUNT++;
else if(trigger == MAP_TRIGGER_EXIT) TEST_EXIT_COUNT++;
}
static void testMapAreaReset(void) {
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
TEST_ENTER_COUNT = 0;
TEST_STEP_COUNT = 0;
TEST_EXIT_COUNT = 0;
}
static entity_t testEntityAt(const entitytype_t type, const worldpos_t pos) {
entity_t entity;
memoryZero(&entity, sizeof(entity));
entity.id = 0;
entity.type = type;
entity.position = pos;
return entity;
}
static void test_mapAreaCheckEntityFiresEnterAndExit(void **state) {
testMapAreaReset();
mapAreaAdd(
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
testAreaCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
);
entity_t entity = testEntityAt(ENTITY_TYPE_PLAYER, (worldpos_t){ 10, 10, 0 });
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_ENTER_COUNT, 0);
entity.position = (worldpos_t){ 2, 2, 0 };
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_ENTER_COUNT, 1);
assert_int_equal(TEST_STEP_COUNT, 0);
entity.position = (worldpos_t){ 10, 10, 0 };
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_EXIT_COUNT, 1);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_mapAreaCheckEntityStepFiresOncePerNewTile(void **state) {
testMapAreaReset();
mapAreaAdd(
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
testAreaCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
);
entity_t entity = testEntityAt(ENTITY_TYPE_PLAYER, (worldpos_t){ 1, 1, 0 });
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_ENTER_COUNT, 1);
assert_int_equal(TEST_STEP_COUNT, 0);
// Same tile, checked repeatedly (simulating multiple frames without the
// entity moving) - STEP must not fire.
mapAreaCheckEntity(&entity);
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_STEP_COUNT, 0);
// Moves to a new tile, still inside - STEP fires once.
entity.position = (worldpos_t){ 2, 1, 0 };
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_STEP_COUNT, 1);
// Same new tile again over multiple calls - no additional STEP.
mapAreaCheckEntity(&entity);
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_STEP_COUNT, 1);
// Another new tile - STEP fires again.
entity.position = (worldpos_t){ 3, 1, 0 };
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_STEP_COUNT, 2);
assert_int_equal(TEST_ENTER_COUNT, 1);
assert_int_equal(TEST_EXIT_COUNT, 0);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_mapAreaCheckEntityNotifyFlagsRespected(void **state) {
testMapAreaReset();
mapAreaAdd(
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
testAreaCallback, MAP_AREA_NOTIFY_PLAYER, MAP_TRIGGER_ALL
);
entity_t npc = testEntityAt(ENTITY_TYPE_NPC, (worldpos_t){ 1, 1, 0 });
mapAreaCheckEntity(&npc);
assert_int_equal(TEST_ENTER_COUNT, 0);
entity_t player = testEntityAt(ENTITY_TYPE_PLAYER, (worldpos_t){ 1, 1, 0 });
mapAreaCheckEntity(&player);
assert_int_equal(TEST_ENTER_COUNT, 1);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_mapAreaCheckEntityFiresEnterAndExit),
cmocka_unit_test(test_mapAreaCheckEntityStepFiresOncePerNewTile),
cmocka_unit_test(test_mapAreaCheckEntityNotifyFlagsRespected),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-140
View File
@@ -1,140 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "util/memory.h"
#include "rpg/overworld/tileshape.h"
static void test_tileShapeGetRampHeightGroundIsAlwaysFlat(void **state) {
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_GROUND, 0.0f, 0.0f), 0.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_GROUND, 1.0f, 1.0f), 0.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_GROUND, 0.5f, 0.5f), 0.0f, 0.0001f
);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_tileShapeGetRampHeightCardinalRamps(void **state) {
// RAMP_NORTH: rises going north (+y) - height equals localY.
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTH, 0.5f, 0.0f), 0.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTH, 0.5f, 1.0f), 1.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTH, 0.0f, 0.5f), 0.5f, 0.0001f
);
// RAMP_SOUTH: rises going south (-y) - height equals 1 - localY.
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_SOUTH, 0.5f, 0.0f), 1.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_SOUTH, 0.5f, 1.0f), 0.0f, 0.0001f
);
// RAMP_EAST: rises going east (+x) - height equals localX.
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_EAST, 0.0f, 0.5f), 0.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_EAST, 1.0f, 0.5f), 1.0f, 0.0001f
);
// RAMP_WEST: rises going west (-x) - height equals 1 - localX.
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_WEST, 0.0f, 0.5f), 1.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_WEST, 1.0f, 0.5f), 0.0f, 0.0001f
);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_tileShapeGetRampHeightOuterCornerRamps(void **state) {
// RAMP_NORTHEAST: only the NE corner raised - a hip shape, height
// equal to min(localX, localY) since the mesh is split along the
// SW-NE diagonal into two flat triangles.
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST, 0.0f, 0.0f),
0.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST, 1.0f, 1.0f),
1.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST, 0.3f, 0.7f),
0.3f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST, 0.7f, 0.3f),
0.3f, 0.0001f
);
// RAMP_SOUTHWEST: only the SW corner raised - height equal to
// min(1 - localX, 1 - localY).
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_SOUTHWEST, 0.0f, 0.0f),
1.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_SOUTHWEST, 1.0f, 1.0f),
0.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_SOUTHWEST, 1.0f, 0.0f),
0.0f, 0.0001f
);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_tileShapeGetRampHeightInnerCornerRamp(void **state) {
// RAMP_NORTHEAST_INNER: only the opposite (SW) corner lowered, rest of
// the tile raised - height equal to max(localX, localY).
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST_INNER, 0.0f, 0.0f),
0.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST_INNER, 1.0f, 1.0f),
1.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST_INNER, 0.3f, 0.7f),
0.7f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST_INNER, 0.7f, 0.3f),
0.7f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST_INNER, 1.0f, 0.0f),
1.0f, 0.0001f
);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_tileShapeGetRampHeightGroundIsAlwaysFlat),
cmocka_unit_test(test_tileShapeGetRampHeightCardinalRamps),
cmocka_unit_test(test_tileShapeGetRampHeightOuterCornerRamps),
cmocka_unit_test(test_tileShapeGetRampHeightInnerCornerRamp),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-12
View File
@@ -1,12 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(dusktest)
# Tests
dusktest(test_physicsbody.c)
dusktest(test_physicsworld.c)
# Subdirs
-78
View File
@@ -1,78 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "util/memory.h"
#include "rpg/physics/physicsbody.h"
static void test_physicsBodyInit(void **state) {
physicsbody_t body;
const vec3 position = { 1.0f, 2.0f, 3.0f };
const vec3 extents = { 1.0f, 2.0f, 3.0f };
physicsBodyInit(&body, position, extents);
assert_float_equal(body.position[0], 1.0f, 0.0001f);
assert_float_equal(body.position[1], 2.0f, 0.0001f);
assert_float_equal(body.position[2], 3.0f, 0.0001f);
assert_float_equal(body.extents[0], 1.0f, 0.0001f);
assert_float_equal(body.extents[1], 2.0f, 0.0001f);
assert_float_equal(body.extents[2], 3.0f, 0.0001f);
assert_float_equal(body.velocity[0], 0.0f, 0.0001f);
assert_float_equal(body.velocity[1], 0.0f, 0.0001f);
assert_float_equal(body.velocity[2], 0.0f, 0.0001f);
assert_false(body.grounded);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsBodyGetBounds(void **state) {
physicsbody_t body;
const vec3 position = { 2.0f, 3.0f, 4.0f };
const vec3 extents = { 1.0f, 2.0f, 0.5f };
physicsBodyInit(&body, position, extents);
vec3 min, max;
physicsBodyGetBounds(&body, min, max);
assert_float_equal(min[0], 2.0f, 0.0001f);
assert_float_equal(min[1], 3.0f, 0.0001f);
assert_float_equal(min[2], 4.0f, 0.0001f);
assert_float_equal(max[0], 3.0f, 0.0001f);
assert_float_equal(max[1], 5.0f, 0.0001f);
assert_float_equal(max[2], 4.5f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsBodyGetBoundsNegativeCoordinates(void **state) {
physicsbody_t body;
const vec3 position = { -5.0f, -1.5f, -2.0f };
const vec3 extents = { 2.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
vec3 min, max;
physicsBodyGetBounds(&body, min, max);
assert_float_equal(min[0], -5.0f, 0.0001f);
assert_float_equal(min[1], -1.5f, 0.0001f);
assert_float_equal(min[2], -2.0f, 0.0001f);
assert_float_equal(max[0], -3.0f, 0.0001f);
assert_float_equal(max[1], -0.5f, 0.0001f);
assert_float_equal(max[2], -1.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_physicsBodyInit),
cmocka_unit_test(test_physicsBodyGetBounds),
cmocka_unit_test(test_physicsBodyGetBoundsNegativeCoordinates),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-432
View File
@@ -1,432 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "util/memory.h"
#include "time/time.h"
#include "rpg/physics/physicsbody.h"
#include "rpg/physics/physicsworld.h"
#include "rpg/overworld/map.h"
#include "rpg/overworld/tile.h"
#include "rpg/overworld/tileshape.h"
#include "rpg/overworld/worldpos.h"
static void testMapReset(void) {
memoryZero(&MAP, sizeof(map_t));
MAP.loaded = true;
MAP.chunkPosition = (chunkpos_t){ 0, 0, 0 };
// Push every chunk but the first out of the loaded window, so only
// MAP.chunks[0] (positioned at the origin below) resolves through
// mapRebuildChunkOrder - otherwise every chunk would default to
// position (0,0,0) too and collide on the same chunk order slot.
for(chunkindex_t i = 1; i < MAP_CHUNK_COUNT; i++) {
MAP.chunks[i].position = (chunkpos_t){ 100, 100, 100 };
}
MAP.chunks[0].position = (chunkpos_t){ 0, 0, 0 };
mapRebuildChunkOrder();
}
static void testMapSetTile(
const worldunit_t x,
const worldunit_t y,
const worldunit_t z,
const tileshape_t shape
) {
const worldpos_t pos = { x, y, z };
const chunktileindex_t index = worldPosToChunkTileIndex(&pos);
const uint8_t localZ = worldPosToChunkLocalZ(&pos);
MAP.chunks[0].tiles[index] = (tile_t){ .shape = shape, .z = localZ };
}
static void test_physicsWorldStepStraightLineNoObstacles(void **state) {
testMapReset();
for(worldunit_t x = 0; x < 10; x++) {
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
}
physicsworld_t world;
physicsWorldInit(&world, 0.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 0.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
body.velocity[0] = 1.0f;
const uint32_t steps = 10;
for(uint32_t i = 0; i < steps; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
}
assert_float_equal(body.position[0], 1.0f * steps * DUSK_TIME_STEP, 0.001f);
assert_float_equal(body.position[1], 0.0f, 0.0001f);
assert_float_equal(body.position[2], 0.0f, 0.0001f);
assert_float_equal(body.velocity[0], 1.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepBlockedHorizontally(void **state) {
testMapReset();
testMapSetTile(0, 0, 0, TILE_SHAPE_GROUND);
testMapSetTile(1, 0, 0, TILE_SHAPE_GROUND);
testMapSetTile(2, 0, 0, TILE_SHAPE_GROUND);
// x = 3 left as TILE_SHAPE_NULL, acting as a wall.
physicsworld_t world;
physicsWorldInit(&world, 0.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 0.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
body.velocity[0] = 5.0f;
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
}
assert_float_equal(body.position[0], 2.0f, 0.0001f);
assert_float_equal(body.velocity[0], 0.0f, 0.0001f);
// Further steps must not push it past the wall.
for(uint32_t i = 0; i < 5; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
assert_float_equal(body.position[0], 2.0f, 0.0001f);
}
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepGravitySettlesOnFloor(void **state) {
testMapReset();
testMapSetTile(0, 0, 0, TILE_SHAPE_GROUND);
physicsworld_t world;
physicsWorldInit(&world, 20.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 3.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
for(uint32_t i = 0; i < 200; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
assert_true(body.position[2] >= -0.0001f);
}
assert_float_equal(body.position[2], 0.0f, 0.0001f);
assert_float_equal(body.velocity[2], 0.0f, 0.0001f);
assert_true(body.grounded);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepFallsThroughHole(void **state) {
testMapReset();
// No tiles set anywhere - every column is a hole.
physicsworld_t world;
physicsWorldInit(&world, 20.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 5.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
for(uint32_t i = 0; i < 50; i++) {
const float_t previousZ = body.position[2];
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
assert_true(body.position[2] < previousZ);
assert_false(body.grounded);
}
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepTerminalVelocityClamp(void **state) {
testMapReset();
// No tiles set anywhere - every column is a hole.
physicsworld_t world;
physicsWorldInit(&world, 20.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 5.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
for(uint32_t i = 0; i < 300; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
assert_true(fabsf(body.velocity[2]) <= world.terminalVelocity + 0.0001f);
}
assert_float_equal(fabsf(body.velocity[2]), world.terminalVelocity, 0.01f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepBlockedByCeiling(void **state) {
testMapReset();
testMapSetTile(0, 0, 2, TILE_SHAPE_GROUND);
physicsworld_t world;
physicsWorldInit(&world, 0.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 0.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
body.velocity[2] = 1.0f;
for(uint32_t i = 0; i < 200; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
}
assert_float_equal(body.position[2], 1.0f, 0.0001f);
assert_float_equal(body.velocity[2], 0.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepMultiColumnFootprint(void **state) {
testMapReset();
for(worldunit_t x = 0; x < 6; x++) {
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
}
testMapSetTile(0, 1, 0, TILE_SHAPE_GROUND);
testMapSetTile(1, 1, 0, TILE_SHAPE_GROUND);
// x = 2, y = 1 left as TILE_SHAPE_NULL, blocking only the second row
// spanned by the body's 2-unit-deep footprint.
physicsworld_t world;
physicsWorldInit(&world, 0.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 0.0f };
const vec3 extents = { 1.0f, 2.0f, 1.0f };
physicsBodyInit(&body, position, extents);
body.velocity[0] = 5.0f;
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
}
assert_float_equal(body.position[0], 1.0f, 0.0001f);
assert_float_equal(body.velocity[0], 0.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepClimbsRampGoingUp(void **state) {
testMapReset();
testMapSetTile(0, 0, 0, TILE_SHAPE_GROUND);
testMapSetTile(1, 0, 0, TILE_SHAPE_RAMP_EAST);
for(worldunit_t x = 2; x < 6; x++) {
testMapSetTile(x, 0, 1, TILE_SHAPE_GROUND);
}
physicsworld_t world;
physicsWorldInit(&world, 20.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 0.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
body.velocity[0] = 1.0f;
for(uint32_t i = 0; i < 400; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
// Ground height is driven by the body's center (not its corner
// position), so a 1-wide body barely touching a taller neighbouring
// column isn't yanked onto that column's full height - see
// physicsWorldResolveAxisZ. While the center is over the ramp tile,
// height should track how far across it smoothly, not snap flat.
const float_t centerX = body.position[0] + 0.5f;
if(centerX >= 1.0f && centerX < 2.0f) {
const float_t expected = centerX - 1.0f;
assert_float_equal(body.position[2], expected, 0.005f);
}
}
// Fully across, resting on the elevated ground one layer up.
assert_float_equal(body.position[0], 5.0f, 0.001f);
assert_float_equal(body.position[2], 1.0f, 0.005f);
assert_true(body.grounded);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepDescendsRampGoingDown(void **state) {
testMapReset();
testMapSetTile(0, 0, 1, TILE_SHAPE_GROUND);
testMapSetTile(1, 0, 0, TILE_SHAPE_RAMP_WEST);
for(worldunit_t x = 2; x < 6; x++) {
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
}
physicsworld_t world;
physicsWorldInit(&world, 20.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 1.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
body.velocity[0] = 1.0f;
for(uint32_t i = 0; i < 400; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
// Never falls through as a hole while crossing the ramp - descent
// may lag slightly behind the ideal slope (gravity closes the gap
// each step, see physicsWorldStep's documented limitation) but must
// stay close to it, never dropping toward the void below.
assert_true(body.position[2] >= -0.2f);
}
// Fully across, resting on the lower ground.
assert_float_equal(body.position[0], 5.0f, 0.001f);
assert_float_equal(body.position[2], 0.0f, 0.005f);
assert_true(body.grounded);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepFlatGroundUnaffectedByRampLogic(
void **state
) {
testMapReset();
for(worldunit_t x = 0; x < 6; x++) {
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
}
physicsworld_t world;
physicsWorldInit(&world, 20.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 0.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
body.velocity[0] = 5.0f;
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
assert_float_equal(body.position[2], 0.0f, 0.0001f);
}
assert_true(body.position[0] > 1.5f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepBlockedByOtherBody(void **state) {
testMapReset();
for(worldunit_t x = 0; x < 10; x++) {
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
}
physicsworld_t world;
physicsWorldInit(&world, 0.0f, 40.0f);
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsbody_t other;
physicsBodyInit(&other, (vec3){ 5.0f, 0.0f, 0.0f }, extents);
physicsbody_t body;
physicsBodyInit(&body, (vec3){ 0.0f, 0.0f, 0.0f }, extents);
body.velocity[0] = 5.0f;
physicsbody_t *others[] = { &other };
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, others, 1);
}
// Other occupies [5,6) - body (1 unit wide) should stop exactly
// touching it, at x=4.
assert_float_equal(body.position[0], 4.0f, 0.0001f);
assert_float_equal(body.velocity[0], 0.0f, 0.0001f);
assert_float_equal(other.position[0], 5.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepBlockedByNearestOfMultipleBodies(
void **state
) {
testMapReset();
for(worldunit_t x = 0; x < 20; x++) {
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
}
physicsworld_t world;
physicsWorldInit(&world, 0.0f, 40.0f);
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsbody_t nearOther;
physicsBodyInit(&nearOther, (vec3){ 5.0f, 0.0f, 0.0f }, extents);
physicsbody_t farOther;
physicsBodyInit(&farOther, (vec3){ 15.0f, 0.0f, 0.0f }, extents);
physicsbody_t body;
physicsBodyInit(&body, (vec3){ 0.0f, 0.0f, 0.0f }, extents);
body.velocity[0] = 5.0f;
physicsbody_t *others[] = { &farOther, &nearOther };
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, others, 2);
}
assert_float_equal(body.position[0], 4.0f, 0.0001f);
assert_float_equal(body.velocity[0], 0.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldResolveBodyOverlapIgnoresSelfAndNull(
void **state
) {
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsbody_t body;
physicsBodyInit(&body, (vec3){ 0.0f, 0.0f, 0.0f }, extents);
body.velocity[0] = 1.0f;
physicsbody_t *others[] = { &body, NULL };
physicsWorldResolveBodyOverlap(&body, 0, others, 2);
assert_float_equal(body.position[0], 0.0f, 0.0001f);
assert_float_equal(body.velocity[0], 1.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_physicsWorldStepStraightLineNoObstacles),
cmocka_unit_test(test_physicsWorldStepBlockedHorizontally),
cmocka_unit_test(test_physicsWorldStepGravitySettlesOnFloor),
cmocka_unit_test(test_physicsWorldStepFallsThroughHole),
cmocka_unit_test(test_physicsWorldStepTerminalVelocityClamp),
cmocka_unit_test(test_physicsWorldStepBlockedByCeiling),
cmocka_unit_test(test_physicsWorldStepMultiColumnFootprint),
cmocka_unit_test(test_physicsWorldStepClimbsRampGoingUp),
cmocka_unit_test(test_physicsWorldStepDescendsRampGoingDown),
cmocka_unit_test(test_physicsWorldStepFlatGroundUnaffectedByRampLogic),
cmocka_unit_test(test_physicsWorldStepBlockedByOtherBody),
cmocka_unit_test(test_physicsWorldStepBlockedByNearestOfMultipleBodies),
cmocka_unit_test(test_physicsWorldResolveBodyOverlapIgnoresSelfAndNull),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-16
View File
@@ -1,16 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
int main(int argc, char** argv) {
const struct CMUnitTest tests[] = {
// Add RPG tests here in the future
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
@@ -6,7 +6,4 @@
include(dusktest)
# Tests
dusktest(test_entitydir.c)
dusktest(test_entity.c)
# Subdirs
dusktest(test_scene.c)
+125
View File
@@ -0,0 +1,125 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "util/memory.h"
#include "time/time.h"
#include "scene/scene.h"
#include "entity/entitymanager.h"
static void countingUpdateCallback(
const entityid_t entityId,
const componentid_t componentId,
void *user
) {
uint32_t *counter = (uint32_t *)user;
(*counter)++;
}
static void test_sceneCreateDestroy(void **state) {
sceneInit();
sceneid_t id = sceneCreate();
assert_true(id != SCENE_ID_INVALID);
assert_non_null(sceneGetEntities(id));
sceneDestroy(id);
assert_int_equal(sceneGetActive(), SCENE_ID_INVALID);
sceneDispose();
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_sceneEntitiesAreIsolatedPerScene(void **state) {
sceneInit();
sceneid_t sceneA = sceneCreate();
sceneid_t sceneB = sceneCreate();
assert_true(sceneA != sceneB);
entitymanager_t *entitiesA = sceneGetEntities(sceneA);
entitymanager_t *entitiesB = sceneGetEntities(sceneB);
assert_true(entitiesA != entitiesB);
// Fill scene A completely; scene B must be entirely unaffected.
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
assert_true(entityManagerAdd(entitiesA) != ENTITY_ID_INVALID);
}
expect_assert_failure(entityManagerAdd(entitiesA));
entityid_t entityB = entityManagerAdd(entitiesB);
assert_true(entityB != ENTITY_ID_INVALID);
sceneDispose();
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_sceneUpdateOnlyTicksActiveScene(void **state) {
sceneInit();
timeInit();
TIME.dynamicUpdate = true;
sceneid_t sceneA = sceneCreate();
sceneid_t sceneB = sceneCreate();
uint32_t counterA = 0;
uint32_t counterB = 0;
entitymanager_t *entitiesA = sceneGetEntities(sceneA);
entityid_t entityA = entityManagerAdd(entitiesA);
entityUpdateAdd(
entitiesA, entityA, countingUpdateCallback, COMPONENT_ID_INVALID,
&counterA
);
entitymanager_t *entitiesB = sceneGetEntities(sceneB);
entityid_t entityB = entityManagerAdd(entitiesB);
entityUpdateAdd(
entitiesB, entityB, countingUpdateCallback, COMPONENT_ID_INVALID,
&counterB
);
sceneSetActive(sceneA);
sceneUpdate();
assert_int_equal(counterA, 1);
assert_int_equal(counterB, 0);
sceneSetActive(sceneB);
sceneUpdate();
assert_int_equal(counterA, 1);
assert_int_equal(counterB, 1);
sceneDispose();
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_sceneDestroyClearsActive(void **state) {
sceneInit();
sceneid_t id = sceneCreate();
sceneSetActive(id);
assert_int_equal(sceneGetActive(), id);
sceneDestroy(id);
assert_int_equal(sceneGetActive(), SCENE_ID_INVALID);
sceneDispose();
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(int argc, char **argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_sceneCreateDestroy),
cmocka_unit_test(test_sceneEntitiesAreIsolatedPerScene),
cmocka_unit_test(test_sceneUpdateOnlyTicksActiveScene),
cmocka_unit_test(test_sceneDestroyClearsActive),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}