/** * Copyright (c) 2026 Dominic Masters * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #include "dusktest.h" #include "rpg/cutscene/cutscenesystem.h" #include "rpg/entity/entity.h" #include "time/time.h" #include "util/string.h" static uint8_t callbackFired; static void recordCallback(void *userData) { callbackFired++; } // INNER is jumped into by OUTER's second item, never returning -- this is // the "nested cutscene is a one-way jump" behavior: cutsceneCutsceneStart // replaces CUTSCENE_SYSTEM.scene outright, and the callback item's effect // fires in Start, not Update, so it fires the same frame the jump happens. static cutsceneitem_t CUTSCENE_TEST_INNER_ITEMS[] = { { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordCallback } }; static cutscene_t CUTSCENE_TEST_INNER = { .items = CUTSCENE_TEST_INNER_ITEMS, .itemCount = sizeof(CUTSCENE_TEST_INNER_ITEMS) / sizeof(cutsceneitem_t), .itemsMax = sizeof(CUTSCENE_TEST_INNER_ITEMS) / sizeof(cutsceneitem_t), .pause = CUTSCENE_PAUSE_NONE }; static cutsceneitem_t CUTSCENE_TEST_OUTER_ITEMS[] = { { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 0.5f }, { .type = CUTSCENE_ITEM_TYPE_CUTSCENE, .cutsceneRef = { .cutscene = &CUTSCENE_TEST_INNER } } }; static cutscene_t CUTSCENE_TEST_OUTER = { .items = CUTSCENE_TEST_OUTER_ITEMS, .itemCount = sizeof(CUTSCENE_TEST_OUTER_ITEMS) / sizeof(cutsceneitem_t), .itemsMax = sizeof(CUTSCENE_TEST_OUTER_ITEMS) / sizeof(cutsceneitem_t), .pause = CUTSCENE_PAUSE_DEFAULT }; static void test_cutsceneSystemStartSetsUpInitialItem(void **state) { cutsceneSystemInit(); cutsceneSystemStartCutscene(&CUTSCENE_TEST_OUTER); assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_OUTER); assert_int_equal(CUTSCENE_SYSTEM.currentItem, 0); assert_int_equal(CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_DEFAULT); } static void test_cutsceneSystemNestedCutsceneIsOneWayJump(void **state) { cutsceneSystemInit(); callbackFired = 0; cutsceneSystemStartCutscene(&CUTSCENE_TEST_OUTER); TIME.delta = 0.1f; cutsceneSystemUpdate();// wait not elapsed yet assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_OUTER); assert_int_equal(callbackFired, 0); // The wait elapses, advancing to the nested-cutscene item, which jumps // straight into INNER and starts its first item (the callback) -- all // within this single update call. TIME.delta = 1.0f; cutsceneSystemUpdate(); assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INNER); assert_int_equal(CUTSCENE_SYSTEM.currentItem, 0); assert_int_equal(callbackFired, 1); // INNER's only item (the callback) always reports complete -- one more // update ends the whole cutscene. cutsceneSystemUpdate(); assert_null(CUTSCENE_SYSTEM.scene); assert_int_equal(CUTSCENE_SYSTEM.currentItem, 0xFF); assert_int_equal(CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_NONE); } static void test_cutsceneSystemUpdateIsNoopWithNoActiveCutscene(void **state) { cutsceneSystemInit(); cutsceneSystemUpdate();// should not crash assert_null(CUTSCENE_SYSTEM.scene); } static cutsceneitem_t CUTSCENE_TEST_SINGLE_WAIT_ITEMS[] = { { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f } }; static cutscene_t CUTSCENE_TEST_SINGLE_WAIT = { .items = CUTSCENE_TEST_SINGLE_WAIT_ITEMS, .itemCount = sizeof(CUTSCENE_TEST_SINGLE_WAIT_ITEMS) / sizeof(cutsceneitem_t), .itemsMax = sizeof(CUTSCENE_TEST_SINGLE_WAIT_ITEMS) / sizeof(cutsceneitem_t), .pause = CUTSCENE_PAUSE_NONE }; static void test_cutsceneSystemStartWithSetsInteractEntities(void **state) { cutsceneSystemInit(); entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER); entityInit(&ENTITIES[1], ENTITY_TYPE_NPC); cutsceneSystemStartCutsceneWith( &CUTSCENE_TEST_SINGLE_WAIT, &ENTITIES[0], &ENTITIES[1] ); assert_ptr_equal(CUTSCENE_SYSTEM.entityInteract, &ENTITIES[0]); assert_ptr_equal(CUTSCENE_SYSTEM.entityInteracted, &ENTITIES[1]); assert_ptr_equal( cutsceneSystemGetEntity(CUTSCENE_ENTITY_INTERACT), &ENTITIES[0] ); assert_ptr_equal( cutsceneSystemGetEntity(CUTSCENE_ENTITY_INTERACTED), &ENTITIES[1] ); TIME.delta = 2.0f; cutsceneSystemUpdate();// ends the cutscene assert_null(CUTSCENE_SYSTEM.entityInteract);// reset on end assert_null(CUTSCENE_SYSTEM.entityInteracted); } static void test_cutsceneSystemGetEntitySentinelsRequireBeingSet( void **state ) { cutsceneSystemInit(); expect_assert_failure(cutsceneSystemGetEntity(CUTSCENE_ENTITY_INTERACT)); expect_assert_failure(cutsceneSystemGetEntity(CUTSCENE_ENTITY_INTERACTED)); expect_assert_failure(cutsceneSystemGetEntity(CUTSCENE_ENTITY_LAST_CREATED)); expect_assert_failure(cutsceneSystemGetEntity(CUTSCENE_ENTITY_LAST_REF)); } static void test_cutsceneSystemGetEntityDirectIndexUpdatesLastRef( void **state ) { cutsceneSystemInit(); entityInit(&ENTITIES[2], ENTITY_TYPE_NPC); entity_t *resolved = cutsceneSystemGetEntity(2); assert_ptr_equal(resolved, &ENTITIES[2]); // Resolving by direct index also updates LAST_REF. assert_ptr_equal( cutsceneSystemGetEntity(CUTSCENE_ENTITY_LAST_REF), &ENTITIES[2] ); } static void test_cutsceneSystemGetAreaId(void **state) { cutsceneSystemInit(); // cutsceneSystemInit() zero-inits the field -- only actually starting a // cutscene sets it to the "nothing created yet" sentinel. CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED; expect_assert_failure(cutsceneSystemGetAreaId(CUTSCENE_AREA_LAST_CREATED)); assert_int_equal(cutsceneSystemGetAreaId(5), 5);// direct IDs pass through CUTSCENE_SYSTEM.areaLastCreated = 3; assert_int_equal(cutsceneSystemGetAreaId(CUTSCENE_AREA_LAST_CREATED), 3); } static void test_cutsceneSystemGetTextMiniId(void **state) { cutsceneSystemInit(); CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED; expect_assert_failure( cutsceneSystemGetTextMiniId(CUTSCENE_TEXT_MINI_LAST_CREATED) ); assert_int_equal(cutsceneSystemGetTextMiniId(4), 4); CUTSCENE_SYSTEM.textMiniLastCreated = 1; assert_int_equal( cutsceneSystemGetTextMiniId(CUTSCENE_TEXT_MINI_LAST_CREATED), 1 ); } static void test_cutsceneSystemDisposeResetsState(void **state) { cutsceneSystemInit(); entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER); cutsceneSystemStartCutsceneWith( &CUTSCENE_TEST_SINGLE_WAIT, &ENTITIES[0], &ENTITIES[0] ); cutsceneSystemDispose(); assert_null(CUTSCENE_SYSTEM.scene); assert_int_equal(CUTSCENE_SYSTEM.currentItem, 0xFF); assert_int_equal(CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_NONE); assert_null(CUTSCENE_SYSTEM.entityInteract); assert_null(CUTSCENE_SYSTEM.entityInteracted); } // --- loadedFile survival across cutsceneSystemPrepare ------------------- // // cutsceneCutsceneResolve (and cutsceneSystemLoad) stamp // CUTSCENE_SYSTEM.loadedFile with what they just parsed loadedScene from // - cutsceneGoTo needs that to survive so it can reload the same source // later. cutsceneCutsceneResolve's caller stamps it BEFORE calling // cutsceneSystemStartCutscene(&CUTSCENE_SYSTEM.loadedScene), so // cutsceneSystemPrepare must not blindly clear loadedFile on every // start - only when switching away from loadedScene to some other // cutscene_t entirely (see cutsceneSystemPrepare's doc comment). This // regression-tests the exact bug hit via initial.jsonc's CUTSCENE item // into main_menu.jsonc: cutsceneCutsceneResolve stamped loadedFile, then // the immediately following cutsceneSystemStartCutscene call wiped it // straight back out, so a later cutsceneGoTo on the still-running // cutscene had nothing to reload from. static void test_cutsceneSystemPrepareKeepsLoadedFileWhenReenteringLoadedScene( void **state ) { cutsceneSystemInit(); // Simulate cutsceneCutsceneResolve: populate loadedScene/loadedItems // and stamp loadedFile with its source, before the cutscene has // actually started running. CUTSCENE_SYSTEM.loadedItems[0] = (cutsceneitem_t){ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f }; CUTSCENE_SYSTEM.loadedScene = (cutscene_t){ .items = CUTSCENE_SYSTEM.loadedItems, .itemCount = 1, .itemsMax = CUTSCENE_LOADED_ITEMS_MAX, .pause = CUTSCENE_PAUSE_NONE }; stringCopy( CUTSCENE_SYSTEM.loadedFile, "cutscenes/main_menu.jsonc", ASSET_FILE_NAME_MAX - 1 ); cutsceneSystemStartCutscene(&CUTSCENE_SYSTEM.loadedScene); assert_string_equal( CUTSCENE_SYSTEM.loadedFile, "cutscenes/main_menu.jsonc" ); // Starting a genuinely different cutscene, though, must still // invalidate it - there's nothing left to reload it from. cutsceneSystemStartCutscene(&CUTSCENE_TEST_SINGLE_WAIT); assert_string_equal(CUTSCENE_SYSTEM.loadedFile, ""); } // --- CUTSCENE_INSERT --------------------------------------------------- // // INSERT now splices its target's items directly into the running // scene's own array (see cutsceneSystemInsertCutscene) rather than // swapping CUTSCENE_SYSTEM.scene to a different cutscene_t and pushing a // return frame - CUTSCENE_SYSTEM.scene therefore stays the same outer // cutscene throughout every test below. cutsceneSystemNext pops each // already-run item off the front of the array as it advances (see // CUTSCENE_SYSTEM.currentItem's doc comment), so itemCount reflects only // what's left to run at any given point, not a running total of // everything ever spliced in - the itemCount assertions below are sized // accordingly. Since the running scene's own array is what gets appended // into, any outer cutscene used with INSERT must still declare its items // array (and .itemsMax) with enough spare capacity for whatever ends up // spliced into it at once, including transitively through any nested // INSERT. static uint8_t insertOrderLog[8]; static uint8_t insertOrderLogCount; static void recordOrder0(void *userData) { insertOrderLog[insertOrderLogCount++] = 0; } static void recordOrder1(void *userData) { insertOrderLog[insertOrderLogCount++] = 1; } static void recordOrder2(void *userData) { insertOrderLog[insertOrderLogCount++] = 2; } static void recordOrderA(void *userData) { insertOrderLog[insertOrderLogCount++] = 3; } static void recordOrderB(void *userData) { insertOrderLog[insertOrderLogCount++] = 4; } static cutsceneitem_t CUTSCENE_TEST_INSERT_SNIPPET_ITEMS[] = { { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA }, { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 2.0f }, { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderB } }; static cutscene_t CUTSCENE_TEST_INSERT_SNIPPET = { .items = CUTSCENE_TEST_INSERT_SNIPPET_ITEMS, .itemCount = sizeof(CUTSCENE_TEST_INSERT_SNIPPET_ITEMS) / sizeof(cutsceneitem_t), .itemsMax = sizeof(CUTSCENE_TEST_INSERT_SNIPPET_ITEMS) / sizeof(cutsceneitem_t), .pause = CUTSCENE_PAUSE_NONE }; // 5 items to start, +3 spare slots for CUTSCENE_TEST_INSERT_SNIPPET's items. static cutsceneitem_t CUTSCENE_TEST_INSERT_OUTER_ITEMS[8] = { { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder0 }, { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f }, { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder1 }, { .type = CUTSCENE_ITEM_TYPE_INSERT, .insert = { .cutscene = &CUTSCENE_TEST_INSERT_SNIPPET } }, { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder2 } }; static cutscene_t CUTSCENE_TEST_INSERT_OUTER = { .items = CUTSCENE_TEST_INSERT_OUTER_ITEMS, .itemCount = 5, .itemsMax = sizeof(CUTSCENE_TEST_INSERT_OUTER_ITEMS) / sizeof(cutsceneitem_t), .pause = CUTSCENE_PAUSE_NONE }; static void test_cutsceneInsertSplicesItemsInPlaceThenResumes(void **state) { insertOrderLogCount = 0; cutsceneSystemInit(); cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_OUTER); TIME.delta = 1.5f;// elapses the outer WAIT(1.0) cutsceneSystemUpdate();// order0 done -> starts the outer WAIT cutsceneSystemUpdate();// the WAIT elapses -> starts order1 cutsceneSystemUpdate();// order1 done -> starts the INSERT, cascading into orderA // The splice happened in place - CUTSCENE_SYSTEM.scene never changed. // order0/the WAIT/order1/the INSERT item itself have all since been // popped off the front, leaving the snippet's 3 items (orderA now // running, its WAIT, orderB) plus order2. assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_OUTER); assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 4); TIME.delta = 2.5f;// elapses the snippet's WAIT(2.0) cutsceneSystemUpdate();// orderA done -> starts snippet's WAIT cutsceneSystemUpdate();// snippet's WAIT elapses -> starts orderB cutsceneSystemUpdate();// orderB done -> continues on to order2 assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_OUTER); cutsceneSystemUpdate();// order2 done -> cutscene ends naturally assert_null(CUTSCENE_SYSTEM.scene); const uint8_t expected[] = { 0, 1, 3, 4, 2 }; assert_int_equal(insertOrderLogCount, sizeof(expected)); assert_memory_equal(insertOrderLog, expected, sizeof(expected)); } static cutsceneitem_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS[] = { { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA } }; static cutscene_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET = { .items = CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS, .itemCount = sizeof(CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS) / sizeof(cutsceneitem_t), .itemsMax = sizeof(CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS) / sizeof(cutsceneitem_t), .pause = CUTSCENE_PAUSE_NONE }; // 2 items to start, +1 spare slot for the snippet's single item. static cutsceneitem_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER_ITEMS[3] = { { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder0 }, { .type = CUTSCENE_ITEM_TYPE_INSERT, .insert = { .cutscene = &CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET } } }; static cutscene_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER = { .items = CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER_ITEMS, .itemCount = 2, .itemsMax = sizeof(CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER_ITEMS) / sizeof(cutsceneitem_t), .pause = CUTSCENE_PAUSE_NONE }; static void test_cutsceneInsertAsLastItemEndsCutsceneNaturally( void **state ) { insertOrderLogCount = 0; cutsceneSystemInit(); cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER); cutsceneSystemUpdate();// order0 done -> starts INSERT, cascades to orderA assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER); // order0 and the INSERT item itself have both been popped off the // front by now, leaving just orderA (currently running). assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 1); // orderA is the snippet's only item, and the snippet was spliced in as // the outer's last item -- finishing it should reach the natural end // of the whole cutscene, not get stuck. cutsceneSystemUpdate(); assert_null(CUTSCENE_SYSTEM.scene); const uint8_t expected[] = { 0, 3 }; assert_int_equal(insertOrderLogCount, sizeof(expected)); assert_memory_equal(insertOrderLog, expected, sizeof(expected)); } static cutsceneitem_t CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS[] = { { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA } }; static cutscene_t CUTSCENE_TEST_INSERT_NESTED_INNER = { .items = CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS, .itemCount = sizeof(CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS) / sizeof(cutsceneitem_t), .itemsMax = sizeof(CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS) / sizeof(cutsceneitem_t), .pause = CUTSCENE_PAUSE_NONE }; // INSERT is MID's first item, so entering MID (from OUTER) cascades // straight through into INNER within the same Start chain, one level // deeper than a single insert. MID is only ever read from as a splice // source (see CUTSCENE_TEST_INSERT_NESTED_OUTER) - it's never itself the // running scene, so it needs no spare capacity of its own. static cutsceneitem_t CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS[] = { { .type = CUTSCENE_ITEM_TYPE_INSERT, .insert = { .cutscene = &CUTSCENE_TEST_INSERT_NESTED_INNER } }, { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderB } }; static cutscene_t CUTSCENE_TEST_INSERT_NESTED_MID = { .items = CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS, .itemCount = sizeof(CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS) / sizeof(cutsceneitem_t), .itemsMax = sizeof(CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS) / sizeof(cutsceneitem_t), .pause = CUTSCENE_PAUSE_NONE }; // 3 items to start, +2 spare for MID's items, +1 more for MID's own // nested INSERT(INNER) once MID's items become part of this array too. static cutsceneitem_t CUTSCENE_TEST_INSERT_NESTED_OUTER_ITEMS[6] = { { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder0 }, { .type = CUTSCENE_ITEM_TYPE_INSERT, .insert = { .cutscene = &CUTSCENE_TEST_INSERT_NESTED_MID } }, { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder2 } }; static cutscene_t CUTSCENE_TEST_INSERT_NESTED_OUTER = { .items = CUTSCENE_TEST_INSERT_NESTED_OUTER_ITEMS, .itemCount = 3, .itemsMax = sizeof(CUTSCENE_TEST_INSERT_NESTED_OUTER_ITEMS) / sizeof(cutsceneitem_t), .pause = CUTSCENE_PAUSE_NONE }; static void test_cutsceneInsertNestsThroughMultipleLevels(void **state) { insertOrderLogCount = 0; cutsceneSystemInit(); cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_NESTED_OUTER); // order0 done -> starts INSERT(MID), cascading through INSERT(INNER) -> // orderA, all within this one Update call's Start chain - both splices // land in OUTER's own array. cutsceneSystemUpdate(); assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_NESTED_OUTER); // order0 and both INSERT items themselves have all been popped off the // front by now, leaving orderA (currently running), orderB, order2. assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 3); cutsceneSystemUpdate();// orderA done -> continues on to orderB cutsceneSystemUpdate();// orderB done -> continues on to order2 cutsceneSystemUpdate();// order2 done -> ends naturally assert_null(CUTSCENE_SYSTEM.scene); const uint8_t expected[] = { 0, 3, 4, 2 }; assert_int_equal(insertOrderLogCount, sizeof(expected)); assert_memory_equal(insertOrderLog, expected, sizeof(expected)); } static cutsceneitem_t CUTSCENE_TEST_INSERT_NO_CAPACITY_SNIPPET_ITEMS[] = { { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA } }; static cutscene_t CUTSCENE_TEST_INSERT_NO_CAPACITY_SNIPPET = { .items = CUTSCENE_TEST_INSERT_NO_CAPACITY_SNIPPET_ITEMS, .itemCount = 1, .itemsMax = 1, .pause = CUTSCENE_PAUSE_NONE }; // No spare capacity at all - itemsMax equals itemCount, leaving no room // to splice anything into it. static cutsceneitem_t CUTSCENE_TEST_INSERT_NO_CAPACITY_OUTER_ITEMS[1] = { { .type = CUTSCENE_ITEM_TYPE_INSERT, .insert = { .cutscene = &CUTSCENE_TEST_INSERT_NO_CAPACITY_SNIPPET } } }; static cutscene_t CUTSCENE_TEST_INSERT_NO_CAPACITY_OUTER = { .items = CUTSCENE_TEST_INSERT_NO_CAPACITY_OUTER_ITEMS, .itemCount = 1, .itemsMax = 1, .pause = CUTSCENE_PAUSE_NONE }; static void test_cutsceneInsertAssertsWhenExceedingItemsMax(void **state) { cutsceneSystemInit(); expect_assert_failure( cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_NO_CAPACITY_OUTER) ); } static void noopInsertCallback(void *userData) {} static cutsceneitem_t CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS[] = { { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = noopInsertCallback } }; static cutscene_t CUTSCENE_TEST_INSERT_PERSIST_SNIPPET = { .items = CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS, .itemCount = sizeof(CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS) / sizeof(cutsceneitem_t), .itemsMax = sizeof(CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS) / sizeof(cutsceneitem_t), .pause = CUTSCENE_PAUSE_ALL }; // 1 item to start, +1 spare slot for the snippet's single item. static cutsceneitem_t CUTSCENE_TEST_INSERT_PERSIST_OUTER_ITEMS[2] = { { .type = CUTSCENE_ITEM_TYPE_INSERT, .insert = { .cutscene = &CUTSCENE_TEST_INSERT_PERSIST_SNIPPET } } }; static cutscene_t CUTSCENE_TEST_INSERT_PERSIST_OUTER = { .items = CUTSCENE_TEST_INSERT_PERSIST_OUTER_ITEMS, .itemCount = 1, .itemsMax = sizeof(CUTSCENE_TEST_INSERT_PERSIST_OUTER_ITEMS) / sizeof(cutsceneitem_t), .pause = CUTSCENE_PAUSE_NPC }; static void test_cutsceneInsertDoesNotResetPauseOrInteractEntities( void **state ) { cutsceneSystemInit(); entityInit(&ENTITIES[3], ENTITY_TYPE_PLAYER); entityInit(&ENTITIES[4], ENTITY_TYPE_NPC); cutsceneSystemStartCutsceneWith( &CUTSCENE_TEST_INSERT_PERSIST_OUTER, &ENTITIES[3], &ENTITIES[4] ); // The INSERT item's Start already spliced the snippet into place and // cascaded into it by now - and since the INSERT item itself has since // been popped off the front, only the snippet's one item is left. assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_PERSIST_OUTER); assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 1); assert_ptr_equal(CUTSCENE_SYSTEM.entityInteract, &ENTITIES[3]); assert_ptr_equal(CUTSCENE_SYSTEM.entityInteracted, &ENTITIES[4]); // The snippet declares ALL, but INSERT doesn't apply it -- the outer // cutscene's NPC pause, already in effect, stands. assert_int_equal(CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_NPC); } int main(int argc, char** argv) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_cutsceneSystemStartSetsUpInitialItem), cmocka_unit_test(test_cutsceneSystemNestedCutsceneIsOneWayJump), cmocka_unit_test(test_cutsceneSystemUpdateIsNoopWithNoActiveCutscene), cmocka_unit_test(test_cutsceneSystemStartWithSetsInteractEntities), cmocka_unit_test(test_cutsceneSystemGetEntitySentinelsRequireBeingSet), cmocka_unit_test(test_cutsceneSystemGetEntityDirectIndexUpdatesLastRef), cmocka_unit_test(test_cutsceneSystemGetAreaId), cmocka_unit_test(test_cutsceneSystemGetTextMiniId), cmocka_unit_test(test_cutsceneSystemDisposeResetsState), cmocka_unit_test( test_cutsceneSystemPrepareKeepsLoadedFileWhenReenteringLoadedScene ), cmocka_unit_test(test_cutsceneInsertSplicesItemsInPlaceThenResumes), cmocka_unit_test(test_cutsceneInsertAsLastItemEndsCutsceneNaturally), cmocka_unit_test(test_cutsceneInsertNestsThroughMultipleLevels), cmocka_unit_test(test_cutsceneInsertAssertsWhenExceedingItemsMax), cmocka_unit_test(test_cutsceneInsertDoesNotResetPauseOrInteractEntities), }; return cmocka_run_group_tests(tests, NULL, NULL); }