diff --git a/cmake/targets/linux.cmake b/cmake/targets/linux.cmake index ae00ad3a..ac198623 100644 --- a/cmake/targets/linux.cmake +++ b/cmake/targets/linux.cmake @@ -27,17 +27,7 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC ) set(DUSK_BACKTRACE ON CACHE BOOL "Enable backtrace support for assert failures.") - -# Records the file/line of every memoryAllocate() call into a fixed-size -# tracking table (removed again on memoryFree()) so a leak still present at -# engineDispose()'s memoryGetAllocatedCount() check can be traced back to -# where it was allocated. Linux only for now. -set(DUSK_TRACK_MEMORY OFF CACHE BOOL "Track memoryAllocate() call sites to help find memory leaks.") -if(DUSK_TRACK_MEMORY) - target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC - DUSK_TRACK_MEMORY - ) -endif() +set(DUSK_TRACK_MEMORY ON CACHE BOOL "Track memoryAllocate() call sites to help find memory leaks.") # Define platform-specific macros. target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC @@ -56,5 +46,4 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC DUSK_TIME_DYNAMIC DUSK_NETWORK_IPV6 DUSK_THREAD_PTHREAD - DUSK_TRACK_MEMORY ) \ No newline at end of file diff --git a/src/dusk/CMakeLists.txt b/src/dusk/CMakeLists.txt index a801be9a..c0347f89 100644 --- a/src/dusk/CMakeLists.txt +++ b/src/dusk/CMakeLists.txt @@ -50,6 +50,12 @@ if(DUSK_BACKTRACE) ) endif() +if(DUSK_TRACK_MEMORY) + target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC + DUSK_TRACK_MEMORY + ) +endif() + # Includes target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PUBLIC diff --git a/src/dusk/asset/loader/cutscene/assetcutsceneloader.c b/src/dusk/asset/loader/cutscene/assetcutsceneloader.c index 55976dae..b85e2fbf 100644 --- a/src/dusk/asset/loader/cutscene/assetcutsceneloader.c +++ b/src/dusk/asset/loader/cutscene/assetcutsceneloader.c @@ -76,20 +76,13 @@ size_t assetCutsceneComputePoolSize(yyjson_val *itemsArr) { return size; } -void assetCutsceneFreeParsed(assetcutsceneoutput_t *out) { - if(out->items != NULL) { - memoryFree(out->items); - out->items = NULL; - } - if(out->pool != NULL) { - memoryFree(out->pool); - out->pool = NULL; - } - if(out->doc != NULL) { - yyjson_doc_free(out->doc); - out->doc = NULL; - } -} +// Raw file-read scratch space - fixed-size (ASSET_CUTSCENE_FILE_SIZE_MAX is +// already a hard cap below) rather than malloc'd per load. Safe to share: +// ASSET only ever runs one async load at a time on its single load thread +// (see assetUpdateAsync), and yyjson_read copies everything it needs out +// of this buffer into the doc before this function returns, so it's free +// to be overwritten by the next load. +static uint8_t CUTSCENE_FILE_BUFFER[ASSET_CUTSCENE_FILE_SIZE_MAX]; errorret_t assetCutsceneLoaderAsync(assetloading_t *loading) { assertNotNull(loading, "Loading cannot be NULL"); @@ -113,9 +106,10 @@ errorret_t assetCutsceneLoaderAsync(assetloading_t *loading) { } size_t fileSize = (size_t)file->size; - uint8_t *buffer = memoryAllocate(fileSize); assetLoaderErrorChain(loading, assetFileOpen(file)); - assetLoaderErrorChain(loading, assetFileRead(file, buffer, fileSize)); + assetLoaderErrorChain( + loading, assetFileRead(file, CUTSCENE_FILE_BUFFER, fileSize) + ); assertTrue( file->lastRead == file->size, "Failed to read entire cutscene file." @@ -127,48 +121,55 @@ errorret_t assetCutsceneLoaderAsync(assetloading_t *loading) { // type's load callback just parses fields - see cutsceneCutsceneLoad's // doc comment for how CUTSCENE items avoid needing one), so it happens // right here instead of bouncing to the sync phase. - assetcutsceneoutput_t *out = &loading->entry->data.cutscene; - yyjson_doc *doc = yyjson_read( - (char *)buffer, fileSize, + (char *)CUTSCENE_FILE_BUFFER, fileSize, YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS ); - memoryFree(buffer); - if(doc == NULL) { assetLoaderErrorThrow(loading, "Failed to parse cutscene JSON"); } - out->doc = doc; yyjson_val *root = yyjson_doc_get_root(doc); yyjson_val *itemsArr = yyjson_obj_get(root, "items"); if(!yyjson_is_arr(itemsArr)) { - assetCutsceneFreeParsed(out); + yyjson_doc_free(doc); assetLoaderErrorThrow(loading, "Cutscene JSON is missing an 'items' array"); } size_t itemCount = yyjson_arr_size(itemsArr); - if(itemCount > UINT8_MAX) { - assetCutsceneFreeParsed(out); - assetLoaderErrorThrow(loading, "Cutscene has more than 255 items"); + if(itemCount > CUTSCENE_LOADED_ITEMS_MAX) { + yyjson_doc_free(doc); + assetLoaderErrorThrow( + loading, "Cutscene has more than CUTSCENE_LOADED_ITEMS_MAX items" + ); } cutscenepause_t pause = CUTSCENE_PAUSE_DEFAULT; yyjson_val *pauseArr = yyjson_obj_get(root, "pause"); if(yyjson_is_arr(pauseArr)) { if(!cutsceneJsonParsePauseFlags(pauseArr, &pause)) { - assetCutsceneFreeParsed(out); + yyjson_doc_free(doc); assetLoaderErrorThrow( loading, "Cutscene JSON has an invalid 'pause' flag" ); } } - out->items = memoryAllocate(itemCount * sizeof(cutsceneitem_t)); - memoryZero(out->items, itemCount * sizeof(cutsceneitem_t)); - size_t poolSize = assetCutsceneComputePoolSize(itemsArr); - out->pool = poolSize > 0 ? memoryAllocate(poolSize) : NULL; + if(poolSize > CUTSCENE_LOADED_POOL_SIZE_MAX) { + yyjson_doc_free(doc); + assetLoaderErrorThrow( + loading, "Cutscene needs more pool space than CUTSCENE_LOADED_POOL_SIZE_MAX" + ); + } + + // Everything structural has validated by this point, so it's safe to + // start overwriting the shared CUTSCENE_LOADED_ITEMS/POOL buffers - a + // per-item parse failure past this point still leaves them partially + // overwritten, but that's fine: any load failure here already sends the + // game into the fatal-error overlay (see cutsceneSystemLoad/ + // cutsceneCutsceneResolve), so nothing keeps running against stale data. + memoryZero(CUTSCENE_LOADED_ITEMS, itemCount * sizeof(cutsceneitem_t)); size_t poolOffset = 0; yyjson_arr_iter itemIter = yyjson_arr_iter_with(itemsArr); @@ -176,19 +177,24 @@ errorret_t assetCutsceneLoaderAsync(assetloading_t *loading) { uint8_t index = 0; while((itemVal = yyjson_arr_iter_next(&itemIter)) != NULL) { errorret_t itemResult = assetCutsceneParseItem( - loading, itemVal, &out->items[index], out->pool, &poolOffset, index + loading, itemVal, &CUTSCENE_LOADED_ITEMS[index], + CUTSCENE_LOADED_POOL, &poolOffset, index ); if(errorIsNotOk(itemResult)) { - assetCutsceneFreeParsed(out); + yyjson_doc_free(doc); errorChain(itemResult); } index++; } - out->cutscene.items = out->items; - out->cutscene.itemCount = (uint8_t)itemCount; - out->cutscene.pause = pause; - out->cutscene.dataSize = 0; + // Takes ownership of doc - marker/cutscene-reference name fields point + // directly into it (see cutsceneCutsceneRefLoad/cutsceneMarkerLoad), so + // it must outlive this load, replacing whatever the previously loaded + // cutscene left behind. + cutsceneLoadedSetDoc(doc); + CUTSCENE_LOADED_SCENE.itemCount = (uint8_t)itemCount; + CUTSCENE_LOADED_SCENE.pause = pause; + CUTSCENE_LOADED_SCENE.dataSize = 0; loading->loading.cutscene.state = ASSET_CUTSCENE_LOADING_STATE_PARSE; loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; @@ -224,7 +230,10 @@ errorret_t assetCutsceneDispose(assetentry_t *entry) { assertTrue(entry->type == ASSET_LOADER_TYPE_CUTSCENE, "Invalid type."); assertIsMainThread("Must be called from the main thread."); - assetCutsceneFreeParsed(&entry->data.cutscene); + // Nothing to free here - a loaded cutscene's data lives in the fixed, + // shared CUTSCENE_LOADED_SCENE/ITEMS/POOL globals (see cutscenesystem.h), + // not per-entry storage; this entry only ever drove the read+parse + // pipeline into them. errorOk(); } diff --git a/src/dusk/asset/loader/cutscene/assetcutsceneloader.h b/src/dusk/asset/loader/cutscene/assetcutsceneloader.h index c7c8339c..9ae4a731 100644 --- a/src/dusk/asset/loader/cutscene/assetcutsceneloader.h +++ b/src/dusk/asset/loader/cutscene/assetcutsceneloader.h @@ -8,6 +8,7 @@ #pragma once #include "asset/assetfile.h" #include "rpg/cutscene/cutscene.h" +#include "rpg/cutscene/cutscenesystem.h" #include "rpg/cutscene/item/cutsceneitem.h" #include "rpg/cutscene/item/json/cutscenejsonpool.h" #include "yyjson.h" @@ -33,10 +34,13 @@ typedef struct { } assetcutsceneloaderloading_t; typedef struct { - cutscene_t cutscene; - cutsceneitem_t *items; - uint8_t *pool; - yyjson_doc *doc; + // Nothing - a loaded cutscene's data lives entirely in the fixed, + // shared CUTSCENE_LOADED_SCENE/ITEMS/POOL globals (see cutscenesystem.h) + // that assetCutsceneLoaderAsync parses directly into, not per-entry + // storage. This entry only ever exists to drive the read+parse + // pipeline, and is unlocked (see cutsceneSystemLoad/ + // cutsceneCutsceneResolve) the instant that finishes. + uint8_t unused; } assetcutsceneoutput_t; /** @@ -67,22 +71,15 @@ errorret_t assetCutsceneParseItem( const uint8_t index ); -/** - * Frees whatever of out->items/out->pool/out->doc is currently non-NULL - * and clears them - shared by both the normal disposer and every parse - * failure path's cleanup. - * - * @param out The output to free. - */ -void assetCutsceneFreeParsed(assetcutsceneoutput_t *out); - /** * Asynchronous loader for cutscene assets. Reads the raw JSONC file bytes - * and, in the same call, parses them into a heap-allocated cutsceneitem_t - * array + pool - none of that work touches the main thread (every item - * type's load callback is pure parsing now; CUTSCENE items defer their - * nested asset resolution to cutsceneCutsceneStart), so doing it all here - * keeps a large cutscene from ever blocking a frame. + * and, in the same call, parses them directly into the fixed, shared + * CUTSCENE_LOADED_ITEMS/CUTSCENE_LOADED_POOL buffers (see cutscenesystem.h) + * - no heap allocation at all, since only one cutscene is ever resident. + * None of that work touches the main thread (every item type's load + * callback is pure parsing now; CUTSCENE items defer their nested asset + * resolution to cutsceneCutsceneStart), so doing it all here keeps a large + * cutscene from ever blocking a frame. * * @param loading Loading information for the asset being loaded. * @return Error code indicating success or failure of the load operation. diff --git a/src/dusk/rpg/cutscene/cutscenesystem.c b/src/dusk/rpg/cutscene/cutscenesystem.c index 56fab869..a61b1bb7 100644 --- a/src/dusk/rpg/cutscene/cutscenesystem.c +++ b/src/dusk/rpg/cutscene/cutscenesystem.c @@ -15,6 +15,21 @@ cutscenesystem_t CUTSCENE_SYSTEM; +cutsceneitem_t CUTSCENE_LOADED_ITEMS[CUTSCENE_LOADED_ITEMS_MAX]; +uint8_t CUTSCENE_LOADED_POOL[CUTSCENE_LOADED_POOL_SIZE_MAX]; +cutscene_t CUTSCENE_LOADED_SCENE = { + .items = CUTSCENE_LOADED_ITEMS, + .itemCount = 0, + .pause = CUTSCENE_PAUSE_NONE, + .dataSize = 0 +}; +static yyjson_doc *CUTSCENE_LOADED_DOC = NULL; + +void cutsceneLoadedSetDoc(yyjson_doc *doc) { + if(CUTSCENE_LOADED_DOC != NULL) yyjson_doc_free(CUTSCENE_LOADED_DOC); + CUTSCENE_LOADED_DOC = doc; +} + void cutsceneSystemInit() { memoryZero(&CUTSCENE_SYSTEM, sizeof(cutscenesystem_t)); } @@ -69,13 +84,18 @@ void cutsceneSystemLoad(const char_t *file) { assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_CUTSCENE, NULL); errorret_t result = assetRequireLoaded(entry); + // Released the instant loading finishes - the loader already parsed + // everything we need into CUTSCENE_LOADED_SCENE/ITEMS/POOL, so nothing + // further depends on this entry (see cutscenesystem.h's doc comment on + // CUTSCENE_LOADED_ITEMS_MAX for why only one cutscene stays resident). + assetUnlockEntry(entry); if(errorIsNotOk(result)) { errorCatch(errorPrint(result)); uiFatalErrorOpen(NULL); return; } - cutsceneSystemStartCutscene(&entry->data.cutscene.cutscene); + cutsceneSystemStartCutscene(&CUTSCENE_LOADED_SCENE); } void cutsceneRestart(void) { @@ -259,4 +279,6 @@ void cutsceneSystemDispose() { CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED; CUTSCENE_SYSTEM.textCache[0] = '\0'; CUTSCENE_SYSTEM.onComplete = NULL; + + cutsceneLoadedSetDoc(NULL); } diff --git a/src/dusk/rpg/cutscene/cutscenesystem.h b/src/dusk/rpg/cutscene/cutscenesystem.h index b4c1f4f3..85473723 100644 --- a/src/dusk/rpg/cutscene/cutscenesystem.h +++ b/src/dusk/rpg/cutscene/cutscenesystem.h @@ -26,6 +26,39 @@ typedef struct entity_s entity_t; // CUTSCENE_ITEM_TYPE_KEYBOARD item is never truncated caching it here. #define CUTSCENE_TEXT_CACHE_MAX 64 +// Only one JSON-authored cutscene is ever loaded/playing at a time - a +// CUTSCENE item hands control off completely (see cutsceneCutsceneStart) +// and cutsceneSystemLoad/cutsceneCutsceneResolve release their asset lock +// the instant loading finishes - so there's no benefit to malloc'ing a +// fresh items/pool buffer per load. Instead the asset loader +// (assetCutsceneLoaderAsync) parses directly into these fixed, static +// buffers. Bump these if a real cutscene ever needs more; C-authored +// cutscenes (the CUTSCENE(...) macro) don't use these at all - they're +// compiled directly into their own static const arrays. +#define CUTSCENE_LOADED_ITEMS_MAX 128 +#define CUTSCENE_LOADED_POOL_SIZE_MAX 4096 + +extern cutsceneitem_t CUTSCENE_LOADED_ITEMS[CUTSCENE_LOADED_ITEMS_MAX]; +extern uint8_t CUTSCENE_LOADED_POOL[CUTSCENE_LOADED_POOL_SIZE_MAX]; + +// The cutscene_t for whatever was last parsed into CUTSCENE_LOADED_ITEMS/ +// CUTSCENE_LOADED_POOL - .items always points at CUTSCENE_LOADED_ITEMS; +// itemCount/pause/dataSize are overwritten by the loader on each load. +extern cutscene_t CUTSCENE_LOADED_SCENE; + +/** + * Frees whatever yyjson_doc currently backs CUTSCENE_LOADED_ITEMS' string + * fields (marker/cutscene-reference names point directly into it - see + * cutsceneCutsceneRefLoad/cutsceneMarkerLoad) and takes ownership of doc + * instead. Called by the cutscene asset loader once a fresh parse into + * CUTSCENE_LOADED_ITEMS/CUTSCENE_LOADED_POOL completes successfully, and + * from cutsceneSystemDispose to release the last one at shutdown. + * + * @param doc The new doc to take ownership of (NULL just frees/clears the + * current one). + */ +void cutsceneLoadedSetDoc(yyjson_doc *doc); + typedef struct { const cutscene_t *scene; uint8_t currentItem; diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.c b/src/dusk/rpg/cutscene/item/cutsceneitem.c index a81d4f98..f613c6aa 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.c +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.c @@ -373,10 +373,13 @@ void cutsceneCutsceneStart( ) { const cutscene_t *cutscene = item->cutsceneRef.cutscene; if(cutscene == NULL && item->cutsceneRef.name != NULL) { + // Not cached back into item - with only one shared, fixed load buffer + // (see cutscenesystem.h's CUTSCENE_LOADED_ITEMS_MAX doc comment), + // every JSON-authored reference resolves to the same fixed address + // every time anyway, and item itself lives inside that very buffer - + // caching a pointer into it here would go stale (or, worse, get + // clobbered mid-resolve) the moment a later load overwrites it. cutscene = cutsceneCutsceneResolve(item->cutsceneRef.name); - // Cache the resolved pointer back so replaying this same item (e.g. - // a looping intro) never re-resolves/re-locks the asset again. - ((cutsceneitem_t *)item)->cutsceneRef.cutscene = cutscene; } if(cutscene != NULL) cutsceneSystemStartCutscene(cutscene); } @@ -394,13 +397,16 @@ const cutscene_t * cutsceneCutsceneResolve(const char_t *name) { assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_CUTSCENE, NULL); errorret_t result = assetRequireLoaded(entry); + // Released the instant loading finishes - see cutsceneSystemLoad's doc + // comment for why nothing further depends on this entry once loaded. + assetUnlockEntry(entry); if(errorIsNotOk(result)) { errorCatch(errorPrint(result)); uiFatalErrorOpen(NULL); return NULL; } - return &entry->data.cutscene.cutscene; + return &CUTSCENE_LOADED_SCENE; } errorret_t cutsceneCutsceneLoad( diff --git a/src/dusk/rpg/entity/global/entitygloballist.h b/src/dusk/rpg/entity/global/entitygloballist.h index cd22a50a..36b9f7d9 100644 --- a/src/dusk/rpg/entity/global/entitygloballist.h +++ b/src/dusk/rpg/entity/global/entitygloballist.h @@ -22,8 +22,8 @@ ENTITY_GLOBAL_CALLBACK(3) { } ENTITY_GLOBAL_CALLBACK(4) { - create->entity->interact.type = ENTITY_INTERACT_CUTSCENE; - create->entity->interact.data.cutscene = cutsceneCutsceneResolve("battletest"); + create->entity->interact.type = ENTITY_INTERACT_CUTSCENE_NAME; + create->entity->interact.data.cutsceneName = "battletest"; } static const entityglobaldef_t ENTITY_GLOBAL_LIST[] = { diff --git a/src/dusk/rpg/entity/interact/entityinteract.c b/src/dusk/rpg/entity/interact/entityinteract.c index d7a8b224..f956c203 100644 --- a/src/dusk/rpg/entity/interact/entityinteract.c +++ b/src/dusk/rpg/entity/interact/entityinteract.c @@ -27,6 +27,15 @@ void entityInteractWith(entity_t *player, entity_t *target) { ); break; + case ENTITY_INTERACT_CUTSCENE_NAME: { + const cutscene_t *cutscene = + cutsceneCutsceneResolve(target->interact.data.cutsceneName); + if(cutscene != NULL) { + cutsceneSystemStartCutsceneWith(cutscene, player, target); + } + break; + } + case ENTITY_INTERACT_PRINT: uiTextboxMainSetText(target->interact.data.message); diff --git a/src/dusk/rpg/entity/interact/entityinteract.h b/src/dusk/rpg/entity/interact/entityinteract.h index 4f1f39d4..94abf2dc 100644 --- a/src/dusk/rpg/entity/interact/entityinteract.h +++ b/src/dusk/rpg/entity/interact/entityinteract.h @@ -18,6 +18,7 @@ typedef enum { ENTITY_INTERACT_NULL = 0, ENTITY_INTERACT_CUTSCENE, + ENTITY_INTERACT_CUTSCENE_NAME, ENTITY_INTERACT_PRINT, ENTITY_INTERACT_CALLBACK, @@ -28,7 +29,18 @@ typedef enum { * Per-type data for an entity's interact component. */ typedef union { + // A compile-time cutscene_t* (e.g. CUTSCENE_REFERENCE(...)) - safe to + // hold onto indefinitely, since C-authored cutscenes are compiled + // directly into their own static const arrays, never the shared + // CUTSCENE_LOADED_ITEMS buffer (see cutscenesystem.h). const cutscene_t *cutscene; + // A bare cutscene name (as passed to cutsceneCutsceneResolve), resolved + // lazily at interact time rather than once up front - only one + // JSON-authored cutscene is ever resident at a time (see + // CUTSCENE_LOADED_ITEMS_MAX's doc comment), so resolving early and + // holding onto the result here would go stale the moment any other + // cutscene loads in the meantime. + const char_t *cutsceneName; const char_t *message; void (*callback)(entity_t *player, entity_t *target); } entityinteractdata_t; diff --git a/src/dusk/ui/screen/mainmenu/uimainmenu.c b/src/dusk/ui/screen/mainmenu/uimainmenu.c index 4340388b..dfca5b44 100644 --- a/src/dusk/ui/screen/mainmenu/uimainmenu.c +++ b/src/dusk/ui/screen/mainmenu/uimainmenu.c @@ -26,21 +26,15 @@ #define UI_MAIN_MENU_INDEX_QUIT 2 uimainmenu_t UI_MAIN_MENU; -assetentry_t *MAIN_MENU_CUTSCENE_ENTRY = NULL; -cutscene_t *uiMainMenuLoadCutscene(void) { - if(MAIN_MENU_CUTSCENE_ENTRY == NULL) { - MAIN_MENU_CUTSCENE_ENTRY = assetLock( - "cutscenes/main_menu.jsonc", ASSET_LOADER_TYPE_CUTSCENE, NULL - ); - } - errorret_t result = assetRequireLoaded(MAIN_MENU_CUTSCENE_ENTRY); - if(errorIsNotOk(result)) { - errorCatch(errorPrint(result)); - assertTrue(false, "Failed to load main menu cutscene asset"); - } - - return &MAIN_MENU_CUTSCENE_ENTRY->data.cutscene.cutscene; +const cutscene_t *uiMainMenuLoadCutscene(void) { + // Delegates entirely to cutsceneCutsceneResolve rather than keeping our + // own locked-forever entry, since only one JSON-authored cutscene is + // ever resident at a time (see CUTSCENE_LOADED_ITEMS_MAX's doc comment + // in cutscenesystem.h) - caching an entry pointer here across calls + // would go stale the moment any other cutscene loads in the meantime. + // May return NULL on failure (already reported via uiFatalErrorOpen). + return cutsceneCutsceneResolve("main_menu"); } void uiMainMenuSelectSaveResult(const uint8_t slotIndex, void *user) { @@ -79,9 +73,9 @@ void uiMainMenuOpenSelectSave(void *userData) { void uiMainMenuStartGame(void) { if(CUTSCENE_SYSTEM.scene == NULL) { - cutsceneSystemStartCutsceneAndGoToMarker( - uiMainMenuLoadCutscene(), "NEW_GAME" - ); + const cutscene_t *cutscene = uiMainMenuLoadCutscene(); + if(cutscene == NULL) return; + cutsceneSystemStartCutsceneAndGoToMarker(cutscene, "NEW_GAME"); } else { cutsceneGoTo("NEW_GAME"); } @@ -91,9 +85,9 @@ void uiMainMenuStartGame(void) { void uiMainMenuOptions(void) { if(CUTSCENE_SYSTEM.scene == NULL) { - cutsceneSystemStartCutsceneAndGoToMarker( - uiMainMenuLoadCutscene(), "OPTIONS" - ); + const cutscene_t *cutscene = uiMainMenuLoadCutscene(); + if(cutscene == NULL) return; + cutsceneSystemStartCutsceneAndGoToMarker(cutscene, "OPTIONS"); } else { cutsceneGoTo("OPTIONS"); } @@ -101,7 +95,9 @@ void uiMainMenuOptions(void) { void uiMainMenuQuit(void) { if(CUTSCENE_SYSTEM.scene == NULL) { - cutsceneSystemStartCutsceneAndGoToMarker(uiMainMenuLoadCutscene(), "QUIT"); + const cutscene_t *cutscene = uiMainMenuLoadCutscene(); + if(cutscene == NULL) return; + cutsceneSystemStartCutsceneAndGoToMarker(cutscene, "QUIT"); } else { cutsceneGoTo("QUIT"); } diff --git a/src/dusk/ui/screen/mainmenu/uimainmenu.h b/src/dusk/ui/screen/mainmenu/uimainmenu.h index 5e0a0436..dd352b0d 100644 --- a/src/dusk/ui/screen/mainmenu/uimainmenu.h +++ b/src/dusk/ui/screen/mainmenu/uimainmenu.h @@ -39,17 +39,18 @@ errorret_t uiMainMenuInit(void); void uiMainMenuOpen(void); /** - * Lazily locks/loads the main menu cutscene asset (assets/cutscenes/ + * Loads (or re-loads) the main menu cutscene asset (assets/cutscenes/ * main_menu.jsonc, parsed directly at load time rather than compiled in) - * and returns it, asserting on failure. Kept resident for the rest of the - * process once loaded - the persistent cutscene is what actually drives - * this whole screen (opening this panel is its first item's job, see - * UI_SHOW/rpg/cutscene/item/ui/cutsceneuishow.h), this is just the shared - * accessor for the "Start Game" flow below to jump/restart it. + * and returns it - the shared accessor for the "Start Game"/Options/Quit + * flows below to jump/restart it. Only one JSON-authored cutscene is ever + * resident at a time (see CUTSCENE_LOADED_ITEMS_MAX's doc comment in + * cutscenesystem.h), so this resolves fresh on every call rather than + * caching anything. * - * @return The main menu cutscene. + * @return The main menu cutscene, or NULL on failure (already reported via + * uiFatalErrorOpen). */ -cutscene_t *uiMainMenuLoadCutscene(void); +const cutscene_t *uiMainMenuLoadCutscene(void); /** * Starts the "Start Game" flow: jumps the already-running main menu