Add cutsceneAppend/cutsceneAppendNext for mutating a running cutscene

cutsceneAppend adds items to the end of a cutscene_t's item array;
cutsceneAppendNext inserts them at a given index instead (e.g.
CUTSCENE_SYSTEM.currentItem + 1, to run immediately after the
currently-executing item), shifting everything from that index
onward to make room. Both are capacity-bounded against the caller-
supplied itemsMax, matching cutsceneParseDoc's convention of never
assuming ownership of the backing buffer's size.

cutscene_t.items drops its const qualifier to allow this in-place
mutation; cutsceneitem_t backing arrays in the test fixtures that get
assigned into a mutable cutscene_t.items are no longer declared const
either, since a const source would otherwise silently violate the
field's new type.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-09-15 13:16:02 -05:00
co-authored by Claude Sonnet 5
parent dbb5301ade
commit 44594f7011
5 changed files with 97 additions and 21 deletions
+38
View File
@@ -105,3 +105,41 @@ errorret_t cutsceneParseDoc(
errorOk(); errorOk();
} }
void cutsceneAppend(
cutscene_t *scene,
const size_t itemsMax,
const cutsceneitem_t *items,
const uint8_t count
) {
assertNotNull(scene, "Scene cannot be NULL");
cutsceneAppendNext(scene, itemsMax, scene->itemCount, items, count);
}
void cutsceneAppendNext(
cutscene_t *scene,
const size_t itemsMax,
const uint8_t atIndex,
const cutsceneitem_t *items,
const uint8_t count
) {
assertNotNull(scene, "Scene cannot be NULL");
assertNotNull(items, "Items cannot be NULL");
assertTrue(count > 0, "Count cannot be 0");
assertTrue(atIndex <= scene->itemCount, "atIndex out of bounds");
assertTrue(
(size_t)scene->itemCount + count <= itemsMax,
"Cutscene item append exceeds itemsMax"
);
const size_t tailCount = (size_t)scene->itemCount - atIndex;
if(tailCount > 0) {
memoryMove(
&scene->items[atIndex + count],
&scene->items[atIndex],
tailCount * sizeof(cutsceneitem_t)
);
}
memoryCopy(&scene->items[atIndex], items, count * sizeof(cutsceneitem_t));
scene->itemCount += count;
}
+39 -1
View File
@@ -10,7 +10,7 @@
#include "rpg/cutscene/cutscenepause.h" #include "rpg/cutscene/cutscenepause.h"
typedef struct cutscene_s { typedef struct cutscene_s {
const cutsceneitem_t *items; cutsceneitem_t *items;
uint8_t itemCount; uint8_t itemCount;
cutscenepause_t pause; cutscenepause_t pause;
} cutscene_t; } cutscene_t;
@@ -66,3 +66,41 @@ errorret_t cutsceneParseDoc(
cutsceneitem_t *items, cutsceneitem_t *items,
size_t itemsMax size_t itemsMax
); );
/**
* Appends items onto the end of scene->items, growing scene->itemCount.
* scene->items must point at a buffer with capacity itemsMax - asserts if
* appending would exceed it.
*
* @param scene Cutscene to append to.
* @param itemsMax Capacity of scene->items.
* @param items Items to append, copied in order.
* @param count Number of entries in items.
*/
void cutsceneAppend(
cutscene_t *scene,
const size_t itemsMax,
const cutsceneitem_t *items,
const uint8_t count
);
/**
* Same as cutsceneAppend, but inserts items starting at atIndex instead of
* at the end - e.g. pass CUTSCENE_SYSTEM.currentItem + 1 to make these
* items run immediately after the currently-executing one. Every existing
* item from atIndex onward shifts right by count to make room.
*
* @param scene Cutscene to insert into.
* @param itemsMax Capacity of scene->items.
* @param atIndex Index the first inserted item should end up at - must be
* <= scene->itemCount.
* @param items Items to insert, copied in order.
* @param count Number of entries in items.
*/
void cutsceneAppendNext(
cutscene_t *scene,
const size_t itemsMax,
const uint8_t atIndex,
const cutsceneitem_t *items,
const uint8_t count
);
+1 -1
View File
@@ -11,7 +11,7 @@
// Fighter slots as added by test_battleCutsceneForceActionOverridesTarget: // Fighter slots as added by test_battleCutsceneForceActionOverridesTarget:
// 0 = allyA, 1 = allyB, 2 = enemy. // 0 = allyA, 1 = allyB, 2 = enemy.
static const cutsceneitem_t CUTSCENE_TEST_SCRATCH_ITEMS[] = { static cutsceneitem_t CUTSCENE_TEST_SCRATCH_ITEMS[] = {
// Waiting BEFORE pausing is the correct order: pausing first would // Waiting BEFORE pausing is the correct order: pausing first would
// freeze BATTLE.state wherever it happened to be and it would never // freeze BATTLE.state wherever it happened to be and it would never
// reach PRE_ROUND on its own to satisfy the wait. // reach PRE_ROUND on its own to satisfy the wait.
+18 -18
View File
@@ -20,7 +20,7 @@ static void recordCallback(void *userData) {
// the "nested cutscene is a one-way jump" behavior: cutsceneCutsceneStart // the "nested cutscene is a one-way jump" behavior: cutsceneCutsceneStart
// replaces CUTSCENE_SYSTEM.scene outright, and the callback item's effect // 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. // fires in Start, not Update, so it fires the same frame the jump happens.
static const cutsceneitem_t CUTSCENE_TEST_INNER_ITEMS[] = { static cutsceneitem_t CUTSCENE_TEST_INNER_ITEMS[] = {
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordCallback } { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordCallback }
}; };
static const cutscene_t CUTSCENE_TEST_INNER = { static const cutscene_t CUTSCENE_TEST_INNER = {
@@ -29,7 +29,7 @@ static const cutscene_t CUTSCENE_TEST_INNER = {
.pause = CUTSCENE_PAUSE_NONE .pause = CUTSCENE_PAUSE_NONE
}; };
static const cutsceneitem_t CUTSCENE_TEST_OUTER_ITEMS[] = { static cutsceneitem_t CUTSCENE_TEST_OUTER_ITEMS[] = {
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 0.5f }, { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 0.5f },
{ {
.type = CUTSCENE_ITEM_TYPE_CUTSCENE, .type = CUTSCENE_ITEM_TYPE_CUTSCENE,
@@ -88,7 +88,7 @@ static void test_cutsceneSystemUpdateIsNoopWithNoActiveCutscene(void **state) {
assert_null(CUTSCENE_SYSTEM.scene); assert_null(CUTSCENE_SYSTEM.scene);
} }
static const cutsceneitem_t CUTSCENE_TEST_SINGLE_WAIT_ITEMS[] = { static cutsceneitem_t CUTSCENE_TEST_SINGLE_WAIT_ITEMS[] = {
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f } { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f }
}; };
static const cutscene_t CUTSCENE_TEST_SINGLE_WAIT = { static const cutscene_t CUTSCENE_TEST_SINGLE_WAIT = {
@@ -206,7 +206,7 @@ static void recordOrder2(void *userData) { insertOrderLog[insertOrderLogCount++]
static void recordOrderA(void *userData) { insertOrderLog[insertOrderLogCount++] = 3; } static void recordOrderA(void *userData) { insertOrderLog[insertOrderLogCount++] = 3; }
static void recordOrderB(void *userData) { insertOrderLog[insertOrderLogCount++] = 4; } static void recordOrderB(void *userData) { insertOrderLog[insertOrderLogCount++] = 4; }
static const cutsceneitem_t CUTSCENE_TEST_INSERT_SNIPPET_ITEMS[] = { static cutsceneitem_t CUTSCENE_TEST_INSERT_SNIPPET_ITEMS[] = {
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA }, { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA },
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 2.0f }, { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 2.0f },
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderB } { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderB }
@@ -218,7 +218,7 @@ static const cutscene_t CUTSCENE_TEST_INSERT_SNIPPET = {
.pause = CUTSCENE_PAUSE_NONE .pause = CUTSCENE_PAUSE_NONE
}; };
static const cutsceneitem_t CUTSCENE_TEST_INSERT_OUTER_ITEMS[] = { static cutsceneitem_t CUTSCENE_TEST_INSERT_OUTER_ITEMS[] = {
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder0 }, { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder0 },
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f }, { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f },
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder1 }, { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder1 },
@@ -263,7 +263,7 @@ static void test_cutsceneInsertSplicesItemsInPlaceThenResumes(void **state) {
assert_memory_equal(insertOrderLog, expected, sizeof(expected)); assert_memory_equal(insertOrderLog, expected, sizeof(expected));
} }
static const cutsceneitem_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS[] = { static cutsceneitem_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS[] = {
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA } { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA }
}; };
static const cutscene_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET = { static const cutscene_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET = {
@@ -273,7 +273,7 @@ static const cutscene_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET = {
.pause = CUTSCENE_PAUSE_NONE .pause = CUTSCENE_PAUSE_NONE
}; };
static const cutsceneitem_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER_ITEMS[] = { static cutsceneitem_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER_ITEMS[] = {
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder0 }, { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder0 },
{ {
.type = CUTSCENE_ITEM_TYPE_INSERT, .type = CUTSCENE_ITEM_TYPE_INSERT,
@@ -313,7 +313,7 @@ static void test_cutsceneInsertAsLastItemEndsCutsceneNaturally(
assert_memory_equal(insertOrderLog, expected, sizeof(expected)); assert_memory_equal(insertOrderLog, expected, sizeof(expected));
} }
static const cutsceneitem_t CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS[] = { static cutsceneitem_t CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS[] = {
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA } { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA }
}; };
static const cutscene_t CUTSCENE_TEST_INSERT_NESTED_INNER = { static const cutscene_t CUTSCENE_TEST_INSERT_NESTED_INNER = {
@@ -326,7 +326,7 @@ static const cutscene_t CUTSCENE_TEST_INSERT_NESTED_INNER = {
// INSERT is MID's first item, so entering MID (from OUTER) cascades // INSERT is MID's first item, so entering MID (from OUTER) cascades
// straight through into INNER within the same Start chain, one level // straight through into INNER within the same Start chain, one level
// deeper than a single insert. // deeper than a single insert.
static const cutsceneitem_t CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS[] = { static cutsceneitem_t CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS[] = {
{ {
.type = CUTSCENE_ITEM_TYPE_INSERT, .type = CUTSCENE_ITEM_TYPE_INSERT,
.insert = { .cutscene = &CUTSCENE_TEST_INSERT_NESTED_INNER, .name = NULL } .insert = { .cutscene = &CUTSCENE_TEST_INSERT_NESTED_INNER, .name = NULL }
@@ -340,7 +340,7 @@ static const cutscene_t CUTSCENE_TEST_INSERT_NESTED_MID = {
.pause = CUTSCENE_PAUSE_NONE .pause = CUTSCENE_PAUSE_NONE
}; };
static const cutsceneitem_t CUTSCENE_TEST_INSERT_NESTED_OUTER_ITEMS[] = { static cutsceneitem_t CUTSCENE_TEST_INSERT_NESTED_OUTER_ITEMS[] = {
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder0 }, { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder0 },
{ {
.type = CUTSCENE_ITEM_TYPE_INSERT, .type = CUTSCENE_ITEM_TYPE_INSERT,
@@ -380,7 +380,7 @@ static void test_cutsceneInsertNestsThroughMultipleLevels(void **state) {
assert_memory_equal(insertOrderLog, expected, sizeof(expected)); assert_memory_equal(insertOrderLog, expected, sizeof(expected));
} }
static const cutsceneitem_t CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL5_ITEMS[] = { static cutsceneitem_t CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL5_ITEMS[] = {
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA } { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA }
}; };
static const cutscene_t CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL5 = { static const cutscene_t CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL5 = {
@@ -389,7 +389,7 @@ static const cutscene_t CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL5 = {
sizeof(cutsceneitem_t), sizeof(cutsceneitem_t),
.pause = CUTSCENE_PAUSE_NONE .pause = CUTSCENE_PAUSE_NONE
}; };
static const cutsceneitem_t CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL4_ITEMS[] = { static cutsceneitem_t CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL4_ITEMS[] = {
{ {
.type = CUTSCENE_ITEM_TYPE_INSERT, .type = CUTSCENE_ITEM_TYPE_INSERT,
.insert = { .cutscene = &CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL5, .name = NULL } .insert = { .cutscene = &CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL5, .name = NULL }
@@ -401,7 +401,7 @@ static const cutscene_t CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL4 = {
sizeof(cutsceneitem_t), sizeof(cutsceneitem_t),
.pause = CUTSCENE_PAUSE_NONE .pause = CUTSCENE_PAUSE_NONE
}; };
static const cutsceneitem_t CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL3_ITEMS[] = { static cutsceneitem_t CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL3_ITEMS[] = {
{ {
.type = CUTSCENE_ITEM_TYPE_INSERT, .type = CUTSCENE_ITEM_TYPE_INSERT,
.insert = { .cutscene = &CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL4, .name = NULL } .insert = { .cutscene = &CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL4, .name = NULL }
@@ -413,7 +413,7 @@ static const cutscene_t CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL3 = {
sizeof(cutsceneitem_t), sizeof(cutsceneitem_t),
.pause = CUTSCENE_PAUSE_NONE .pause = CUTSCENE_PAUSE_NONE
}; };
static const cutsceneitem_t CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL2_ITEMS[] = { static cutsceneitem_t CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL2_ITEMS[] = {
{ {
.type = CUTSCENE_ITEM_TYPE_INSERT, .type = CUTSCENE_ITEM_TYPE_INSERT,
.insert = { .cutscene = &CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL3, .name = NULL } .insert = { .cutscene = &CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL3, .name = NULL }
@@ -425,7 +425,7 @@ static const cutscene_t CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL2 = {
sizeof(cutsceneitem_t), sizeof(cutsceneitem_t),
.pause = CUTSCENE_PAUSE_NONE .pause = CUTSCENE_PAUSE_NONE
}; };
static const cutsceneitem_t CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL1_ITEMS[] = { static cutsceneitem_t CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL1_ITEMS[] = {
{ {
.type = CUTSCENE_ITEM_TYPE_INSERT, .type = CUTSCENE_ITEM_TYPE_INSERT,
.insert = { .cutscene = &CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL2, .name = NULL } .insert = { .cutscene = &CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL2, .name = NULL }
@@ -437,7 +437,7 @@ static const cutscene_t CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL1 = {
sizeof(cutsceneitem_t), sizeof(cutsceneitem_t),
.pause = CUTSCENE_PAUSE_NONE .pause = CUTSCENE_PAUSE_NONE
}; };
static const cutsceneitem_t CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL0_ITEMS[] = { static cutsceneitem_t CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL0_ITEMS[] = {
{ {
.type = CUTSCENE_ITEM_TYPE_INSERT, .type = CUTSCENE_ITEM_TYPE_INSERT,
.insert = { .cutscene = &CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL1, .name = NULL } .insert = { .cutscene = &CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL1, .name = NULL }
@@ -466,7 +466,7 @@ static void test_cutsceneInsertAssertsWhenNestingExceedsStackMax(
static void noopInsertCallback(void *userData) {} static void noopInsertCallback(void *userData) {}
static const cutsceneitem_t CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS[] = { static cutsceneitem_t CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS[] = {
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = noopInsertCallback } { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = noopInsertCallback }
}; };
static const cutscene_t CUTSCENE_TEST_INSERT_PERSIST_SNIPPET = { static const cutscene_t CUTSCENE_TEST_INSERT_PERSIST_SNIPPET = {
@@ -476,7 +476,7 @@ static const cutscene_t CUTSCENE_TEST_INSERT_PERSIST_SNIPPET = {
.pause = CUTSCENE_PAUSE_ALL .pause = CUTSCENE_PAUSE_ALL
}; };
static const cutsceneitem_t CUTSCENE_TEST_INSERT_PERSIST_OUTER_ITEMS[] = { static cutsceneitem_t CUTSCENE_TEST_INSERT_PERSIST_OUTER_ITEMS[] = {
{ {
.type = CUTSCENE_ITEM_TYPE_INSERT, .type = CUTSCENE_ITEM_TYPE_INSERT,
.insert = { .cutscene = &CUTSCENE_TEST_INSERT_PERSIST_SNIPPET, .name = NULL } .insert = { .cutscene = &CUTSCENE_TEST_INSERT_PERSIST_SNIPPET, .name = NULL }
+1 -1
View File
@@ -11,7 +11,7 @@
#include "ui/focus/uifocus.h" #include "ui/focus/uifocus.h"
#include "rpg/cutscene/cutscenesystem.h" #include "rpg/cutscene/cutscenesystem.h"
static const cutsceneitem_t CUTSCENE_TEST_INTERACT_ITEMS[] = { static cutsceneitem_t CUTSCENE_TEST_INTERACT_ITEMS[] = {
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f } { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f }
}; };
static const cutscene_t CUTSCENE_TEST_INTERACT = { static const cutscene_t CUTSCENE_TEST_INTERACT = {