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,9 +6,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "battlestateending.h"
|
#include "battlestateending.h"
|
||||||
|
#include "rpg/battle/battle.h"
|
||||||
|
#include "scene/scene.h"
|
||||||
|
|
||||||
void battleStateEndingInit(void) {
|
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) {
|
void battleStateEndingUpdate(void) {
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ typedef struct {
|
|||||||
} battlestateending_t;
|
} 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);
|
void battleStateEndingInit(void);
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,6 @@
|
|||||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
#include "rpg/cutscene/cutscenesystem.h"
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
#include "rpg/cutscene/cutscene.h"
|
#include "rpg/cutscene/cutscene.h"
|
||||||
#include "asset/asset.h"
|
|
||||||
#include "asset/loader/assetloader.h"
|
|
||||||
#include "asset/assetfile.h"
|
#include "asset/assetfile.h"
|
||||||
#include "ui/overlay/uifatalerror.h"
|
#include "ui/overlay/uifatalerror.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
@@ -61,33 +59,20 @@ void battleStateExecutingInit(void) {
|
|||||||
"cutscenes/battle/moves/%s.jsonc", ability->name
|
"cutscenes/battle/moves/%s.jsonc", ability->name
|
||||||
);
|
);
|
||||||
|
|
||||||
// Load cutscene asset.
|
// Load and parse the move's cutscene straight into the tail of items.
|
||||||
assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_JSON, NULL);
|
const uint8_t moveItemsStart = itemsCount;
|
||||||
errorret_t result = assetRequireLoaded(entry);
|
cutscene_t scene;
|
||||||
|
errorret_t result = cutsceneLoadParse(
|
||||||
|
path, &scene, &items[itemsCount],
|
||||||
|
BATTLE_STATE_EXECUTING_ITEMS_MAX - itemsCount
|
||||||
|
);
|
||||||
if(errorIsNotOk(result)) {
|
if(errorIsNotOk(result)) {
|
||||||
assetUnlockEntry(entry);
|
|
||||||
errorCatch(errorPrint(result));
|
errorCatch(errorPrint(result));
|
||||||
uiFatalErrorOpen(NULL);
|
uiFatalErrorOpen(NULL);
|
||||||
continue;
|
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;
|
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
|
// Resolve BATTLE_FIGHTER_POS_TARGET/USER sentinels against this
|
||||||
// action, now that its user/target fighters are known.
|
// action, now that its user/target fighters are known.
|
||||||
for(uint8_t j = moveItemsStart; j < itemsCount; j++) {
|
for(uint8_t j = moveItemsStart; j < itemsCount; j++) {
|
||||||
|
|||||||
@@ -32,6 +32,25 @@ bool_t battleStateSelectionIsComplete(void) {
|
|||||||
return BATTLE.stateData.selection.selectionIndex >= BATTLE.fighterOrderCount;
|
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) {
|
void battleStateSelectionUpdate(void) {
|
||||||
battlestateselection_t *state = &BATTLE.stateData.selection;
|
battlestateselection_t *state = &BATTLE.stateData.selection;
|
||||||
|
|
||||||
@@ -49,8 +68,7 @@ void battleStateSelectionUpdate(void) {
|
|||||||
battlefighter_t *fighter = BATTLE.fighters[fighterIndex];
|
battlefighter_t *fighter = BATTLE.fighters[fighterIndex];
|
||||||
if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_AI) return;
|
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
|
battleStateSelectionAiDecide(fighterIndex);
|
||||||
// going to do anything.
|
|
||||||
state->selectionIndex++;
|
state->selectionIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,13 @@ typedef struct {
|
|||||||
void battleStateSelectionInit(void);
|
void battleStateSelectionInit(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates BATTLE_STATE_SELECTION for one frame: auto-decides for every
|
* Updates BATTLE_STATE_SELECTION for one frame: auto-decides (see
|
||||||
* AI-controlled fighter the cursor passes, stopping once it reaches a
|
* battleStateSelectionAiDecide) for every AI-controlled fighter the
|
||||||
* player-controlled fighter awaiting input (see
|
* cursor passes, stopping once it reaches a player-controlled fighter
|
||||||
* battleStateSelectionGetCurrentFighter), or transitions to
|
* awaiting input (see battleStateSelectionGetCurrentFighter). Once every
|
||||||
* BATTLE_STATE_EXECUTING once every position has decided.
|
* position has decided, BATTLE_WAIT_SELECTION (see
|
||||||
|
* cutsceneBattleWaitSelectionUpdate) queues the move to
|
||||||
|
* BATTLE_STATE_EXECUTING.
|
||||||
*/
|
*/
|
||||||
void battleStateSelectionUpdate(void);
|
void battleStateSelectionUpdate(void);
|
||||||
|
|
||||||
@@ -52,6 +54,17 @@ bool_t battleStateSelectionFighterHasDecided(const uint8_t fighterIndex);
|
|||||||
*/
|
*/
|
||||||
bool_t battleStateSelectionFighterNeedsDecision(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
|
* Checks whether every fighter in BATTLE.fighterOrder has decided a move
|
||||||
* this round.
|
* this round.
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
#include "item/json/cutscenejsonpauseflags.h"
|
#include "item/json/cutscenejsonpauseflags.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
|
#include "asset/asset.h"
|
||||||
|
#include "asset/loader/assetloader.h"
|
||||||
|
|
||||||
errorret_t cutsceneParseItem(
|
errorret_t cutsceneParseItem(
|
||||||
yyjson_val *itemObj,
|
yyjson_val *itemObj,
|
||||||
@@ -145,3 +147,51 @@ void cutsceneAppendNext(
|
|||||||
) {
|
) {
|
||||||
cutsceneAppendAt(scene, (uint8_t)(currentIndex + 1), items, count);
|
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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -127,3 +127,45 @@ void cutsceneAppendNext(
|
|||||||
const cutsceneitem_t *items,
|
const cutsceneitem_t *items,
|
||||||
const uint8_t count
|
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
|
||||||
|
);
|
||||||
|
|||||||
@@ -10,8 +10,6 @@
|
|||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "asset/asset.h"
|
|
||||||
#include "asset/loader/assetloader.h"
|
|
||||||
#include "ui/overlay/uifatalerror.h"
|
#include "ui/overlay/uifatalerror.h"
|
||||||
|
|
||||||
cutscenesystem_t CUTSCENE_SYSTEM;
|
cutscenesystem_t CUTSCENE_SYSTEM;
|
||||||
@@ -55,8 +53,20 @@ void cutsceneSystemPrepare(
|
|||||||
CUTSCENE_SYSTEM.textCache[0] = '\0';
|
CUTSCENE_SYSTEM.textCache[0] = '\0';
|
||||||
CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0.
|
CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0.
|
||||||
CUTSCENE_SYSTEM.onComplete = NULL;
|
CUTSCENE_SYSTEM.onComplete = NULL;
|
||||||
|
|
||||||
|
// 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';
|
CUTSCENE_SYSTEM.loadedFile[0] = '\0';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void cutsceneSystemStartCutscene(cutscene_t *cutscene) {
|
void cutsceneSystemStartCutscene(cutscene_t *cutscene) {
|
||||||
cutsceneSystemStartCutsceneWith(cutscene, NULL, NULL);
|
cutsceneSystemStartCutsceneWith(cutscene, NULL, NULL);
|
||||||
@@ -118,27 +128,12 @@ void cutsceneSystemInsertCutscene(const cutscene_t *cutscene) {
|
|||||||
void cutsceneSystemLoad(const char_t *file) {
|
void cutsceneSystemLoad(const char_t *file) {
|
||||||
assertNotNull(file, "File cannot be NULL");
|
assertNotNull(file, "File cannot be NULL");
|
||||||
|
|
||||||
assetentry_t *entry = assetLock(file, ASSET_LOADER_TYPE_JSON, NULL);
|
errorret_t result = cutsceneLoadParse(
|
||||||
errorret_t result = assetRequireLoaded(entry);
|
file, &CUTSCENE_SYSTEM.loadedScene, CUTSCENE_SYSTEM.loadedItems,
|
||||||
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,
|
|
||||||
CUTSCENE_LOADED_ITEMS_MAX
|
CUTSCENE_LOADED_ITEMS_MAX
|
||||||
);
|
);
|
||||||
assetUnlockEntry(entry);
|
if(errorIsNotOk(result)) {
|
||||||
if(errorIsNotOk(parseResult)) {
|
errorCatch(errorPrint(result));
|
||||||
errorCatch(errorPrint(parseResult));
|
|
||||||
uiFatalErrorOpen(NULL);
|
uiFatalErrorOpen(NULL);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -256,15 +251,21 @@ void cutsceneSystemSetTextCache(const char_t *text) {
|
|||||||
void cutsceneSystemNext() {
|
void cutsceneSystemNext() {
|
||||||
if(CUTSCENE_SYSTEM.scene == NULL) return;
|
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
|
// End of the cutscene? Note that a CUTSCENE_ITEM_TYPE_INSERT item's
|
||||||
// spliced-in items are physically part of this same array (see
|
// spliced-in items are physically part of this same array (see
|
||||||
// cutsceneSystemInsertCutscene), so there's no separate "parent scene"
|
// cutsceneSystemInsertCutscene), so there's no separate "parent scene"
|
||||||
// to fall back into here - itemCount already accounts for them.
|
// to fall back into here - itemCount already accounts for them.
|
||||||
if(
|
if(CUTSCENE_SYSTEM.scene->itemCount == 0) {
|
||||||
CUTSCENE_SYSTEM.currentItem >= CUTSCENE_SYSTEM.scene->itemCount
|
|
||||||
) {
|
|
||||||
// Saved and cleared before firing so a callback that immediately
|
// Saved and cleared before firing so a callback that immediately
|
||||||
// starts another cutscene (or sets its own onComplete) isn't clobbered
|
// starts another cutscene (or sets its own onComplete) isn't clobbered
|
||||||
// by this function's own cleanup running after it - same reentrancy
|
// by this function's own cleanup running after it - same reentrancy
|
||||||
@@ -298,18 +299,57 @@ void cutsceneGoTo(const char_t *name) {
|
|||||||
assertNotNull(
|
assertNotNull(
|
||||||
CUTSCENE_SYSTEM.scene, "cutsceneGoTo called with no cutscene running"
|
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++) {
|
cutscene_t freshScene = { .items = CUTSCENE_SYSTEM.gotoScratchItems };
|
||||||
const cutsceneitem_t *item = &CUTSCENE_SYSTEM.scene->items[i];
|
errorret_t result = cutsceneLoadParse(
|
||||||
if(
|
CUTSCENE_SYSTEM.loadedFile, &freshScene, CUTSCENE_SYSTEM.gotoScratchItems,
|
||||||
item->type == CUTSCENE_ITEM_TYPE_MARKER &&
|
CUTSCENE_LOADED_ITEMS_MAX
|
||||||
stringEquals(item->marker.name, name)
|
);
|
||||||
) {
|
if(errorIsNotOk(result)) {
|
||||||
CUTSCENE_SYSTEM.currentItem = i;
|
errorCatch(errorPrint(result));
|
||||||
memoryZero(&CUTSCENE_SYSTEM.data, sizeof(CUTSCENE_SYSTEM.data));
|
uiFatalErrorOpen(NULL);
|
||||||
cutsceneItemStart(item, &CUTSCENE_SYSTEM.data);
|
|
||||||
return;
|
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)
|
||||||
|
) 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");
|
assertTrue(false, "cutsceneGoTo: no marker found with that name");
|
||||||
@@ -325,5 +365,6 @@ void cutsceneSystemUpdate() {
|
|||||||
const cutsceneitem_t * cutsceneSystemGetCurrentItem() {
|
const cutsceneitem_t * cutsceneSystemGetCurrentItem() {
|
||||||
if(CUTSCENE_SYSTEM.scene == NULL) return NULL;
|
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];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ typedef struct entity_s entity_t;
|
|||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
cutscene_t *scene;
|
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;
|
uint8_t currentItem;
|
||||||
cutscenepause_t pause;
|
cutscenepause_t pause;
|
||||||
entity_t *entityInteract;
|
entity_t *entityInteract;
|
||||||
@@ -47,6 +54,13 @@ typedef struct {
|
|||||||
|
|
||||||
// Filename last passed to cutsceneSystemLoad - see cutsceneRestart.
|
// Filename last passed to cutsceneSystemLoad - see cutsceneRestart.
|
||||||
char_t loadedFile[ASSET_FILE_NAME_MAX];
|
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;
|
} cutscenesystem_t;
|
||||||
|
|
||||||
extern cutscenesystem_t CUTSCENE_SYSTEM;
|
extern cutscenesystem_t CUTSCENE_SYSTEM;
|
||||||
@@ -211,8 +225,20 @@ void cutsceneSystemNext();
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Jumps the running cutscene directly to the marker with the given name
|
* Jumps the running cutscene directly to the marker with the given name
|
||||||
* and starts it immediately. Asserts if no cutscene is running or no
|
* and starts it immediately. Re-parses the cutscene fresh from its
|
||||||
* marker with that name exists.
|
* 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.
|
* @param name Marker name to search for.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,9 +7,6 @@
|
|||||||
|
|
||||||
#include "rpg/cutscene/cutscenesystem.h"
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
#include "rpg/cutscene/item/cutsceneitembase.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/memory.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include "ui/overlay/uifatalerror.h"
|
#include "ui/overlay/uifatalerror.h"
|
||||||
@@ -433,30 +430,21 @@ bool_t cutsceneCutsceneUpdate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
cutscene_t * cutsceneCutsceneResolve(const char_t *name) {
|
cutscene_t * cutsceneCutsceneResolve(const char_t *name) {
|
||||||
assetentry_t *entry = assetLock(name, ASSET_LOADER_TYPE_JSON, NULL);
|
errorret_t result = cutsceneLoadParse(
|
||||||
errorret_t result = assetRequireLoaded(entry);
|
name, &CUTSCENE_SYSTEM.loadedScene, CUTSCENE_SYSTEM.loadedItems,
|
||||||
|
CUTSCENE_LOADED_ITEMS_MAX
|
||||||
|
);
|
||||||
if(errorIsNotOk(result)) {
|
if(errorIsNotOk(result)) {
|
||||||
assetUnlockEntry(entry);
|
|
||||||
errorCatch(errorPrint(result));
|
errorCatch(errorPrint(result));
|
||||||
uiFatalErrorOpen(NULL);
|
uiFatalErrorOpen(NULL);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse straight out of the entry's own doc while still locked - every
|
// Shares loadedScene/loadedItems with cutsceneSystemLoad, so track the
|
||||||
// cutsceneitem_t field that could reference it owns its string data by
|
// source the same way it does - cutsceneGoTo needs this to reload the
|
||||||
// value (see cutscenecutsceneref_t/cutscenemarker_t's doc comments), so
|
// untouched original when jumping to a marker behind the current
|
||||||
// nothing needs to survive past this call and the entry can just be
|
// position (see cutsceneGoTo's doc comment).
|
||||||
// unlocked like any other asset once parsing finishes.
|
stringCopy(CUTSCENE_SYSTEM.loadedFile, name, ASSET_FILE_NAME_MAX - 1);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return &CUTSCENE_SYSTEM.loadedScene;
|
return &CUTSCENE_SYSTEM.loadedScene;
|
||||||
}
|
}
|
||||||
@@ -511,29 +499,14 @@ bool_t cutsceneInsertUpdate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void cutsceneInsertResolveAndSplice(const char_t *name) {
|
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
|
// Local scratch, not a shared buffer - this target is spliced straight
|
||||||
// into the running scene below and never referenced again afterward, so
|
// 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
|
// it only needs to live for the rest of this call.
|
||||||
// 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.
|
|
||||||
cutsceneitem_t items[CUTSCENE_INSERT_ITEMS_MAX];
|
cutsceneitem_t items[CUTSCENE_INSERT_ITEMS_MAX];
|
||||||
cutscene_t scene = { .items = items };
|
cutscene_t scene = { .items = items };
|
||||||
errorret_t parseResult = cutsceneParseDoc(
|
errorret_t result = cutsceneLoadParse(name, &scene, items, CUTSCENE_INSERT_ITEMS_MAX);
|
||||||
entry->data.json, &scene, items, CUTSCENE_INSERT_ITEMS_MAX
|
if(errorIsNotOk(result)) {
|
||||||
);
|
errorCatch(errorPrint(result));
|
||||||
assetUnlockEntry(entry);
|
|
||||||
if(errorIsNotOk(parseResult)) {
|
|
||||||
errorCatch(errorPrint(parseResult));
|
|
||||||
uiFatalErrorOpen(NULL);
|
uiFatalErrorOpen(NULL);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
include(dusktest)
|
include(dusktest)
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
|
dusktest(test_cutscene.c)
|
||||||
dusktest(test_cutscenesystem.c)
|
dusktest(test_cutscenesystem.c)
|
||||||
dusktest(test_cutscenecontrol.c)
|
dusktest(test_cutscenecontrol.c)
|
||||||
dusktest(test_cutscenemaparea.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/cutscene/cutscenesystem.h"
|
||||||
#include "rpg/entity/entity.h"
|
#include "rpg/entity/entity.h"
|
||||||
#include "time/time.h"
|
#include "time/time.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
|
||||||
static uint8_t callbackFired;
|
static uint8_t callbackFired;
|
||||||
|
|
||||||
@@ -198,18 +199,72 @@ static void test_cutsceneSystemDisposeResetsState(void **state) {
|
|||||||
assert_null(CUTSCENE_SYSTEM.entityInteracted);
|
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 ---------------------------------------------------
|
// --- CUTSCENE_INSERT ---------------------------------------------------
|
||||||
//
|
//
|
||||||
// INSERT now splices its target's items directly into the running
|
// INSERT now splices its target's items directly into the running
|
||||||
// scene's own array (see cutsceneSystemInsertCutscene) rather than
|
// scene's own array (see cutsceneSystemInsertCutscene) rather than
|
||||||
// swapping CUTSCENE_SYSTEM.scene to a different cutscene_t and pushing a
|
// swapping CUTSCENE_SYSTEM.scene to a different cutscene_t and pushing a
|
||||||
// return frame - CUTSCENE_SYSTEM.scene therefore stays the same outer
|
// return frame - CUTSCENE_SYSTEM.scene therefore stays the same outer
|
||||||
// cutscene throughout every test below; itemCount growing is what shows
|
// cutscene throughout every test below. cutsceneSystemNext pops each
|
||||||
// the splice actually happened. Since the running scene's own array is
|
// already-run item off the front of the array as it advances (see
|
||||||
// what gets appended into now, any outer cutscene used with INSERT must
|
// CUTSCENE_SYSTEM.currentItem's doc comment), so itemCount reflects only
|
||||||
// declare its items array (and .itemsMax) with enough spare capacity for
|
// what's left to run at any given point, not a running total of
|
||||||
// whatever ends up spliced into it, including transitively through any
|
// everything ever spliced in - the itemCount assertions below are sized
|
||||||
// nested INSERT.
|
// 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 insertOrderLog[8];
|
||||||
static uint8_t insertOrderLogCount;
|
static uint8_t insertOrderLogCount;
|
||||||
@@ -263,10 +318,12 @@ static void test_cutsceneInsertSplicesItemsInPlaceThenResumes(void **state) {
|
|||||||
cutsceneSystemUpdate();// the WAIT elapses -> starts order1
|
cutsceneSystemUpdate();// the WAIT elapses -> starts order1
|
||||||
cutsceneSystemUpdate();// order1 done -> starts the INSERT, cascading into orderA
|
cutsceneSystemUpdate();// order1 done -> starts the INSERT, cascading into orderA
|
||||||
|
|
||||||
// The splice happened in place - CUTSCENE_SYSTEM.scene never changed,
|
// The splice happened in place - CUTSCENE_SYSTEM.scene never changed.
|
||||||
// but it grew by the snippet's 3 items.
|
// 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_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)
|
TIME.delta = 2.5f;// elapses the snippet's WAIT(2.0)
|
||||||
cutsceneSystemUpdate();// orderA done -> starts snippet's WAIT
|
cutsceneSystemUpdate();// orderA done -> starts snippet's WAIT
|
||||||
@@ -323,7 +380,9 @@ static void test_cutsceneInsertAsLastItemEndsCutsceneNaturally(
|
|||||||
|
|
||||||
cutsceneSystemUpdate();// order0 done -> starts INSERT, cascades to orderA
|
cutsceneSystemUpdate();// order0 done -> starts INSERT, cascades to orderA
|
||||||
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER);
|
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
|
// 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
|
// 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.
|
// land in OUTER's own array.
|
||||||
cutsceneSystemUpdate();
|
cutsceneSystemUpdate();
|
||||||
assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_NESTED_OUTER);
|
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();// orderA done -> continues on to orderB
|
||||||
cutsceneSystemUpdate();// orderB done -> continues on to order2
|
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
|
// 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_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.entityInteract, &ENTITIES[3]);
|
||||||
assert_ptr_equal(CUTSCENE_SYSTEM.entityInteracted, &ENTITIES[4]);
|
assert_ptr_equal(CUTSCENE_SYSTEM.entityInteracted, &ENTITIES[4]);
|
||||||
// The snippet declares ALL, but INSERT doesn't apply it -- the outer
|
// 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_cutsceneSystemGetAreaId),
|
||||||
cmocka_unit_test(test_cutsceneSystemGetTextMiniId),
|
cmocka_unit_test(test_cutsceneSystemGetTextMiniId),
|
||||||
cmocka_unit_test(test_cutsceneSystemDisposeResetsState),
|
cmocka_unit_test(test_cutsceneSystemDisposeResetsState),
|
||||||
|
cmocka_unit_test(
|
||||||
|
test_cutsceneSystemPrepareKeepsLoadedFileWhenReenteringLoadedScene
|
||||||
|
),
|
||||||
cmocka_unit_test(test_cutsceneInsertSplicesItemsInPlaceThenResumes),
|
cmocka_unit_test(test_cutsceneInsertSplicesItemsInPlaceThenResumes),
|
||||||
cmocka_unit_test(test_cutsceneInsertAsLastItemEndsCutsceneNaturally),
|
cmocka_unit_test(test_cutsceneInsertAsLastItemEndsCutsceneNaturally),
|
||||||
cmocka_unit_test(test_cutsceneInsertNestsThroughMultipleLevels),
|
cmocka_unit_test(test_cutsceneInsertNestsThroughMultipleLevels),
|
||||||
|
|||||||
Reference in New Issue
Block a user