From 6af0e9cf28d5770649a5cda3841b97c67203d35d Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sun, 13 Sep 2026 08:56:26 -0500 Subject: [PATCH] Add CUTSCENE_INSERT item to splice another cutscene's items in place Unlike CUTSCENE (a one-way jump that replaces the running cutscene), INSERT splices a referenced cutscene's items into the running one and resumes right after itself once they run out. Backed by a small return stack on CUTSCENE_SYSTEM so inserts can nest. Co-Authored-By: Claude Sonnet 5 --- src/dusk/rpg/cutscene/cutscene.h | 17 ++ src/dusk/rpg/cutscene/cutscenesystem.c | 39 ++++ src/dusk/rpg/cutscene/cutscenesystem.h | 42 ++++ src/dusk/rpg/cutscene/item/cutsceneitem.c | 53 ++++- src/dusk/rpg/cutscene/item/cutsceneitem.h | 86 +++++++- src/dusk/rpg/cutscene/item/cutsceneitembase.h | 1 + test/rpg/cutscene/test_cutscenesystem.c | 200 ++++++++++++++++++ 7 files changed, 425 insertions(+), 13 deletions(-) diff --git a/src/dusk/rpg/cutscene/cutscene.h b/src/dusk/rpg/cutscene/cutscene.h index a307d7af..402b4c7f 100644 --- a/src/dusk/rpg/cutscene/cutscene.h +++ b/src/dusk/rpg/cutscene/cutscene.h @@ -62,3 +62,20 @@ typedef struct cutscene_s { .type = CUTSCENE_ITEM_TYPE_CUTSCENE, \ .cutsceneRef = { .cutscene = CUTSCENE_REFERENCE(CUTSCENE), .name = NULL } \ } + +/** + * Splices another CUTSCENE(NAME, ...)-declared cutscene's items into this + * one in place, resuming right after this item once the referenced + * cutscene's items run out - unlike CUTSCENE_CUTSCENE (a one-way jump that + * never returns). Has no dedicated item header of its own, same rationale + * as CUTSCENE_CUTSCENE - cutsceneInsertStart/Update/Load are defined + * inline in cutsceneitem.h/.c, tightly bound to the asset/loader system. + * + * @param CUTSCENE The referenced cutscene's NAME, as passed to + * CUTSCENE(...). + */ +#define CUTSCENE_INSERT(CUTSCENE) \ + { \ + .type = CUTSCENE_ITEM_TYPE_INSERT, \ + .insert = { .cutscene = CUTSCENE_REFERENCE(CUTSCENE), .name = NULL } \ + } diff --git a/src/dusk/rpg/cutscene/cutscenesystem.c b/src/dusk/rpg/cutscene/cutscenesystem.c index 56fab869..9ab3f79b 100644 --- a/src/dusk/rpg/cutscene/cutscenesystem.c +++ b/src/dusk/rpg/cutscene/cutscenesystem.c @@ -40,6 +40,7 @@ void cutsceneSystemPrepare( CUTSCENE_SYSTEM.textCache[0] = '\0'; CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0. CUTSCENE_SYSTEM.onComplete = NULL; + CUTSCENE_SYSTEM.insertStackSize = 0; } void cutsceneSystemStartCutscene(const cutscene_t *cutscene) { @@ -107,6 +108,21 @@ void cutsceneSystemNext() { CUTSCENE_SYSTEM.currentItem++; + // Run off the end of the current scene? If it was spliced in via + // CUTSCENE_ITEM_TYPE_INSERT, pop back to whatever it was inserted into + // and resume right after the INSERT item - possibly chaining through + // several pops if that lands on the end of its own parent in turn (an + // insert as the very last item, or a nested insert-of-an-insert). + while( + CUTSCENE_SYSTEM.currentItem >= CUTSCENE_SYSTEM.scene->itemCount && + CUTSCENE_SYSTEM.insertStackSize > 0 + ) { + cutsceneinsertframe_t frame = + CUTSCENE_SYSTEM.insertStack[--CUTSCENE_SYSTEM.insertStackSize]; + CUTSCENE_SYSTEM.scene = frame.scene; + CUTSCENE_SYSTEM.currentItem = frame.currentItem + 1; + } + // End of the cutscene? if( CUTSCENE_SYSTEM.currentItem >= CUTSCENE_SYSTEM.scene->itemCount @@ -129,6 +145,7 @@ void cutsceneSystemNext() { CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED; CUTSCENE_SYSTEM.textCache[0] = '\0'; CUTSCENE_SYSTEM.onComplete = NULL; + CUTSCENE_SYSTEM.insertStackSize = 0; if(onComplete != NULL) onComplete(userData); return; @@ -140,6 +157,27 @@ void cutsceneSystemNext() { cutsceneItemStart(item, &CUTSCENE_SYSTEM.data); } +void cutsceneSystemInsertCutscene(const cutscene_t *cutscene) { + assertNotNull( + CUTSCENE_SYSTEM.scene, + "cutsceneSystemInsertCutscene called with no cutscene running" + ); + assertTrue( + CUTSCENE_SYSTEM.insertStackSize < CUTSCENE_INSERT_STACK_MAX, + "Cutscene INSERT nesting exceeds CUTSCENE_INSERT_STACK_MAX" + ); + + CUTSCENE_SYSTEM.insertStack[CUTSCENE_SYSTEM.insertStackSize++] = + (cutsceneinsertframe_t){ + .scene = CUTSCENE_SYSTEM.scene, + .currentItem = CUTSCENE_SYSTEM.currentItem + }; + + CUTSCENE_SYSTEM.scene = cutscene; + CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0. + cutsceneSystemNext(); +} + void cutsceneSystemSetOnComplete(cutscenecallback_t onComplete) { assertNotNull( CUTSCENE_SYSTEM.scene, @@ -259,4 +297,5 @@ void cutsceneSystemDispose() { CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED; CUTSCENE_SYSTEM.textCache[0] = '\0'; CUTSCENE_SYSTEM.onComplete = NULL; + CUTSCENE_SYSTEM.insertStackSize = 0; } diff --git a/src/dusk/rpg/cutscene/cutscenesystem.h b/src/dusk/rpg/cutscene/cutscenesystem.h index b4c1f4f3..0ac423ec 100644 --- a/src/dusk/rpg/cutscene/cutscenesystem.h +++ b/src/dusk/rpg/cutscene/cutscenesystem.h @@ -26,6 +26,18 @@ typedef struct entity_s entity_t; // CUTSCENE_ITEM_TYPE_KEYBOARD item is never truncated caching it here. #define CUTSCENE_TEXT_CACHE_MAX 64 +// Maximum nesting depth of CUTSCENE_ITEM_TYPE_INSERT items - how many +// "return to the item after this one" frames cutsceneSystemInsertCutscene +// may have pushed at once (see CUTSCENE_SYSTEM.insertStack). +#define CUTSCENE_INSERT_STACK_MAX 4 + +// One "return address" pushed by cutsceneSystemInsertCutscene - the scene +// and item index to resume at once the inserted cutscene's items run out. +typedef struct { + const cutscene_t *scene; + uint8_t currentItem; +} cutsceneinsertframe_t; + typedef struct { const cutscene_t *scene; uint8_t currentItem; @@ -52,6 +64,12 @@ typedef struct { // See cutsceneSystemSetOnComplete. cutscenecallback_t onComplete; + + // Return stack pushed by CUTSCENE_ITEM_TYPE_INSERT items - see + // cutsceneSystemInsertCutscene. Reset (insertStackSize = 0) whenever a + // cutscene freshly starts or the system is disposed. + cutsceneinsertframe_t insertStack[CUTSCENE_INSERT_STACK_MAX]; + uint8_t insertStackSize; } cutscenesystem_t; extern cutscenesystem_t CUTSCENE_SYSTEM; @@ -96,6 +114,30 @@ void cutsceneSystemStartCutsceneAndGoToMarker( const char_t *marker ); +/** + * Splices cutscene's items into the running cutscene in place, resuming + * the current cutscene right after the CUTSCENE_ITEM_TYPE_INSERT item that + * called this once cutscene's items run out - unlike + * cutsceneSystemStartCutscene (a one-way jump that replaces the running + * cutscene outright). Pushes a return frame onto + * CUTSCENE_SYSTEM.insertStack; cutsceneSystemNext pops it once cutscene + * ends. Deliberately does not touch pause flags, interact entities, + * "last created" state, the text cache or userData - those all keep + * whatever the outer cutscene set, since this is meant to feel like + * pasting cutscene's items in place rather than starting an independent + * cutscene. Because of this, cutscene's own .pause/.dataSize are ignored. + * Asserts if no cutscene is currently running, or if inserts are already + * nested CUTSCENE_INSERT_STACK_MAX deep. + * + * Note: cutsceneRestart() resets the insert stack (via + * cutsceneSystemPrepare), so a RESTART item used from inside an inserted + * cutscene restarts just that inserted cutscene and does not return to + * whatever it was inserted into. + * + * @param cutscene The cutscene whose items to splice in. + */ +void cutsceneSystemInsertCutscene(const cutscene_t *cutscene); + /** * Locks, loads and immediately starts a cutscene asset by file name, * e.g. cutsceneSystemLoad("main_menu.jsonc") loads and starts diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.c b/src/dusk/rpg/cutscene/item/cutsceneitem.c index a81d4f98..42be7ef1 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.c +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.c @@ -333,6 +333,13 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = { .update = cutsceneRegularBattleUpdate, .jsonName = "REGULAR_BATTLE", .load = cutsceneItemLoadNoop + }, + + [CUTSCENE_ITEM_TYPE_INSERT] = { + .init = cutsceneInsertStart, + .update = cutsceneInsertUpdate, + .jsonName = "INSERT", + .load = cutsceneInsertLoad } }; @@ -408,10 +415,18 @@ errorret_t cutsceneCutsceneLoad( cutsceneitem_t *item, uint8_t *pool, size_t *poolOffset +) { + return cutsceneCutsceneRefLoad(itemObj, &item->cutsceneRef, "CUTSCENE"); +} + +errorret_t cutsceneCutsceneRefLoad( + yyjson_val *itemObj, + cutscenecutsceneref_t *outRef, + const char_t *itemTypeLabel ) { const char_t *name = yyjson_get_str(yyjson_obj_get(itemObj, "name")); if(name == NULL) { - errorThrow("Cutscene item (CUTSCENE): missing 'name'"); + errorThrow("Cutscene item (%s): missing 'name'", itemTypeLabel); } // Only validate that the eventual "cutscenes/.jsonc" path would @@ -419,11 +434,41 @@ errorret_t cutsceneCutsceneLoad( // actually runs (see cutsceneCutsceneStart/cutsceneCutsceneResolve). int32_t pathLen = stringFormat(NULL, 0, "cutscenes/%s.jsonc", name); if(pathLen < 0 || (size_t)pathLen >= ASSET_FILE_NAME_MAX) { - errorThrow("Cutscene item (CUTSCENE): 'name' too long"); + errorThrow("Cutscene item (%s): 'name' too long", itemTypeLabel); } - item->cutsceneRef.cutscene = NULL; - item->cutsceneRef.name = name; + outRef->cutscene = NULL; + outRef->name = name; errorOk(); } + +void cutsceneInsertStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + const cutscene_t *cutscene = item->insert.cutscene; + if(cutscene == NULL && item->insert.name != NULL) { + cutscene = cutsceneCutsceneResolve(item->insert.name); + // Cache the resolved pointer back so replaying this same item never + // re-resolves/re-locks the asset again, same as cutsceneCutsceneStart. + ((cutsceneitem_t *)item)->insert.cutscene = cutscene; + } + if(cutscene != NULL) cutsceneSystemInsertCutscene(cutscene); +} + +bool_t cutsceneInsertUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return false; +} + +errorret_t cutsceneInsertLoad( + yyjson_val *itemObj, + cutsceneitem_t *item, + uint8_t *pool, + size_t *poolOffset +) { + return cutsceneCutsceneRefLoad(itemObj, &item->insert, "INSERT"); +} diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.h b/src/dusk/rpg/cutscene/item/cutsceneitem.h index 069df684..45222a35 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.h @@ -57,15 +57,18 @@ typedef struct cutscene_s cutscene_t; -// A CUTSCENE item's target. C-authored cutscenes (the CUTSCENE_CUTSCENE -// macro in cutscene.h) resolve cutscene at compile time and leave name -// NULL. JSON-authored cutscenes (cutsceneCutsceneLoad) do the opposite - -// only name is set at parse time, since resolving/locking the referenced -// asset there would eagerly load every cutscene a cutscene merely -// references, even ones never played. cutsceneCutsceneStart resolves -// name to cutscene the first time this item actually runs (via -// cutsceneCutsceneResolve) and caches the result back into this field so -// replaying the same item never re-resolves it. +// A CUTSCENE (or INSERT) item's target. C-authored cutscenes (the +// CUTSCENE_CUTSCENE/CUTSCENE_INSERT macros in cutscene.h) resolve cutscene +// at compile time and leave name NULL. JSON-authored cutscenes +// (cutsceneCutsceneRefLoad) do the opposite - only name is set at parse +// time, since resolving/locking the referenced asset there would eagerly +// load every cutscene a cutscene merely references, even ones never +// played. cutsceneCutsceneStart/cutsceneInsertStart resolve name to +// cutscene the first time this item actually runs (via +// cutsceneCutsceneResolve) and cache the result back into this field so +// replaying the same item never re-resolves it. Shared by both CUTSCENE +// (item->cutsceneRef) and INSERT (item->insert), since both need exactly +// this "resolved pointer, or a name to lazily resolve" shape. typedef struct { const cutscene_t *cutscene; const char_t *name; @@ -81,6 +84,7 @@ struct cutsceneitem_s { cutscenecallback_t callback; cutscenewait_t wait; cutscenecutsceneref_t cutsceneRef; + cutscenecutsceneref_t insert; cutsceneentityteleport_t entityTeleport; cutsceneentitywalkto_t entityWalkTo; cutscenefade_t fade; @@ -262,3 +266,67 @@ errorret_t cutsceneCutsceneLoad( uint8_t *pool, size_t *poolOffset ); + +/** + * Shared JSON parsing body for both CUTSCENE and INSERT items - both need + * exactly a 'name' string field, validated to fit the eventual + * "cutscenes/.jsonc" asset path. Only stores the name (see + * cutsceneCutsceneLoad for why resolving is deferred). + * + * @param itemObj The item's JSON object. + * @param outRef Destination ref, already zeroed by the caller. + * @param itemTypeLabel Item type name used only in the error message on + * failure (e.g. "CUTSCENE" or "INSERT"). + * @return Error code indicating success or failure of the parse. + */ +errorret_t cutsceneCutsceneRefLoad( + yyjson_val *itemObj, + cutscenecutsceneref_t *outRef, + const char_t *itemTypeLabel +); + +/** + * Starts an INSERT item, splicing the referenced cutscene's items into the + * running cutscene in place - unlike CUTSCENE (a one-way jump that replaces + * the running cutscene outright), control returns to the item right after + * this one once the inserted items run out. If item->insert.cutscene hasn't + * been resolved yet (a JSON-authored reference, only known by name so far), + * resolves and caches it first via cutsceneCutsceneResolve, same as + * cutsceneCutsceneStart. + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneInsertStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates an INSERT item. By the time this would run, control has already + * moved on to the inserted items, so this always reports incomplete. + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns false always. + */ +bool_t cutsceneInsertUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Parses an INSERT cutscene item's JSON fields into item. + * + * @param itemObj The item's JSON object. + * @param item Destination item, already zeroed by the caller. + * @param pool Unused (INSERT needs no pool storage). + * @param poolOffset Unused (INSERT needs no pool storage). + * @return Error code indicating success or failure of the parse. + */ +errorret_t cutsceneInsertLoad( + yyjson_val *itemObj, + cutsceneitem_t *item, + uint8_t *pool, + size_t *poolOffset +); diff --git a/src/dusk/rpg/cutscene/item/cutsceneitembase.h b/src/dusk/rpg/cutscene/item/cutsceneitembase.h index 15741311..97764ade 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitembase.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitembase.h @@ -63,6 +63,7 @@ typedef enum { CUTSCENE_ITEM_TYPE_IDLE, CUTSCENE_ITEM_TYPE_UI_SHOW, CUTSCENE_ITEM_TYPE_REGULAR_BATTLE, + CUTSCENE_ITEM_TYPE_INSERT, CUTSCENE_ITEM_TYPE_COUNT } cutsceneitemtype_t; diff --git a/test/rpg/cutscene/test_cutscenesystem.c b/test/rpg/cutscene/test_cutscenesystem.c index cf4fd0a2..2e6def8b 100644 --- a/test/rpg/cutscene/test_cutscenesystem.c +++ b/test/rpg/cutscene/test_cutscenesystem.c @@ -177,6 +177,201 @@ static void test_cutsceneSystemDisposeResetsState(void **state) { assert_null(CUTSCENE_SYSTEM.entityInteracted); } +// --- CUTSCENE_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; } + +CUTSCENE(TEST_INSERT_SNIPPET, 0, NONE, + CUTSCENE_CALLBACK(recordOrderA), + CUTSCENE_WAIT(2.0f), + CUTSCENE_CALLBACK(recordOrderB) +); + +CUTSCENE(TEST_INSERT_OUTER, 0, NONE, + CUTSCENE_CALLBACK(recordOrder0), + CUTSCENE_WAIT(1.0f), + CUTSCENE_CALLBACK(recordOrder1), + CUTSCENE_INSERT(TEST_INSERT_SNIPPET), + CUTSCENE_CALLBACK(recordOrder2) +); + +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 + + assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_SNIPPET); + + 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 -> pops back, starts 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)); +} + +CUTSCENE(TEST_INSERT_AS_LAST_ITEM_SNIPPET, 0, NONE, + CUTSCENE_CALLBACK(recordOrderA) +); + +CUTSCENE(TEST_INSERT_AS_LAST_ITEM_OUTER, 0, NONE, + CUTSCENE_CALLBACK(recordOrder0), + CUTSCENE_INSERT(TEST_INSERT_AS_LAST_ITEM_SNIPPET) +); + +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_SNIPPET + ); + + // orderA is the snippet's only item, and the snippet is the outer's last + // item -- finishing it should pop straight through to 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)); +} + +CUTSCENE(TEST_INSERT_NESTED_INNER, 0, NONE, + CUTSCENE_CALLBACK(recordOrderA) +); + +// 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. +CUTSCENE(TEST_INSERT_NESTED_MID, 0, NONE, + CUTSCENE_INSERT(TEST_INSERT_NESTED_INNER), + CUTSCENE_CALLBACK(recordOrderB) +); + +CUTSCENE(TEST_INSERT_NESTED_OUTER, 0, NONE, + CUTSCENE_CALLBACK(recordOrder0), + CUTSCENE_INSERT(TEST_INSERT_NESTED_MID), + CUTSCENE_CALLBACK(recordOrder2) +); + +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. + cutsceneSystemUpdate(); + assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_NESTED_INNER); + + cutsceneSystemUpdate();// orderA done -> pops to MID, starts orderB + assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_NESTED_MID); + + cutsceneSystemUpdate();// orderB done -> pops to OUTER, starts order2 + assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_NESTED_OUTER); + + 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)); +} + +CUTSCENE(TEST_INSERT_OVERFLOW_LEVEL5, 0, NONE, + CUTSCENE_CALLBACK(recordOrderA) +); +CUTSCENE(TEST_INSERT_OVERFLOW_LEVEL4, 0, NONE, + CUTSCENE_INSERT(TEST_INSERT_OVERFLOW_LEVEL5) +); +CUTSCENE(TEST_INSERT_OVERFLOW_LEVEL3, 0, NONE, + CUTSCENE_INSERT(TEST_INSERT_OVERFLOW_LEVEL4) +); +CUTSCENE(TEST_INSERT_OVERFLOW_LEVEL2, 0, NONE, + CUTSCENE_INSERT(TEST_INSERT_OVERFLOW_LEVEL3) +); +CUTSCENE(TEST_INSERT_OVERFLOW_LEVEL1, 0, NONE, + CUTSCENE_INSERT(TEST_INSERT_OVERFLOW_LEVEL2) +); +CUTSCENE(TEST_INSERT_OVERFLOW_LEVEL0, 0, NONE, + CUTSCENE_INSERT(TEST_INSERT_OVERFLOW_LEVEL1) +); + +static void test_cutsceneInsertAssertsWhenNestingExceedsStackMax( + void **state +) { + + cutsceneSystemInit(); + + // 5 levels of INSERT deep, one past CUTSCENE_INSERT_STACK_MAX (4) -- + // all cascade synchronously within this one call, since each level's + // only item is the INSERT starting immediately. + expect_assert_failure( + cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL0) + ); +} + +static void noopInsertCallback(void *userData) {} + +CUTSCENE(TEST_INSERT_PERSIST_SNIPPET, 0, ALL, + CUTSCENE_CALLBACK(noopInsertCallback) +); + +CUTSCENE(TEST_INSERT_PERSIST_OUTER, 0, NPC, + CUTSCENE_INSERT(TEST_INSERT_PERSIST_SNIPPET) +); + +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 cascaded into the snippet by now. + assert_ptr_equal( + CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_PERSIST_SNIPPET + ); + 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), @@ -188,6 +383,11 @@ int main(int argc, char** argv) { cmocka_unit_test(test_cutsceneSystemGetAreaId), cmocka_unit_test(test_cutsceneSystemGetTextMiniId), cmocka_unit_test(test_cutsceneSystemDisposeResetsState), + cmocka_unit_test(test_cutsceneInsertSplicesItemsInPlaceThenResumes), + cmocka_unit_test(test_cutsceneInsertAsLastItemEndsCutsceneNaturally), + cmocka_unit_test(test_cutsceneInsertNestsThroughMultipleLevels), + cmocka_unit_test(test_cutsceneInsertAssertsWhenNestingExceedsStackMax), + cmocka_unit_test(test_cutsceneInsertDoesNotResetPauseOrInteractEntities), }; return cmocka_run_group_tests(tests, NULL, NULL);