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 <[email protected]>
This commit is contained in:
2026-09-13 08:56:26 -05:00
co-authored by Claude Sonnet 5
parent 083deb8357
commit 6af0e9cf28
7 changed files with 425 additions and 13 deletions
+200
View File
@@ -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);