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
@@ -6,9 +6,17 @@
*/
#include "battlestateending.h"
#include "rpg/battle/battle.h"
#include "scene/scene.h"
void battleStateEndingInit(void) {
// Tear down the battle and hand control back to the overworld. The
// overworld scene keeps no state of its own in scenedata_t (player/map
// state lives in the separate global entity/map systems, untouched by
// switching to/from SCENE_TYPE_BATTLE), so there's nothing else to
// restore here.
battleDispose();
sceneSet(SCENE_TYPE_OVERWORLD);
}
void battleStateEndingUpdate(void) {
@@ -13,7 +13,8 @@ typedef struct {
} battlestateending_t;
/**
* Called when the battle enters BATTLE_STATE_ENDING.
* Called when the battle enters BATTLE_STATE_ENDING. Disposes the battle
* (see battleDispose) and switches back to SCENE_TYPE_OVERWORLD.
*/
void battleStateEndingInit(void);
@@ -10,8 +10,6 @@
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "rpg/cutscene/cutscene.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
#include "asset/assetfile.h"
#include "ui/overlay/uifatalerror.h"
#include "assert/assert.h"
@@ -61,33 +59,20 @@ void battleStateExecutingInit(void) {
"cutscenes/battle/moves/%s.jsonc", ability->name
);
// Load cutscene asset.
assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_JSON, NULL);
errorret_t result = assetRequireLoaded(entry);
// Load and parse the move's cutscene straight into the tail of items.
const uint8_t moveItemsStart = itemsCount;
cutscene_t scene;
errorret_t result = cutsceneLoadParse(
path, &scene, &items[itemsCount],
BATTLE_STATE_EXECUTING_ITEMS_MAX - itemsCount
);
if(errorIsNotOk(result)) {
assetUnlockEntry(entry);
errorCatch(errorPrint(result));
uiFatalErrorOpen(NULL);
continue;
}
// Parse out cutscene items and append to the list.
const uint8_t moveItemsStart = itemsCount;
cutscene_t scene;
errorret_t parseResult = cutsceneParseDoc(
entry->data.json, &scene, &items[itemsCount],
BATTLE_STATE_EXECUTING_ITEMS_MAX - itemsCount
);
itemsCount += scene.itemCount;
// Release asset
assetUnlockEntry(entry);
if(errorIsNotOk(parseResult)) {
errorCatch(errorPrint(parseResult));
uiFatalErrorOpen(NULL);
continue;
}
// Resolve BATTLE_FIGHTER_POS_TARGET/USER sentinels against this
// action, now that its user/target fighters are known.
for(uint8_t j = moveItemsStart; j < itemsCount; j++) {
@@ -32,6 +32,25 @@ bool_t battleStateSelectionIsComplete(void) {
return BATTLE.stateData.selection.selectionIndex >= BATTLE.fighterOrderCount;
}
void battleStateSelectionAiDecide(const uint8_t fighterIndex) {
battlefighter_t *fighter = BATTLE.fighters[fighterIndex];
const battlefighterteam_t enemyTeam =
fighter->team == BATTLE_FIGHTER_TEAM_ALLY ?
BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY;
// Simple placeholder AI: attack the first living fighter on the
// opposing team. No move variety or target scoring yet.
for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) {
battlefighter_t *candidate = BATTLE.fighters[i];
if(candidate == NULL) continue;
if(candidate->team != enemyTeam) continue;
if(!battleFighterIsAlive(candidate)) continue;
battleStateSelectionQueueAttack(fighterIndex, i);
return;
}
}
void battleStateSelectionUpdate(void) {
battlestateselection_t *state = &BATTLE.stateData.selection;
@@ -49,8 +68,7 @@ void battleStateSelectionUpdate(void) {
battlefighter_t *fighter = BATTLE.fighters[fighterIndex];
if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_AI) return;
// This is AI, in future I'll have proper AI here, for now I'm actually not
// going to do anything.
battleStateSelectionAiDecide(fighterIndex);
state->selectionIndex++;
}
}
@@ -20,11 +20,13 @@ typedef struct {
void battleStateSelectionInit(void);
/**
* Updates BATTLE_STATE_SELECTION for one frame: auto-decides for every
* AI-controlled fighter the cursor passes, stopping once it reaches a
* player-controlled fighter awaiting input (see
* battleStateSelectionGetCurrentFighter), or transitions to
* BATTLE_STATE_EXECUTING once every position has decided.
* Updates BATTLE_STATE_SELECTION for one frame: auto-decides (see
* battleStateSelectionAiDecide) for every AI-controlled fighter the
* cursor passes, stopping once it reaches a player-controlled fighter
* awaiting input (see battleStateSelectionGetCurrentFighter). Once every
* position has decided, BATTLE_WAIT_SELECTION (see
* cutsceneBattleWaitSelectionUpdate) queues the move to
* BATTLE_STATE_EXECUTING.
*/
void battleStateSelectionUpdate(void);
@@ -52,6 +54,17 @@ bool_t battleStateSelectionFighterHasDecided(const uint8_t fighterIndex);
*/
bool_t battleStateSelectionFighterNeedsDecision(const uint8_t fighterIndex);
/**
* Decides and queues a move for an AI-controlled fighter: currently
* always a basic attack against the first living fighter on the
* opposing team (see battleStateSelectionQueueAttack). No-ops if there's
* no living target (the battle should already be over by then via
* BATTLE_POST_MOVE).
*
* @param fighterIndex Index into BATTLE.fighters of the deciding fighter.
*/
void battleStateSelectionAiDecide(const uint8_t fighterIndex);
/**
* Checks whether every fighter in BATTLE.fighterOrder has decided a move
* this round.
+50
View File
@@ -9,6 +9,8 @@
#include "item/json/cutscenejsonpauseflags.h"
#include "util/memory.h"
#include "assert/assert.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
errorret_t cutsceneParseItem(
yyjson_val *itemObj,
@@ -145,3 +147,51 @@ void cutsceneAppendNext(
) {
cutsceneAppendAt(scene, (uint8_t)(currentIndex + 1), items, count);
}
void cutsceneRemoveFront(
cutscene_t *scene,
const uint8_t count
) {
assertNotNull(scene, "Scene cannot be NULL");
assertTrue(count <= scene->itemCount, "count exceeds itemCount");
const size_t tailCount = (size_t)scene->itemCount - count;
// count == 0 has nothing to shift (and would pass dest == src into
// memoryMove, which asserts against that as a no-op call).
if(count > 0 && tailCount > 0) {
memoryMove(
&scene->items[0],
&scene->items[count],
tailCount * sizeof(cutsceneitem_t)
);
}
scene->itemCount -= count;
}
errorret_t cutsceneLoadParse(
const char_t *path,
cutscene_t *scene,
cutsceneitem_t *items,
const size_t itemsMax
) {
assertNotNull(path, "Path cannot be NULL");
assertNotNull(scene, "Scene cannot be NULL");
assertNotNull(items, "Items cannot be NULL");
assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_JSON, NULL);
errorret_t result = assetRequireLoaded(entry);
if(errorIsNotOk(result)) {
assetUnlockEntry(entry);
errorChain(result);
}
// Parse straight out of the entry's own doc while still locked - every
// cutsceneitem_t field that could reference it owns its string data by
// value, so nothing needs to survive past this call and the entry can
// just be unlocked like any other asset once parsing finishes.
errorret_t parseResult = cutsceneParseDoc(entry->data.json, scene, items, itemsMax);
assetUnlockEntry(entry);
errorChain(parseResult);
errorOk();
}
+42
View File
@@ -127,3 +127,45 @@ void cutsceneAppendNext(
const cutsceneitem_t *items,
const uint8_t count
);
/**
* Removes count items from the front of scene->items, shifting the
* remainder down to index 0 and shrinking scene->itemCount to match. Used
* by cutsceneSystemNext/cutsceneGoTo to reclaim already-run items' slots
* instead of leaving them as dead weight forever - see
* CUTSCENE_SYSTEM.currentItem's doc comment, since the currently-running
* item is always scene->items[0] once a cutscene has started.
*
* @param scene Cutscene to remove from.
* @param count Number of items to remove from the front - must be
* <= scene->itemCount.
*/
void cutsceneRemoveFront(
cutscene_t *scene,
const uint8_t count
);
/**
* Loads a cutscene JSON asset by path and parses it into scene/items (see
* cutsceneParseDoc) - the common "lock the asset, require it loaded,
* parse its 'items' array, unlock it again" sequence shared by every
* place that resolves a cutscene by file path (cutsceneSystemLoad,
* cutsceneCutsceneResolve, cutsceneInsertResolveAndSplice,
* battleStateExecutingInit, cutsceneGoTo's reload). Purely propagates the
* error on failure (like cutsceneParseDoc) - this is core cutscene logic,
* not UI, so it doesn't reach for uiFatalErrorOpen itself; each caller
* decides how to surface the failure (typically errorCatch(errorPrint())
* + uiFatalErrorOpen(NULL), same as any other asset load failure).
*
* @param path Asset path of the cutscene JSON file to load.
* @param scene Destination scene - itemCount/itemsMax/pause overwritten.
* @param items Destination item array, capacity itemsMax.
* @param itemsMax Capacity of items.
* @return Error code indicating success or failure of the load/parse.
*/
errorret_t cutsceneLoadParse(
const char_t *path,
cutscene_t *scene,
cutsceneitem_t *items,
const size_t itemsMax
);
+78 -37
View File
@@ -10,8 +10,6 @@
#include "util/memory.h"
#include "util/string.h"
#include "assert/assert.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
#include "ui/overlay/uifatalerror.h"
cutscenesystem_t CUTSCENE_SYSTEM;
@@ -55,7 +53,19 @@ void cutsceneSystemPrepare(
CUTSCENE_SYSTEM.textCache[0] = '\0';
CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0.
CUTSCENE_SYSTEM.onComplete = NULL;
CUTSCENE_SYSTEM.loadedFile[0] = '\0';
// Only invalidate the known source file when switching away from
// loadedScene entirely. Starting loadedScene itself means
// cutsceneSystemLoad/cutsceneCutsceneResolve just populated it - for
// cutsceneCutsceneResolve (see cutsceneCutsceneStart), that already
// happened before this call, so clearing loadedFile here would
// immediately undo it. cutsceneGoTo needs loadedFile intact to reload
// a marker behind the current position; cutsceneSystemLoad re-stamps
// it itself right after this call anyway, so it's unaffected either
// way.
if(cutscene != &CUTSCENE_SYSTEM.loadedScene) {
CUTSCENE_SYSTEM.loadedFile[0] = '\0';
}
}
void cutsceneSystemStartCutscene(cutscene_t *cutscene) {
@@ -118,27 +128,12 @@ void cutsceneSystemInsertCutscene(const cutscene_t *cutscene) {
void cutsceneSystemLoad(const char_t *file) {
assertNotNull(file, "File cannot be NULL");
assetentry_t *entry = assetLock(file, ASSET_LOADER_TYPE_JSON, NULL);
errorret_t result = assetRequireLoaded(entry);
if(errorIsNotOk(result)) {
assetUnlockEntry(entry);
errorCatch(errorPrint(result));
uiFatalErrorOpen(NULL);
return;
}
// Parse straight out of the entry's own doc while still locked - every
// cutsceneitem_t field that could reference it owns its string data by
// value (see cutscenecutsceneref_t/cutscenemarker_t's doc comments), so
// nothing needs to survive past this call and the entry can just be
// unlocked like any other asset once parsing finishes.
errorret_t parseResult = cutsceneParseDoc(
entry->data.json, &CUTSCENE_SYSTEM.loadedScene, CUTSCENE_SYSTEM.loadedItems,
errorret_t result = cutsceneLoadParse(
file, &CUTSCENE_SYSTEM.loadedScene, CUTSCENE_SYSTEM.loadedItems,
CUTSCENE_LOADED_ITEMS_MAX
);
assetUnlockEntry(entry);
if(errorIsNotOk(parseResult)) {
errorCatch(errorPrint(parseResult));
if(errorIsNotOk(result)) {
errorCatch(errorPrint(result));
uiFatalErrorOpen(NULL);
return;
}
@@ -256,15 +251,21 @@ void cutsceneSystemSetTextCache(const char_t *text) {
void cutsceneSystemNext() {
if(CUTSCENE_SYSTEM.scene == NULL) return;
CUTSCENE_SYSTEM.currentItem++;
// Pop the just-finished item off the front rather than advancing an
// index further into the array - see CUTSCENE_SYSTEM.currentItem's doc
// comment. The very first call (currentItem still 0xFF) has nothing to
// pop yet; it just marks the cutscene as started.
if(CUTSCENE_SYSTEM.currentItem == 0xFF) {
CUTSCENE_SYSTEM.currentItem = 0;
} else {
cutsceneRemoveFront(CUTSCENE_SYSTEM.scene, 1);
}
// End of the cutscene? Note that a CUTSCENE_ITEM_TYPE_INSERT item's
// spliced-in items are physically part of this same array (see
// cutsceneSystemInsertCutscene), so there's no separate "parent scene"
// to fall back into here - itemCount already accounts for them.
if(
CUTSCENE_SYSTEM.currentItem >= CUTSCENE_SYSTEM.scene->itemCount
) {
if(CUTSCENE_SYSTEM.scene->itemCount == 0) {
// Saved and cleared before firing so a callback that immediately
// starts another cutscene (or sets its own onComplete) isn't clobbered
// by this function's own cleanup running after it - same reentrancy
@@ -298,18 +299,57 @@ void cutsceneGoTo(const char_t *name) {
assertNotNull(
CUTSCENE_SYSTEM.scene, "cutsceneGoTo called with no cutscene running"
);
// A marker behind the current position can't be found in the live
// queue - already-run items are evicted from its front as the
// cutscene advances (see cutsceneSystemNext/cutsceneRemoveFront), so
// the untouched original has to be re-parsed from source instead. Only
// supported when the running cutscene is the one cutsceneSystemLoad/
// cutsceneCutsceneResolve populated - a C-authored cutscene_t, or
// content spliced in via INSERT/battleStateExecutingInit, has no
// source to reload from, so a backward jump into those isn't supported
// yet.
assertTrue(
CUTSCENE_SYSTEM.scene == &CUTSCENE_SYSTEM.loadedScene &&
CUTSCENE_SYSTEM.loadedFile[0] != '\0',
"cutsceneGoTo requires the running cutscene to have come from "
"cutsceneSystemLoad/cutsceneCutsceneResolve"
);
for(uint8_t i = 0; i < CUTSCENE_SYSTEM.scene->itemCount; i++) {
const cutsceneitem_t *item = &CUTSCENE_SYSTEM.scene->items[i];
cutscene_t freshScene = { .items = CUTSCENE_SYSTEM.gotoScratchItems };
errorret_t result = cutsceneLoadParse(
CUTSCENE_SYSTEM.loadedFile, &freshScene, CUTSCENE_SYSTEM.gotoScratchItems,
CUTSCENE_LOADED_ITEMS_MAX
);
if(errorIsNotOk(result)) {
errorCatch(errorPrint(result));
uiFatalErrorOpen(NULL);
return;
}
for(uint8_t i = 0; i < freshScene.itemCount; i++) {
const cutsceneitem_t *item = &freshScene.items[i];
if(
item->type == CUTSCENE_ITEM_TYPE_MARKER &&
stringEquals(item->marker.name, name)
) {
CUTSCENE_SYSTEM.currentItem = i;
memoryZero(&CUTSCENE_SYSTEM.data, sizeof(CUTSCENE_SYSTEM.data));
cutsceneItemStart(item, &CUTSCENE_SYSTEM.data);
return;
}
item->type != CUTSCENE_ITEM_TYPE_MARKER ||
!stringEquals(item->marker.name, name)
) continue;
// Commit: replace the live queue outright with marker-onward from
// the fresh parse. Nothing before the marker (including whatever was
// running) gets Started - only the marker item itself does, same as
// the old in-place jump's semantics.
const uint8_t remaining = freshScene.itemCount - i;
memoryCopy(
CUTSCENE_SYSTEM.loadedItems, &freshScene.items[i],
remaining * sizeof(cutsceneitem_t)
);
CUTSCENE_SYSTEM.loadedScene.itemCount = remaining;
CUTSCENE_SYSTEM.currentItem = 0;
memoryZero(&CUTSCENE_SYSTEM.data, sizeof(CUTSCENE_SYSTEM.data));
cutsceneItemStart(
&CUTSCENE_SYSTEM.loadedItems[0], &CUTSCENE_SYSTEM.data
);
return;
}
assertTrue(false, "cutsceneGoTo: no marker found with that name");
@@ -325,5 +365,6 @@ void cutsceneSystemUpdate() {
const cutsceneitem_t * cutsceneSystemGetCurrentItem() {
if(CUTSCENE_SYSTEM.scene == NULL) return NULL;
return &CUTSCENE_SYSTEM.scene->items[CUTSCENE_SYSTEM.currentItem];
// Always index 0 - see CUTSCENE_SYSTEM.currentItem's doc comment.
return &CUTSCENE_SYSTEM.scene->items[0];
}
+28 -2
View File
@@ -23,6 +23,13 @@ typedef struct entity_s entity_t;
typedef struct {
cutscene_t *scene;
// 0xFF if nothing has started running yet, otherwise always 0 - the
// currently-running item is always scene->items[0]; cutsceneSystemNext
// removes it from the front (see cutsceneRemoveFront) rather than
// advancing an index further into the array, so an item's slot is
// reclaimed the moment it finishes instead of sitting there as dead
// weight for the rest of the cutscene's run.
uint8_t currentItem;
cutscenepause_t pause;
entity_t *entityInteract;
@@ -47,6 +54,13 @@ typedef struct {
// Filename last passed to cutsceneSystemLoad - see cutsceneRestart.
char_t loadedFile[ASSET_FILE_NAME_MAX];
// Scratch space for cutsceneGoTo's re-parse of loadedFile - a fixed,
// persistent buffer rather than a local one since cutsceneitem_t is
// large (e.g. cutscenemodal_t alone carries a 256-byte message buffer),
// making a CUTSCENE_LOADED_ITEMS_MAX-sized array too big to put on the
// stack on constrained platforms (PSP/GameCube/Wii).
cutsceneitem_t gotoScratchItems[CUTSCENE_LOADED_ITEMS_MAX];
} cutscenesystem_t;
extern cutscenesystem_t CUTSCENE_SYSTEM;
@@ -211,8 +225,20 @@ void cutsceneSystemNext();
/**
* Jumps the running cutscene directly to the marker with the given name
* and starts it immediately. Asserts if no cutscene is running or no
* marker with that name exists.
* and starts it immediately. Re-parses the cutscene fresh from its
* source file (see cutsceneLoadParse) and replaces the live queue with
* marker-onward from that fresh parse - items already run may have been
* evicted from the front of the live queue (see
* cutsceneSystemNext/cutsceneRemoveFront), so a marker behind the
* current position can only be found in the untouched original. Nothing
* before the marker gets Started, only the marker item itself.
*
* Requires the running cutscene to be the one cutsceneSystemLoad/
* cutsceneCutsceneResolve populated (i.e. CUTSCENE_SYSTEM.scene ==
* &CUTSCENE_SYSTEM.loadedScene) - asserts otherwise. A C-authored
* cutscene_t, or content spliced in via INSERT/battleStateExecutingInit,
* has no source file to reload from, so backward jumps into those
* aren't supported yet.
*
* @param name Marker name to search for.
*/
+13 -40
View File
@@ -7,9 +7,6 @@
#include "rpg/cutscene/cutscenesystem.h"
#include "rpg/cutscene/item/cutsceneitembase.h"
#include "asset/asset.h"
#include "asset/assetfile.h"
#include "asset/loader/assetloader.h"
#include "util/memory.h"
#include "util/string.h"
#include "ui/overlay/uifatalerror.h"
@@ -433,30 +430,21 @@ bool_t cutsceneCutsceneUpdate(
}
cutscene_t * cutsceneCutsceneResolve(const char_t *name) {
assetentry_t *entry = assetLock(name, ASSET_LOADER_TYPE_JSON, NULL);
errorret_t result = assetRequireLoaded(entry);
errorret_t result = cutsceneLoadParse(
name, &CUTSCENE_SYSTEM.loadedScene, CUTSCENE_SYSTEM.loadedItems,
CUTSCENE_LOADED_ITEMS_MAX
);
if(errorIsNotOk(result)) {
assetUnlockEntry(entry);
errorCatch(errorPrint(result));
uiFatalErrorOpen(NULL);
return NULL;
}
// Parse straight out of the entry's own doc while still locked - every
// cutsceneitem_t field that could reference it owns its string data by
// value (see cutscenecutsceneref_t/cutscenemarker_t's doc comments), so
// nothing needs to survive past this call and the entry can just be
// unlocked like any other asset once parsing finishes.
errorret_t parseResult = cutsceneParseDoc(
entry->data.json, &CUTSCENE_SYSTEM.loadedScene, CUTSCENE_SYSTEM.loadedItems,
CUTSCENE_LOADED_ITEMS_MAX
);
assetUnlockEntry(entry);
if(errorIsNotOk(parseResult)) {
errorCatch(errorPrint(parseResult));
uiFatalErrorOpen(NULL);
return NULL;
}
// Shares loadedScene/loadedItems with cutsceneSystemLoad, so track the
// source the same way it does - cutsceneGoTo needs this to reload the
// untouched original when jumping to a marker behind the current
// position (see cutsceneGoTo's doc comment).
stringCopy(CUTSCENE_SYSTEM.loadedFile, name, ASSET_FILE_NAME_MAX - 1);
return &CUTSCENE_SYSTEM.loadedScene;
}
@@ -511,29 +499,14 @@ bool_t cutsceneInsertUpdate(
}
void cutsceneInsertResolveAndSplice(const char_t *name) {
assetentry_t *entry = assetLock(name, ASSET_LOADER_TYPE_JSON, NULL);
errorret_t result = assetRequireLoaded(entry);
if(errorIsNotOk(result)) {
assetUnlockEntry(entry);
errorCatch(errorPrint(result));
uiFatalErrorOpen(NULL);
return;
}
// Local scratch, not a shared buffer - this target is spliced straight
// into the running scene below and never referenced again afterward, so
// it only needs to live for the rest of this call. Parsed straight out
// of the entry's own doc while still locked, then unlocked like any
// other asset - see cutsceneCutsceneResolve's matching comment for why
// nothing needs to keep the doc itself resident past the parse.
// it only needs to live for the rest of this call.
cutsceneitem_t items[CUTSCENE_INSERT_ITEMS_MAX];
cutscene_t scene = { .items = items };
errorret_t parseResult = cutsceneParseDoc(
entry->data.json, &scene, items, CUTSCENE_INSERT_ITEMS_MAX
);
assetUnlockEntry(entry);
if(errorIsNotOk(parseResult)) {
errorCatch(errorPrint(parseResult));
errorret_t result = cutsceneLoadParse(name, &scene, items, CUTSCENE_INSERT_ITEMS_MAX);
if(errorIsNotOk(result)) {
errorCatch(errorPrint(result));
uiFatalErrorOpen(NULL);
return;
}