/** * 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" #include "entity/component/physics/entityphysics.h" #include "entity/component/trigger/entitytrigger.h" #include "entity/component/trigger/entitytriggerevents.h" #include "physics/triggersystem.h" static uint32_t TS_ENTER_COUNT; static uint32_t TS_ACTIVE_COUNT; static uint32_t TS_MOVE_COUNT; static uint32_t TS_LEAVE_COUNT; static void test_resetCounts(void) { TS_ENTER_COUNT = 0; TS_ACTIVE_COUNT = 0; TS_MOVE_COUNT = 0; TS_LEAVE_COUNT = 0; } static void ts_onEnter( entitymanager_t *mgr, const entityid_t triggerEntityId, const componentid_t triggerComponentId, const entityid_t otherEntityId, const componentid_t otherComponentId, void *user ) { TS_ENTER_COUNT++; } static void ts_onActive( entitymanager_t *mgr, const entityid_t triggerEntityId, const componentid_t triggerComponentId, const entityid_t otherEntityId, const componentid_t otherComponentId, void *user ) { TS_ACTIVE_COUNT++; } static void ts_onMove( entitymanager_t *mgr, const entityid_t triggerEntityId, const componentid_t triggerComponentId, const entityid_t otherEntityId, const componentid_t otherComponentId, void *user ) { TS_MOVE_COUNT++; } static void ts_onLeave( entitymanager_t *mgr, const entityid_t triggerEntityId, const componentid_t triggerComponentId, const entityid_t otherEntityId, const componentid_t otherComponentId, void *user ) { TS_LEAVE_COUNT++; } static void test_triggerSystemLifecycle(void **state) { test_resetCounts(); entitymanager_t mgr; entityManagerInit(&mgr); entityid_t trigEntity = entityManagerAdd(&mgr); entityAddComponent(&mgr, trigEntity, COMPONENT_TYPE_POSITION); componentid_t trig = entityAddComponent( &mgr, trigEntity, COMPONENT_TYPE_TRIGGER ); entityTriggerOnEnterAdd(&mgr, trigEntity, trig, ts_onEnter, NULL); entityTriggerOnActiveAdd(&mgr, trigEntity, trig, ts_onActive, NULL); entityTriggerOnMoveAdd(&mgr, trigEntity, trig, ts_onMove, NULL); entityTriggerOnLeaveAdd(&mgr, trigEntity, trig, ts_onLeave, NULL); entityid_t otherEntity = entityManagerAdd(&mgr); componentid_t otherPos = entityAddComponent( &mgr, otherEntity, COMPONENT_TYPE_POSITION ); entityAddComponent(&mgr, otherEntity, COMPONENT_TYPE_PHYSICS); // Starts far away: no overlap, no events. vec3 farPos = { 10.0f, 0.0f, 0.0f }; entityPositionSetLocalPosition(&mgr, otherEntity, otherPos, farPos); triggerSystemStep(&mgr); assert_int_equal(TS_ENTER_COUNT, 0); assert_int_equal(entityTriggerGetOccupantCount(&mgr, trigEntity, trig), 0); // Moves fully inside: onEnter fires once, not onActive. vec3 insidePos = { 0.0f, 0.0f, 0.0f }; entityPositionSetLocalPosition(&mgr, otherEntity, otherPos, insidePos); triggerSystemStep(&mgr); assert_int_equal(TS_ENTER_COUNT, 1); assert_int_equal(TS_ACTIVE_COUNT, 0); assert_int_equal(entityTriggerGetOccupantCount(&mgr, trigEntity, trig), 1); assert_true( entityTriggerIsOccupyingEntity(&mgr, trigEntity, trig, otherEntity) ); // Stays put: onActive fires, no onMove (position unchanged). triggerSystemStep(&mgr); assert_int_equal(TS_ENTER_COUNT, 1); assert_int_equal(TS_ACTIVE_COUNT, 1); assert_int_equal(TS_MOVE_COUNT, 0); // Shifts slightly but still overlapping: onActive + onMove both fire. vec3 shiftedPos = { 0.1f, 0.0f, 0.0f }; entityPositionSetLocalPosition(&mgr, otherEntity, otherPos, shiftedPos); triggerSystemStep(&mgr); assert_int_equal(TS_ACTIVE_COUNT, 2); assert_int_equal(TS_MOVE_COUNT, 1); // Leaves: onLeave fires, occupant list empties. entityPositionSetLocalPosition(&mgr, otherEntity, otherPos, farPos); triggerSystemStep(&mgr); assert_int_equal(TS_LEAVE_COUNT, 1); assert_int_equal(entityTriggerGetOccupantCount(&mgr, trigEntity, trig), 0); assert_false( entityTriggerIsOccupyingEntity(&mgr, trigEntity, trig, otherEntity) ); entityManagerDispose(&mgr); assert_int_equal(memoryGetAllocatedCount(), 0); } static void test_triggerSystemCollideMaskFilter(void **state) { test_resetCounts(); entitymanager_t mgr; entityManagerInit(&mgr); entityid_t trigEntity = entityManagerAdd(&mgr); entityAddComponent(&mgr, trigEntity, COMPONENT_TYPE_POSITION); componentid_t trig = entityAddComponent( &mgr, trigEntity, COMPONENT_TYPE_TRIGGER ); entityTriggerSetCollideMask(&mgr, trigEntity, trig, 0x1); entityTriggerOnEnterAdd(&mgr, trigEntity, trig, ts_onEnter, NULL); entityid_t otherEntity = entityManagerAdd(&mgr); entityAddComponent(&mgr, otherEntity, COMPONENT_TYPE_POSITION); componentid_t otherPhys = entityAddComponent( &mgr, otherEntity, COMPONENT_TYPE_PHYSICS ); entityPhysicsSetCollideMask(&mgr, otherEntity, otherPhys, 0x2); // Fully overlapping (both default to the origin), but masks share no // bits -- must not be detected. triggerSystemStep(&mgr); assert_int_equal(TS_ENTER_COUNT, 0); assert_int_equal(entityTriggerGetOccupantCount(&mgr, trigEntity, trig), 0); // Sharing bit 0x1 now -- detected. entityPhysicsSetCollideMask(&mgr, otherEntity, otherPhys, 0x3); triggerSystemStep(&mgr); assert_int_equal(TS_ENTER_COUNT, 1); assert_int_equal(entityTriggerGetOccupantCount(&mgr, trigEntity, trig), 1); entityManagerDispose(&mgr); assert_int_equal(memoryGetAllocatedCount(), 0); } static void test_triggerSystemStepNoTriggersIsNoop(void **state) { entitymanager_t mgr; entityManagerInit(&mgr); entityManagerAdd(&mgr); triggerSystemStep(&mgr); entityManagerDispose(&mgr); assert_int_equal(memoryGetAllocatedCount(), 0); } int main(int argc, char **argv) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_triggerSystemLifecycle), cmocka_unit_test(test_triggerSystemCollideMaskFilter), cmocka_unit_test(test_triggerSystemStepNoTriggersIsNoop), }; return cmocka_run_group_tests(tests, NULL, NULL); }