Load cutscenes directly from JSONC at runtime, fix asset bundling staleness
Cutscenes now parse their authored .jsonc straight into the runtime cutsceneitem_t/pool representation via yyjson, instead of going through a separate Python-compiled DCTS binary format - removes the whole build/compile step and the byte-format contract between the Python encoder and the C decoder, at the cost of a (still tiny, one-time) parse per cutscene load. Also fixes dusk.dsk going stale after a build: the old custom_command depended on a CMake-configure-time file glob, which only re-detects added/removed assets on the next configure and could miss edits entirely. tools.asset.pack now always runs and decides for itself (via a small manifest) whether anything actually needs repacking, so asset changes are never missed regardless of add/edit/remove. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,13 @@
|
||||
#include "asset/assetfile.h"
|
||||
#include "rpg/cutscene/cutscene.h"
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/overworld/worldpos.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
#define ASSET_CUTSCENE_FILE_VERSION 1
|
||||
// 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;
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
@@ -28,94 +33,195 @@ typedef enum {
|
||||
typedef struct {
|
||||
assetfile_t file;
|
||||
assetcutsceneloadingstate_t state;
|
||||
uint8_t *data;
|
||||
size_t dataSize;// Saved before assetFileDispose zeroes file.size.
|
||||
uint8_t *buffer;
|
||||
size_t size;
|
||||
} assetcutsceneloaderloading_t;
|
||||
|
||||
// Runtime-loaded cutscene: items/pool are heap-allocated to the file's
|
||||
// actual declared sizes (not fixed-capacity), so an entry that never holds
|
||||
// a cutscene costs nothing extra in the shared assetloaderoutput_t union -
|
||||
// see assetchunkoutput_t.tiles for the same pattern.
|
||||
// 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
|
||||
cutsceneitem_t *items;
|
||||
char_t *pool;
|
||||
uint8_t *pool;
|
||||
yyjson_doc *doc;
|
||||
} assetcutsceneoutput_t;
|
||||
|
||||
/**
|
||||
* Reads a uint8_t from the current offset, advancing *offset past it.
|
||||
*
|
||||
* @param data The buffer to read from.
|
||||
* @param offset In/out cursor into data, advanced past the value read.
|
||||
* @return The decoded uint8_t.
|
||||
*/
|
||||
uint8_t assetCutsceneReadU8(const uint8_t *data, size_t *offset);
|
||||
typedef struct {
|
||||
const char_t *name;
|
||||
int32_t value;
|
||||
} assetcutsceneenumentry_t;
|
||||
|
||||
typedef struct {
|
||||
const char_t *name;
|
||||
} assetcutsceneitemtypeentry_t;
|
||||
|
||||
/**
|
||||
* Reads a little-endian uint16_t from a potentially-unaligned offset,
|
||||
* advancing *offset past it.
|
||||
* Looks up name in a {name, value} table via stringEquals - exact case,
|
||||
* used only for the top-level item "type" name (the one enum-ish field
|
||||
* the original tool's ITEM_TYPE dict looked up without .upper()).
|
||||
*
|
||||
* @param data The buffer to read from.
|
||||
* @param offset In/out cursor into data, advanced past the value read.
|
||||
* @return The decoded uint16_t.
|
||||
* @param table Table to search.
|
||||
* @param tableCount Number of entries in table.
|
||||
* @param name Name to search for.
|
||||
* @param outValue Set to the matching entry's value on success.
|
||||
* @return true if a matching entry was found, false otherwise.
|
||||
*/
|
||||
uint16_t assetCutsceneReadU16(const uint8_t *data, size_t *offset);
|
||||
bool_t assetCutsceneLookupEnum(
|
||||
const assetcutsceneenumentry_t *table,
|
||||
const size_t tableCount,
|
||||
const char_t *name,
|
||||
int32_t *outValue
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian uint32_t from a potentially-unaligned offset,
|
||||
* advancing *offset past it.
|
||||
* Looks up name (exact case) against the item type table, which is indexed
|
||||
* directly by cutsceneitemtype_t rather than searched by value.
|
||||
*
|
||||
* @param data The buffer to read from.
|
||||
* @param offset In/out cursor into data, advanced past the value read.
|
||||
* @return The decoded uint32_t.
|
||||
* @param name Name to search for.
|
||||
* @param outType Set to the matching item type on success.
|
||||
* @return true if a matching entry was found, false otherwise.
|
||||
*/
|
||||
uint32_t assetCutsceneReadU32(const uint8_t *data, size_t *offset);
|
||||
bool_t assetCutsceneLookupItemType(
|
||||
const char_t *name,
|
||||
cutsceneitemtype_t *outType
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian float_t from a potentially-unaligned offset,
|
||||
* advancing *offset past it.
|
||||
* Looks up name in a {name, value} table via stringCompareInsensitive -
|
||||
* every other enum-ish field (pause flags, entity/scene/battle/audio
|
||||
* enums, ...) is authored case-insensitively, matching the original
|
||||
* tool's ENUM_TABLE[name.upper()] lookups.
|
||||
*
|
||||
* @param data The buffer to read from.
|
||||
* @param offset In/out cursor into data, advanced past the value read.
|
||||
* @return The decoded float_t.
|
||||
* @param table Table to search.
|
||||
* @param tableCount Number of entries in table.
|
||||
* @param name Name to search for.
|
||||
* @param outValue Set to the matching entry's value on success.
|
||||
* @return true if a matching entry was found, false otherwise.
|
||||
*/
|
||||
float_t assetCutsceneReadFloat(const uint8_t *data, size_t *offset);
|
||||
bool_t assetCutsceneLookupEnumInsensitive(
|
||||
const assetcutsceneenumentry_t *table,
|
||||
const size_t tableCount,
|
||||
const char_t *name,
|
||||
int32_t *outValue
|
||||
);
|
||||
|
||||
/**
|
||||
* Copies a length-prefixed string directly into an item's own embedded
|
||||
* char_t[destCapacity] field (CUTSCENE_TEXT_MAX_CHARS and friends) - these
|
||||
* are never pool references, see the item-field inventory in the runtime
|
||||
* cutscene file design.
|
||||
* Resolves a JSON value that's either an entity index sentinel string
|
||||
* ("INTERACT"/"INTERACTED") or a plain integer entity index.
|
||||
*
|
||||
* @param data The buffer to read from.
|
||||
* @param offset In/out cursor into data, advanced past the string read.
|
||||
* @param dest Destination buffer to copy the string into.
|
||||
* @param val JSON value to resolve (a string or a number).
|
||||
* @param outIndex Set to the resolved index on success.
|
||||
* @return true if val resolved to a valid index, false otherwise.
|
||||
*/
|
||||
bool_t assetCutsceneResolveEntityIndex(yyjson_val *val, uint8_t *outIndex);
|
||||
|
||||
/**
|
||||
* Resolves a JSON value that's either the "LAST_CREATED" map area sentinel
|
||||
* string or a plain integer area id.
|
||||
*
|
||||
* @param val JSON value to resolve (a string or a number).
|
||||
* @param outAreaId Set to the resolved area id on success.
|
||||
* @return true if val resolved to a valid area id, false otherwise.
|
||||
*/
|
||||
bool_t assetCutsceneResolveAreaId(yyjson_val *val, uint8_t *outAreaId);
|
||||
|
||||
/**
|
||||
* Reads obj[key] as a string (falling back to defaultValue if the key is
|
||||
* absent and defaultValue is non-NULL) into dest, a fixed-capacity
|
||||
* char_t[destCapacity] item field. Fails rather than truncating/asserting
|
||||
* if the string doesn't fit - this is untrusted, authored file content,
|
||||
* not an in-memory invariant.
|
||||
*
|
||||
* @param obj The item's JSON object.
|
||||
* @param key Field name to read.
|
||||
* @param defaultValue Value to use if key is absent, or NULL to require it.
|
||||
* @param dest Destination buffer.
|
||||
* @param destCapacity Capacity of dest, including the null terminator.
|
||||
* @return true on success, false if the field is missing (with no
|
||||
* default) or exceeds destCapacity.
|
||||
*/
|
||||
void assetCutsceneReadEmbeddedString(
|
||||
const uint8_t *data,
|
||||
size_t *offset,
|
||||
bool_t assetCutsceneCopyStringField(
|
||||
yyjson_val *obj,
|
||||
const char_t *key,
|
||||
const char_t *defaultValue,
|
||||
char_t *dest,
|
||||
const size_t destCapacity
|
||||
);
|
||||
|
||||
/**
|
||||
* Resolves a u16 pool offset (read from the item stream) to a real pointer
|
||||
* into the entry's own persistent pool allocation.
|
||||
* Reads a 3-element JSON array of integers into a worldpos_t.
|
||||
*
|
||||
* @param data The buffer to read the pool offset from.
|
||||
* @param offset In/out cursor into data, advanced past the pool offset.
|
||||
* @param pool The base pointer of the entry's persistent pool allocation.
|
||||
* @return Pointer to the string within pool.
|
||||
* @param arr JSON array value, expected to hold exactly 3 numbers.
|
||||
* @param out Set to the decoded position on success.
|
||||
* @return true if arr was a valid 3-element numeric array, false otherwise.
|
||||
*/
|
||||
const char_t * assetCutsceneReadPoolString(
|
||||
const uint8_t *data,
|
||||
size_t *offset,
|
||||
const char_t *pool
|
||||
bool_t assetCutsceneReadWorldPos(yyjson_val *arr, worldpos_t *out);
|
||||
|
||||
/**
|
||||
* Rounds size up to the next multiple of 4 - every pool entry starts
|
||||
* 4-byte aligned so worldpos_t/multi-byte reads out of it never trap on
|
||||
* alignment-sensitive hardware (e.g. PSP's MIPS core).
|
||||
*
|
||||
* @param size Size to round up.
|
||||
* @return size rounded up to the next multiple of 4.
|
||||
*/
|
||||
size_t assetCutscenePoolAlign(const size_t size);
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*
|
||||
* @param itemsArr The cutscene's "items" JSON array.
|
||||
* @return Total pool bytes required (0 if none of the items need one).
|
||||
*/
|
||||
size_t assetCutsceneComputePoolSize(yyjson_val *itemsArr);
|
||||
|
||||
/**
|
||||
* Parses one cutscene item object into item, writing any pool-backed
|
||||
* array data (entityWalkTo positions, mapAreaWait areaIds) into pool at
|
||||
* *poolOffset and advancing it. Every field/enum name is validated as
|
||||
* real (untrusted, authored) file content - failures are real errors, not
|
||||
* asserts.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @param itemObj The item's JSON object.
|
||||
* @param item Destination item, already zeroed by the caller.
|
||||
* @param pool Base pointer of the entry's pool allocation (may be NULL if
|
||||
* assetCutsceneComputePoolSize returned 0).
|
||||
* @param poolOffset In/out cursor into pool.
|
||||
* @param index Index of this item within the cutscene, for error messages.
|
||||
* @return Error code indicating success or failure of the parse.
|
||||
*/
|
||||
errorret_t assetCutsceneParseItem(
|
||||
assetloading_t *loading,
|
||||
yyjson_val *itemObj,
|
||||
cutsceneitem_t *item,
|
||||
uint8_t *pool,
|
||||
size_t *poolOffset,
|
||||
const uint8_t index
|
||||
);
|
||||
|
||||
/**
|
||||
* Asynchronous loader for cutscene assets. Reads the raw DCTS file bytes
|
||||
* 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
|
||||
* into the loading buffer so the sync phase can parse without blocking the
|
||||
* main thread on I/O.
|
||||
*
|
||||
@@ -125,9 +231,8 @@ const char_t * assetCutsceneReadPoolString(
|
||||
errorret_t assetCutsceneLoaderAsync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Synchronous loader for cutscene assets. Validates the DCTS binary
|
||||
* previously read by the async phase and decodes it into a heap-allocated
|
||||
* cutsceneitem_t array + string/data pool.
|
||||
* Synchronous loader for cutscene assets. Parses the JSONC previously read
|
||||
* by the async phase into a heap-allocated cutsceneitem_t array + pool.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure of the load operation.
|
||||
|
||||
@@ -71,6 +71,11 @@ typedef struct cutscene_s {
|
||||
#define CUTSCENE_IDLE() \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_IDLE }
|
||||
|
||||
// Opens a full-screen UI panel (e.g. the main menu) declaratively, then
|
||||
// immediately continues on to whatever follows this item.
|
||||
#define CUTSCENE_UI_SHOW(SCREEN) \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_UI_SHOW, .uiShow = { .screen = SCREEN } }
|
||||
|
||||
// Requests a switch to a different SCENE_TYPE via sceneSet, then
|
||||
// immediately continues on to whatever follows this item - the switch
|
||||
// itself doesn't happen until the next sceneUpdate() tick, so it does
|
||||
|
||||
@@ -225,6 +225,11 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_IDLE] = {
|
||||
.update = cutsceneIdleUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_UI_SHOW] = {
|
||||
.init = cutsceneUIShowStart,
|
||||
.update = cutsceneUIShowUpdate
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "ui/cutscenemodal.h"
|
||||
#include "ui/cutscenemodaloptionsmarkers.h"
|
||||
#include "ui/cutscenekeyboard.h"
|
||||
#include "ui/cutsceneuishow.h"
|
||||
#include "item/cutsceneitemgive.h"
|
||||
#include "maparea/cutscenemapareaadd.h"
|
||||
#include "maparea/cutscenemaparearemove.h"
|
||||
@@ -98,6 +99,7 @@ typedef enum {
|
||||
CUTSCENE_ITEM_TYPE_AUDIO_SET_LOOP,
|
||||
CUTSCENE_ITEM_TYPE_AUDIO_SET,
|
||||
CUTSCENE_ITEM_TYPE_IDLE,
|
||||
CUTSCENE_ITEM_TYPE_UI_SHOW,
|
||||
|
||||
CUTSCENE_ITEM_TYPE_COUNT
|
||||
} cutsceneitemtype_t;
|
||||
@@ -147,6 +149,7 @@ struct cutsceneitem_s {
|
||||
cutsceneaudiosetpan_t audioSetPan;
|
||||
cutsceneaudiosetloop_t audioSetLoop;
|
||||
cutsceneaudioset_t audioSet;
|
||||
cutsceneuishow_t uiShow;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -14,4 +14,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
cutscenemodal.c
|
||||
cutscenemodaloptionsmarkers.c
|
||||
cutscenekeyboard.c
|
||||
cutsceneuishow.c
|
||||
)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "ui/screen/mainmenu/uimainmenu.h"
|
||||
|
||||
void cutsceneUIShowStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
switch(item->uiShow.screen) {
|
||||
case UI_SCREEN_TYPE_MAIN_MENU:
|
||||
uiMainMenuOpen();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool_t cutsceneUIShowUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
|
||||
// Full-screen UI panels a cutscene can declaratively open via UI_SHOW - not
|
||||
// modals/dialogs (those already have their own dedicated item types, e.g.
|
||||
// MODAL/MODAL_OPTIONS_MARKERS/KEYBOARD), just whole-screen views like the
|
||||
// main menu. Add a case here (and to cutsceneUIShowStart) as more screens
|
||||
// need to be openable this way.
|
||||
typedef enum {
|
||||
UI_SCREEN_TYPE_MAIN_MENU,
|
||||
} uiscreentype_t;
|
||||
|
||||
typedef struct {
|
||||
uiscreentype_t screen;
|
||||
} cutsceneuishow_t;
|
||||
|
||||
/**
|
||||
* Starts a UI_SHOW item, opening the requested full-screen UI panel.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneUIShowStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a UI_SHOW item (always completes immediately).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true always.
|
||||
*/
|
||||
bool_t cutsceneUIShowUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
@@ -26,14 +26,12 @@ errorret_t sceneInitialInit(scenedata_t *sceneData) {
|
||||
// Set background color to black for the initial scene
|
||||
SCREEN.background = COLOR_BLACK;
|
||||
|
||||
// Runtime-loaded from assets/cutscenes/initial.cts (authored at
|
||||
// assetsraw/cutscenes/initial.jsonc via `python3 -m tools.asset.cutscene`)
|
||||
// - checks for a save device, retrying on failure, then hands off to the
|
||||
// main menu scene via a plain CUTSCENE_SCENE item (no native callback
|
||||
// needed here, unlike the main menu's own start-game cutscene).
|
||||
// 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.cts", ASSET_LOADER_TYPE_CUTSCENE, NULL
|
||||
"cutscenes/initial.jsonc", ASSET_LOADER_TYPE_CUTSCENE, NULL
|
||||
);
|
||||
}
|
||||
errorret_t result = assetRequireLoaded(INITIAL_CUTSCENE_ENTRY);
|
||||
|
||||
@@ -36,15 +36,14 @@ void sceneMainMenuOpenSelectSave(void *userData) {
|
||||
}
|
||||
|
||||
// Lazily locks/loads the main menu cutscene (runtime-loaded from
|
||||
// assets/cutscenes/main_menu.cts, authored at
|
||||
// assetsraw/cutscenes/main_menu.jsonc via `python3 -m tools.asset.cutscene`
|
||||
// rather than compiled in, since it's player-facing flow rather than core
|
||||
// engine wiring) and asserts it's ready to run. Shared by both the initial
|
||||
// assets/cutscenes/main_menu.jsonc, parsed directly at load time rather
|
||||
// than compiled in, since it's player-facing flow rather than core engine
|
||||
// wiring) and asserts it's ready to run. Shared by both the initial
|
||||
// scene-entry start and the New-Game fallback restart below.
|
||||
cutscene_t *sceneMainMenuLoadCutscene(void) {
|
||||
if(MAIN_MENU_CUTSCENE_ENTRY == NULL) {
|
||||
MAIN_MENU_CUTSCENE_ENTRY = assetLock(
|
||||
"cutscenes/main_menu.cts", ASSET_LOADER_TYPE_CUTSCENE, NULL
|
||||
"cutscenes/main_menu.jsonc", ASSET_LOADER_TYPE_CUTSCENE, NULL
|
||||
);
|
||||
}
|
||||
errorret_t result = assetRequireLoaded(MAIN_MENU_CUTSCENE_ENTRY);
|
||||
@@ -79,8 +78,8 @@ void sceneMainMenuStartGame(void) {
|
||||
}
|
||||
|
||||
errorret_t sceneMainMenuInit(scenedata_t *sceneData) {
|
||||
uiMainMenuOpen();
|
||||
|
||||
// Opening the main menu panel itself is now the cutscene's job (its
|
||||
// first item is a UI_SHOW), not this function's.
|
||||
cutsceneSystemStartCutscene(sceneMainMenuLoadCutscene());
|
||||
cutsceneSystemSetOnComplete(sceneMainMenuOpenSelectSave);
|
||||
|
||||
|
||||
@@ -17,8 +17,9 @@ typedef struct {
|
||||
} scenemainmenu_t;
|
||||
|
||||
/**
|
||||
* Initializes the main menu scene: opens the main menu panel and starts
|
||||
* the main menu cutscene, which plays the menu's BGM and idles until
|
||||
* Initializes the main menu scene by starting the main menu cutscene,
|
||||
* whose own first item opens the main menu panel (see UI_SHOW,
|
||||
* rpg/cutscene/item/ui/cutsceneuishow.h) before it idles until
|
||||
* sceneMainMenuStartGame jumps it to its NEW_GAME marker.
|
||||
*
|
||||
* @param sceneData The scene data used for this scene.
|
||||
|
||||
Reference in New Issue
Block a user