584 lines
22 KiB
C
584 lines
22 KiB
C
/**
|
|
* 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.refs.entityInteract, &ENTITIES[0]);
|
|
assert_ptr_equal(CUTSCENE_SYSTEM.refs.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.refs.entityInteract);// reset on end
|
|
assert_null(CUTSCENE_SYSTEM.refs.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.refs.areaLastCreated = CUTSCENE_REF_AREA_LAST_CREATED;
|
|
|
|
expect_assert_failure(cutsceneSystemGetAreaId(CUTSCENE_REF_AREA_LAST_CREATED));
|
|
assert_int_equal(cutsceneSystemGetAreaId(5), 5);// direct IDs pass through
|
|
|
|
CUTSCENE_SYSTEM.refs.areaLastCreated = 3;
|
|
assert_int_equal(cutsceneSystemGetAreaId(CUTSCENE_REF_AREA_LAST_CREATED), 3);
|
|
}
|
|
|
|
static void test_cutsceneSystemGetTextMiniId(void **state) {
|
|
|
|
cutsceneSystemInit();
|
|
CUTSCENE_SYSTEM.refs.textMiniLastCreated = CUTSCENE_REF_TEXT_MINI_LAST_CREATED;
|
|
|
|
expect_assert_failure(
|
|
cutsceneSystemGetTextMiniId(CUTSCENE_REF_TEXT_MINI_LAST_CREATED)
|
|
);
|
|
assert_int_equal(cutsceneSystemGetTextMiniId(4), 4);
|
|
|
|
CUTSCENE_SYSTEM.refs.textMiniLastCreated = 1;
|
|
assert_int_equal(
|
|
cutsceneSystemGetTextMiniId(CUTSCENE_REF_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.refs.entityInteract);
|
|
assert_null(CUTSCENE_SYSTEM.refs.entityInteracted);
|
|
}
|
|
|
|
// --- loadedFile survival across cutsceneSystemPrepare -------------------
|
|
//
|
|
// cutsceneCutsceneResolve (and cutsceneSystemLoad) stamp
|
|
// CUTSCENE_SYSTEM.loadedFile with what they just parsed loadedScene from
|
|
// - cutsceneSystemGoTo 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 cutsceneSystemGoTo 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.refs.entityInteract, &ENTITIES[3]);
|
|
assert_ptr_equal(CUTSCENE_SYSTEM.refs.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);
|
|
}
|