Adds BATTLE_START, BATTLE_SET_STATE and BATTLE_WAIT_SELECTION cutscene items so a battle's state transitions (opening -> pre-round -> selection -> executing) are chained through the running cutscene, with cutsceneSystemInsertItemsNext/InsertItems splitting "splice" from "splice and advance immediately". Adds battlestateselection's get-current-fighter/player-attack/player-flee API backing the battle UI, and battleMoveGetIdByName for JSON-authored ability lookups. Fixes: off-by-one in battleMoveInit that let the first-defined move alias BATTLE_MOVE_ID_NULL; a segfault in scenebattle.c from rendering unfilled (NULL) fighter slots; stale battleGetCurrentFighter/ battlePlayerAttack/battlePlayerFlee references in the battle UI. Adds assertions across the battle/scene/UI code for fighter-index bounds and non-null invariants. Updates battletest.jsonc for the new BATTLE_START item schema. Co-Authored-By: Claude Sonnet 5 <[email protected]>
330 lines
10 KiB
C
330 lines
10 KiB
C
/**
|
|
* Copyright (c) 2025 Dominic Masters
|
|
*
|
|
* This software is released under the MIT License.
|
|
* https://opensource.org/licenses/MIT
|
|
*/
|
|
|
|
#include "cutscenesystem.h"
|
|
#include "rpg/entity/entity.h"
|
|
#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;
|
|
|
|
void cutsceneSystemInit() {
|
|
memoryZero(&CUTSCENE_SYSTEM, sizeof(cutscenesystem_t));
|
|
CUTSCENE_SYSTEM.loadedScene.items = CUTSCENE_SYSTEM.loadedItems;
|
|
CUTSCENE_SYSTEM.loadedScene.itemsMax = CUTSCENE_LOADED_ITEMS_MAX;
|
|
}
|
|
|
|
void cutsceneSystemDispose() {
|
|
CUTSCENE_SYSTEM.scene = NULL;
|
|
CUTSCENE_SYSTEM.currentItem = 0xFF;
|
|
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
|
|
CUTSCENE_SYSTEM.entityInteract = NULL;
|
|
CUTSCENE_SYSTEM.entityInteracted = NULL;
|
|
CUTSCENE_SYSTEM.entityLastCreated = NULL;
|
|
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
|
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
|
|
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
|
|
CUTSCENE_SYSTEM.textCache[0] = '\0';
|
|
CUTSCENE_SYSTEM.onComplete = NULL;
|
|
CUTSCENE_SYSTEM.loadedFile[0] = '\0';
|
|
}
|
|
|
|
void cutsceneSystemPrepare(
|
|
cutscene_t *cutscene,
|
|
entity_t *interact,
|
|
entity_t *interacted
|
|
) {
|
|
assertNotNull(cutscene, "Cutscene cannot be NULL");
|
|
|
|
CUTSCENE_SYSTEM.scene = cutscene;
|
|
CUTSCENE_SYSTEM.pause = cutscene->pause;
|
|
CUTSCENE_SYSTEM.entityInteract = interact;
|
|
CUTSCENE_SYSTEM.entityInteracted = interacted;
|
|
CUTSCENE_SYSTEM.entityLastCreated = NULL;
|
|
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
|
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
|
|
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
|
|
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';
|
|
}
|
|
|
|
void cutsceneSystemStartCutscene(cutscene_t *cutscene) {
|
|
cutsceneSystemStartCutsceneWith(cutscene, NULL, NULL);
|
|
}
|
|
|
|
void cutsceneSystemStartCutsceneWith(
|
|
cutscene_t *cutscene,
|
|
entity_t *interact,
|
|
entity_t *interacted
|
|
) {
|
|
assertNotNull(cutscene, "Cutscene cannot be NULL");
|
|
assertTrue(
|
|
cutscene->itemCount <= cutscene->itemsMax,
|
|
"Cutscene itemCount exceeds itemsMax"
|
|
);
|
|
|
|
cutsceneSystemPrepare(cutscene, interact, interacted);
|
|
cutsceneSystemNext();
|
|
}
|
|
|
|
void cutsceneSystemStartCutsceneAndGoToMarker(
|
|
cutscene_t *cutscene,
|
|
const char_t *marker
|
|
) {
|
|
assertNotNull(marker, "Marker cannot be NULL");
|
|
|
|
cutsceneSystemPrepare(cutscene, NULL, NULL);
|
|
cutsceneGoTo(marker);
|
|
}
|
|
|
|
void cutsceneSystemInsertItemsNext(
|
|
const cutsceneitem_t *items,
|
|
const size_t itemCount
|
|
) {
|
|
assertNotNull(items, "Items cannot be NULL");
|
|
assertNotNull(
|
|
CUTSCENE_SYSTEM.scene,
|
|
"cutsceneSystemInsertItemsNext called with no cutscene running"
|
|
);
|
|
|
|
cutsceneAppendNext(
|
|
CUTSCENE_SYSTEM.scene, CUTSCENE_SYSTEM.currentItem, items, itemCount
|
|
);
|
|
}
|
|
|
|
void cutsceneSystemInsertItems(
|
|
const cutsceneitem_t *items,
|
|
const size_t itemCount
|
|
) {
|
|
cutsceneSystemInsertItemsNext(items, itemCount);
|
|
cutsceneSystemNext();
|
|
}
|
|
|
|
void cutsceneSystemInsertCutscene(const cutscene_t *cutscene) {
|
|
assertNotNull(cutscene, "Cutscene cannot be NULL");
|
|
cutsceneSystemInsertItems(cutscene->items, cutscene->itemCount);
|
|
}
|
|
|
|
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,
|
|
CUTSCENE_LOADED_ITEMS_MAX
|
|
);
|
|
assetUnlockEntry(entry);
|
|
if(errorIsNotOk(parseResult)) {
|
|
errorCatch(errorPrint(parseResult));
|
|
uiFatalErrorOpen(NULL);
|
|
return;
|
|
}
|
|
|
|
cutsceneSystemStartCutscene(&CUTSCENE_SYSTEM.loadedScene);
|
|
stringCopy(CUTSCENE_SYSTEM.loadedFile, file, ASSET_FILE_NAME_MAX - 1);
|
|
}
|
|
|
|
void cutsceneRestart(void) {
|
|
assertNotNull(
|
|
CUTSCENE_SYSTEM.scene, "cutsceneRestart called with no cutscene running"
|
|
);
|
|
|
|
cutscenecallback_t onComplete = CUTSCENE_SYSTEM.onComplete;
|
|
entity_t *entityInteract = CUTSCENE_SYSTEM.entityInteract;
|
|
entity_t *entityInteracted = CUTSCENE_SYSTEM.entityInteracted;
|
|
|
|
if(CUTSCENE_SYSTEM.loadedFile[0] != '\0') {
|
|
char_t file[ASSET_FILE_NAME_MAX];
|
|
stringCopy(file, CUTSCENE_SYSTEM.loadedFile, ASSET_FILE_NAME_MAX - 1);
|
|
cutsceneSystemLoad(file);
|
|
} else {
|
|
cutsceneSystemStartCutsceneWith(
|
|
CUTSCENE_SYSTEM.scene, entityInteract, entityInteracted
|
|
);
|
|
}
|
|
|
|
CUTSCENE_SYSTEM.onComplete = onComplete;
|
|
CUTSCENE_SYSTEM.entityInteract = entityInteract;
|
|
CUTSCENE_SYSTEM.entityInteracted = entityInteracted;
|
|
}
|
|
|
|
void cutsceneSystemSetOnComplete(cutscenecallback_t onComplete) {
|
|
assertNotNull(
|
|
CUTSCENE_SYSTEM.scene,
|
|
"cutsceneSystemSetOnComplete called with no cutscene running"
|
|
);
|
|
CUTSCENE_SYSTEM.onComplete = onComplete;
|
|
}
|
|
|
|
entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex) {
|
|
entity_t *entity;
|
|
|
|
if(entityIndex == CUTSCENE_ENTITY_INTERACT) {
|
|
assertNotNull(
|
|
CUTSCENE_SYSTEM.entityInteract,
|
|
"CUTSCENE_ENTITY_INTERACT used but no interact entity is set"
|
|
);
|
|
entity = CUTSCENE_SYSTEM.entityInteract;
|
|
} else if(entityIndex == CUTSCENE_ENTITY_INTERACTED) {
|
|
assertNotNull(
|
|
CUTSCENE_SYSTEM.entityInteracted,
|
|
"CUTSCENE_ENTITY_INTERACTED used but no interacted entity is set"
|
|
);
|
|
entity = CUTSCENE_SYSTEM.entityInteracted;
|
|
} else if(entityIndex == CUTSCENE_ENTITY_LAST_CREATED) {
|
|
assertNotNull(
|
|
CUTSCENE_SYSTEM.entityLastCreated,
|
|
"CUTSCENE_ENTITY_LAST_CREATED used but no entity has been created"
|
|
);
|
|
entity = CUTSCENE_SYSTEM.entityLastCreated;
|
|
} else if(entityIndex == CUTSCENE_ENTITY_LAST_REF) {
|
|
assertNotNull(
|
|
CUTSCENE_SYSTEM.entityLastRef,
|
|
"CUTSCENE_ENTITY_LAST_REF used but no entity has been referenced"
|
|
);
|
|
entity = CUTSCENE_SYSTEM.entityLastRef;
|
|
} else {
|
|
assertTrue(
|
|
entityIndex < ENTITY_COUNT,
|
|
"Entity index is out of range"
|
|
);
|
|
entity = &ENTITIES[entityIndex];
|
|
}
|
|
|
|
CUTSCENE_SYSTEM.entityLastRef = entity;
|
|
return entity;
|
|
}
|
|
|
|
uint8_t cutsceneSystemGetAreaId(const uint8_t areaId) {
|
|
if(areaId == CUTSCENE_AREA_LAST_CREATED) {
|
|
assertTrue(
|
|
CUTSCENE_SYSTEM.areaLastCreated != CUTSCENE_AREA_LAST_CREATED,
|
|
"CUTSCENE_AREA_LAST_CREATED used but no map area has been created"
|
|
);
|
|
return CUTSCENE_SYSTEM.areaLastCreated;
|
|
}
|
|
return areaId;
|
|
}
|
|
|
|
uint8_t cutsceneSystemGetTextMiniId(const uint8_t index) {
|
|
if(index == CUTSCENE_TEXT_MINI_LAST_CREATED) {
|
|
assertTrue(
|
|
CUTSCENE_SYSTEM.textMiniLastCreated != CUTSCENE_TEXT_MINI_LAST_CREATED,
|
|
"CUTSCENE_TEXT_MINI_LAST_CREATED used but no mini textbox has been "
|
|
"shown"
|
|
);
|
|
return CUTSCENE_SYSTEM.textMiniLastCreated;
|
|
}
|
|
return index;
|
|
}
|
|
|
|
const char_t * cutsceneSystemGetTextCache(void) {
|
|
return CUTSCENE_SYSTEM.textCache;
|
|
}
|
|
|
|
void cutsceneSystemSetTextCache(const char_t *text) {
|
|
assertNotNull(text, "Text cannot be NULL");
|
|
|
|
stringCopy(
|
|
CUTSCENE_SYSTEM.textCache, text, CUTSCENE_TEXT_CACHE_MAX - 1
|
|
);
|
|
}
|
|
|
|
void cutsceneSystemNext() {
|
|
if(CUTSCENE_SYSTEM.scene == NULL) return;
|
|
|
|
CUTSCENE_SYSTEM.currentItem++;
|
|
|
|
// 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
|
|
) {
|
|
// 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
|
|
// hazard as uiFocusPop, see src/dusk/ui/focus/uifocus.c.
|
|
cutscenecallback_t onComplete = CUTSCENE_SYSTEM.onComplete;
|
|
|
|
CUTSCENE_SYSTEM.scene = NULL;
|
|
CUTSCENE_SYSTEM.currentItem = 0xFF;
|
|
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
|
|
CUTSCENE_SYSTEM.entityInteract = NULL;
|
|
CUTSCENE_SYSTEM.entityInteracted = NULL;
|
|
CUTSCENE_SYSTEM.entityLastCreated = NULL;
|
|
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
|
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
|
|
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
|
|
CUTSCENE_SYSTEM.textCache[0] = '\0';
|
|
CUTSCENE_SYSTEM.onComplete = NULL;
|
|
|
|
if(onComplete != NULL) onComplete(NULL);
|
|
return;
|
|
}
|
|
|
|
// Start item.
|
|
const cutsceneitem_t *item = cutsceneSystemGetCurrentItem();
|
|
memset(&CUTSCENE_SYSTEM.data, 0, sizeof(CUTSCENE_SYSTEM.data));
|
|
cutsceneItemStart(item, &CUTSCENE_SYSTEM.data);
|
|
}
|
|
|
|
void cutsceneGoTo(const char_t *name) {
|
|
assertNotNull(name, "Name cannot be NULL");
|
|
assertNotNull(
|
|
CUTSCENE_SYSTEM.scene, "cutsceneGoTo called with no cutscene running"
|
|
);
|
|
|
|
for(uint8_t i = 0; i < CUTSCENE_SYSTEM.scene->itemCount; i++) {
|
|
const cutsceneitem_t *item = &CUTSCENE_SYSTEM.scene->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;
|
|
}
|
|
}
|
|
|
|
assertTrue(false, "cutsceneGoTo: no marker found with that name");
|
|
}
|
|
|
|
void cutsceneSystemUpdate() {
|
|
if(CUTSCENE_SYSTEM.scene == NULL) return;
|
|
|
|
const cutsceneitem_t *item = cutsceneSystemGetCurrentItem();
|
|
if(cutsceneItemUpdate(item, &CUTSCENE_SYSTEM.data)) cutsceneSystemNext();
|
|
}
|
|
|
|
const cutsceneitem_t * cutsceneSystemGetCurrentItem() {
|
|
if(CUTSCENE_SYSTEM.scene == NULL) return NULL;
|
|
|
|
return &CUTSCENE_SYSTEM.scene->items[CUTSCENE_SYSTEM.currentItem];
|
|
}
|