diff --git a/src/dusk/asset/loader/cutscene/assetcutsceneloader.c b/src/dusk/asset/loader/cutscene/assetcutsceneloader.c index 55524e6c..5475a44b 100644 --- a/src/dusk/asset/loader/cutscene/assetcutsceneloader.c +++ b/src/dusk/asset/loader/cutscene/assetcutsceneloader.c @@ -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); diff --git a/src/dusk/asset/loader/cutscene/assetcutsceneloader.h b/src/dusk/asset/loader/cutscene/assetcutsceneloader.h index 05128f4f..1eb9609a 100644 --- a/src/dusk/asset/loader/cutscene/assetcutsceneloader.h +++ b/src/dusk/asset/loader/cutscene/assetcutsceneloader.h @@ -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; diff --git a/src/dusk/console/console.c b/src/dusk/console/console.c index 035cd526..edbe3b4d 100644 --- a/src/dusk/console/console.c +++ b/src/dusk/console/console.c @@ -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); } diff --git a/src/dusk/rpg/cutscene/cutscenesystem.c b/src/dusk/rpg/cutscene/cutscenesystem.c index 1abe3b57..56fab869 100644 --- a/src/dusk/rpg/cutscene/cutscenesystem.c +++ b/src/dusk/rpg/cutscene/cutscenesystem.c @@ -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" diff --git a/src/dusk/rpg/cutscene/cutscenesystem.h b/src/dusk/rpg/cutscene/cutscenesystem.h index 823285d0..b4c1f4f3 100644 --- a/src/dusk/rpg/cutscene/cutscenesystem.h +++ b/src/dusk/rpg/cutscene/cutscenesystem.h @@ -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. diff --git a/src/dusk/scene/initial/sceneinitial.c b/src/dusk/scene/initial/sceneinitial.c index ddcb4a2a..2242f142 100644 --- a/src/dusk/scene/initial/sceneinitial.c +++ b/src/dusk/scene/initial/sceneinitial.c @@ -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(); } diff --git a/src/dusk/ui/debug/CMakeLists.txt b/src/dusk/ui/debug/CMakeLists.txt index 74e31662..52d8095a 100644 --- a/src/dusk/ui/debug/CMakeLists.txt +++ b/src/dusk/ui/debug/CMakeLists.txt @@ -6,6 +6,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC uiconsole.c + uicutscene.c uifps.c uiplayerpos.c ) diff --git a/src/dusk/ui/debug/uicutscene.c b/src/dusk/ui/debug/uicutscene.c new file mode 100644 index 00000000..94bdf321 --- /dev/null +++ b/src/dusk/ui/debug/uicutscene.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(); +} diff --git a/src/dusk/ui/debug/uicutscene.h b/src/dusk/ui/debug/uicutscene.h new file mode 100644 index 00000000..07fa9441 --- /dev/null +++ b/src/dusk/ui/debug/uicutscene.h @@ -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(); diff --git a/src/dusk/ui/overlay/uifatalerror.c b/src/dusk/ui/overlay/uifatalerror.c index e5c75a34..53305e48 100644 --- a/src/dusk/ui/overlay/uifatalerror.c +++ b/src/dusk/ui/overlay/uifatalerror.c @@ -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 diff --git a/src/dusk/ui/overlay/uifatalerror.h b/src/dusk/ui/overlay/uifatalerror.h index c392960c..043b58c1 100644 --- a/src/dusk/ui/overlay/uifatalerror.h +++ b/src/dusk/ui/overlay/uifatalerror.h @@ -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); diff --git a/src/dusk/ui/screen/mainmenu/uimainmenu.c b/src/dusk/ui/screen/mainmenu/uimainmenu.c index ca05dee7..29e06cce 100644 --- a/src/dusk/ui/screen/mainmenu/uimainmenu.c +++ b/src/dusk/ui/screen/mainmenu/uimainmenu.c @@ -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(); diff --git a/src/dusk/ui/uielementlist.c b/src/dusk/ui/uielementlist.c index 3a0426a5..9f3783dd 100644 --- a/src/dusk/ui/uielementlist.c +++ b/src/dusk/ui/uielementlist.c @@ -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