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:
2026-09-21 15:04:41 -05:00
co-authored by Claude Sonnet 5
parent 7aeffac9fc
commit c9f098693c
13 changed files with 430 additions and 123 deletions
+1
View File
@@ -6,6 +6,7 @@
include(dusktest)
# Tests
dusktest(test_cutscene.c)
dusktest(test_cutscenesystem.c)
dusktest(test_cutscenecontrol.c)
dusktest(test_cutscenemaparea.c)
+84
View File
@@ -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);
}
+78 -13
View File
@@ -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),