From 9edb2aa0c1fc9493ad09c6ed78a1ed2cf7c081cb Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sat, 6 Jun 2026 18:46:08 -0500 Subject: [PATCH] Example scene working --- assets/init.js | 4 +- assets/testscene.js | 69 +++++++++++----------------- cmake/modules/duskjs2c.cmake | 43 +++++++++++++++++ src/dusk/engine/engine.c | 9 ++-- src/dusk/scene/CMakeLists.txt | 5 ++ src/dusk/scene/scene.c | 18 +++++++- src/dusk/scene/scene.h | 10 +++- src/dusk/scene/scene.js | 36 +++++++++++++++ src/dusk/script/script.c | 52 ++++++++++++++------- test/asset/test_asset.c | 41 +++++++++++++++++ test/asset/test_assetjsonloader.c | 40 +++++++++++++--- test/asset/test_assettilesetloader.c | 43 +++++++++++++---- tools/js2c/__main__.py | 53 +++++++++++++++++++++ 13 files changed, 342 insertions(+), 81 deletions(-) create mode 100644 cmake/modules/duskjs2c.cmake create mode 100644 src/dusk/scene/scene.js create mode 100644 tools/js2c/__main__.py diff --git a/assets/init.js b/assets/init.js index 3709b379..ec80478f 100644 --- a/assets/init.js +++ b/assets/init.js @@ -6,4 +6,6 @@ const platformNames = { [System.PLATFORM_WII]: 'Wii', }; -Console.print('Platform: ' + (platformNames[System.platform] || 'Unknown')); \ No newline at end of file +Console.print('Platform: ' + (platformNames[System.platform] || 'Unknown')); + +requireAsync('testscene.js').then(Scene.set).catch(Engine.exit); \ No newline at end of file diff --git a/assets/testscene.js b/assets/testscene.js index b9fe17ba..6626a54c 100644 --- a/assets/testscene.js +++ b/assets/testscene.js @@ -1,14 +1,4 @@ -Console.print('testscene.js is loaded'); - -module.exports = { - test: function() { - return 'Hello string'; - } -} - -// var scene = { -// 'test': 'teststring' -// }; +var scene = {}; // var assets = AssetBatch([ // { path: 'test.png', type: Asset.TYPE_TEXTURE, format: Texture.FORMAT_RGBA } @@ -21,38 +11,35 @@ module.exports = { // var testRenderable; // var texEntry; -// scene.init = function() { -// assets.lock(); -// assets.onLoaded[0] = scene.loaded; -// }; +scene.init = function() { + Console.print('Scene Init'); + // texEntry = assets.entry(0); -// scene.loaded = function() { -// texEntry = assets.entry(0); + // Camera at (3, 3, 3) looking at origin + cam = Entity.create(); + camPos = cam.add(Component.POSITION); + cam.add(Component.CAMERA); + camPos.localPosition = new Vec3(3, 3, 3); + camPos.lookAt(new Vec3(0, 0, 0)); -// // Camera at (3, 3, 3) looking at origin -// cam = Entity.create(); -// camPos = cam.add(Component.POSITION); -// cam.add(Component.CAMERA); -// camPos.localPosition = new Vec3(3, 3, 3); -// camPos.lookAt(new Vec3(0, 0, 0)); + // Test entity with textured quad at origin + testEntity = Entity.create(); + testPos = testEntity.add(Component.POSITION); + testRenderable = testEntity.add(Component.RENDERABLE); -// // Test entity with textured quad at origin -// testEntity = Entity.create(); -// testPos = testEntity.add(Component.POSITION); -// testRenderable = testEntity.add(Component.RENDERABLE); + // testRenderable.texture = texEntry.texture; + testRenderable.type = Renderable.SPRITEBATCH; + testRenderable.sprites = [ + [0, 0, 1, 1, 0, 1, 1, 0] + ]; + testPos.localPosition = new Vec3(0, 0, 0); +} -// testRenderable.texture = texEntry.texture; -// testRenderable.sprites = [ -// [0, 0, 1, 1, 0, 1, 1, 0] -// ]; -// testPos.localPosition = new Vec3(0, 0, 0); -// } +scene.dispose = function() { + Console.print('Scene Dispose'); + Entity.dispose(cam); + Entity.dispose(testEntity); + // assets.unlock(); +}; -// scene.dispose = function() { -// Console.print('Scene Dispose'); -// Entity.dispose(cam); -// Entity.dispose(testEntity); -// assets.unlock(); -// }; - -// module.exports = scene; +module.exports = scene; diff --git a/cmake/modules/duskjs2c.cmake b/cmake/modules/duskjs2c.cmake new file mode 100644 index 00000000..dada53df --- /dev/null +++ b/cmake/modules/duskjs2c.cmake @@ -0,0 +1,43 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# dusk_embed_js(TARGET JS_FILE [NAME identifier]) +# +# Converts a JS file into a C string header in DUSK_GENERATED_HEADERS_DIR. +# The generated header defines: +# static const char [] = "..."; +# static const size_t _SIZE = sizeof() - 1; +# +# NAME defaults to the uppercase stem + "_JS" (e.g. scene.js -> SCENE_JS). +function(dusk_embed_js TARGET JS_FILE) + cmake_parse_arguments(ARG "" "NAME" "" ${ARGN}) + + get_filename_component(JS_ABS "${JS_FILE}" ABSOLUTE) + get_filename_component(JS_STEM "${JS_FILE}" NAME_WE) + + set(OUTPUT_HEADER "${DUSK_GENERATED_HEADERS_DIR}/${JS_STEM}_js.h") + + set(NAME_ARG "") + if(ARG_NAME) + set(NAME_ARG "--name" "${ARG_NAME}") + endif() + + add_custom_command( + OUTPUT "${OUTPUT_HEADER}" + COMMAND ${Python3_EXECUTABLE} -m tools.js2c + --input "${JS_ABS}" + --output "${OUTPUT_HEADER}" + ${NAME_ARG} + WORKING_DIRECTORY "${DUSK_ROOT_DIR}" + DEPENDS "${JS_ABS}" + COMMENT "js2c: ${JS_STEM}.js -> ${JS_STEM}_js.h" + VERBATIM + ) + + file(RELATIVE_PATH JS_REL "${DUSK_ROOT_DIR}" "${JS_ABS}") + string(MAKE_C_IDENTIFIER "dusk_js2c_${JS_REL}" JS_TARGET) + add_custom_target(${JS_TARGET} DEPENDS "${OUTPUT_HEADER}") + add_dependencies(${TARGET} ${JS_TARGET}) +endfunction() diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index 16fdfa74..56d62ed2 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -49,15 +49,14 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { errorChain(uiInit()); errorChain(uiTextboxInit()); errorChain(cutsceneInit()); - errorChain(sceneInit()); entityManagerInit(); backpackInit(); physicsManagerInit(); errorChain(networkInit()); errorChain(scriptInit()); + errorChain(sceneInit()); consolePrint("Engine initialized"); - errorChain(scriptExecFile("init.js")); errorOk(); @@ -75,7 +74,7 @@ errorret_t engineUpdate(void) { physicsManagerUpdate(); errorChain(displayUpdate()); errorChain(cutsceneUpdate()); - sceneUpdate(); + errorChain(sceneUpdate()); errorChain(assetUpdate()); errorChain(scriptUpdate()); @@ -90,7 +89,8 @@ void engineExit(void) { errorret_t engineDispose(void) { uiTextboxDispose(); cutsceneDispose(); - sceneDispose(); + errorChain(sceneDispose()); + errorChain(scriptDispose()); errorChain(networkDispose()); entityManagerDispose(); localeManagerDispose(); @@ -98,7 +98,6 @@ errorret_t engineDispose(void) { consoleDispose(); errorChain(displayDispose()); // errorChain(saveDispose()); - errorChain(scriptDispose()); errorChain(assetDispose()); errorOk(); diff --git a/src/dusk/scene/CMakeLists.txt b/src/dusk/scene/CMakeLists.txt index 5329dd01..771c68bd 100644 --- a/src/dusk/scene/CMakeLists.txt +++ b/src/dusk/scene/CMakeLists.txt @@ -3,6 +3,11 @@ # This software is released under the MIT License. # https://opensource.org/licenses/MIT +include(duskjs2c) +dusk_embed_js(${DUSK_LIBRARY_TARGET_NAME} + ${CMAKE_CURRENT_SOURCE_DIR}/scene.js +) + target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC scene.c diff --git a/src/dusk/scene/scene.c b/src/dusk/scene/scene.c index c07eb40e..03f37def 100644 --- a/src/dusk/scene/scene.c +++ b/src/dusk/scene/scene.c @@ -18,15 +18,30 @@ #include "asset/loader/assetloader.h" #include "script/module/modulebase.h" #include "console/console.h" +#include "script/script.h" +#include "scene_js.h" scene_t SCENE; errorret_t sceneInit(void) { memoryZero(&SCENE, sizeof(scene_t)); + errorChain(scriptExecString(SCENE_JS)); errorOk(); } -void sceneUpdate(void) { +errorret_t sceneUpdate(void) { + #ifdef DUSK_TIME_DYNAMIC + if(TIME.dynamicUpdate) { + errorChain(scriptExecString("Scene.dynamicUpdate();")); + } else { + errorChain(scriptExecString("Scene.update();")); + } + #else + errorChain(scriptExecString("Scene.update();")); + errorChain(scriptExecString("Scene.dynamicUpdate();")); + #endif + + errorOk(); } errorret_t sceneRender(void) { @@ -65,5 +80,6 @@ errorret_t sceneRender(void) { } errorret_t sceneDispose(void) { + errorChain(scriptExecString("Scene.dispose();")); errorOk(); } diff --git a/src/dusk/scene/scene.h b/src/dusk/scene/scene.h index a9101a9a..e7ecfe39 100644 --- a/src/dusk/scene/scene.h +++ b/src/dusk/scene/scene.h @@ -16,21 +16,29 @@ extern scene_t SCENE; /** * Initialises the scene manager. + * + * @return An error if the init failed, or errorOk() if it succeeded. */ errorret_t sceneInit(void); /** * Ticks the scene manager. Processes any pending scene transition, then * calls scriptSceneUpdate on the active scene. + * + * @return An error if the update failed, or errorOk() if it succeeded. */ -void sceneUpdate(void); +errorret_t sceneUpdate(void); /** * Renders the current scene (entities, render pipeline, UI). + * + * @return An error if the render failed, or errorOk() if it succeeded. */ errorret_t sceneRender(void); /** * Disposes the active scene immediately. + * + * @return An error if the dispose failed, or errorOk() if it succeeded. */ errorret_t sceneDispose(void); diff --git a/src/dusk/scene/scene.js b/src/dusk/scene/scene.js new file mode 100644 index 00000000..5e6710be --- /dev/null +++ b/src/dusk/scene/scene.js @@ -0,0 +1,36 @@ +// Copyright (c) 2026 Dominic Masters +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +var Scene = { +}; + +Scene.update = () => { +} + +Scene.dynamicUpdate = () => { +} + +Scene.set = (newScene) => { + // Current scene active? + if(Scene.current && Scene.current.dispose) { + Scene.current.dispose(); + } + + // Set new scene + Scene.current = newScene; + if(!newScene) return; + + // Init + if(newScene.init) { + newScene.init(); + } +}; + +Scene.dispose = () => { + if(Scene.current && Scene.current.dispose) { + Scene.current.dispose(); + } + Scene.current = null; +} \ No newline at end of file diff --git a/src/dusk/script/script.c b/src/dusk/script/script.c index cc9fcbec..e4d2c1dc 100644 --- a/src/dusk/script/script.c +++ b/src/dusk/script/script.c @@ -16,6 +16,36 @@ script_t SCRIPT; +static void scriptExceptionInfo( + jerry_value_t exception, + char_t *msgBuf, size_t msgSize, + char_t *stackBuf, size_t stackSize +) { + jerry_value_t errVal = jerry_exception_value(exception, false); + + jerry_value_t errStr = jerry_value_to_string(errVal); + jerry_size_t len = jerry_string_to_buffer( + errStr, JERRY_ENCODING_UTF8, (jerry_char_t *)msgBuf, msgSize - 1 + ); + msgBuf[len] = '\0'; + jerry_value_free(errStr); + + stackBuf[0] = '\0'; + jerry_value_t stackKey = jerry_string_sz("stack"); + jerry_value_t stackVal = jerry_object_get(errVal, stackKey); + jerry_value_free(stackKey); + if(!jerry_value_is_exception(stackVal) && !jerry_value_is_undefined(stackVal)) { + jerry_value_t stackStr = jerry_value_to_string(stackVal); + jerry_size_t stackLen = jerry_string_to_buffer( + stackStr, JERRY_ENCODING_UTF8, (jerry_char_t *)stackBuf, stackSize - 1 + ); + stackBuf[stackLen] = '\0'; + jerry_value_free(stackStr); + } + jerry_value_free(stackVal); + jerry_value_free(errVal); +} + errorret_t scriptInit(void) { memoryZero(&SCRIPT, sizeof(script_t)); @@ -33,15 +63,10 @@ errorret_t scriptUpdate() { if(jerry_value_is_exception(ret)) { char_t buf[256]; - jerry_value_t errVal = jerry_exception_value(ret, false); - jerry_value_t errStr = jerry_value_to_string(errVal); - jerry_size_t len = jerry_string_to_buffer( - errStr, JERRY_ENCODING_UTF8, (jerry_char_t *)buf, sizeof(buf) - 1 - ); - buf[len] = '\0'; - jerry_value_free(errStr); - jerry_value_free(errVal); + char_t stack[512]; + scriptExceptionInfo(ret, buf, sizeof(buf), stack, sizeof(stack)); jerry_value_free(ret); + if(stack[0]) errorThrow("Script error: %s\n%s", buf, stack); errorThrow("Script error: %s", buf); } @@ -61,15 +86,10 @@ errorret_t scriptExecString(const char_t *source) { if(jerry_value_is_exception(result)) { char_t buf[256]; - jerry_value_t errVal = jerry_exception_value(result, false); - jerry_value_t errStr = jerry_value_to_string(errVal); - jerry_size_t len = jerry_string_to_buffer( - errStr, JERRY_ENCODING_UTF8, (jerry_char_t *)buf, sizeof(buf) - 1 - ); - buf[len] = '\0'; - jerry_value_free(errStr); - jerry_value_free(errVal); + char_t stack[512]; + scriptExceptionInfo(result, buf, sizeof(buf), stack, sizeof(stack)); jerry_value_free(result); + if(stack[0]) errorThrow("Script error: %s\n%s", buf, stack); errorThrow("Script error: %s", buf); } diff --git a/test/asset/test_asset.c b/test/asset/test_asset.c index 261aba4a..a64c3197 100644 --- a/test/asset/test_asset.c +++ b/test/asset/test_asset.c @@ -124,9 +124,11 @@ static void test_update_entry_reaches_loaded(void **state) { assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); assert_int_equal(entry->state, ASSET_ENTRY_STATE_NOT_STARTED); + assetEntryLock(entry); errorret_t ret = assetUpdate(); assert_true(errorIsOk(ret)); assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); + assetEntryUnlock(entry); assert_int_equal(memoryGetAllocatedCount(), 0); } @@ -134,11 +136,13 @@ static void test_update_entry_reaches_loaded(void **state) { static void test_update_slot_occupied_after_first_update(void **state) { assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); + assetEntryLock(entry); assetUpdate(); assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); // Slot not cleared until the second update processes the LOADED case. assert_true(loading_slot_has_entry(entry)); + assetEntryUnlock(entry); assert_int_equal(memoryGetAllocatedCount(), 0); } @@ -146,11 +150,13 @@ static void test_update_slot_occupied_after_first_update(void **state) { static void test_update_slot_cleared_after_second_update(void **state) { assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); + assetEntryLock(entry); assetUpdate(); assetUpdate(); assert_false(loading_slot_has_entry(entry)); assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); + assetEntryUnlock(entry); assert_int_equal(memoryGetAllocatedCount(), 0); } @@ -162,6 +168,7 @@ static void test_update_four_slots_fill_independently(void **state) { char_t name[ASSET_FILE_NAME_MAX]; snprintf(name, sizeof(name), "asset%d.locale", i); entries[i] = assetGetEntry(name, ASSET_LOADER_TYPE_LOCALE, NULL); + assetEntryLock(entries[i]); } errorret_t ret = assetUpdate(); @@ -169,6 +176,7 @@ static void test_update_four_slots_fill_independently(void **state) { for(int i = 0; i < ASSET_LOADING_COUNT_MAX; i++) { assert_int_equal(entries[i]->state, ASSET_ENTRY_STATE_LOADED); + assetEntryUnlock(entries[i]); } assert_int_equal(memoryGetAllocatedCount(), 0); @@ -201,6 +209,8 @@ static void test_update_noop_on_empty_table(void **state) { static void test_update_loaded_entry_not_redispatched(void **state) { assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); + + assetEntryLock(entry); assetUpdate(); assetUpdate(); // slot freed assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); @@ -212,6 +222,8 @@ static void test_update_loaded_entry_not_redispatched(void **state) { assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); assert_false(loading_slot_has_entry(entry)); + assetEntryUnlock(entry); + assert_int_equal(memoryGetAllocatedCount(), 0); } @@ -226,6 +238,11 @@ static void test_update_overflow_queues_entries(void **state) { entries[i] = assetGetEntry(name, ASSET_LOADER_TYPE_LOCALE, NULL); } + // Lock first batch so the reaper won't collect them before we check. + for(int i = 0; i < ASSET_LOADING_COUNT_MAX; i++) { + assetEntryLock(entries[i]); + } + // Update 1: fills all slots, first ASSET_LOADING_COUNT_MAX entries reach LOADED. // The overflow entry has no slot yet and stays NOT_STARTED. errorret_t ret = assetUpdate(); @@ -235,17 +252,27 @@ static void test_update_overflow_queues_entries(void **state) { } assert_int_equal(entries[ASSET_LOADING_COUNT_MAX]->state, ASSET_ENTRY_STATE_NOT_STARTED); + // Unlock first batch — the reaper can collect them in update 2. + for(int i = 0; i < ASSET_LOADING_COUNT_MAX; i++) { + assetEntryUnlock(entries[i]); + } + // Update 2: LOADED slots are freed. Overflow entry still NOT_STARTED because // the dispatch phase ran before the slots were cleared this turn. ret = assetUpdate(); assert_true(errorIsOk(ret)); assert_int_equal(entries[ASSET_LOADING_COUNT_MAX]->state, ASSET_ENTRY_STATE_NOT_STARTED); + // Lock overflow entry so it stays LOADED after update 3. + assetEntryLock(entries[ASSET_LOADING_COUNT_MAX]); + // Update 3: now a slot is available; the overflow entry is dispatched and loaded. ret = assetUpdate(); assert_true(errorIsOk(ret)); assert_int_equal(entries[ASSET_LOADING_COUNT_MAX]->state, ASSET_ENTRY_STATE_LOADED); + assetEntryUnlock(entries[ASSET_LOADING_COUNT_MAX]); + assert_int_equal(memoryGetAllocatedCount(), 0); } @@ -340,6 +367,8 @@ static void test_update_reentrant_sync_loader(void **state) { static void test_getEntry_returns_loaded_entry(void **state) { assetentry_t *a = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); + + assetEntryLock(a); assetUpdate(); assert_int_equal(a->state, ASSET_ENTRY_STATE_LOADED); @@ -348,6 +377,7 @@ static void test_getEntry_returns_loaded_entry(void **state) { assetentry_t *b = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); assert_ptr_equal(a, b); assert_int_equal(b->state, ASSET_ENTRY_STATE_LOADED); + assetEntryUnlock(a); assert_int_equal(memoryGetAllocatedCount(), 0); } @@ -358,9 +388,12 @@ static void test_getEntry_returns_loaded_entry(void **state) { static void test_entry_dispose_clears_entry(void **state) { assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); + + assetEntryLock(entry); assetUpdate(); assetUpdate(); // ensure loading slot is freed before disposing assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); + assetEntryUnlock(entry); errorret_t ret = assetEntryDispose(entry); assert_true(errorIsOk(ret)); @@ -371,8 +404,11 @@ static void test_entry_dispose_clears_entry(void **state) { static void test_entry_dispose_slot_reusable(void **state) { assetentry_t *a = assetGetEntry("a.locale", ASSET_LOADER_TYPE_LOCALE, NULL); + + assetEntryLock(a); assetUpdate(); assetUpdate(); + assetEntryUnlock(a); assert_false(loading_slot_has_entry(a)); assetEntryDispose(a); @@ -383,9 +419,11 @@ static void test_entry_dispose_slot_reusable(void **state) { assert_non_null(b); assert_int_equal(b->state, ASSET_ENTRY_STATE_NOT_STARTED); + assetEntryLock(b); errorret_t ret = assetUpdate(); assert_true(errorIsOk(ret)); assert_int_equal(b->state, ASSET_ENTRY_STATE_LOADED); + assetEntryUnlock(b); assert_int_equal(memoryGetAllocatedCount(), 0); } @@ -396,8 +434,11 @@ static void test_entry_dispose_slot_reusable(void **state) { static void test_requireLoaded_already_loaded(void **state) { assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); + + assetEntryLock(entry); assetUpdate(); assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); + assetEntryUnlock(entry); // Should return immediately without calling assetUpdate again. errorret_t ret = assetRequireLoaded(entry); diff --git a/test/asset/test_assetjsonloader.c b/test/asset/test_assetjsonloader.c index b1bdbf44..52756b2a 100644 --- a/test/asset/test_assetjsonloader.c +++ b/test/asset/test_assetjsonloader.c @@ -10,6 +10,7 @@ #include "asset/loader/assetloader.h" #include "asset/loader/assetentry.h" #include "asset/loader/json/assetjsonloader.h" +#include "thread/thread.h" #include "util/memory.h" #include @@ -20,6 +21,32 @@ static const char_t *JSON_VALID = "{\"hello\":\"world\",\"count\":42}"; static const char_t *JSON_INVALID = "{ this is definitely not valid json !!!"; +// ============================================================ +// Async thread helper +// ============================================================ + +typedef struct { + assetloading_t *loading; + bool_t ok; +} json_async_run_t; + +static void json_async_thread_cb(thread_t *thread) { + json_async_run_t *run = (json_async_run_t *)thread->data; + errorret_t ret = assetJsonLoaderAsync(run->loading); + run->ok = errorIsOk(ret); + if(errorIsNotOk(ret)) errorCatch(ret); +} + +static bool_t run_json_async(assetloading_t *loading) { + json_async_run_t run = { .loading = loading, .ok = false }; + thread_t thread; + threadInit(&thread, json_async_thread_cb); + thread.data = &run; + threadStart(&thread); + threadStop(&thread); + return run.ok; +} + // ============================================================ // In-memory ZIP // ============================================================ @@ -110,8 +137,10 @@ static errorret_t loader_ctx_run(loader_ctx_t *ctx) { errorret_t ret = assetJsonLoaderSync(&ctx->loading); if(errorIsNotOk(ret)) return ret; - ret = assetJsonLoaderAsync(&ctx->loading); - if(errorIsNotOk(ret)) return ret; + if(!run_json_async(&ctx->loading)) { + ctx->entry.state = ASSET_ENTRY_STATE_ERROR; + errorThrow("Async JSON load failed"); + } return assetJsonLoaderSync(&ctx->loading); } @@ -162,8 +191,7 @@ static void test_json_parse_error(void **state) { errorret_t ret = assetJsonLoaderSync(&ctx.loading); assert_true(errorIsOk(ret)); - ret = assetJsonLoaderAsync(&ctx.loading); - assert_true(errorIsOk(ret)); + assert_true(run_json_async(&ctx.loading)); ret = assetJsonLoaderSync(&ctx.loading); assert_true(errorIsNotOk(ret)); @@ -184,9 +212,7 @@ static void test_json_missing_file(void **state) { assert_true(errorIsOk(ret)); // Async phase stat-fails because the file isn't in the ZIP. - ret = assetJsonLoaderAsync(&ctx.loading); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); + assert_false(run_json_async(&ctx.loading)); assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR); diff --git a/test/asset/test_assettilesetloader.c b/test/asset/test_assettilesetloader.c index f9a81eac..2b7f9f4f 100644 --- a/test/asset/test_assettilesetloader.c +++ b/test/asset/test_assettilesetloader.c @@ -10,6 +10,7 @@ #include "asset/loader/assetloader.h" #include "asset/loader/assetentry.h" #include "asset/loader/display/assettilesetloader.h" +#include "thread/thread.h" #include "util/memory.h" #include @@ -91,6 +92,32 @@ static const uint8_t DTF_INVALID_UV[] = { 0x00, 0x00, 0x00, 0x40 }; +// ============================================================ +// Async thread helper +// ============================================================ + +typedef struct { + assetloading_t *loading; + bool_t ok; +} tileset_async_run_t; + +static void tileset_async_thread_cb(thread_t *thread) { + tileset_async_run_t *run = (tileset_async_run_t *)thread->data; + errorret_t ret = assetTilesetLoaderAsync(run->loading); + run->ok = errorIsOk(ret); + if(errorIsNotOk(ret)) errorCatch(ret); +} + +static bool_t run_tileset_async(assetloading_t *loading) { + tileset_async_run_t run = { .loading = loading, .ok = false }; + thread_t thread; + threadInit(&thread, tileset_async_thread_cb); + thread.data = &run; + threadStart(&thread); + threadStop(&thread); + return run.ok; +} + // ============================================================ // In-memory ZIP // ============================================================ @@ -183,8 +210,10 @@ static errorret_t loader_ctx_run(loader_ctx_t *ctx) { errorret_t ret = assetTilesetLoaderSync(&ctx->loading); if(errorIsNotOk(ret)) return ret; - ret = assetTilesetLoaderAsync(&ctx->loading); - if(errorIsNotOk(ret)) return ret; + if(!run_tileset_async(&ctx->loading)) { + ctx->entry.state = ASSET_ENTRY_STATE_ERROR; + errorThrow("Async tileset load failed"); + } return assetTilesetLoaderSync(&ctx->loading); } @@ -238,8 +267,7 @@ static void test_tileset_bad_magic(void **state) { errorret_t ret = assetTilesetLoaderSync(&ctx.loading); assert_true(errorIsOk(ret)); - ret = assetTilesetLoaderAsync(&ctx.loading); - assert_true(errorIsOk(ret)); + assert_true(run_tileset_async(&ctx.loading)); ret = assetTilesetLoaderSync(&ctx.loading); assert_true(errorIsNotOk(ret)); errorCatch(ret); @@ -255,8 +283,7 @@ static void test_tileset_bad_version(void **state) { errorret_t ret = assetTilesetLoaderSync(&ctx.loading); assert_true(errorIsOk(ret)); - ret = assetTilesetLoaderAsync(&ctx.loading); - assert_true(errorIsOk(ret)); + assert_true(run_tileset_async(&ctx.loading)); ret = assetTilesetLoaderSync(&ctx.loading); assert_true(errorIsNotOk(ret)); errorCatch(ret); @@ -328,9 +355,7 @@ static void test_tileset_missing_file(void **state) { errorret_t ret = assetTilesetLoaderSync(&ctx.loading); assert_true(errorIsOk(ret)); - ret = assetTilesetLoaderAsync(&ctx.loading); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); + assert_false(run_tileset_async(&ctx.loading)); assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR); loader_ctx_dispose(&ctx); diff --git a/tools/js2c/__main__.py b/tools/js2c/__main__.py new file mode 100644 index 00000000..14874501 --- /dev/null +++ b/tools/js2c/__main__.py @@ -0,0 +1,53 @@ +import argparse +import os +import re + +parser = argparse.ArgumentParser(description="Embed a JS file as a C string header") +parser.add_argument("--input", required=True, help="Path to input JS file") +parser.add_argument("--output", required=True, help="Path to output .h file") +parser.add_argument("--name", help="C identifier name (default: derived from filename)") +args = parser.parse_args() + +if args.name: + name = args.name +else: + stem = os.path.splitext(os.path.basename(args.input))[0] + name = re.sub(r"[^a-zA-Z0-9]", "_", stem).upper() + "_JS" + +with open(args.input, "r", encoding="utf-8") as f: + source = f.read() + +def escape_line(s): + s = s.replace("\\", "\\\\") + s = s.replace('"', '\\"') + s = s.replace("\r", "\\r") + s = s.replace("\t", "\\t") + return s + +lines = source.split("\n") +if lines and lines[-1] == "": + lines = lines[:-1] + +out = [ + "#pragma once", + "#include ", + "", + f"static const char {name}[] =", +] + +if not lines: + out[-1] += ' "";' +else: + for i, line in enumerate(lines): + suffix = ";" if i == len(lines) - 1 else "" + out.append(f' "{escape_line(line)}\\n"{suffix}') + +out += [ + "", + f"static const size_t {name}_SIZE = sizeof({name}) - 1;", + "", +] + +os.makedirs(os.path.dirname(args.output), exist_ok=True) +with open(args.output, "w", encoding="utf-8") as f: + f.write("\n".join(out))