/** * 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" #include "entity/component/display/entityposition.h" #include "yyjson.h" static void countingUpdateCallback( entitymanager_t *mgr, 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_sceneFixedUpdateOnlyTicksActiveScene(void **state) { sceneInit(); timeInit(); 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); sceneFixedUpdate(); assert_int_equal(counterA, 1); assert_int_equal(counterB, 0); sceneSetActive(sceneB); sceneFixedUpdate(); assert_int_equal(counterA, 1); assert_int_equal(counterB, 1); sceneDispose(); assert_int_equal(memoryGetAllocatedCount(), 0); } static void test_sceneUpdateOnlyRunsFixedUpdateOnNonDynamicFrames(void **state) { sceneInit(); timeInit(); sceneid_t scene = sceneCreate(); sceneSetActive(scene); uint32_t counter = 0; entitymanager_t *entities = sceneGetEntities(scene); entityid_t entity = entityManagerAdd(entities); entityUpdateAdd( entities, entity, countingUpdateCallback, COMPONENT_ID_INVALID, &counter ); #if DUSK_TIME_DYNAMIC // Still accumulating toward the next fixed timestep: sceneUpdate must // not run sceneFixedUpdate yet. TIME.dynamicUpdate = true; sceneUpdate(); assert_int_equal(counter, 0); // A fixed timestep boundary was just crossed: sceneUpdate must run // sceneFixedUpdate exactly once. TIME.dynamicUpdate = false; sceneUpdate(); assert_int_equal(counter, 1); #else // Non-dynamic-time platforms: every frame is a fixed timestep. sceneUpdate(); assert_int_equal(counter, 1); sceneUpdate(); assert_int_equal(counter, 2); #endif sceneDispose(); assert_int_equal(memoryGetAllocatedCount(), 0); } static void test_sceneSerializeDeserializeParentLink(void **state) { sceneInit(); sceneid_t sceneA = sceneCreate(); entitymanager_t *mgrA = sceneGetEntities(sceneA); entityid_t parent = entityManagerAdd(mgrA); componentid_t parentPos = entityAddComponent( mgrA, parent, COMPONENT_TYPE_POSITION ); entitySetName(mgrA, parent, "root"); entityPositionSetLocalPosition(mgrA, parent, parentPos, (vec3){ 10, 0, 0 }); entityid_t child = entityManagerAdd(mgrA); componentid_t childPos = entityAddComponent( mgrA, child, COMPONENT_TYPE_POSITION ); entityPositionSetLocalPosition(mgrA, child, childPos, (vec3){ 1, 0, 0 }); entityPositionSetParent(mgrA, child, childPos, parent, parentPos); vec3 childWorldBefore; entityPositionGetWorldPosition(mgrA, child, childPos, childWorldBefore); assert_float_equal(childWorldBefore[0], 11.0f, 0.0001f); yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); yyjson_mut_val *root = yyjson_mut_obj(doc); yyjson_mut_doc_set_root(doc, root); errorret_t ret = sceneSerialize(sceneA, doc, root); assert_true(errorIsOk(ret)); size_t len = 0; char_t *jsonStr = yyjson_mut_write(doc, 0, &len); assert_non_null(jsonStr); yyjson_mut_doc_free(doc); yyjson_doc *readDoc = yyjson_read(jsonStr, len, 0); free(jsonStr); assert_non_null(readDoc); sceneid_t sceneB = sceneCreate(); entitymanager_t *mgrB = sceneGetEntities(sceneB); ret = sceneDeserialize(sceneB, yyjson_doc_get_root(readDoc)); yyjson_doc_free(readDoc); assert_true(errorIsOk(ret)); entityid_t reloadedParent = entityFindByName(mgrB, "root"); assert_true(reloadedParent != ENTITY_ID_INVALID); entityid_t reloadedChild = ENTITY_ID_INVALID; for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) { if(!(mgrB->entities[i].state & ENTITY_STATE_ACTIVE)) continue; if(i == reloadedParent) continue; reloadedChild = i; break; } assert_true(reloadedChild != ENTITY_ID_INVALID); componentid_t reloadedChildPos = entityGetComponent( mgrB, reloadedChild, COMPONENT_TYPE_POSITION ); assert_true(reloadedChildPos != COMPONENT_ID_INVALID); assert_int_equal( entityPositionGetParent(mgrB, reloadedChild, reloadedChildPos), reloadedParent ); vec3 childWorldAfter; entityPositionGetWorldPosition( mgrB, reloadedChild, reloadedChildPos, childWorldAfter ); assert_float_equal(childWorldAfter[0], childWorldBefore[0], 0.0001f); 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_sceneFixedUpdateOnlyTicksActiveScene), cmocka_unit_test(test_sceneUpdateOnlyRunsFixedUpdateOnNonDynamicFrames), cmocka_unit_test(test_sceneSerializeDeserializeParentLink), cmocka_unit_test(test_sceneDestroyClearsActive), }; return cmocka_run_group_tests(tests, NULL, NULL); }