diff --git a/src/dusk/asset/loader/cutscene/assetcutsceneloader.c b/src/dusk/asset/loader/cutscene/assetcutsceneloader.c index b85e2fbf..d86fe9f8 100644 --- a/src/dusk/asset/loader/cutscene/assetcutsceneloader.c +++ b/src/dusk/asset/loader/cutscene/assetcutsceneloader.c @@ -94,9 +94,27 @@ errorret_t assetCutsceneLoaderAsync(assetloading_t *loading) { errorOk(); } + const assetcutsceneloaderinput_t *input = loading->entry->input != NULL + ? &loading->entry->input->cutscene + : NULL; + const bool_t toInsert = + input != NULL && input->dest == ASSET_CUTSCENE_LOAD_DEST_INSERT; + const char_t *filePath = (input != NULL && input->path[0] != '\0') + ? input->path + : loading->entry->name; + + cutsceneitem_t *targetItems = + toInsert ? CUTSCENE_INSERT_ITEMS : CUTSCENE_LOADED_ITEMS; + const size_t targetItemsMax = + toInsert ? CUTSCENE_INSERT_ITEMS_MAX : CUTSCENE_LOADED_ITEMS_MAX; + uint8_t *targetPool = toInsert ? CUTSCENE_INSERT_POOL : CUTSCENE_LOADED_POOL; + const size_t targetPoolMax = + toInsert ? CUTSCENE_INSERT_POOL_SIZE_MAX : CUTSCENE_LOADED_POOL_SIZE_MAX; + cutscene_t *targetScene = toInsert ? &CUTSCENE_INSERT_SCENE : &CUTSCENE_LOADED_SCENE; + assetfile_t *file = &loading->loading.cutscene.file; assetLoaderErrorChain(loading, - assetFileInit(file, loading->entry->name, NULL, NULL) + assetFileInit(file, filePath, NULL, NULL) ); if(file->size > ASSET_CUTSCENE_FILE_SIZE_MAX) { @@ -137,10 +155,10 @@ errorret_t assetCutsceneLoaderAsync(assetloading_t *loading) { } size_t itemCount = yyjson_arr_size(itemsArr); - if(itemCount > CUTSCENE_LOADED_ITEMS_MAX) { + if(itemCount > targetItemsMax) { yyjson_doc_free(doc); assetLoaderErrorThrow( - loading, "Cutscene has more than CUTSCENE_LOADED_ITEMS_MAX items" + loading, "Cutscene has more items than its target buffer allows" ); } @@ -156,20 +174,20 @@ errorret_t assetCutsceneLoaderAsync(assetloading_t *loading) { } size_t poolSize = assetCutsceneComputePoolSize(itemsArr); - if(poolSize > CUTSCENE_LOADED_POOL_SIZE_MAX) { + if(poolSize > targetPoolMax) { yyjson_doc_free(doc); assetLoaderErrorThrow( - loading, "Cutscene needs more pool space than CUTSCENE_LOADED_POOL_SIZE_MAX" + loading, "Cutscene needs more pool space than its target buffer allows" ); } // 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)); + // start overwriting the target's shared 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/ + // cutsceneInsertResolve), so nothing keeps running against stale data. + memoryZero(targetItems, itemCount * sizeof(cutsceneitem_t)); size_t poolOffset = 0; yyjson_arr_iter itemIter = yyjson_arr_iter_with(itemsArr); @@ -177,8 +195,8 @@ errorret_t assetCutsceneLoaderAsync(assetloading_t *loading) { uint8_t index = 0; while((itemVal = yyjson_arr_iter_next(&itemIter)) != NULL) { errorret_t itemResult = assetCutsceneParseItem( - loading, itemVal, &CUTSCENE_LOADED_ITEMS[index], - CUTSCENE_LOADED_POOL, &poolOffset, index + loading, itemVal, &targetItems[index], + targetPool, &poolOffset, index ); if(errorIsNotOk(itemResult)) { yyjson_doc_free(doc); @@ -190,11 +208,12 @@ errorret_t assetCutsceneLoaderAsync(assetloading_t *loading) { // 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; + // cutscene left behind for this same destination. + if(toInsert) cutsceneInsertSetDoc(doc); + else cutsceneLoadedSetDoc(doc); + targetScene->itemCount = (uint8_t)itemCount; + targetScene->pause = pause; + targetScene->dataSize = 0; loading->loading.cutscene.state = ASSET_CUTSCENE_LOADING_STATE_PARSE; loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; diff --git a/src/dusk/asset/loader/cutscene/assetcutsceneloader.h b/src/dusk/asset/loader/cutscene/assetcutsceneloader.h index 9ae4a731..7cb8e571 100644 --- a/src/dusk/asset/loader/cutscene/assetcutsceneloader.h +++ b/src/dusk/asset/loader/cutscene/assetcutsceneloader.h @@ -18,8 +18,26 @@ typedef struct assetloading_s assetloading_t; typedef struct assetentry_s assetentry_t; +// Which fixed, shared buffer set (see cutscenesystem.h) a load should +// parse into - MAIN for the one JSON-authored cutscene resident via +// cutsceneSystemLoad/cutsceneCutsceneResolve, INSERT for the one resident +// via cutsceneInsertResolve (see CUTSCENE_ITEM_TYPE_INSERT). +typedef enum { + ASSET_CUTSCENE_LOAD_DEST_MAIN, + ASSET_CUTSCENE_LOAD_DEST_INSERT +} assetcutsceneloaddest_t; + typedef struct { - void *nothing; + assetcutsceneloaddest_t dest; + + // Optional file path override, same trick as assetjsonloaderinput_t - + // when path[0] is non-zero this is read instead of loading->entry->name, + // so a synthetic cache key (e.g. "insert:cutscenes/foo.jsonc", used to + // avoid a single entry being shared - and its dest silently ignored - + // between two different logical loads of the same file, see + // asset.c/assetGetEntry's name-only dedup) can still point at the real + // file. + char_t path[ASSET_FILE_NAME_MAX]; } assetcutsceneloaderinput_t; typedef enum { @@ -73,13 +91,15 @@ errorret_t assetCutsceneParseItem( /** * Asynchronous loader for cutscene assets. Reads the raw JSONC file bytes - * 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. + * and, in the same call, parses them directly into one of the fixed, + * shared buffer sets in cutscenesystem.h - CUTSCENE_LOADED_ITEMS/POOL by + * default, or CUTSCENE_INSERT_ITEMS/POOL when + * loading->entry->input->cutscene.dest is ASSET_CUTSCENE_LOAD_DEST_INSERT - + * no heap allocation at all, since only one cutscene is ever resident per + * destination. 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/cutscene.h b/src/dusk/rpg/cutscene/cutscene.h index a307d7af..402b4c7f 100644 --- a/src/dusk/rpg/cutscene/cutscene.h +++ b/src/dusk/rpg/cutscene/cutscene.h @@ -62,3 +62,20 @@ typedef struct cutscene_s { .type = CUTSCENE_ITEM_TYPE_CUTSCENE, \ .cutsceneRef = { .cutscene = CUTSCENE_REFERENCE(CUTSCENE), .name = NULL } \ } + +/** + * Splices another CUTSCENE(NAME, ...)-declared cutscene's items into this + * one in place, resuming right after this item once the referenced + * cutscene's items run out - unlike CUTSCENE_CUTSCENE (a one-way jump that + * never returns). Has no dedicated item header of its own, same rationale + * as CUTSCENE_CUTSCENE - cutsceneInsertStart/Update/Load are defined + * inline in cutsceneitem.h/.c, tightly bound to the asset/loader system. + * + * @param CUTSCENE The referenced cutscene's NAME, as passed to + * CUTSCENE(...). + */ +#define CUTSCENE_INSERT(CUTSCENE) \ + { \ + .type = CUTSCENE_ITEM_TYPE_INSERT, \ + .insert = { .cutscene = CUTSCENE_REFERENCE(CUTSCENE), .name = NULL } \ + } diff --git a/src/dusk/rpg/cutscene/cutscenesystem.c b/src/dusk/rpg/cutscene/cutscenesystem.c index a61b1bb7..6813ea9f 100644 --- a/src/dusk/rpg/cutscene/cutscenesystem.c +++ b/src/dusk/rpg/cutscene/cutscenesystem.c @@ -30,6 +30,21 @@ void cutsceneLoadedSetDoc(yyjson_doc *doc) { CUTSCENE_LOADED_DOC = doc; } +cutsceneitem_t CUTSCENE_INSERT_ITEMS[CUTSCENE_INSERT_ITEMS_MAX]; +uint8_t CUTSCENE_INSERT_POOL[CUTSCENE_INSERT_POOL_SIZE_MAX]; +cutscene_t CUTSCENE_INSERT_SCENE = { + .items = CUTSCENE_INSERT_ITEMS, + .itemCount = 0, + .pause = CUTSCENE_PAUSE_NONE, + .dataSize = 0 +}; +static yyjson_doc *CUTSCENE_INSERT_DOC = NULL; + +void cutsceneInsertSetDoc(yyjson_doc *doc) { + if(CUTSCENE_INSERT_DOC != NULL) yyjson_doc_free(CUTSCENE_INSERT_DOC); + CUTSCENE_INSERT_DOC = doc; +} + void cutsceneSystemInit() { memoryZero(&CUTSCENE_SYSTEM, sizeof(cutscenesystem_t)); } @@ -55,6 +70,7 @@ void cutsceneSystemPrepare( CUTSCENE_SYSTEM.textCache[0] = '\0'; CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0. CUTSCENE_SYSTEM.onComplete = NULL; + CUTSCENE_SYSTEM.insertStackSize = 0; } void cutsceneSystemStartCutscene(const cutscene_t *cutscene) { @@ -127,6 +143,21 @@ void cutsceneSystemNext() { CUTSCENE_SYSTEM.currentItem++; + // Run off the end of the current scene? If it was spliced in via + // CUTSCENE_ITEM_TYPE_INSERT, pop back to whatever it was inserted into + // and resume right after the INSERT item - possibly chaining through + // several pops if that lands on the end of its own parent in turn (an + // insert as the very last item, or a nested insert-of-an-insert). + while( + CUTSCENE_SYSTEM.currentItem >= CUTSCENE_SYSTEM.scene->itemCount && + CUTSCENE_SYSTEM.insertStackSize > 0 + ) { + cutsceneinsertframe_t frame = + CUTSCENE_SYSTEM.insertStack[--CUTSCENE_SYSTEM.insertStackSize]; + CUTSCENE_SYSTEM.scene = frame.scene; + CUTSCENE_SYSTEM.currentItem = frame.currentItem + 1; + } + // End of the cutscene? if( CUTSCENE_SYSTEM.currentItem >= CUTSCENE_SYSTEM.scene->itemCount @@ -149,6 +180,7 @@ void cutsceneSystemNext() { CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED; CUTSCENE_SYSTEM.textCache[0] = '\0'; CUTSCENE_SYSTEM.onComplete = NULL; + CUTSCENE_SYSTEM.insertStackSize = 0; if(onComplete != NULL) onComplete(userData); return; @@ -160,6 +192,27 @@ void cutsceneSystemNext() { cutsceneItemStart(item, &CUTSCENE_SYSTEM.data); } +void cutsceneSystemInsertCutscene(const cutscene_t *cutscene) { + assertNotNull( + CUTSCENE_SYSTEM.scene, + "cutsceneSystemInsertCutscene called with no cutscene running" + ); + assertTrue( + CUTSCENE_SYSTEM.insertStackSize < CUTSCENE_INSERT_STACK_MAX, + "Cutscene INSERT nesting exceeds CUTSCENE_INSERT_STACK_MAX" + ); + + CUTSCENE_SYSTEM.insertStack[CUTSCENE_SYSTEM.insertStackSize++] = + (cutsceneinsertframe_t){ + .scene = CUTSCENE_SYSTEM.scene, + .currentItem = CUTSCENE_SYSTEM.currentItem + }; + + CUTSCENE_SYSTEM.scene = cutscene; + CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0. + cutsceneSystemNext(); +} + void cutsceneSystemSetOnComplete(cutscenecallback_t onComplete) { assertNotNull( CUTSCENE_SYSTEM.scene, @@ -279,6 +332,8 @@ void cutsceneSystemDispose() { CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED; CUTSCENE_SYSTEM.textCache[0] = '\0'; CUTSCENE_SYSTEM.onComplete = NULL; + CUTSCENE_SYSTEM.insertStackSize = 0; cutsceneLoadedSetDoc(NULL); + cutsceneInsertSetDoc(NULL); } diff --git a/src/dusk/rpg/cutscene/cutscenesystem.h b/src/dusk/rpg/cutscene/cutscenesystem.h index 85473723..9b585f87 100644 --- a/src/dusk/rpg/cutscene/cutscenesystem.h +++ b/src/dusk/rpg/cutscene/cutscenesystem.h @@ -59,6 +59,52 @@ extern cutscene_t CUTSCENE_LOADED_SCENE; */ void cutsceneLoadedSetDoc(yyjson_doc *doc); +// A second, much smaller buffer set dedicated to CUTSCENE_ITEM_TYPE_INSERT +// (see cutsceneSystemInsertCutscene) - kept entirely separate from +// CUTSCENE_LOADED_ITEMS/POOL so that a JSON-authored INSERT item resolving +// its target by name (via cutsceneInsertResolve) never overwrites the +// buffer the currently-running outer cutscene's own items live in. Sized +// small deliberately - an inserted snippet is meant to be a short splice, +// not a full cutscene. +#define CUTSCENE_INSERT_ITEMS_MAX 16 +#define CUTSCENE_INSERT_POOL_SIZE_MAX 512 + +extern cutsceneitem_t CUTSCENE_INSERT_ITEMS[CUTSCENE_INSERT_ITEMS_MAX]; +extern uint8_t CUTSCENE_INSERT_POOL[CUTSCENE_INSERT_POOL_SIZE_MAX]; + +// The cutscene_t for whatever was last parsed into CUTSCENE_INSERT_ITEMS/ +// CUTSCENE_INSERT_POOL - .items always points at CUTSCENE_INSERT_ITEMS. +// Only one JSON-authored INSERT target is ever resident at a time, same +// restriction as CUTSCENE_LOADED_SCENE - a JSON-authored INSERT whose +// target itself contains a JSON-authored INSERT would overwrite this out +// from under itself, so that isn't supported (compile-time CUTSCENE_INSERT +// nesting is unaffected, since it never touches asset loading). +extern cutscene_t CUTSCENE_INSERT_SCENE; + +/** + * Same as cutsceneLoadedSetDoc, but for whatever yyjson_doc currently + * backs CUTSCENE_INSERT_ITEMS' string fields. Called by the cutscene asset + * loader once a fresh parse into CUTSCENE_INSERT_ITEMS/CUTSCENE_INSERT_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 cutsceneInsertSetDoc(yyjson_doc *doc); + +// Maximum nesting depth of CUTSCENE_ITEM_TYPE_INSERT items - how many +// "return to the item after this one" frames cutsceneSystemInsertCutscene +// may have pushed at once (see CUTSCENE_SYSTEM.insertStack). +#define CUTSCENE_INSERT_STACK_MAX 4 + +// One "return address" pushed by cutsceneSystemInsertCutscene - the scene +// and item index to resume at once the inserted cutscene's items run out. +typedef struct { + const cutscene_t *scene; + uint8_t currentItem; +} cutsceneinsertframe_t; + typedef struct { const cutscene_t *scene; uint8_t currentItem; @@ -85,6 +131,12 @@ typedef struct { // See cutsceneSystemSetOnComplete. cutscenecallback_t onComplete; + + // Return stack pushed by CUTSCENE_ITEM_TYPE_INSERT items - see + // cutsceneSystemInsertCutscene. Reset (insertStackSize = 0) whenever a + // cutscene freshly starts or the system is disposed. + cutsceneinsertframe_t insertStack[CUTSCENE_INSERT_STACK_MAX]; + uint8_t insertStackSize; } cutscenesystem_t; extern cutscenesystem_t CUTSCENE_SYSTEM; @@ -129,6 +181,30 @@ void cutsceneSystemStartCutsceneAndGoToMarker( const char_t *marker ); +/** + * Splices cutscene's items into the running cutscene in place, resuming + * the current cutscene right after the CUTSCENE_ITEM_TYPE_INSERT item that + * called this once cutscene's items run out - unlike + * cutsceneSystemStartCutscene (a one-way jump that replaces the running + * cutscene outright). Pushes a return frame onto + * CUTSCENE_SYSTEM.insertStack; cutsceneSystemNext pops it once cutscene + * ends. Deliberately does not touch pause flags, interact entities, + * "last created" state, the text cache or userData - those all keep + * whatever the outer cutscene set, since this is meant to feel like + * pasting cutscene's items in place rather than starting an independent + * cutscene. Because of this, cutscene's own .pause/.dataSize are ignored. + * Asserts if no cutscene is currently running, or if inserts are already + * nested CUTSCENE_INSERT_STACK_MAX deep. + * + * Note: cutsceneRestart() resets the insert stack (via + * cutsceneSystemPrepare), so a RESTART item used from inside an inserted + * cutscene restarts just that inserted cutscene and does not return to + * whatever it was inserted into. + * + * @param cutscene The cutscene whose items to splice in. + */ +void cutsceneSystemInsertCutscene(const cutscene_t *cutscene); + /** * Locks, loads and immediately starts a cutscene asset by file name, * e.g. cutsceneSystemLoad("main_menu.jsonc") loads and starts diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.c b/src/dusk/rpg/cutscene/item/cutsceneitem.c index f613c6aa..48df4f61 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.c +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.c @@ -10,6 +10,7 @@ #include "asset/asset.h" #include "asset/assetfile.h" #include "asset/loader/assetloader.h" +#include "util/memory.h" #include "util/string.h" #include "ui/overlay/uifatalerror.h" @@ -333,6 +334,13 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = { .update = cutsceneRegularBattleUpdate, .jsonName = "REGULAR_BATTLE", .load = cutsceneItemLoadNoop + }, + + [CUTSCENE_ITEM_TYPE_INSERT] = { + .init = cutsceneInsertStart, + .update = cutsceneInsertUpdate, + .jsonName = "INSERT", + .load = cutsceneInsertLoad } }; @@ -414,10 +422,18 @@ errorret_t cutsceneCutsceneLoad( cutsceneitem_t *item, uint8_t *pool, size_t *poolOffset +) { + return cutsceneCutsceneRefLoad(itemObj, &item->cutsceneRef, "CUTSCENE"); +} + +errorret_t cutsceneCutsceneRefLoad( + yyjson_val *itemObj, + cutscenecutsceneref_t *outRef, + const char_t *itemTypeLabel ) { const char_t *name = yyjson_get_str(yyjson_obj_get(itemObj, "name")); if(name == NULL) { - errorThrow("Cutscene item (CUTSCENE): missing 'name'"); + errorThrow("Cutscene item (%s): missing 'name'", itemTypeLabel); } // Only validate that the eventual "cutscenes/.jsonc" path would @@ -425,11 +441,69 @@ errorret_t cutsceneCutsceneLoad( // actually runs (see cutsceneCutsceneStart/cutsceneCutsceneResolve). int32_t pathLen = stringFormat(NULL, 0, "cutscenes/%s.jsonc", name); if(pathLen < 0 || (size_t)pathLen >= ASSET_FILE_NAME_MAX) { - errorThrow("Cutscene item (CUTSCENE): 'name' too long"); + errorThrow("Cutscene item (%s): 'name' too long", itemTypeLabel); } - item->cutsceneRef.cutscene = NULL; - item->cutsceneRef.name = name; + outRef->cutscene = NULL; + outRef->name = name; errorOk(); } + +void cutsceneInsertStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + const cutscene_t *cutscene = item->insert.cutscene; + if(cutscene == NULL && item->insert.name != NULL) { + // Not cached back into item - same reasoning as cutsceneCutsceneStart, + // but resolved via cutsceneInsertResolve (the dedicated INSERT buffer) + // rather than cutsceneCutsceneResolve, since item itself may live + // inside the currently-running outer cutscene's own CUTSCENE_LOADED_* + // buffer and must not be clobbered by this resolve. + cutscene = cutsceneInsertResolve(item->insert.name); + } + if(cutscene != NULL) cutsceneSystemInsertCutscene(cutscene); +} + +bool_t cutsceneInsertUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return false; +} + +const cutscene_t * cutsceneInsertResolve(const char_t *name) { + char_t path[ASSET_FILE_NAME_MAX]; + stringFormat(path, sizeof(path), "cutscenes/%s.jsonc", name); + + char_t key[ASSET_FILE_NAME_MAX]; + stringFormat(key, sizeof(key), "insert:%s", path); + + assetloaderinput_t input; + memoryZero(&input, sizeof(input)); + input.cutscene.dest = ASSET_CUTSCENE_LOAD_DEST_INSERT; + stringCopy(input.cutscene.path, path, ASSET_FILE_NAME_MAX); + + assetentry_t *entry = assetLock(key, ASSET_LOADER_TYPE_CUTSCENE, &input); + 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 &CUTSCENE_INSERT_SCENE; +} + +errorret_t cutsceneInsertLoad( + yyjson_val *itemObj, + cutsceneitem_t *item, + uint8_t *pool, + size_t *poolOffset +) { + return cutsceneCutsceneRefLoad(itemObj, &item->insert, "INSERT"); +} diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.h b/src/dusk/rpg/cutscene/item/cutsceneitem.h index 069df684..36f42e2c 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.h @@ -57,15 +57,25 @@ typedef struct cutscene_s cutscene_t; -// A CUTSCENE item's target. C-authored cutscenes (the CUTSCENE_CUTSCENE -// macro in cutscene.h) resolve cutscene at compile time and leave name -// NULL. JSON-authored cutscenes (cutsceneCutsceneLoad) do the opposite - -// only name is set at parse time, since resolving/locking the referenced -// asset there would eagerly load every cutscene a cutscene merely -// references, even ones never played. cutsceneCutsceneStart resolves +// A CUTSCENE (or INSERT) item's target. C-authored cutscenes (the +// CUTSCENE_CUTSCENE/CUTSCENE_INSERT macros in cutscene.h) resolve cutscene +// at compile time and leave name NULL - safe to hold onto indefinitely, +// since C-authored cutscenes are compiled directly into their own static +// const arrays, never a shared load buffer. JSON-authored cutscenes +// (cutsceneCutsceneLoad/cutsceneInsertLoad) do the opposite - only name is +// set at parse time, since resolving/locking the referenced asset there +// would eagerly load every cutscene a cutscene merely references, even +// ones never played. cutsceneCutsceneStart/cutsceneInsertStart resolve // name to cutscene the first time this item actually runs (via -// cutsceneCutsceneResolve) and caches the result back into this field so -// replaying the same item never re-resolves it. +// cutsceneCutsceneResolve/cutsceneInsertResolve respectively) but do NOT +// cache the result back into this field - with only one shared, fixed +// buffer per destination (see cutscenesystem.h), every JSON-authored +// reference resolves to the same fixed address every time anyway, and +// item itself may live inside that very buffer, so caching a pointer into +// it here would go stale (or, worse, get clobbered mid-resolve) the +// moment a later load overwrites it. Shared by both CUTSCENE +// (item->cutsceneRef) and INSERT (item->insert), since both need exactly +// this "resolved pointer, or a name to lazily resolve" shape. typedef struct { const cutscene_t *cutscene; const char_t *name; @@ -81,6 +91,7 @@ struct cutsceneitem_s { cutscenecallback_t callback; cutscenewait_t wait; cutscenecutsceneref_t cutsceneRef; + cutscenecutsceneref_t insert; cutsceneentityteleport_t entityTeleport; cutsceneentitywalkto_t entityWalkTo; cutscenefade_t fade; @@ -197,10 +208,11 @@ bool_t cutsceneItemLookupType( ); /** - * Starts a nested-cutscene item, handing control over to the - * referenced cutscene. If item->cutsceneRef.cutscene hasn't been - * resolved yet (a JSON-authored reference, only known by name so far), - * resolves and caches it first via cutsceneCutsceneResolve. + * Starts a nested-cutscene item, handing control over to the referenced + * cutscene. If item->cutsceneRef.cutscene hasn't been resolved yet (a + * JSON-authored reference, only known by name so far), resolves it first + * via cutsceneCutsceneResolve (not cached back - see cutscenecutsceneref_t's + * doc comment for why). * * @param item The cutscene item. * @param data Runtime data storage. @@ -262,3 +274,89 @@ errorret_t cutsceneCutsceneLoad( uint8_t *pool, size_t *poolOffset ); + +/** + * Shared JSON parsing body for both CUTSCENE and INSERT items - both need + * exactly a 'name' string field, validated to fit the eventual + * "cutscenes/.jsonc" asset path. Only stores the name (see + * cutsceneCutsceneLoad for why resolving is deferred). + * + * @param itemObj The item's JSON object. + * @param outRef Destination ref, already zeroed by the caller. + * @param itemTypeLabel Item type name used only in the error message on + * failure (e.g. "CUTSCENE" or "INSERT"). + * @return Error code indicating success or failure of the parse. + */ +errorret_t cutsceneCutsceneRefLoad( + yyjson_val *itemObj, + cutscenecutsceneref_t *outRef, + const char_t *itemTypeLabel +); + +/** + * Starts an INSERT item, splicing the referenced cutscene's items into the + * running cutscene in place - unlike CUTSCENE (a one-way jump that replaces + * the running cutscene outright), control returns to the item right after + * this one once the inserted items run out. If item->insert.cutscene hasn't + * been resolved yet (a JSON-authored reference, only known by name so + * far), resolves it first via cutsceneInsertResolve - not + * cutsceneCutsceneResolve, so the referenced cutscene lands in the + * dedicated CUTSCENE_INSERT_ITEMS/POOL buffer rather than overwriting + * CUTSCENE_LOADED_ITEMS/POOL out from under whatever outer cutscene is + * currently running (see cutscenesystem.h's CUTSCENE_INSERT_ITEMS_MAX doc + * comment). Not cached back into item, same reasoning as + * cutsceneCutsceneStart. + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneInsertStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Resolves a JSON-authored INSERT item's referenced cutscene by name, + * locking (under a synthetic "insert:cutscenes/.jsonc" cache key, + * distinct from the plain "cutscenes/.jsonc" key cutsceneCutsceneResolve + * uses for the same file, so a load driven by an INSERT item is never + * silently shared with - or overwritten by - a load driven by a CUTSCENE + * item, or vice versa - see assetGetEntry's name-only dedup) and blocking + * on assets/cutscenes/.jsonc, parsed into CUTSCENE_INSERT_ITEMS/POOL + * rather than CUTSCENE_LOADED_ITEMS/POOL. On failure, opens the fatal + * error overlay and returns NULL. + * + * @param name Bare cutscene name (without "cutscenes/" prefix or + * ".jsonc" suffix). + * @return The resolved cutscene, or NULL on failure. + */ +const cutscene_t * cutsceneInsertResolve(const char_t *name); + +/** + * Updates an INSERT item. By the time this would run, control has already + * moved on to the inserted items, so this always reports incomplete. + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns false always. + */ +bool_t cutsceneInsertUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Parses an INSERT cutscene item's JSON fields into item. + * + * @param itemObj The item's JSON object. + * @param item Destination item, already zeroed by the caller. + * @param pool Unused (INSERT needs no pool storage). + * @param poolOffset Unused (INSERT needs no pool storage). + * @return Error code indicating success or failure of the parse. + */ +errorret_t cutsceneInsertLoad( + yyjson_val *itemObj, + cutsceneitem_t *item, + uint8_t *pool, + size_t *poolOffset +); diff --git a/src/dusk/rpg/cutscene/item/cutsceneitembase.h b/src/dusk/rpg/cutscene/item/cutsceneitembase.h index 15741311..97764ade 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitembase.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitembase.h @@ -63,6 +63,7 @@ typedef enum { CUTSCENE_ITEM_TYPE_IDLE, CUTSCENE_ITEM_TYPE_UI_SHOW, CUTSCENE_ITEM_TYPE_REGULAR_BATTLE, + CUTSCENE_ITEM_TYPE_INSERT, CUTSCENE_ITEM_TYPE_COUNT } cutsceneitemtype_t; diff --git a/test/rpg/cutscene/test_cutscenesystem.c b/test/rpg/cutscene/test_cutscenesystem.c index cf4fd0a2..2e6def8b 100644 --- a/test/rpg/cutscene/test_cutscenesystem.c +++ b/test/rpg/cutscene/test_cutscenesystem.c @@ -177,6 +177,201 @@ static void test_cutsceneSystemDisposeResetsState(void **state) { assert_null(CUTSCENE_SYSTEM.entityInteracted); } +// --- CUTSCENE_INSERT --------------------------------------------------- + +static uint8_t insertOrderLog[8]; +static uint8_t insertOrderLogCount; + +static void recordOrder0(void *userData) { insertOrderLog[insertOrderLogCount++] = 0; } +static void recordOrder1(void *userData) { insertOrderLog[insertOrderLogCount++] = 1; } +static void recordOrder2(void *userData) { insertOrderLog[insertOrderLogCount++] = 2; } +static void recordOrderA(void *userData) { insertOrderLog[insertOrderLogCount++] = 3; } +static void recordOrderB(void *userData) { insertOrderLog[insertOrderLogCount++] = 4; } + +CUTSCENE(TEST_INSERT_SNIPPET, 0, NONE, + CUTSCENE_CALLBACK(recordOrderA), + CUTSCENE_WAIT(2.0f), + CUTSCENE_CALLBACK(recordOrderB) +); + +CUTSCENE(TEST_INSERT_OUTER, 0, NONE, + CUTSCENE_CALLBACK(recordOrder0), + CUTSCENE_WAIT(1.0f), + CUTSCENE_CALLBACK(recordOrder1), + CUTSCENE_INSERT(TEST_INSERT_SNIPPET), + CUTSCENE_CALLBACK(recordOrder2) +); + +static void test_cutsceneInsertSplicesItemsInPlaceThenResumes(void **state) { + + insertOrderLogCount = 0; + cutsceneSystemInit(); + cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_OUTER); + + TIME.delta = 1.5f;// elapses the outer WAIT(1.0) + cutsceneSystemUpdate();// order0 done -> starts the outer WAIT + cutsceneSystemUpdate();// the WAIT elapses -> starts order1 + cutsceneSystemUpdate();// order1 done -> starts the INSERT, cascading into orderA + + assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_SNIPPET); + + TIME.delta = 2.5f;// elapses the snippet's WAIT(2.0) + cutsceneSystemUpdate();// orderA done -> starts snippet's WAIT + cutsceneSystemUpdate();// snippet's WAIT elapses -> starts orderB + cutsceneSystemUpdate();// orderB done -> pops back, starts order2 + + assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_OUTER); + + cutsceneSystemUpdate();// order2 done -> cutscene ends naturally + assert_null(CUTSCENE_SYSTEM.scene); + + const uint8_t expected[] = { 0, 1, 3, 4, 2 }; + assert_int_equal(insertOrderLogCount, sizeof(expected)); + assert_memory_equal(insertOrderLog, expected, sizeof(expected)); +} + +CUTSCENE(TEST_INSERT_AS_LAST_ITEM_SNIPPET, 0, NONE, + CUTSCENE_CALLBACK(recordOrderA) +); + +CUTSCENE(TEST_INSERT_AS_LAST_ITEM_OUTER, 0, NONE, + CUTSCENE_CALLBACK(recordOrder0), + CUTSCENE_INSERT(TEST_INSERT_AS_LAST_ITEM_SNIPPET) +); + +static void test_cutsceneInsertAsLastItemEndsCutsceneNaturally( + void **state +) { + + insertOrderLogCount = 0; + cutsceneSystemInit(); + cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER); + + cutsceneSystemUpdate();// order0 done -> starts INSERT, cascades to orderA + assert_ptr_equal( + CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET + ); + + // orderA is the snippet's only item, and the snippet is the outer's last + // item -- finishing it should pop straight through to the natural end + // of the whole cutscene, not get stuck. + cutsceneSystemUpdate(); + assert_null(CUTSCENE_SYSTEM.scene); + + const uint8_t expected[] = { 0, 3 }; + assert_int_equal(insertOrderLogCount, sizeof(expected)); + assert_memory_equal(insertOrderLog, expected, sizeof(expected)); +} + +CUTSCENE(TEST_INSERT_NESTED_INNER, 0, NONE, + CUTSCENE_CALLBACK(recordOrderA) +); + +// INSERT is MID's first item, so entering MID (from OUTER) cascades +// straight through into INNER within the same Start chain, one level +// deeper than a single insert. +CUTSCENE(TEST_INSERT_NESTED_MID, 0, NONE, + CUTSCENE_INSERT(TEST_INSERT_NESTED_INNER), + CUTSCENE_CALLBACK(recordOrderB) +); + +CUTSCENE(TEST_INSERT_NESTED_OUTER, 0, NONE, + CUTSCENE_CALLBACK(recordOrder0), + CUTSCENE_INSERT(TEST_INSERT_NESTED_MID), + CUTSCENE_CALLBACK(recordOrder2) +); + +static void test_cutsceneInsertNestsThroughMultipleLevels(void **state) { + + insertOrderLogCount = 0; + cutsceneSystemInit(); + cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_NESTED_OUTER); + + // order0 done -> starts INSERT(MID), cascading through INSERT(INNER) -> + // orderA, all within this one Update call's Start chain. + cutsceneSystemUpdate(); + assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_NESTED_INNER); + + cutsceneSystemUpdate();// orderA done -> pops to MID, starts orderB + assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_NESTED_MID); + + cutsceneSystemUpdate();// orderB done -> pops to OUTER, starts order2 + assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_NESTED_OUTER); + + cutsceneSystemUpdate();// order2 done -> ends naturally + assert_null(CUTSCENE_SYSTEM.scene); + + const uint8_t expected[] = { 0, 3, 4, 2 }; + assert_int_equal(insertOrderLogCount, sizeof(expected)); + assert_memory_equal(insertOrderLog, expected, sizeof(expected)); +} + +CUTSCENE(TEST_INSERT_OVERFLOW_LEVEL5, 0, NONE, + CUTSCENE_CALLBACK(recordOrderA) +); +CUTSCENE(TEST_INSERT_OVERFLOW_LEVEL4, 0, NONE, + CUTSCENE_INSERT(TEST_INSERT_OVERFLOW_LEVEL5) +); +CUTSCENE(TEST_INSERT_OVERFLOW_LEVEL3, 0, NONE, + CUTSCENE_INSERT(TEST_INSERT_OVERFLOW_LEVEL4) +); +CUTSCENE(TEST_INSERT_OVERFLOW_LEVEL2, 0, NONE, + CUTSCENE_INSERT(TEST_INSERT_OVERFLOW_LEVEL3) +); +CUTSCENE(TEST_INSERT_OVERFLOW_LEVEL1, 0, NONE, + CUTSCENE_INSERT(TEST_INSERT_OVERFLOW_LEVEL2) +); +CUTSCENE(TEST_INSERT_OVERFLOW_LEVEL0, 0, NONE, + CUTSCENE_INSERT(TEST_INSERT_OVERFLOW_LEVEL1) +); + +static void test_cutsceneInsertAssertsWhenNestingExceedsStackMax( + void **state +) { + + cutsceneSystemInit(); + + // 5 levels of INSERT deep, one past CUTSCENE_INSERT_STACK_MAX (4) -- + // all cascade synchronously within this one call, since each level's + // only item is the INSERT starting immediately. + expect_assert_failure( + cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_OVERFLOW_LEVEL0) + ); +} + +static void noopInsertCallback(void *userData) {} + +CUTSCENE(TEST_INSERT_PERSIST_SNIPPET, 0, ALL, + CUTSCENE_CALLBACK(noopInsertCallback) +); + +CUTSCENE(TEST_INSERT_PERSIST_OUTER, 0, NPC, + CUTSCENE_INSERT(TEST_INSERT_PERSIST_SNIPPET) +); + +static void test_cutsceneInsertDoesNotResetPauseOrInteractEntities( + void **state +) { + + cutsceneSystemInit(); + entityInit(&ENTITIES[3], ENTITY_TYPE_PLAYER); + entityInit(&ENTITIES[4], ENTITY_TYPE_NPC); + + cutsceneSystemStartCutsceneWith( + &CUTSCENE_TEST_INSERT_PERSIST_OUTER, &ENTITIES[3], &ENTITIES[4] + ); + + // The INSERT item's Start already cascaded into the snippet by now. + assert_ptr_equal( + CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_PERSIST_SNIPPET + ); + assert_ptr_equal(CUTSCENE_SYSTEM.entityInteract, &ENTITIES[3]); + assert_ptr_equal(CUTSCENE_SYSTEM.entityInteracted, &ENTITIES[4]); + // The snippet declares ALL, but INSERT doesn't apply it -- the outer + // cutscene's NPC pause, already in effect, stands. + assert_int_equal(CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_NPC); +} + int main(int argc, char** argv) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_cutsceneSystemStartSetsUpInitialItem), @@ -188,6 +383,11 @@ int main(int argc, char** argv) { cmocka_unit_test(test_cutsceneSystemGetAreaId), cmocka_unit_test(test_cutsceneSystemGetTextMiniId), cmocka_unit_test(test_cutsceneSystemDisposeResetsState), + cmocka_unit_test(test_cutsceneInsertSplicesItemsInPlaceThenResumes), + cmocka_unit_test(test_cutsceneInsertAsLastItemEndsCutsceneNaturally), + cmocka_unit_test(test_cutsceneInsertNestsThroughMultipleLevels), + cmocka_unit_test(test_cutsceneInsertAssertsWhenNestingExceedsStackMax), + cmocka_unit_test(test_cutsceneInsertDoesNotResetPauseOrInteractEntities), }; return cmocka_run_group_tests(tests, NULL, NULL);