Wire up battle AI/scene-return, reclaim cutscene item slots, fix GOTO
- battleStateSelectionAiDecide: AI-controlled fighters now actually queue a basic attack against the first living enemy instead of doing nothing every round. - battleStateEndingInit: disposes the battle and switches back to SCENE_TYPE_OVERWORLD instead of leaving the game stuck in the battle scene forever. - cutsceneSystemNext now pops each finished item off the front of the running cutscene (cutsceneRemoveFront) instead of leaving it in place forever, so a long-running cutscene (many battle rounds splicing items in repeatedly) no longer grows scene->items unboundedly. - cutsceneGoTo reworked to match: since consumed items are evicted, a marker behind the current position can't be found in the live queue anymore, so it now re-parses the cutscene fresh from its source file (cutsceneLoadParse, into a persistent scratch buffer, not the stack) and replaces the live queue with marker-onward from that fresh parse. Only supported when the running cutscene is the one cutsceneSystemLoad/cutsceneCutsceneResolve populated. - Fixed a real bug this surfaced: cutsceneSystemPrepare unconditionally cleared loadedFile, which ran *after* cutsceneCutsceneResolve had just stamped it but *before* the cutscene started - wiping it out immediately and breaking cutsceneGoTo for any cutscene entered via a CUTSCENE item or cutsceneCutsceneResolve (e.g. the main menu's NEW_GAME/OPTIONS/QUIT navigation). Now only cleared when switching away from loadedScene entirely. - Added cutsceneLoadParse, the shared "lock asset, require loaded, parse items, unlock" sequence, replacing four near-identical copies across cutsceneSystemLoad/cutsceneCutsceneResolve/ cutsceneInsertResolveAndSplice/battleStateExecutingInit. Each call site handles its own fatal-error display; cutsceneLoadParse itself just propagates. - Added test_cutscene.c (cutsceneRemoveFront) and a regression test for the loadedFile bug. Writing the zero-count RemoveFront test caught a real self-move bug (count == 0 passed dest == src into memoryMove). Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
include(dusktest)
|
||||
|
||||
# Tests
|
||||
dusktest(test_cutscene.c)
|
||||
dusktest(test_cutscenesystem.c)
|
||||
dusktest(test_cutscenecontrol.c)
|
||||
dusktest(test_cutscenemaparea.c)
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "rpg/cutscene/cutscene.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
|
||||
// ============================================================
|
||||
// cutsceneRemoveFront
|
||||
// ============================================================
|
||||
|
||||
static cutsceneitem_t MARKER_ITEM(const char_t *name) {
|
||||
cutsceneitem_t item;
|
||||
memoryZero(&item, sizeof(item));
|
||||
item.type = CUTSCENE_ITEM_TYPE_MARKER;
|
||||
stringCopy(item.marker.name, name, CUTSCENE_MARKER_NAME_MAX - 1);
|
||||
return item;
|
||||
}
|
||||
|
||||
static void test_cutsceneRemoveFrontShiftsRemainderToIndexZero(
|
||||
void **state
|
||||
) {
|
||||
|
||||
cutsceneitem_t items[4] = {
|
||||
MARKER_ITEM("a"), MARKER_ITEM("b"), MARKER_ITEM("c"), MARKER_ITEM("d")
|
||||
};
|
||||
cutscene_t scene = { .items = items, .itemCount = 4, .itemsMax = 4 };
|
||||
|
||||
cutsceneRemoveFront(&scene, 2);
|
||||
|
||||
assert_int_equal(scene.itemCount, 2);
|
||||
assert_string_equal(scene.items[0].marker.name, "c");
|
||||
assert_string_equal(scene.items[1].marker.name, "d");
|
||||
}
|
||||
|
||||
static void test_cutsceneRemoveFrontAllItemsLeavesEmptyScene(void **state) {
|
||||
|
||||
cutsceneitem_t items[3] = {
|
||||
MARKER_ITEM("a"), MARKER_ITEM("b"), MARKER_ITEM("c")
|
||||
};
|
||||
cutscene_t scene = { .items = items, .itemCount = 3, .itemsMax = 3 };
|
||||
|
||||
cutsceneRemoveFront(&scene, 3);
|
||||
|
||||
assert_int_equal(scene.itemCount, 0);
|
||||
}
|
||||
|
||||
static void test_cutsceneRemoveFrontZeroIsNoop(void **state) {
|
||||
|
||||
cutsceneitem_t items[2] = { MARKER_ITEM("a"), MARKER_ITEM("b") };
|
||||
cutscene_t scene = { .items = items, .itemCount = 2, .itemsMax = 2 };
|
||||
|
||||
cutsceneRemoveFront(&scene, 0);
|
||||
|
||||
assert_int_equal(scene.itemCount, 2);
|
||||
assert_string_equal(scene.items[0].marker.name, "a");
|
||||
assert_string_equal(scene.items[1].marker.name, "b");
|
||||
}
|
||||
|
||||
static void test_cutsceneRemoveFrontAssertsWhenCountExceedsItemCount(
|
||||
void **state
|
||||
) {
|
||||
|
||||
cutsceneitem_t items[2] = { MARKER_ITEM("a"), MARKER_ITEM("b") };
|
||||
cutscene_t scene = { .items = items, .itemCount = 2, .itemsMax = 2 };
|
||||
|
||||
expect_assert_failure(cutsceneRemoveFront(&scene, 3));
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_cutsceneRemoveFrontShiftsRemainderToIndexZero),
|
||||
cmocka_unit_test(test_cutsceneRemoveFrontAllItemsLeavesEmptyScene),
|
||||
cmocka_unit_test(test_cutsceneRemoveFrontZeroIsNoop),
|
||||
cmocka_unit_test(test_cutsceneRemoveFrontAssertsWhenCountExceedsItemCount),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "time/time.h"
|
||||
#include "util/string.h"
|
||||
|
||||
static uint8_t callbackFired;
|
||||
|
||||
@@ -198,18 +199,72 @@ static void test_cutsceneSystemDisposeResetsState(void **state) {
|
||||
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; itemCount growing is what shows
|
||||
// the splice actually happened. Since the running scene's own array is
|
||||
// what gets appended into now, any outer cutscene used with INSERT must
|
||||
// declare its items array (and .itemsMax) with enough spare capacity for
|
||||
// whatever ends up spliced into it, including transitively through any
|
||||
// nested INSERT.
|
||||
// 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;
|
||||
@@ -263,10 +318,12 @@ static void test_cutsceneInsertSplicesItemsInPlaceThenResumes(void **state) {
|
||||
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,
|
||||
// but it grew by the snippet's 3 items.
|
||||
// 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, 8);
|
||||
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
|
||||
@@ -323,7 +380,9 @@ static void test_cutsceneInsertAsLastItemEndsCutsceneNaturally(
|
||||
|
||||
cutsceneSystemUpdate();// order0 done -> starts INSERT, cascades to orderA
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER);
|
||||
assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 3);
|
||||
// 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
|
||||
@@ -398,7 +457,9 @@ static void test_cutsceneInsertNestsThroughMultipleLevels(void **state) {
|
||||
// land in OUTER's own array.
|
||||
cutsceneSystemUpdate();
|
||||
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_NESTED_OUTER);
|
||||
assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 6);
|
||||
// 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
|
||||
@@ -486,9 +547,10 @@ static void test_cutsceneInsertDoesNotResetPauseOrInteractEntities(
|
||||
);
|
||||
|
||||
// The INSERT item's Start already spliced the snippet into place and
|
||||
// cascaded into it by now.
|
||||
// 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, 2);
|
||||
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
|
||||
@@ -507,6 +569,9 @@ 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_cutsceneSystemPrepareKeepsLoadedFileWhenReenteringLoadedScene
|
||||
),
|
||||
cmocka_unit_test(test_cutsceneInsertSplicesItemsInPlaceThenResumes),
|
||||
cmocka_unit_test(test_cutsceneInsertAsLastItemEndsCutsceneNaturally),
|
||||
cmocka_unit_test(test_cutsceneInsertNestsThroughMultipleLevels),
|
||||
|
||||
Reference in New Issue
Block a user