Add cutsceneSystemLoad, a debug cutscene overlay, and NULL-safe fatal error
cutsceneSystemLoad(file) centralizes the lock/assetRequireLoaded/start sequence previously duplicated in sceneInitialInit, and falls back to the fatal error overlay instead of asserting when the asset fails to load. uiFatalErrorOpen now accepts a NULL message, showing a generic "contact support" message for callers with no specific error text. Also adds a ui/debug/uicutscene overlay showing the running cutscene's item count and current item type, and wires main_menu's post-save-picker flow through cutsceneSystemLoad. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -58,13 +58,6 @@ errorret_t assetCutsceneParseItem(
|
||||
errorOk();
|
||||
}
|
||||
|
||||
// First pass over the parsed "items" array: sums the exact pool bytes
|
||||
// ENTITY_WALK_TO/MAP_AREA_WAIT items will need, so the real parse pass can
|
||||
// allocate the pool exactly once up front rather than growing/moving it
|
||||
// (which would dangle pointers already written into earlier items). Any
|
||||
// item with an invalid/missing type is silently skipped here - the real
|
||||
// error for that gets thrown later during the actual per-item parse pass
|
||||
// in assetCutsceneParseItem, this first pass is pool-sizing only.
|
||||
size_t assetCutsceneComputePoolSize(yyjson_val *itemsArr) {
|
||||
size_t size = 0;
|
||||
yyjson_arr_iter iter = yyjson_arr_iter_with(itemsArr);
|
||||
|
||||
@@ -12,9 +12,6 @@
|
||||
#include "rpg/cutscene/item/json/cutscenejsonpool.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
// Above this, something is almost certainly wrong (or malicious) rather
|
||||
// than a legitimately large cutscene - matches ASSET_JSON_FILE_SIZE_MAX's
|
||||
// role in assetjsonloader.h.
|
||||
#define ASSET_CUTSCENE_FILE_SIZE_MAX (1024 * 64)
|
||||
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
@@ -37,18 +34,8 @@ typedef struct {
|
||||
size_t size;
|
||||
} assetcutsceneloaderloading_t;
|
||||
|
||||
// Runtime-loaded cutscene, parsed directly from the cutscene's authored
|
||||
// JSONC (no separate compiled binary format/build step - see
|
||||
// assetCutsceneLoaderSync). `doc` is kept alive for the entry's whole
|
||||
// lifetime rather than freed after parsing: every pool-string-shaped
|
||||
// field (item names, markers, modal option text, ...) points straight at
|
||||
// yyjson's own internally-owned string storage instead of being copied
|
||||
// out, so freeing doc early would dangle every one of those pointers.
|
||||
// `pool` only exists for the handful of fields that need a packed native
|
||||
// array yyjson can't hand back a pointer into directly - entityWalkTo's
|
||||
// worldpos_t positions and mapAreaWait's uint8_t areaIds.
|
||||
typedef struct {
|
||||
cutscene_t cutscene; // .items points at the items array below
|
||||
cutscene_t cutscene;
|
||||
cutsceneitem_t *items;
|
||||
uint8_t *pool;
|
||||
yyjson_doc *doc;
|
||||
|
||||
@@ -17,8 +17,8 @@ console_t CONSOLE;
|
||||
|
||||
void consoleInit(void) {
|
||||
memoryZero(&CONSOLE, sizeof(console_t));
|
||||
// CONSOLE.visible = false;
|
||||
CONSOLE.visible = true;
|
||||
CONSOLE.visible = false;
|
||||
// CONSOLE.visible = true;
|
||||
threadMutexInit(&CONSOLE.printMutex);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
#include "asset/asset.h"
|
||||
#include "ui/overlay/uifatalerror.h"
|
||||
|
||||
cutscenesystem_t CUTSCENE_SYSTEM;
|
||||
|
||||
@@ -61,6 +63,21 @@ void cutsceneSystemStartCutsceneAndGoToMarker(
|
||||
cutsceneGoTo(marker);
|
||||
}
|
||||
|
||||
void cutsceneSystemLoad(const char_t *file) {
|
||||
char_t path[ASSET_FILE_NAME_MAX];
|
||||
stringFormat(path, sizeof(path), "cutscenes/%s", file);
|
||||
|
||||
assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_CUTSCENE, NULL);
|
||||
errorret_t result = assetRequireLoaded(entry);
|
||||
if(errorIsNotOk(result)) {
|
||||
errorCatch(errorPrint(result));
|
||||
uiFatalErrorOpen(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
cutsceneSystemStartCutscene(&entry->data.cutscene.cutscene);
|
||||
}
|
||||
|
||||
void cutsceneRestart(void) {
|
||||
assertNotNull(
|
||||
CUTSCENE_SYSTEM.scene, "cutsceneRestart called with no cutscene running"
|
||||
|
||||
@@ -96,6 +96,22 @@ void cutsceneSystemStartCutsceneAndGoToMarker(
|
||||
const char_t *marker
|
||||
);
|
||||
|
||||
/**
|
||||
* Locks, loads and immediately starts a cutscene asset by file name,
|
||||
* e.g. cutsceneSystemLoad("main_menu.jsonc") loads and starts
|
||||
* assets/cutscenes/main_menu.jsonc - same lock/assetRequireLoaded/start
|
||||
* sequence as sceneInitialInit's INITIAL_CUTSCENE_ENTRY. The entry is
|
||||
* locked forever (never unlocked), same lifetime convention as
|
||||
* LOCALE.entry - it's fine to call this again later for the same file,
|
||||
* it'll just resolve to the same already-loaded entry. Opens the fatal
|
||||
* error overlay (see uiFatalErrorOpen) instead of starting anything if
|
||||
* the asset fails to load.
|
||||
*
|
||||
* @param file Cutscene file name (with .jsonc extension), relative to
|
||||
* assets/cutscenes/.
|
||||
*/
|
||||
void cutsceneSystemLoad(const char_t *file);
|
||||
|
||||
/**
|
||||
* Restarts the currently running cutscene from its first item,
|
||||
* preserving whatever interact/interacted entities triggered it.
|
||||
|
||||
@@ -12,12 +12,6 @@
|
||||
#include "display/screen/screen.h"
|
||||
#include "console/console.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "asset/asset.h"
|
||||
|
||||
// Loaded lazily and kept resident for the rest of the process - this scene
|
||||
// only ever runs once at boot, but there's no reason to unlock it (same
|
||||
// lifetime convention as e.g. LOCALE.entry).
|
||||
static assetentry_t *INITIAL_CUTSCENE_ENTRY = NULL;
|
||||
|
||||
errorret_t sceneInitialInit(scenedata_t *sceneData) {
|
||||
assertNotNull(sceneData, "Scene data cannot be null");
|
||||
@@ -29,18 +23,7 @@ errorret_t sceneInitialInit(scenedata_t *sceneData) {
|
||||
// Runtime-loaded from assets/cutscenes/initial.jsonc, parsed directly at
|
||||
// load time (no separate compile step) - checks for a save device,
|
||||
// retrying on failure, then hands off to the main menu.
|
||||
if(INITIAL_CUTSCENE_ENTRY == NULL) {
|
||||
INITIAL_CUTSCENE_ENTRY = assetLock(
|
||||
"cutscenes/initial.jsonc", ASSET_LOADER_TYPE_CUTSCENE, NULL
|
||||
);
|
||||
}
|
||||
errorret_t result = assetRequireLoaded(INITIAL_CUTSCENE_ENTRY);
|
||||
if(errorIsNotOk(result)) {
|
||||
errorCatch(errorPrint(result));
|
||||
assertTrue(false, "Failed to load initial scene cutscene asset");
|
||||
}
|
||||
|
||||
cutsceneSystemStartCutscene(&INITIAL_CUTSCENE_ENTRY->data.cutscene.cutscene);
|
||||
cutsceneSystemLoad("initial.jsonc");
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
uiconsole.c
|
||||
uicutscene.c
|
||||
uifps.c
|
||||
uiplayerpos.c
|
||||
)
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uicutscene.h"
|
||||
#include "util/string.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/color.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
|
||||
uicutscene_t UICUTSCENE;
|
||||
|
||||
errorret_t uiCutsceneInit() {
|
||||
uiLabelInit(
|
||||
&UICUTSCENE.countLabel,
|
||||
UICUTSCENE.countText,
|
||||
UICUTSCENE.countSprites, UI_CUTSCENE_SPRITES_MAX
|
||||
);
|
||||
uiLabelSetX(&UICUTSCENE.countLabel, (float_t)SCREEN.scanX);
|
||||
uiLabelSetY(&UICUTSCENE.countLabel, (float_t)SCREEN.scanY +
|
||||
(float_t)FONT_DEFAULT.tileset->tileHeight * 2
|
||||
);
|
||||
|
||||
uiLabelInit(
|
||||
&UICUTSCENE.itemLabel,
|
||||
UICUTSCENE.itemText,
|
||||
UICUTSCENE.itemSprites, UI_CUTSCENE_SPRITES_MAX
|
||||
);
|
||||
uiLabelSetX(&UICUTSCENE.itemLabel, (float_t)SCREEN.scanX);
|
||||
uiLabelSetY(&UICUTSCENE.itemLabel, (float_t)SCREEN.scanY +
|
||||
(float_t)FONT_DEFAULT.tileset->tileHeight * 3
|
||||
);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t uiCutsceneDraw() {
|
||||
const cutsceneitem_t *item = cutsceneSystemGetCurrentItem();
|
||||
if(!item || !CUTSCENE_SYSTEM.scene) {
|
||||
stringCopy(
|
||||
UICUTSCENE.countText, "Cutscene Inactive", UI_CUTSCENE_TEXT_MAX - 1
|
||||
);
|
||||
UICUTSCENE.itemText[0] = '\0';
|
||||
} else {
|
||||
const char_t *name = CUTSCENE_ITEM_CALLBACKS[item->type].jsonName;
|
||||
if(!name) name = "???";
|
||||
|
||||
stringFormat(
|
||||
UICUTSCENE.countText,
|
||||
UI_CUTSCENE_TEXT_MAX - 1,
|
||||
"Cutscene Item Count: %d",
|
||||
(int_t)CUTSCENE_SYSTEM.scene->itemCount
|
||||
);
|
||||
stringFormat(
|
||||
UICUTSCENE.itemText,
|
||||
UI_CUTSCENE_TEXT_MAX - 1,
|
||||
"Current Item: %s",
|
||||
name
|
||||
);
|
||||
}
|
||||
UICUTSCENE.countLabel.dirty = true;
|
||||
UICUTSCENE.itemLabel.dirty = true;
|
||||
|
||||
errorChain(uiLabelRender(&UICUTSCENE.countLabel, COLOR_GREEN));
|
||||
errorChain(uiLabelRender(&UICUTSCENE.itemLabel, COLOR_GREEN));
|
||||
return spriteBatchFlush();
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "ui/widget/uilabel.h"
|
||||
|
||||
#define UI_CUTSCENE_TEXT_MAX 64
|
||||
#define UI_CUTSCENE_SPRITES_MAX UI_CUTSCENE_TEXT_MAX
|
||||
|
||||
typedef struct {
|
||||
uilabel_t countLabel;
|
||||
char_t countText[UI_CUTSCENE_TEXT_MAX];
|
||||
spritebatchsprite_t countSprites[UI_CUTSCENE_SPRITES_MAX];
|
||||
|
||||
uilabel_t itemLabel;
|
||||
char_t itemText[UI_CUTSCENE_TEXT_MAX];
|
||||
spritebatchsprite_t itemSprites[UI_CUTSCENE_SPRITES_MAX];
|
||||
} uicutscene_t;
|
||||
|
||||
extern uicutscene_t UICUTSCENE;
|
||||
|
||||
/**
|
||||
* Initializes the cutscene debug labels.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiCutsceneInit();
|
||||
|
||||
/**
|
||||
* Draws the currently running cutscene's item count and current item
|
||||
* type as two lines ("Item Count: %d" / "Current Item: %s"), or a
|
||||
* single "Cutscene Inactive" line if no cutscene is running. Renders
|
||||
* below UIPLAYERPOS (see ui/debug/uiplayerpos.h).
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiCutsceneDraw();
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "ui/widget/uiframe.h"
|
||||
#include "ui/widget/uibutton.h"
|
||||
#include "engine/engine.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "util/math.h"
|
||||
@@ -40,11 +39,11 @@ errorret_t uiFatalErrorInit(void) {
|
||||
}
|
||||
|
||||
void uiFatalErrorOpen(const char_t *message) {
|
||||
assertNotNull(message, "Message cannot be NULL");
|
||||
// This is the last line of defense - it must never itself fail, so a
|
||||
// second fatal error while one is already showing is silently dropped
|
||||
// rather than asserting.
|
||||
if(UI_FATAL_ERROR.open) return;
|
||||
if(message == NULL) message = UI_FATAL_ERROR_GENERIC_MESSAGE;
|
||||
|
||||
stringCopy(
|
||||
UI_FATAL_ERROR.messageText, message, UI_FATAL_ERROR_MESSAGE_MAX - 1
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
#define UI_FATAL_ERROR_MESSAGE_SPRITES_MAX UI_FATAL_ERROR_MESSAGE_MAX
|
||||
#define UI_FATAL_ERROR_ITEM_COUNT 1
|
||||
#define UI_FATAL_ERROR_QUIT_LABEL "QUIT"
|
||||
#define UI_FATAL_ERROR_GENERIC_MESSAGE \
|
||||
"An unexpected error occurred and the game must exit. " \
|
||||
"Please contact support if this issue persists."
|
||||
|
||||
typedef struct {
|
||||
bool_t open;
|
||||
@@ -46,6 +49,10 @@ errorret_t uiFatalErrorInit(void);
|
||||
*
|
||||
* @param message The error text to display; copied internally, safe to
|
||||
* be transient. Truncated to UI_FATAL_ERROR_MESSAGE_MAX - 1 characters.
|
||||
* May be NULL, in which case UI_FATAL_ERROR_GENERIC_MESSAGE is shown
|
||||
* instead - useful for callers with no specific error text to report
|
||||
* (or that don't want to risk formatting/locale-lookup failures of their
|
||||
* own right before calling this, the last line of defense).
|
||||
*/
|
||||
void uiFatalErrorOpen(const char_t *message);
|
||||
|
||||
|
||||
@@ -26,17 +26,7 @@
|
||||
#define UI_MAIN_MENU_INDEX_QUIT 2
|
||||
|
||||
uimainmenu_t UI_MAIN_MENU;
|
||||
|
||||
// Loaded lazily on first activation and kept resident for the rest of the
|
||||
// process - it's tiny and reused every time, so there's no benefit to
|
||||
// unlocking/reloading it between attempts (same lifetime convention as
|
||||
// e.g. LOCALE.entry). The persistent cutscene is normally already running
|
||||
// by the time this screen is reachable at all (chained straight from the
|
||||
// initial cutscene's own CUTSCENE item, see assets/cutscenes/initial.jsonc
|
||||
// and CUTSCENE_ITEM_TYPE_CUTSCENE in asset/loader/cutscene/
|
||||
// assetcutsceneloader.c) - this lock just gives uiMainMenuStartGame a
|
||||
// stable pointer to restart it with if that run already completed.
|
||||
static assetentry_t *MAIN_MENU_CUTSCENE_ENTRY = NULL;
|
||||
assetentry_t *MAIN_MENU_CUTSCENE_ENTRY = NULL;
|
||||
|
||||
void uiMainMenuQuitConfirmed(const bool_t result, void *user) {
|
||||
if(result) ENGINE.running = false;
|
||||
@@ -59,7 +49,7 @@ cutscene_t *uiMainMenuLoadCutscene(void) {
|
||||
|
||||
void uiMainMenuSelectSaveResult(const uint8_t slotIndex, void *user) {
|
||||
if(slotIndex == UI_SELECT_SAVE_RESULT_NONE) {
|
||||
uiMainMenuOpen();
|
||||
cutsceneSystemLoad("main_menu.jsonc");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,10 +65,6 @@ void uiMainMenuOpenSelectSave(void *userData) {
|
||||
|
||||
void uiMainMenuStartGame(void) {
|
||||
if(CUTSCENE_SYSTEM.scene == NULL) {
|
||||
// The menu cutscene already ran to completion once (e.g. the player
|
||||
// backed out of the save picker back to this menu) - restart it
|
||||
// landing straight on NEW_GAME rather than replaying the idle/BGM
|
||||
// setup from the top.
|
||||
cutsceneSystemStartCutsceneAndGoToMarker(
|
||||
uiMainMenuLoadCutscene(), "NEW_GAME"
|
||||
);
|
||||
@@ -86,11 +72,6 @@ void uiMainMenuStartGame(void) {
|
||||
cutsceneGoTo("NEW_GAME");
|
||||
}
|
||||
|
||||
// Armed unconditionally, whether this just (re)started the cutscene or
|
||||
// jumped one already running since boot (chained straight from the
|
||||
// initial cutscene - see assets/cutscenes/initial.jsonc) - that first
|
||||
// run is never otherwise armed by anything, so setting this only in the
|
||||
// restart branch above would silently drop it on a fresh boot.
|
||||
cutsceneSystemSetOnComplete(uiMainMenuOpenSelectSave);
|
||||
}
|
||||
|
||||
@@ -107,7 +88,6 @@ void uiMainMenuSelected(
|
||||
break;
|
||||
|
||||
case UI_MAIN_MENU_INDEX_OPTIONS:
|
||||
// TODO: options.
|
||||
break;
|
||||
|
||||
case UI_MAIN_MENU_INDEX_QUIT:
|
||||
@@ -151,9 +131,6 @@ errorret_t uiMainMenuInit(void) {
|
||||
MENU_BUTTON(UI_MAIN_MENU.optionsLabel);
|
||||
MENU_BUTTON(UI_MAIN_MENU.quitLabel);
|
||||
MENU_END(UI_MAIN_MENU.items, 1);
|
||||
|
||||
// The main menu is the root screen once past the initial boot check -
|
||||
// there's nothing to back out to, so back/cancel does nothing here.
|
||||
uiMenuSetDisableBack(&UI_MAIN_MENU.menu, true);
|
||||
|
||||
errorOk();
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "ui/overlay/uiloading.h"
|
||||
#include "ui/overlay/uifatalerror.h"
|
||||
#include "ui/debug/uiplayerpos.h"
|
||||
#include "ui/debug/uicutscene.h"
|
||||
#include "ui/overlay/uicrop.h"
|
||||
#include "ui/transition/uitransition.h"
|
||||
#include "ui/debug/uiconsole.h"
|
||||
@@ -156,6 +157,7 @@ uielement_t UI_ELEMENTS[] = {
|
||||
{ .init = uiConsoleInit, .draw = uiConsoleDraw },
|
||||
{ .init = uiFPSInit, .draw = uiFPSDraw },
|
||||
{ .init = uiPlayerPosInit, .draw = uiPlayerPosDraw },
|
||||
{ .init = uiCutsceneInit, .draw = uiCutsceneDraw },
|
||||
|
||||
// ===========================================================================
|
||||
// Null terminator
|
||||
|
||||
Reference in New Issue
Block a user