27 Commits

Author SHA1 Message Date
YourWishes ed0420fdce Cleanup of script modules. 2026-06-07 12:59:17 -05:00
YourWishes 47a6f396fa Fix typedefs 2026-06-06 19:06:52 -05:00
YourWishes 9edb2aa0c1 Example scene working 2026-06-06 18:46:08 -05:00
YourWishes 003b647d83 Fix tests 2026-06-06 17:36:13 -05:00
YourWishes 2849ff8844 Fix linux? 2026-06-06 17:14:42 -05:00
YourWishes 9f3089742a Fixed ISO builds. 2026-06-06 17:07:30 -05:00
YourWishes b286a9bbcd Fixed wii build 2026-06-06 16:53:06 -05:00
YourWishes 6204e745ba Fixed gamecube building. 2026-06-06 16:47:07 -05:00
YourWishes bbe0e48d23 Builds again? 2026-06-06 16:42:12 -05:00
YourWishes 79054080c0 Cleanup a bit. 2026-06-06 16:39:27 -05:00
YourWishes 81024c4c09 Require async 2026-06-06 10:55:10 -05:00
YourWishes 9068d96130 Add timeouts 2026-06-06 10:38:10 -05:00
YourWishes 6f47543720 Update jerryscript each frame. 2026-06-06 10:30:22 -05:00
YourWishes 5a08384ae1 start async 2026-06-05 19:42:24 -05:00
YourWishes 45d8fda0e4 require() working as I like 2026-06-05 18:49:10 -05:00
YourWishes a9e664492f First round of asset refactoring 2026-06-05 13:18:08 -05:00
YourWishes 3c8b6cb2cc Scene script code 2026-06-04 13:36:35 -05:00
YourWishes 2b3abbe13b ABout to try scene and script merger 2026-06-02 16:46:39 -05:00
YourWishes 241a52b94a nuke unused overworld code 2026-06-02 13:23:11 -05:00
YourWishes 82c300b077 Add some script modules 2026-06-02 12:55:32 -05:00
YourWishes 0f8b629e20 Add logging to Wii 2026-06-02 11:01:54 -05:00
YourWishes 36f6ac65f2 Builds and works on Gamecube 2026-06-02 09:53:56 -05:00
YourWishes a25871a849 Test sprite from script 2026-06-02 09:32:07 -05:00
YourWishes 57766a9104 Merge branch 'main' into scriptentity 2026-06-02 07:36:44 -05:00
YourWishes 3770ae1645 Fix tests? 2026-06-02 07:35:28 -05:00
YourWishes d73edb403f Example Camera 2026-06-01 23:04:55 -05:00
YourWishes b14196ff0d Basic entity script 2026-06-01 22:56:37 -05:00
177 changed files with 7194 additions and 2375 deletions
+1 -1
View File
@@ -144,7 +144,7 @@ jobs:
- name: Copy output files. - name: Copy output files.
run: | run: |
mkdir -p ./git-artifcats/Dusk/apps/Dusk mkdir -p ./git-artifcats/Dusk/apps/Dusk
cp build-wii/Dusk.dol ./git-artifcats/Dusk/apps/Dusk/boot.dol cp build-wii/boot.dol ./git-artifcats/Dusk/apps/Dusk/boot.dol
cp build-wii/dusk.dsk ./git-artifcats/Dusk/apps/Dusk/dusk.dsk cp build-wii/dusk.dsk ./git-artifcats/Dusk/apps/Dusk/dusk.dsk
cp build-wii/meta.xml ./git-artifcats/Dusk/apps/Dusk/meta.xml cp build-wii/meta.xml ./git-artifcats/Dusk/apps/Dusk/meta.xml
- name: Upload Wii binary - name: Upload Wii binary
+1
View File
@@ -106,3 +106,4 @@ yarn.lock
/build* /build*
/assets/test /assets/test
/tools_old /tools_old
/assets/test.png
+6 -4
View File
@@ -1,5 +1,3 @@
Console.print('This is called from JavaScript');
const platformNames = { const platformNames = {
[System.PLATFORM_LINUX]: 'Linux', [System.PLATFORM_LINUX]: 'Linux',
[System.PLATFORM_KNULLI]: 'Knulli', [System.PLATFORM_KNULLI]: 'Knulli',
@@ -8,5 +6,9 @@ const platformNames = {
[System.PLATFORM_WII]: 'Wii', [System.PLATFORM_WII]: 'Wii',
}; };
const platformName = platformNames[System.platform] || 'Unknown'; Console.print('Platform: ' + (platformNames[System.platform] || 'Unknown'));
Console.print('Platform: ' + platformName);
requireAsync('testscene.js').then(Scene.set).catch(err => {
Console.print('Error loading scene: ' + err);
Engine.exit();
});
+48
View File
@@ -0,0 +1,48 @@
var scene = {};
// var assets = AssetBatch([
// { path: 'test.png', type: Asset.TYPE_TEXTURE, format: Texture.FORMAT_RGBA }
// ]);
var cam;
var camPos;
var testEntity;
var testPos;
var testRenderable;
var texEntry;
scene.init = function() {
// assets.lock();
// await assets.loaded();
Console.print('Scene Init');
// 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));
// 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);
}
scene.dispose = function() {
Console.print('Scene Dispose');
Entity.dispose(cam);
Entity.dispose(testEntity);
// assets.unlock();
};
module.exports = scene;
+43
View File
@@ -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 <NAME>[] = "...";
# static const size_t <NAME>_SIZE = sizeof(<NAME>) - 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()
+8 -5
View File
@@ -1,15 +1,18 @@
# Build type: FAT (SD/USB via libfat) or ISO (DVD disc via libogc DVD driver) # Build type: DOL (SD/USB via libfat) or ISO (DVD disc via libogc DVD driver)
set(DUSK_DOLPHIN_BUILD_TYPE "FAT" CACHE STRING "Dolphin asset source: FAT (SD/USB) or ISO (DVD disc)") set(DUSK_DOLPHIN_BUILD_TYPE "DOL" CACHE STRING "Dolphin asset source: DOL (SD/USB) or ISO (DVD disc)")
set_property(CACHE DUSK_DOLPHIN_BUILD_TYPE PROPERTY STRINGS "FAT" "ISO") set_property(CACHE DUSK_DOLPHIN_BUILD_TYPE PROPERTY STRINGS "DOL" "ISO")
# Target definitions # Numeric tokens so #if DUSK_DOLPHIN_BUILD_TYPE == DOL works in C.
# DUSK_DOLPHIN_BUILD_TYPE is passed without quotes so it expands to the identifier.
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_DOLPHIN DUSK_DOLPHIN
DUSK_INPUT_GAMEPAD DUSK_INPUT_GAMEPAD
DUSK_DISPLAY_WIDTH=640 DUSK_DISPLAY_WIDTH=640
DUSK_DISPLAY_HEIGHT=480 DUSK_DISPLAY_HEIGHT=480
DUSK_THREAD_PTHREAD DUSK_THREAD_PTHREAD
DUSK_DOLPHIN_BUILD_TYPE="${DUSK_DOLPHIN_BUILD_TYPE}" DOL=1
ISO=2
DUSK_DOLPHIN_BUILD_TYPE=${DUSK_DOLPHIN_BUILD_TYPE}
) )
# Custom compiler flags # Custom compiler flags
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile . docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-dolphin /bin/bash -c "./scripts/build-gamecube.sh" docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-gamecube.sh"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile . docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-dolphin /bin/bash -c "./scripts/build-gamecube-iso.sh" docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-gamecube-iso.sh"
+1
View File
@@ -7,6 +7,7 @@ fi
mkdir -p build-gamecube mkdir -p build-gamecube
cmake -S. -Bbuild-gamecube \ cmake -S. -Bbuild-gamecube \
-DDUSK_TARGET_SYSTEM=gamecube \ -DDUSK_TARGET_SYSTEM=gamecube \
-DDUSK_DOLPHIN_BUILD_TYPE=DOL \
-DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/GameCube.cmake" \ -DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/GameCube.cmake" \
-DDKP_OGC_PLATFORM_LIBRARY=libogc2 -DDKP_OGC_PLATFORM_LIBRARY=libogc2
cd build-gamecube cd build-gamecube
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
docker build -t dusk-knulli -f docker/knulli/Dockerfile . docker build -t dusk-knulli -f docker/knulli/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-knulli /bin/bash -c "./scripts/build-knulli.sh" docker run --rm -v "$(pwd):/workdir" dusk-knulli /bin/bash -c "./scripts/build-knulli.sh"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
docker build -t dusk-linux -f docker/linux/Dockerfile . docker build -t dusk-linux -f docker/linux/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-linux /bin/bash -c "./scripts/build-linux.sh" docker run --rm -v "$(pwd):/workdir" dusk-linux /bin/bash -c "./scripts/build-linux.sh"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
docker build -t dusk-psp -f docker/psp/Dockerfile . docker build -t dusk-psp -f docker/psp/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-psp /bin/bash -c "./scripts/build-psp.sh" docker run --rm -v "$(pwd):/workdir" dusk-psp /bin/bash -c "./scripts/build-psp.sh"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
docker build -t dusk-vita -f docker/vita/Dockerfile . docker build -t dusk-vita -f docker/vita/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-vita /bin/bash -c "./scripts/build-vita.sh" docker run --rm -v "$(pwd):/workdir" dusk-vita /bin/bash -c "./scripts/build-vita.sh"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile . docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-dolphin /bin/bash -c "./scripts/build-wii.sh" docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-wii.sh"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile . docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-dolphin /bin/bash -c "./scripts/build-wii-iso.sh" docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-wii-iso.sh"
+5 -1
View File
@@ -5,6 +5,10 @@ if [ -z "$DEVKITPRO" ]; then
fi fi
mkdir -p build-wii mkdir -p build-wii
cmake -S. -Bbuild-wii -DDUSK_TARGET_SYSTEM=wii -DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/Wii.cmake" cmake -S. -Bbuild-wii \
-DDUSK_TARGET_SYSTEM=wii \
-DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/Wii.cmake" \
-DDUSK_DOLPHIN_BUILD_TYPE=DOL
cd build-wii cd build-wii
make -j$(nproc) VERBOSE=1 make -j$(nproc) VERBOSE=1
mv Dusk.dol boot.dol
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
docker build -t dusk-linux -f docker/linux/Dockerfile . docker build -t dusk-linux -f docker/linux/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-linux /bin/bash -c "./scripts/test-linux.sh" docker run --rm -v "$(pwd):/workdir" dusk-linux /bin/bash -c "./scripts/test-linux.sh"
+4 -4
View File
@@ -5,7 +5,7 @@
#include "easing.h" #include "easing.h"
#include "assert/assert.h" #include "assert/assert.h"
#include <math.h> #include "util/math.h"
const easingfn_t EASING_FUNCTIONS[EASING_COUNT] = { const easingfn_t EASING_FUNCTIONS[EASING_COUNT] = {
easingLinear, easingLinear,
@@ -36,15 +36,15 @@ float_t easingLinear(const float_t t) {
} }
float_t easingInSine(const float_t t) { float_t easingInSine(const float_t t) {
return 1.0f - cosf(t * EASING_PI * 0.5f); return 1.0f - cosf(t * MATH_PI * 0.5f);
} }
float_t easingOutSine(const float_t t) { float_t easingOutSine(const float_t t) {
return sinf(t * EASING_PI * 0.5f); return sinf(t * MATH_PI * 0.5f);
} }
float_t easingInOutSine(const float_t t) { float_t easingInOutSine(const float_t t) {
return -(cosf(EASING_PI * t) - 1.0f) * 0.5f; return -(cosf(MATH_PI * t) - 1.0f) * 0.5f;
} }
float_t easingInQuad(const float_t t) { float_t easingInQuad(const float_t t) {
+35
View File
@@ -10,7 +10,17 @@
#include "util/string.h" #include "util/string.h"
#include "util/memory.h" #include "util/memory.h"
#ifdef DUSK_THREAD_PTHREAD
pthread_t ASSERT_MAIN_THREAD_ID = 0;
#endif
#ifndef DUSK_ASSERTIONS_FAKED #ifndef DUSK_ASSERTIONS_FAKED
void assertInit(void) {
#ifdef DUSK_THREAD_PTHREAD
ASSERT_MAIN_THREAD_ID = pthread_self();
#endif
}
#ifdef DUSK_TEST_ASSERT #ifdef DUSK_TEST_ASSERT
void assertTrueImpl( void assertTrueImpl(
const char *file, const char *file,
@@ -132,4 +142,29 @@
) { ) {
assertTrueImpl(file, line, stringCompare(a, b) == 0, message); assertTrueImpl(file, line, stringCompare(a, b) == 0, message);
} }
void assertIsMainThreadImpl(
const char *file,
const int32_t line,
const char *message
) {
#ifdef DUSK_THREAD_PTHREAD
assertTrueImpl(
file, line, pthread_self() == ASSERT_MAIN_THREAD_ID, message
);
#endif
}
void assertNotMainThreadImpl(
const char *file,
const int32_t line,
const char *message
) {
#ifdef DUSK_THREAD_PTHREAD
assertTrueImpl(
file, line, pthread_self() != ASSERT_MAIN_THREAD_ID, message
);
#endif
}
#endif #endif
+56 -1
View File
@@ -16,7 +16,18 @@
#endif #endif
#endif #endif
#ifdef DUSK_THREAD_PTHREAD
#include "thread/thread.h"
extern pthread_t ASSERT_MAIN_THREAD_ID;
#endif
#ifndef DUSK_ASSERTIONS_FAKED #ifndef DUSK_ASSERTIONS_FAKED
/**
* Initializes the assert system. Must be the very first call in engine
* startup.
*/
void assertInit(void);
/** /**
* Assert a given value to be true. * Assert a given value to be true.
* *
@@ -121,6 +132,28 @@
const char *message const char *message
); );
/**
* Asserts that the current thread is the main thread.
*
* @param message Message to throw against assertion failure.
*/
void assertIsMainThreadImpl(
const char *file,
const int32_t line,
const char *message
);
/**
* Asserts that the current thread is NOT the main thread.
*
* @param message Message to throw against assertion failure.
*/
void assertNotMainThreadImpl(
const char *file,
const int32_t line,
const char *message
);
/** /**
* Asserts a given value to be true. * Asserts a given value to be true.
* *
@@ -205,8 +238,28 @@
#define assertStringEqual(a, b, message) \ #define assertStringEqual(a, b, message) \
assertStringEqualImpl(__FILE__, __LINE__, a, b, message) assertStringEqualImpl(__FILE__, __LINE__, a, b, message)
/**
* Asserts that the current thread is the main thread.
*
* @param message Message to throw against assertion failure.
*/
#define assertIsMainThread(message) \
assertIsMainThreadImpl(__FILE__, __LINE__, message)
/**
* Asserts that the current thread is NOT the main thread.
*
* @param message Message to throw against assertion failure.
*/
#define assertNotMainThread(message) \
assertNotMainThreadImpl(__FILE__, __LINE__, message)
#else #else
// If assertions are faked, we define the macros to do nothing. // If assertions are faked, we define the macros to do nothing.
#define assertInit() ((void)0)
#define assertMainThreadInit() ((void)0)
#define assertIsMainThread(message) ((void)0)
#define assertNotMainThread(message) ((void)0)
#define assertTrue(x, message) ((void)0) #define assertTrue(x, message) ((void)0)
#define assertFalse(x, message) ((void)0) #define assertFalse(x, message) ((void)0)
#define assertUnreachable(message) ((void)0) #define assertUnreachable(message) ((void)0)
@@ -215,11 +268,13 @@
#define assertDeprecated(message) ((void)0) #define assertDeprecated(message) ((void)0)
#define assertStrLenMax(str, len, message) ((void)0) #define assertStrLenMax(str, len, message) ((void)0)
#define assertStrLenMin(str, len, message) ((void)0) #define assertStrLenMin(str, len, message) ((void)0)
#define assertStringEqual(a, b, message) ((void)0)
#define assertIsMainThread(message) ((void)0)
#define assertNotMainThread(message) ((void)0)
#endif #endif
// Static Assertions // Static Assertions
#define assertStructSize(struct, size) \ #define assertStructSize(struct, size) \
_Static_assert(sizeof(struct) == size, "Size of " #struct " must be " #size) _Static_assert(sizeof(struct) == size, "Size of " #struct " must be " #size)
+113 -16
View File
@@ -78,21 +78,83 @@ assetentry_t * assetGetEntry(
return NULL; return NULL;
} }
uint32_t assetGetEntriesOfType(
assetentry_t **outEntries,
const assetloadertype_t type
) {
assertNotNull(outEntries, "Output entries cannot be NULL.");
uint32_t count = 0;
assetentry_t *entry = ASSET.entries;
do {
if(entry->type == type) {
outEntries[count++] = entry;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
return count;
}
errorret_t assetRequireLoaded(assetentry_t *entry) { errorret_t assetRequireLoaded(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL."); assertNotNull(entry, "Entry cannot be NULL.");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type."); assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertIsMainThread("Currently only works on main thread.");
// Already loaded?
if(entry->state == ASSET_ENTRY_STATE_LOADED) { if(entry->state == ASSET_ENTRY_STATE_LOADED) {
errorOk(); errorOk();
} }
// Not loaded, just spin the wheel // Lock to prevent the reaper from collecting the entry mid-spin.
assetEntryLock(entry);
while(entry->state != ASSET_ENTRY_STATE_LOADED) { while(entry->state != ASSET_ENTRY_STATE_LOADED) {
usleep(1000); usleep(1000);
errorChain(assetUpdate()); errorret_t ret = assetUpdate();
if(errorIsNotOk(ret)) {
assetEntryUnlock(entry);
errorChain(ret);
}
} }
assetEntryUnlock(entry);
errorOk();
}
errorret_t assetRequireDisposed(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL.");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertIsMainThread("Currently only works on main thread.");
if(entry->type == ASSET_LOADER_TYPE_NULL) {
errorOk();
}
if(
entry->state == ASSET_ENTRY_STATE_NOT_STARTED ||
entry->state == ASSET_ENTRY_STATE_ERROR
) {
errorOk();
}
assertTrue(
entry->refs.count == 0,
"Cannot require disposal of an entry with active references."
);
// Lock to prevent the reaper from collecting the entry mid-spin.
assetEntryLock(entry);
while(entry->type != ASSET_LOADER_TYPE_NULL) {
usleep(1000);
errorret_t ret = assetUpdate();
if(errorIsNotOk(ret)) {
assetEntryUnlock(entry);
errorChain(ret);
}
}
assetEntryUnlock(entry);
errorOk(); errorOk();
} }
@@ -108,6 +170,7 @@ assetentry_t * assetLock(
void assetUnlock(const char_t *name) { void assetUnlock(const char_t *name) {
assertNotNull(name, "Name cannot be NULL."); assertNotNull(name, "Name cannot be NULL.");
assetentry_t *entry = ASSET.entries; assetentry_t *entry = ASSET.entries;
do { do {
if(entry->type != ASSET_LOADER_TYPE_NULL && stringEquals(entry->name, name)) { if(entry->type != ASSET_LOADER_TYPE_NULL && stringEquals(entry->name, name)) {
@@ -116,6 +179,7 @@ void assetUnlock(const char_t *name) {
} }
entry++; entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX); } while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
assertUnreachable("Asset entry not found for unlock."); assertUnreachable("Asset entry not found for unlock.");
} }
@@ -125,6 +189,8 @@ void assetUnlockEntry(assetentry_t *entry) {
} }
errorret_t assetUpdate(void) { errorret_t assetUpdate(void) {
assertIsMainThread("assetUpdate must be called from the main thread.");
// Determine how many available loading slots we have. // Determine how many available loading slots we have.
assetloading_t *availableLoading[ASSET_LOADING_COUNT_MAX]; assetloading_t *availableLoading[ASSET_LOADING_COUNT_MAX];
uint8_t availableLoadingCount = 0; uint8_t availableLoadingCount = 0;
@@ -191,8 +257,12 @@ errorret_t assetUpdate(void) {
switch(loading->entry->state) { switch(loading->entry->state) {
// This thing is pending synchronous loading. // This thing is pending synchronous loading.
case ASSET_ENTRY_STATE_PENDING_SYNC: case ASSET_ENTRY_STATE_PENDING_SYNC:
// Perform sync load.
loading->entry->state = ASSET_ENTRY_STATE_LOADING_SYNC; loading->entry->state = ASSET_ENTRY_STATE_LOADING_SYNC;
// Unlock before calling loadSync. The sync loader may re-enter
// assetUpdate (e.g. a script loading another asset), and the async
// thread never touches LOADING_SYNC entries, so this is safe.
threadMutexUnlock(&loading->mutex);
errorret_t ret = ( errorret_t ret = (
ASSET_LOADER_CALLBACKS[loading->type].loadSync(loading) ASSET_LOADER_CALLBACKS[loading->type].loadSync(loading)
); );
@@ -206,25 +276,25 @@ errorret_t assetUpdate(void) {
"Loader did not set entry state to loaded or error on finished load." "Loader did not set entry state to loaded or error on finished load."
); );
// If an error occured these things need to be true, basically just
// ensuring the sync loader is setting the error correctly.
if(errorIsNotOk(ret)) { if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret)); errorCatch(errorPrint(ret));
assertTrue( assertTrue(
loading->entry->state == ASSET_ENTRY_STATE_ERROR, loading->entry->state == ASSET_ENTRY_STATE_ERROR,
"Loader did not set entry state to error on failed load." "Loader did not set entry state to error on failed load."
); );
} else if(loading->entry->state == ASSET_ENTRY_STATE_LOADED) {
eventInvoke(&loading->entry->onLoaded, loading->entry);
} }
threadMutexUnlock(&loading->mutex);
loading++; loading++;
break; break;
case ASSET_ENTRY_STATE_LOADING_SYNC: case ASSET_ENTRY_STATE_LOADING_SYNC:
assertUnreachable( // A re-entrant assetUpdate call (e.g. from a script loading another
"Entry is in a pending sync state still?" // asset) will see this entry mid-sync-load. Skip it.
); threadMutexUnlock(&loading->mutex);
break; loading++;
continue;
// Done loading, we can just free it up. // Done loading, we can just free it up.
case ASSET_ENTRY_STATE_LOADED: case ASSET_ENTRY_STATE_LOADED:
@@ -233,10 +303,14 @@ errorret_t assetUpdate(void) {
loading++; loading++;
break; break;
case ASSET_ENTRY_STATE_ERROR: case ASSET_ENTRY_STATE_ERROR: {
assetentry_t *errEntry = loading->entry;
loading->entry = NULL;
threadMutexUnlock(&loading->mutex); threadMutexUnlock(&loading->mutex);
eventInvoke(&errEntry->onError, errEntry);
errorThrow("Failed to load asset asynchronously."); errorThrow("Failed to load asset asynchronously.");
break; break;
}
default: default:
threadMutexUnlock(&loading->mutex); threadMutexUnlock(&loading->mutex);
@@ -246,9 +320,7 @@ errorret_t assetUpdate(void) {
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX); } while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
// Reap entries that have no external locks (refs.count == 1 means only the // Reap unused entries.
// system hold remains). Only safe to reap LOADED and NOT_STARTED states —
// mid-load entries are left for the next cycle.
entry = ASSET.entries; entry = ASSET.entries;
do { do {
if(entry->state != ASSET_ENTRY_STATE_LOADED) { if(entry->state != ASSET_ENTRY_STATE_LOADED) {
@@ -275,6 +347,8 @@ errorret_t assetUpdate(void) {
} }
void assetUpdateAsync(thread_t *thread) { void assetUpdateAsync(thread_t *thread) {
assertNotMainThread("assetUpdateAsync must not run on the main thread.");
while(!threadShouldStop(thread)) { while(!threadShouldStop(thread)) {
// Walk over each asset // Walk over each asset
assetloading_t *loading; assetloading_t *loading;
@@ -331,12 +405,35 @@ void assetUpdateAsync(thread_t *thread) {
} }
errorret_t assetDispose(void) { errorret_t assetDispose(void) {
assertIsMainThread("Must be called from the main thread.");
threadStop(&ASSET.loadThread); threadStop(&ASSET.loadThread);
// Free any script read-buffers left behind by an in-flight async load
// that was interrupted before the sync eval phase ran.
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) { for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
threadMutexDispose(&ASSET.loading[i].mutex); assetloading_t *loading = &ASSET.loading[i];
if(
loading->entry != NULL &&
loading->type == ASSET_LOADER_TYPE_SCRIPT &&
loading->loading.script.buffer != NULL
) {
memoryFree(loading->loading.script.buffer);
loading->loading.script.buffer = NULL;
}
threadMutexDispose(&loading->mutex);
} }
// Dispose every non-null entry so type-specific dispose callbacks
// (e.g. assetScriptDispose freeing jerry values) run before the
// scripting engine is torn down.
assetentry_t *entry = ASSET.entries;
do {
if(entry->type != ASSET_LOADER_TYPE_NULL) {
errorChain(assetEntryDispose(entry));
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
// Cleanup zip file. // Cleanup zip file.
if(ASSET.zip != NULL) { if(ASSET.zip != NULL) {
if(zip_close(ASSET.zip) != 0) { if(zip_close(ASSET.zip) != 0) {
+21
View File
@@ -68,6 +68,18 @@ assetentry_t * assetGetEntry(
assetloaderinput_t *input assetloaderinput_t *input
); );
/**
* Gets all asset entries of a given type.
*
* @param outEntries Output array to write the entries to.
* @param type Type of the asset entries to get.
* @return The number of entries written to outEntries.
*/
uint32_t assetGetEntriesOfType(
assetentry_t **outEntries,
const assetloadertype_t type
);
/** /**
* Gets, creates, and locks an asset entry. The asset will begin loading on * Gets, creates, and locks an asset entry. The asset will begin loading on
* the next assetUpdate. Call assetUnlock when done to allow the entry to be * the next assetUpdate. Call assetUnlock when done to allow the entry to be
@@ -109,6 +121,15 @@ void assetUnlockEntry(assetentry_t *entry);
*/ */
errorret_t assetRequireLoaded(assetentry_t *entry); errorret_t assetRequireLoaded(assetentry_t *entry);
/**
* Requires an asset entry to be disposed. This will block until the asset entry
* is fully disposed.
*
* @param entry The asset entry to require disposal of.
* @return An error code if the asset entry could not be disposed properly.
*/
errorret_t assetRequireDisposed(assetentry_t *entry);
/** /**
* Updates the asset system. * Updates the asset system.
* *
+69 -2
View File
@@ -11,6 +11,38 @@
#include "util/memory.h" #include "util/memory.h"
#include <unistd.h> #include <unistd.h>
/* ---- Per-entry event trampolines ----------------------------------------- */
static void assetBatchEntryOnLoadedCb(void *params, void *user) {
assetentry_t *entry = (assetentry_t *)params;
assetbatch_t *batch = (assetbatch_t *)user;
batch->loadedCount++;
eventInvoke(&batch->onEntryLoaded, entry);
if((uint16_t)(batch->loadedCount + batch->errorCount) >= batch->count) {
if(batch->errorCount == 0) {
eventInvoke(&batch->onLoaded, batch);
} else {
eventInvoke(&batch->onError, batch);
}
}
}
static void assetBatchEntryOnErrorCb(void *params, void *user) {
assetentry_t *entry = (assetentry_t *)params;
assetbatch_t *batch = (assetbatch_t *)user;
batch->errorCount++;
eventInvoke(&batch->onEntryError, entry);
if((uint16_t)(batch->loadedCount + batch->errorCount) >= batch->count) {
eventInvoke(&batch->onError, batch);
}
}
/* ---- Public API ---------------------------------------------------------- */
void assetBatchInit( void assetBatchInit(
assetbatch_t *batch, assetbatch_t *batch,
const uint16_t count, const uint16_t count,
@@ -24,10 +56,40 @@ void assetBatchInit(
memoryZero(batch, sizeof(assetbatch_t)); memoryZero(batch, sizeof(assetbatch_t));
batch->count = count; batch->count = count;
eventInit(
&batch->onLoaded,
batch->onLoadedCallbacks, batch->onLoadedUsers, ASSET_BATCH_EVENT_MAX
);
eventInit(
&batch->onEntryLoaded,
batch->onEntryLoadedCallbacks, batch->onEntryLoadedUsers, ASSET_BATCH_EVENT_MAX
);
eventInit(
&batch->onError,
batch->onErrorCallbacks, batch->onErrorUsers, ASSET_BATCH_EVENT_MAX
);
eventInit(
&batch->onEntryError,
batch->onEntryErrorCallbacks, batch->onEntryErrorUsers, ASSET_BATCH_EVENT_MAX
);
for(uint16_t i = 0; i < count; i++) { for(uint16_t i = 0; i < count; i++) {
// Copy input into batch-owned storage so the descriptor need not persist.
batch->inputs[i] = descs[i].input; batch->inputs[i] = descs[i].input;
batch->entries[i] = assetLock(descs[i].path, descs[i].type, &batch->inputs[i]); batch->entries[i] = assetLock(descs[i].path, descs[i].type, &batch->inputs[i]);
if(batch->entries[i]->state == ASSET_ENTRY_STATE_LOADED) {
/* Already loaded (cached) — count it now, no subscription needed. */
batch->loadedCount++;
} else if(batch->entries[i]->state == ASSET_ENTRY_STATE_ERROR) {
batch->errorCount++;
} else {
eventSubscribe(
&batch->entries[i]->onLoaded, assetBatchEntryOnLoadedCb, batch
);
eventSubscribe(
&batch->entries[i]->onError, assetBatchEntryOnErrorCb, batch
);
}
} }
} }
@@ -88,7 +150,12 @@ errorret_t assetBatchRequireLoaded(assetbatch_t *batch) {
void assetBatchDispose(assetbatch_t *batch) { void assetBatchDispose(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL."); assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) { for(uint16_t i = 0; i < batch->count; i++) {
assetUnlockEntry(batch->entries[i]); if(batch->entries[i]) {
/* Unsubscribe while we still hold a lock so the entry is guaranteed live. */
eventUnsubscribe(&batch->entries[i]->onLoaded, assetBatchEntryOnLoadedCb);
eventUnsubscribe(&batch->entries[i]->onError, assetBatchEntryOnErrorCb);
assetUnlockEntry(batch->entries[i]);
}
} }
memoryZero(batch, sizeof(assetbatch_t)); memoryZero(batch, sizeof(assetbatch_t));
} }
+25 -1
View File
@@ -8,8 +8,10 @@
#pragma once #pragma once
#include "asset/loader/assetentry.h" #include "asset/loader/assetentry.h"
#include "asset/loader/assetloader.h" #include "asset/loader/assetloader.h"
#include "event/event.h"
#define ASSET_BATCH_COUNT_MAX 64 #define ASSET_BATCH_COUNT_MAX 64
#define ASSET_BATCH_EVENT_MAX 4
typedef struct { typedef struct {
const char_t *path; const char_t *path;
@@ -21,6 +23,28 @@ typedef struct {
assetentry_t *entries[ASSET_BATCH_COUNT_MAX]; assetentry_t *entries[ASSET_BATCH_COUNT_MAX];
assetloaderinput_t inputs[ASSET_BATCH_COUNT_MAX]; assetloaderinput_t inputs[ASSET_BATCH_COUNT_MAX];
uint16_t count; uint16_t count;
uint16_t loadedCount;
uint16_t errorCount;
/** Fires once when every entry has loaded successfully. params = assetbatch_t * */
event_t onLoaded;
eventcallback_t onLoadedCallbacks[ASSET_BATCH_EVENT_MAX];
void *onLoadedUsers[ASSET_BATCH_EVENT_MAX];
/** Fires each time a single entry finishes loading. params = assetentry_t * */
event_t onEntryLoaded;
eventcallback_t onEntryLoadedCallbacks[ASSET_BATCH_EVENT_MAX];
void *onEntryLoadedUsers[ASSET_BATCH_EVENT_MAX];
/** Fires once when all entries have finished (any with errors). params = assetbatch_t * */
event_t onError;
eventcallback_t onErrorCallbacks[ASSET_BATCH_EVENT_MAX];
void *onErrorUsers[ASSET_BATCH_EVENT_MAX];
/** Fires each time a single entry errors. params = assetentry_t * */
event_t onEntryError;
eventcallback_t onEntryErrorCallbacks[ASSET_BATCH_EVENT_MAX];
void *onEntryErrorUsers[ASSET_BATCH_EVENT_MAX];
} assetbatch_t; } assetbatch_t;
/** /**
+42
View File
@@ -117,6 +117,48 @@ errorret_t assetFileDispose(assetfile_t *file) {
errorOk(); errorOk();
} }
errorret_t assetFileReadEntire(
assetfile_t *file,
uint8_t **outBuffer,
size_t *outSize
) {
assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(outBuffer, "outBuffer cannot be NULL.");
assertNotNull(outSize, "outSize cannot be NULL.");
assertTrue(file->size > 0, "Asset file has no size; call assetFileInit first.");
// File should be closed currently.
assertNull(file->zipFile, "Asset file must be closed before reading entire.");
// Open file
errorret_t ret = assetFileOpen(file);
if(errorIsNotOk(ret)) {
errorChain(ret);
}
// Set output.
size_t size = (size_t)file->size;
uint8_t *buffer = (uint8_t *)memoryAllocate(size);
// Read entire file.
ret = assetFileRead(file, buffer, size);
if(errorIsNotOk(ret)) {
memoryFree(buffer);
errorChain(ret);
}
// Close the file.
ret = assetFileClose(file);
if(errorIsNotOk(ret)) {
memoryFree(buffer);
errorChain(ret);
}
*outBuffer = buffer;
*outSize = size;
errorOk();
}
// Line Reader; // Line Reader;
void assetFileLineReaderInit( void assetFileLineReaderInit(
assetfilelinereader_t *reader, assetfilelinereader_t *reader,
+15
View File
@@ -92,6 +92,21 @@ errorret_t assetFileClose(assetfile_t *file);
*/ */
errorret_t assetFileDispose(assetfile_t *file); errorret_t assetFileDispose(assetfile_t *file);
/**
* Reads the entire contents of the asset file into a newly allocated buffer.
* The caller is responsible for freeing the buffer with memoryFree.
*
* @param file The asset file to read. Must be initialized but not open.
* @param outBuffer Receives a pointer to the allocated buffer.
* @param outSize Receives the number of bytes written to the buffer.
* @return An error code if the file could not be read.
*/
errorret_t assetFileReadEntire(
assetfile_t *file,
uint8_t **outBuffer,
size_t *outSize
);
typedef struct { typedef struct {
assetfile_t *file; assetfile_t *file;
uint8_t *readBuffer; uint8_t *readBuffer;
+33 -2
View File
@@ -21,24 +21,49 @@ void assetEntryInit(
assertStrLenMax(name, ASSET_FILE_NAME_MAX - 1, "Name too long"); assertStrLenMax(name, ASSET_FILE_NAME_MAX - 1, "Name too long");
assertTrue(type != ASSET_LOADER_TYPE_NULL, "Invalid loader type."); assertTrue(type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertTrue(type < ASSET_LOADER_TYPE_COUNT, "Invalid loader type."); assertTrue(type < ASSET_LOADER_TYPE_COUNT, "Invalid loader type.");
assertIsMainThread("Must be called from the main thread.");
memoryZero(entry, sizeof(assetentry_t)); memoryZero(entry, sizeof(assetentry_t));
stringCopy(entry->name, name, ASSET_FILE_NAME_MAX); stringCopy(entry->name, name, ASSET_FILE_NAME_MAX);
entry->type = type; entry->type = type;
entry->input = input;
entry->state = ASSET_ENTRY_STATE_NOT_STARTED; entry->state = ASSET_ENTRY_STATE_NOT_STARTED;
if(input) {
entry->inputData = *input;
entry->input = &entry->inputData;
} else {
memoryZero(&entry->inputData, sizeof(assetloaderinput_t));
entry->input = NULL;
}
refInit(&entry->refs, entry, NULL, NULL, NULL); refInit(&entry->refs, entry, NULL, NULL, NULL);
eventInit(
&entry->onLoaded,
entry->onLoadedCallbacks, entry->onLoadedUsers,
ASSET_ENTRY_EVENT_MAX
);
eventInit(
&entry->onUnloaded,
entry->onUnloadedCallbacks, entry->onUnloadedUsers,
ASSET_ENTRY_EVENT_MAX
);
eventInit(
&entry->onError,
entry->onErrorCallbacks, entry->onErrorUsers,
ASSET_ENTRY_EVENT_MAX
);
} }
void assetEntryLock(assetentry_t *entry) { void assetEntryLock(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL"); assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type."); assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
refLock(&entry->refs); refLock(&entry->refs);
} }
void assetEntryUnlock(assetentry_t *entry) { void assetEntryUnlock(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL"); assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type."); assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
refUnlock(&entry->refs); refUnlock(&entry->refs);
} }
@@ -53,6 +78,7 @@ void assetEntryStartLoading(
entry->state == ASSET_ENTRY_STATE_NOT_STARTED, entry->state == ASSET_ENTRY_STATE_NOT_STARTED,
"Can only start loading from NOT_STARTED state." "Can only start loading from NOT_STARTED state."
); );
assertIsMainThread("Must be called from the main thread.");
entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
memoryZero(&loading->loading, sizeof(assetloaderloading_t)); memoryZero(&loading->loading, sizeof(assetloaderloading_t));
@@ -63,10 +89,15 @@ void assetEntryStartLoading(
errorret_t assetEntryDispose(assetentry_t *entry) { errorret_t assetEntryDispose(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL"); assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type."); assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertTrue(entry->type < ASSET_LOADER_TYPE_COUNT, "Invalid loader type."); assertTrue(entry->type < ASSET_LOADER_TYPE_COUNT, "Invalid loader type.");
assertIsMainThread("Must be called from the main thread.");
assertTrue(
entry->refs.count == 0,
"Asset entry still refed at dispose time."
);
eventInvoke(&entry->onUnloaded, entry);
errorChain(ASSET_LOADER_CALLBACKS[entry->type].dispose(entry)); errorChain(ASSET_LOADER_CALLBACKS[entry->type].dispose(entry));
memoryZero(entry, sizeof(assetentry_t)); memoryZero(entry, sizeof(assetentry_t));
errorOk(); errorOk();
+34 -8
View File
@@ -7,6 +7,7 @@
#pragma once #pragma once
#include "asset/loader/assetloading.h" #include "asset/loader/assetloading.h"
#include "event/event.h"
#include "util/ref.h" #include "util/ref.h"
typedef enum { typedef enum {
@@ -19,20 +20,44 @@ typedef enum {
ASSET_ENTRY_STATE_ERROR ASSET_ENTRY_STATE_ERROR
} assetentrystate_t; } assetentrystate_t;
typedef struct assetentry_s { /** Maximum number of subscribers for each per-entry event. */
// Filename and cache key #define ASSET_ENTRY_EVENT_MAX 2
typedef struct assetentry_s assetentry_t;
struct assetentry_s {
char_t name[ASSET_FILE_NAME_MAX]; char_t name[ASSET_FILE_NAME_MAX];
// What type of asset is this?
assetloadertype_t type; assetloadertype_t type;
// Data
assetloaderoutput_t data; assetloaderoutput_t data;
// What state is this asset entry in currently?
assetentrystate_t state; assetentrystate_t state;
// What is referencing this asset entry.
ref_t refs; ref_t refs;
// Data that will be passed to the loader about how it should load.
assetloaderinput_t *input; assetloaderinput_t *input;
} assetentry_t; assetloaderinput_t inputData;
/**
* Fired once when loading completes successfully (params = assetentry_t *).
* Always invoked on the main thread.
*/
event_t onLoaded;
eventcallback_t onLoadedCallbacks[ASSET_ENTRY_EVENT_MAX];
void *onLoadedUsers[ASSET_ENTRY_EVENT_MAX];
/**
* Fired once when the entry is disposed/reaped (params = assetentry_t *).
* The asset data is still accessible when the callback runs.
* Always invoked on the main thread.
*/
event_t onUnloaded;
eventcallback_t onUnloadedCallbacks[ASSET_ENTRY_EVENT_MAX];
void *onUnloadedUsers[ASSET_ENTRY_EVENT_MAX];
/**
* Fired once when loading fails (params = assetentry_t *).
* Always invoked on the main thread.
*/
event_t onError;
eventcallback_t onErrorCallbacks[ASSET_ENTRY_EVENT_MAX];
void *onErrorUsers[ASSET_ENTRY_EVENT_MAX];
};
/** /**
* Initializes an asset entry with the given name and type. This does not load * Initializes an asset entry with the given name and type. This does not load
@@ -79,6 +104,7 @@ void assetEntryStartLoading(assetentry_t *entry, assetloading_t *loading);
/** /**
* Disposes an asset entry, freeing any resources it holds. * Disposes an asset entry, freeing any resources it holds.
* Fires the onUnloaded event before releasing asset data.
* *
* @param entry The asset entry to dispose. * @param entry The asset entry to dispose.
* @return Any error that occurs during disposal. * @return Any error that occurs during disposal.
-4
View File
@@ -13,13 +13,9 @@
typedef struct assetentry_s assetentry_t; typedef struct assetentry_s assetentry_t;
typedef struct assetloading_s { typedef struct assetloading_s {
// Protects entry pointer and entry->state from concurrent access.
threadmutex_t mutex; threadmutex_t mutex;
// What type of asset is being loaded.
assetloadertype_t type; assetloadertype_t type;
// Referral back to the asset entry that will be kept alive after load done.
assetentry_t *entry; assetentry_t *entry;
// Information used during the load operation only.
assetloaderloading_t loading; assetloaderloading_t loading;
} assetloading_t; } assetloading_t;
@@ -21,7 +21,7 @@ errorret_t assetMeshLoaderAsync(assetloading_t *loading) {
assetmeshoutput_t *out = &loading->entry->data.mesh; assetmeshoutput_t *out = &loading->entry->data.mesh;
assetfile_t *file = &loading->loading.mesh.file; assetfile_t *file = &loading->loading.mesh.file;
assetmeshinputaxis_t axis = loading->entry->input->mesh; assetmeshinputaxis_t axis = loading->entry->inputData.mesh;
assetLoaderErrorChain(loading, assetFileInit(file, loading->entry->name, NULL, NULL)); assetLoaderErrorChain(loading, assetFileInit(file, loading->entry->name, NULL, NULL));
assetLoaderErrorChain(loading, assetFileOpen(file)); assetLoaderErrorChain(loading, assetFileOpen(file));
@@ -54,6 +54,7 @@ int assetTextureEOF(void *user) {
errorret_t assetTextureLoaderAsync(assetloading_t *loading) { errorret_t assetTextureLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL"); assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread.");
// Only care about loading pixels. // Only care about loading pixels.
if(loading->loading.texture.state != ASSET_TEXTURE_LOADING_STATE_LOAD_PIXELS){ if(loading->loading.texture.state != ASSET_TEXTURE_LOADING_STATE_LOAD_PIXELS){
@@ -76,7 +77,7 @@ errorret_t assetTextureLoaderAsync(assetloading_t *loading) {
// Determine channels // Determine channels
int channelsDesired; int channelsDesired;
switch(loading->entry->input->texture) { switch(loading->entry->inputData.texture) {
case TEXTURE_FORMAT_RGBA: case TEXTURE_FORMAT_RGBA:
channelsDesired = 4; channelsDesired = 4;
break; break;
@@ -122,6 +123,7 @@ errorret_t assetTextureLoaderAsync(assetloading_t *loading) {
errorret_t assetTextureLoaderSync(assetloading_t *loading) { errorret_t assetTextureLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL"); assertNotNull(loading, "Loading cannot be NULL");
assertIsMainThread("Must be called from the main thread.");
switch(loading->loading.texture.state) { switch(loading->loading.texture.state) {
case ASSET_TEXTURE_LOADING_STATE_INITIAL: case ASSET_TEXTURE_LOADING_STATE_INITIAL:
@@ -146,7 +148,7 @@ errorret_t assetTextureLoaderSync(assetloading_t *loading) {
(texture_t*)&loading->entry->data.texture, (texture_t*)&loading->entry->data.texture,
loading->loading.texture.width, loading->loading.texture.width,
loading->loading.texture.height, loading->loading.texture.height,
loading->entry->input->texture, loading->entry->inputData.texture,
(texturedata_t){ (texturedata_t){
.rgbaColors = (color_t*)loading->loading.texture.data .rgbaColors = (color_t*)loading->loading.texture.data
} }
@@ -161,5 +163,7 @@ errorret_t assetTextureLoaderSync(assetloading_t *loading) {
errorret_t assetTextureDispose(assetentry_t *entry) { errorret_t assetTextureDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL"); assertNotNull(entry, "Asset entry cannot be NULL");
assertIsMainThread("Must be called from the main thread.");
return textureDispose(&entry->data.texture); return textureDispose(&entry->data.texture);
} }
@@ -14,6 +14,7 @@
errorret_t assetTilesetLoaderAsync(assetloading_t *loading) { errorret_t assetTilesetLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL"); assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread.");
if(loading->loading.tileset.state != ASSET_TILESET_LOADING_STATE_READ_FILE) { if(loading->loading.tileset.state != ASSET_TILESET_LOADING_STATE_READ_FILE) {
errorOk(); errorOk();
@@ -40,6 +41,7 @@ errorret_t assetTilesetLoaderAsync(assetloading_t *loading) {
errorret_t assetTilesetLoaderSync(assetloading_t *loading) { errorret_t assetTilesetLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL"); assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_TILESET, "Invalid type."); assertTrue(loading->type == ASSET_LOADER_TYPE_TILESET, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
switch(loading->loading.tileset.state) { switch(loading->loading.tileset.state) {
case ASSET_TILESET_LOADING_STATE_INITIAL: case ASSET_TILESET_LOADING_STATE_INITIAL:
@@ -111,5 +113,7 @@ errorret_t assetTilesetLoaderSync(assetloading_t *loading) {
errorret_t assetTilesetDispose(assetentry_t *entry) { errorret_t assetTilesetDispose(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL"); assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_TILESET, "Invalid type."); assertTrue(entry->type == ASSET_LOADER_TYPE_TILESET, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
errorOk(); errorOk();
} }
@@ -13,6 +13,7 @@
errorret_t assetJsonLoaderAsync(assetloading_t *loading) { errorret_t assetJsonLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL"); assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Async loader should not be on main thread.");
if(loading->loading.json.state != ASSET_JSON_LOADING_STATE_READ_FILE) { if(loading->loading.json.state != ASSET_JSON_LOADING_STATE_READ_FILE) {
errorOk(); errorOk();
@@ -45,6 +46,7 @@ errorret_t assetJsonLoaderAsync(assetloading_t *loading) {
errorret_t assetJsonLoaderSync(assetloading_t *loading) { errorret_t assetJsonLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL"); assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_JSON, "Invalid type."); assertTrue(loading->type == ASSET_LOADER_TYPE_JSON, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
switch(loading->loading.json.state) { switch(loading->loading.json.state) {
case ASSET_JSON_LOADING_STATE_INITIAL: case ASSET_JSON_LOADING_STATE_INITIAL:
@@ -82,7 +84,10 @@ errorret_t assetJsonLoaderSync(assetloading_t *loading) {
errorret_t assetJsonDispose(assetentry_t *entry) { errorret_t assetJsonDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL"); assertNotNull(entry, "Asset entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_JSON, "Invalid type."); assertTrue(entry->type == ASSET_LOADER_TYPE_JSON, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
yyjson_doc_free(entry->data.json); yyjson_doc_free(entry->data.json);
entry->data.json = NULL; entry->data.json = NULL;
errorOk(); errorOk();
} }
@@ -16,6 +16,7 @@
errorret_t assetLocaleLoaderAsync(assetloading_t *loading) { errorret_t assetLocaleLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL"); assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Async loader should not be on main thread.");
if(loading->loading.locale.state != ASSET_LOCALE_LOADER_STATE_LOAD_HEADER) { if(loading->loading.locale.state != ASSET_LOCALE_LOADER_STATE_LOAD_HEADER) {
errorOk(); errorOk();
@@ -38,6 +39,7 @@ errorret_t assetLocaleLoaderAsync(assetloading_t *loading) {
errorret_t assetLocaleLoaderSync(assetloading_t *loading) { errorret_t assetLocaleLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL"); assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_LOCALE, "Invalid type."); assertTrue(loading->type == ASSET_LOADER_TYPE_LOCALE, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
switch(loading->loading.locale.state) { switch(loading->loading.locale.state) {
case ASSET_LOCALE_LOADER_STATE_INITIAL: case ASSET_LOCALE_LOADER_STATE_INITIAL:
@@ -60,11 +62,14 @@ errorret_t assetLocaleLoaderSync(assetloading_t *loading) {
errorret_t assetLocaleDispose(assetentry_t *entry) { errorret_t assetLocaleDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL"); assertNotNull(entry, "Asset entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_LOCALE, "Invalid type."); assertTrue(entry->type == ASSET_LOADER_TYPE_LOCALE, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
assetlocalefile_t *localeFile = &entry->data.locale; assetlocalefile_t *localeFile = &entry->data.locale;
errorChain(assetFileClose(&localeFile->file)); errorChain(assetFileClose(&localeFile->file));
return assetFileDispose(&localeFile->file); return assetFileDispose(&localeFile->file);
} }
// These functions probably need some cleaning;
errorret_t assetLocaleParseHeader( errorret_t assetLocaleParseHeader(
assetlocalefile_t *localeFile, assetlocalefile_t *localeFile,
char_t *headerBuffer, char_t *headerBuffer,
@@ -9,12 +9,14 @@
#include "asset/loader/assetloading.h" #include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h" #include "asset/loader/assetentry.h"
#include "asset/loader/assetloader.h" #include "asset/loader/assetloader.h"
#include "script/module/require/modulerequire.h"
#include "util/memory.h" #include "util/memory.h"
#include "assert/assert.h" #include "assert/assert.h"
#include <jerryscript.h> #include <jerryscript.h>
errorret_t assetScriptLoaderAsync(assetloading_t *loading) { errorret_t assetScriptLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL"); assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Async loader should not be on main thread.");
if(loading->loading.script.state != ASSET_SCRIPT_LOADING_STATE_READ_FILE) { if(loading->loading.script.state != ASSET_SCRIPT_LOADING_STATE_READ_FILE) {
errorOk(); errorOk();
@@ -23,33 +25,21 @@ errorret_t assetScriptLoaderAsync(assetloading_t *loading) {
assertNull(loading->loading.script.buffer, "Buffer already defined?"); assertNull(loading->loading.script.buffer, "Buffer already defined?");
assetfile_t *file = &loading->loading.script.file; assetfile_t *file = &loading->loading.script.file;
assetLoaderErrorChain(loading, assetFileInit(file, loading->entry->name, NULL, NULL)); assetLoaderErrorChain(
assetLoaderErrorChain(loading, assetFileOpen(file)); loading, assetFileInit(file, loading->entry->name, NULL, NULL)
);
size_t capacity = ASSET_SCRIPT_CHUNK_SIZE; uint8_t *buffer = NULL;
uint8_t *buffer = memoryAllocate(capacity + 1); size_t size = 0;
size_t offset = 0; assetLoaderErrorChain(loading, assetFileReadEntire(file, &buffer, &size));
while(1) {
if(offset + ASSET_SCRIPT_CHUNK_SIZE > capacity) {
size_t oldCapacity = capacity + 1;
capacity += ASSET_SCRIPT_CHUNK_SIZE;
memoryResize((void **)&buffer, oldCapacity, capacity + 1);
}
assetLoaderErrorChain(loading, assetFileRead(
file, buffer + offset, ASSET_SCRIPT_CHUNK_SIZE
));
size_t chunk = (size_t)file->lastRead;
offset += chunk;
if(chunk == 0) break;
}
buffer[offset] = '\0';
assetLoaderErrorChain(loading, assetFileClose(file));
assetLoaderErrorChain(loading, assetFileDispose(file)); assetLoaderErrorChain(loading, assetFileDispose(file));
// Null-terminate for jerry_eval.
memoryResize((void **)&buffer, size, size + 1);
buffer[size] = '\0';
loading->loading.script.buffer = buffer; loading->loading.script.buffer = buffer;
loading->loading.script.size = offset; loading->loading.script.size = size;
loading->loading.script.state = ASSET_SCRIPT_LOADING_STATE_EXEC; loading->loading.script.state = ASSET_SCRIPT_LOADING_STATE_EXEC;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk(); errorOk();
@@ -58,6 +48,7 @@ errorret_t assetScriptLoaderAsync(assetloading_t *loading) {
errorret_t assetScriptLoaderSync(assetloading_t *loading) { errorret_t assetScriptLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL"); assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_SCRIPT, "Invalid type."); assertTrue(loading->type == ASSET_LOADER_TYPE_SCRIPT, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
switch(loading->loading.script.state) { switch(loading->loading.script.state) {
case ASSET_SCRIPT_LOADING_STATE_INITIAL: case ASSET_SCRIPT_LOADING_STATE_INITIAL:
@@ -73,34 +64,58 @@ errorret_t assetScriptLoaderSync(assetloading_t *loading) {
errorOk(); errorOk();
} }
// Get read buffer
uint8_t *buffer = loading->loading.script.buffer; uint8_t *buffer = loading->loading.script.buffer;
assertNotNull(buffer, "Script buffer should have been loaded by now."); assertNotNull(buffer, "Script buffer should have been loaded by now.");
// Get the current global script realm
jerry_value_t global = jerry_current_realm();
// Replace globalThis.module with a new `module = {}`
jerry_value_t oldModule = jerry_object_get_sz(global, "module");
jerry_value_t module = jerry_object();
jerry_object_set_sz(global, "module", module);
// Eval the script, we handle failure later down the code.
jerry_value_t result = jerry_eval( jerry_value_t result = jerry_eval(
(const jerry_char_t *)buffer, buffer,
loading->loading.script.size, loading->loading.script.size,
JERRY_PARSE_NO_OPTS JERRY_PARSE_NO_OPTS
); );
// Free the read buffer
memoryFree(buffer); memoryFree(buffer);
loading->loading.script.buffer = NULL; loading->loading.script.buffer = NULL;
// Restore globalThis.module
jerry_object_set_sz(global, "module", oldModule);
jerry_value_free(oldModule);
jerry_value_free(global);
if(jerry_value_is_exception(result)) { if(jerry_value_is_exception(result)) {
jerry_value_t errVal = jerry_exception_value(result, false); jerry_value_free(module);
jerry_value_t errStr = jerry_value_to_string(errVal);
loading->entry->data.script.exports = jerry_undefined();
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
// Get error string
char_t buf[256]; char_t buf[256];
jerry_size_t len = jerry_string_to_buffer( moduleBaseExceptionMessage(result, buf, sizeof(buf));
errStr, JERRY_ENCODING_UTF8, (jerry_char_t *)buf, sizeof(buf) - 1
);
buf[len] = '\0';
jerry_value_free(errStr);
jerry_value_free(errVal);
jerry_value_free(result); jerry_value_free(result);
assetLoaderErrorThrow(loading, "Script error in '%s': %s", assetLoaderErrorThrow(
loading->entry->name, buf loading,
"Script execution failed: %s: %s", loading->entry->name, buf
); );
} }
loading->entry->data.script = (assetscriptoutput_t)result; // Get module.exports
jerry_value_t exports = jerry_object_get_sz(module, "exports");
jerry_value_free(result);
jerry_value_free(module);
// Store the exports.
loading->entry->data.script.exports = exports;
loading->entry->state = ASSET_ENTRY_STATE_LOADED; loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk(); errorOk();
} }
@@ -108,9 +123,15 @@ errorret_t assetScriptLoaderSync(assetloading_t *loading) {
errorret_t assetScriptDispose(assetentry_t *entry) { errorret_t assetScriptDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL"); assertNotNull(entry, "Asset entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_SCRIPT, "Invalid type."); assertTrue(entry->type == ASSET_LOADER_TYPE_SCRIPT, "Invalid type.");
if(entry->data.script != 0) { assertIsMainThread("Must be called from the main thread.");
jerry_value_free((jerry_value_t)entry->data.script);
entry->data.script = 0; if(
entry->data.script.exports != 0 &&
!jerry_value_is_undefined(entry->data.script.exports)
) {
jerry_value_free(entry->data.script.exports);
entry->data.script.exports = 0;
} }
errorOk(); errorOk();
} }
@@ -7,14 +7,18 @@
#pragma once #pragma once
#include "asset/assetfile.h" #include "asset/assetfile.h"
#include "script/scriptmodule.h"
#define ASSET_SCRIPT_CHUNK_SIZE 1024 #define ASSET_SCRIPT_CHUNK_SIZE 1024
typedef struct assetloading_s assetloading_t; typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t; typedef struct assetentry_s assetentry_t;
typedef struct { void *nothing; } assetscriptloaderinput_t; typedef struct {
typedef uint32_t assetscriptoutput_t; void *nothing;
} assetscriptloaderinput_t;
typedef scriptmodule_t assetscriptoutput_t;
typedef enum { typedef enum {
ASSET_SCRIPT_LOADING_STATE_INITIAL, ASSET_SCRIPT_LOADING_STATE_INITIAL,
@@ -30,6 +34,27 @@ typedef struct {
size_t size; size_t size;
} assetscriptloaderloading_t; } assetscriptloaderloading_t;
/**
* Asynchronous loader for a script asset/module.
*
* @param loading The loading context.
* @returns An error code and state.
*/
errorret_t assetScriptLoaderAsync(assetloading_t *loading); errorret_t assetScriptLoaderAsync(assetloading_t *loading);
/**
* Synchronous loader for a script asset/module. This executes the script after
* it has been loaded by the async loader.
*
* @param loading The loading context.
* @returns An error code and state.
*/
errorret_t assetScriptLoaderSync(assetloading_t *loading); errorret_t assetScriptLoaderSync(assetloading_t *loading);
/**
* Disposes of a loaded script asset/module.
*
* @param entry The asset entry to dispose.
* @returns An error code and state.
*/
errorret_t assetScriptDispose(assetentry_t *entry); errorret_t assetScriptDispose(assetentry_t *entry);
+6 -6
View File
@@ -30,6 +30,7 @@
engine_t ENGINE; engine_t ENGINE;
errorret_t engineInit(const int32_t argc, const char_t **argv) { errorret_t engineInit(const int32_t argc, const char_t **argv) {
assertInit();
memoryZero(&ENGINE, sizeof(engine_t)); memoryZero(&ENGINE, sizeof(engine_t));
ENGINE.running = true; ENGINE.running = true;
ENGINE.argc = argc; ENGINE.argc = argc;
@@ -47,18 +48,16 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
errorChain(displayInit()); errorChain(displayInit());
errorChain(uiInit()); errorChain(uiInit());
errorChain(uiTextboxInit()); errorChain(uiTextboxInit());
errorChain(cutsceneInit()); errorChain(cutsceneInit());
errorChain(sceneInit());
entityManagerInit(); entityManagerInit();
backpackInit(); backpackInit();
physicsManagerInit(); physicsManagerInit();
errorChain(networkInit()); errorChain(networkInit());
errorChain(scriptInit()); errorChain(scriptInit());
errorChain(scriptExecFile("init.js")); errorChain(sceneInit());
consolePrint("Engine initialized"); consolePrint("Engine initialized");
errorChain(scriptExecFile("init.js"));
errorOk(); errorOk();
} }
@@ -77,6 +76,7 @@ errorret_t engineUpdate(void) {
errorChain(cutsceneUpdate()); errorChain(cutsceneUpdate());
errorChain(sceneUpdate()); errorChain(sceneUpdate());
errorChain(assetUpdate()); errorChain(assetUpdate());
errorChain(scriptUpdate());
if(inputPressed(INPUT_ACTION_RAGEQUIT)) ENGINE.running = false; if(inputPressed(INPUT_ACTION_RAGEQUIT)) ENGINE.running = false;
errorOk(); errorOk();
@@ -89,7 +89,8 @@ void engineExit(void) {
errorret_t engineDispose(void) { errorret_t engineDispose(void) {
uiTextboxDispose(); uiTextboxDispose();
cutsceneDispose(); cutsceneDispose();
sceneDispose(); errorChain(sceneDispose());
errorChain(scriptDispose());
errorChain(networkDispose()); errorChain(networkDispose());
entityManagerDispose(); entityManagerDispose();
localeManagerDispose(); localeManagerDispose();
@@ -98,7 +99,6 @@ errorret_t engineDispose(void) {
errorChain(displayDispose()); errorChain(displayDispose());
// errorChain(saveDispose()); // errorChain(saveDispose());
errorChain(assetDispose()); errorChain(assetDispose());
errorChain(scriptDispose());
errorOk(); errorOk();
} }
-2
View File
@@ -4,7 +4,5 @@
# https://opensource.org/licenses/MIT # https://opensource.org/licenses/MIT
add_subdirectory(display) add_subdirectory(display)
add_subdirectory(overworld)
add_subdirectory(physics) add_subdirectory(physics)
add_subdirectory(script)
add_subdirectory(trigger) add_subdirectory(trigger)
@@ -36,7 +36,6 @@ void entityRenderableDispose(
const entityid_t entityId, const entityid_t entityId,
const componentid_t componentId const componentid_t componentId
) { ) {
} }
void entityRenderableSetType( void entityRenderableSetType(
@@ -80,6 +79,49 @@ void entityRenderableSetDraw(
r->data.custom.drawUser = user; r->data.custom.drawUser = user;
} }
static errorret_t entityRenderableDrawSpritebatch(
const entityrenderablespritebatch_t *sb
) {
if(sb->spriteCount == 0) errorOk();
errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_BLEND
}));
spriteBatchClear();
shadermaterial_t mat;
memoryZero(&mat, sizeof(shadermaterial_t));
mat.unlit.texture = sb->texture;
mat.unlit.color = COLOR_WHITE;
errorChain(spriteBatchBuffer(
sb->sprites, sb->spriteCount,
SHADER_LIST_DEFS[SHADER_LIST_SHADER_UNLIT].shader, mat
));
return spriteBatchFlush();
}
static errorret_t entityRenderableDrawMaterial(
const entityrenderablematerial_t *m
) {
errorChain(displaySetState(m->state));
shader_t *shader = SHADER_LIST_DEFS[m->shaderType].shader;
assertNotNull(shader, "Shader cannot be null for material type");
errorChain(shaderBind(shader));
errorChain(shaderSetMaterial(shader, &m->material));
for(uint8_t i = 0; i < m->meshCount; i++) {
errorChain(meshDraw(m->meshes[i], m->meshOffsets[i], m->meshCounts[i]));
}
errorOk();
}
static errorret_t entityRenderableDrawCustom(
const entityid_t entityId,
const componentid_t componentId,
const entityrenderablecustom_t *custom
) {
return custom->draw(entityId, componentId, custom->drawUser);
}
errorret_t entityRenderableDraw( errorret_t entityRenderableDraw(
const entityid_t entityId, const entityid_t entityId,
const componentid_t componentId const componentid_t componentId
@@ -87,41 +129,13 @@ errorret_t entityRenderableDraw(
entityrenderable_t *r = componentGetData( entityrenderable_t *r = componentGetData(
entityId, componentId, COMPONENT_TYPE_RENDERABLE entityId, componentId, COMPONENT_TYPE_RENDERABLE
); );
switch(r->type) { switch(r->type) {
case ENTITY_RENDERABLE_TYPE_SPRITEBATCH: { case ENTITY_RENDERABLE_TYPE_SPRITEBATCH:
const entityrenderablespritebatch_t *sb = &r->data.spritebatch; return entityRenderableDrawSpritebatch(&r->data.spritebatch);
errorChain(displaySetState((displaystate_t){ case ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL:
.flags = DISPLAY_STATE_FLAG_BLEND return entityRenderableDrawMaterial(&r->data.material);
}));
spriteBatchClear();
shadermaterial_t mat;
memoryZero(&mat, sizeof(shadermaterial_t));
mat.unlit.texture = sb->texture;
mat.unlit.color = COLOR_WHITE;
errorChain(spriteBatchBuffer(
sb->sprites, sb->spriteCount,
SHADER_LIST_DEFS[SHADER_LIST_SHADER_UNLIT].shader, mat
));
return spriteBatchFlush();
}
case ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL: {
const entityrenderablematerial_t *m = &r->data.material;
errorChain(displaySetState(m->state));
shader_t *shader = SHADER_LIST_DEFS[m->shaderType].shader;
assertNotNull(shader, "Shader cannot be null for material type");
errorChain(shaderBind(shader));
errorChain(shaderSetMaterial(shader, &m->material));
for(uint8_t i = 0; i < m->meshCount; i++) {
errorChain(meshDraw(m->meshes[i], m->meshOffsets[i], m->meshCounts[i]));
}
errorOk();
}
case ENTITY_RENDERABLE_TYPE_CUSTOM: case ENTITY_RENDERABLE_TYPE_CUSTOM:
return r->data.custom.draw(entityId, componentId, r->data.custom.drawUser); return entityRenderableDrawCustom(entityId, componentId, &r->data.custom);
default: default:
assertUnreachable("Invalid renderable type"); assertUnreachable("Invalid renderable type");
} }
@@ -1,49 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entityinteractable.h"
#include "entity/entitymanager.h"
void entityInteractableInit(
const entityid_t entityId,
const componentid_t componentId
) {
entityinteractable_t *inter = entityInteractableGet(entityId, componentId);
inter->onInteract = NULL;
inter->user = NULL;
}
entityinteractable_t * entityInteractableGet(
const entityid_t entityId,
const componentid_t componentId
) {
return componentGetData(entityId, componentId, COMPONENT_TYPE_INTERACTABLE);
}
void entityInteractableSetCallback(
const entityid_t entityId,
const componentid_t componentId,
void (*onInteract)(
const entityid_t entityId,
const componentid_t componentId,
void *user
),
void *user
) {
entityinteractable_t *inter = entityInteractableGet(entityId, componentId);
inter->onInteract = onInteract;
inter->user = user;
}
void entityInteractableTrigger(
const entityid_t entityId,
const componentid_t componentId
) {
entityinteractable_t *inter = entityInteractableGet(entityId, componentId);
if(inter->onInteract == NULL) return;
inter->onInteract(entityId, componentId, inter->user);
}
@@ -1,71 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entity/entitybase.h"
typedef struct {
void (*onInteract)(
const entityid_t entityId,
const componentid_t componentId,
void *user
);
void *user;
} entityinteractable_t;
/**
* Initializes the interactable component, clearing the callback and user pointer.
*
* @param entityId The owning entity.
* @param componentId This component's ID.
*/
void entityInteractableInit(
const entityid_t entityId,
const componentid_t componentId
);
/**
* Returns a pointer to the interactable component data.
*
* @param entityId The owning entity.
* @param componentId This component's ID.
* @return Pointer to the entityinteractable_t data.
*/
entityinteractable_t * entityInteractableGet(
const entityid_t entityId,
const componentid_t componentId
);
/**
* Sets the callback invoked when this interactable is triggered.
*
* @param entityId The owning entity.
* @param componentId This component's ID.
* @param onInteract Function called on interaction, or NULL to clear.
* @param user Arbitrary pointer forwarded to the callback.
*/
void entityInteractableSetCallback(
const entityid_t entityId,
const componentid_t componentId,
void (*onInteract)(
const entityid_t entityId,
const componentid_t componentId,
void *user
),
void *user
);
/**
* Fires the interactable's callback if one is set.
*
* @param entityId The owning entity.
* @param componentId This component's ID.
*/
void entityInteractableTrigger(
const entityid_t entityId,
const componentid_t componentId
);
@@ -1,55 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entityoverworld.h"
#include "entity/entitymanager.h"
#include "entity/component/display/entityrenderable.h"
#include "display/shader/shaderunlit.h"
#include "display/mesh/cube.h"
void entityOverworldInit(
const entityid_t entityId,
const componentid_t componentId
) {
entityoverworld_t *ow = entityOverworldGet(entityId, componentId);
ow->type = OVERWORLD_ENTITY_TYPE_NPC;
ow->facing = FACING_DIR_DOWN;
ow->renderCompId = entityGetComponent(entityId, COMPONENT_TYPE_RENDERABLE);
if(ow->renderCompId != COMPONENT_ID_INVALID) {
entityRenderableSetDraw(entityId, ow->renderCompId, entityOverworldDraw, NULL);
}
ow->physCompId = entityGetComponent(entityId, COMPONENT_TYPE_PHYSICS);
}
entityoverworld_t * entityOverworldGet(
const entityid_t entityId,
const componentid_t componentId
) {
return componentGetData(entityId, componentId, COMPONENT_TYPE_OVERWORLD);
}
void entityOverworldSetType(
const entityid_t entityId,
const componentid_t componentId,
const entityoverworldtype_t type
) {
entityOverworldGet(entityId, componentId)->type = type;
}
errorret_t entityOverworldDraw(
const entityid_t entityId,
const componentid_t componentId,
void *user
) {
componentid_t owCompId = entityGetComponent(entityId, COMPONENT_TYPE_OVERWORLD);
entityoverworld_t *ow = entityOverworldGet(entityId, owCompId);
color_t col = ow->type == OVERWORLD_ENTITY_TYPE_PLAYER ? COLOR_WHITE : COLOR_BLUE;
errorChain(shaderSetColor(&SHADER_UNLIT, SHADER_UNLIT_COLOR, col));
errorChain(shaderSetTexture(&SHADER_UNLIT, SHADER_UNLIT_TEXTURE, NULL));
return meshDraw(&CUBE_MESH_SIMPLE, 0, -1);
}
@@ -1,74 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "entity/entitybase.h"
#include "overworld/facingdir.h"
typedef enum {
OVERWORLD_ENTITY_TYPE_PLAYER = 0,
OVERWORLD_ENTITY_TYPE_NPC = 1,
} entityoverworldtype_t;
typedef struct {
entityoverworldtype_t type;
facingdir_t facing;
componentid_t renderCompId;
componentid_t physCompId;
} entityoverworld_t;
/**
* Initializes the overworld component, wiring up the draw callback if a
* renderable component is already present on the entity.
*
* @param entityId The owning entity.
* @param componentId This component's ID.
*/
void entityOverworldInit(
const entityid_t entityId,
const componentid_t componentId
);
/**
* Returns a pointer to the overworld component data.
*
* @param entityId The owning entity.
* @param componentId This component's ID.
* @return Pointer to the entityoverworld_t data.
*/
entityoverworld_t * entityOverworldGet(
const entityid_t entityId,
const componentid_t componentId
);
/**
* Sets the overworld entity type.
*
* @param entityId The owning entity.
* @param componentId This component's ID.
* @param type The type to assign.
*/
void entityOverworldSetType(
const entityid_t entityId,
const componentid_t componentId,
const entityoverworldtype_t type
);
/**
* Draw callback registered on the renderable component.
*
* @param entityId The owning entity.
* @param componentId The renderable component's ID.
* @param user Unused.
* @return Error result.
*/
errorret_t entityOverworldDraw(
const entityid_t entityId,
const componentid_t componentId,
void *user
);
@@ -1,66 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entityoverworldcamera.h"
#include "entity/entitymanager.h"
#include "entity/component/display/entityposition.h"
#include "entity/component/display/entitycamera.h"
void entityOverworldCameraInit(
const entityid_t entityId,
const componentid_t componentId
) {
entityoverworldcamera_t *cam = entityOverworldCameraGet(entityId, componentId);
cam->targetEntityId = ENTITY_ID_INVALID;
cam->targetPosCompId = COMPONENT_ID_INVALID;
glm_vec3_zero(cam->targetOffset);
glm_vec3_zero(cam->eyeOffset);
cam->scale = 1.0f;
}
entityoverworldcamera_t * entityOverworldCameraGet(
const entityid_t entityId,
const componentid_t componentId
) {
return componentGetData(entityId, componentId, COMPONENT_TYPE_OVERWORLD_CAMERA);
}
void entityOverworldCameraSetTarget(
const entityid_t entityId,
const componentid_t componentId,
const entityid_t targetEntityId,
const componentid_t targetPosCompId
) {
entityoverworldcamera_t *cam = entityOverworldCameraGet(entityId, componentId);
cam->targetEntityId = targetEntityId;
cam->targetPosCompId = targetPosCompId;
}
errorret_t entityOverworldCameraRender(
const entityid_t entityId,
const componentid_t componentId
) {
entityoverworldcamera_t *cam = entityOverworldCameraGet(entityId, componentId);
vec3 targetPos;
entityPositionGetWorldPosition(
cam->targetEntityId, cam->targetPosCompId, targetPos
);
vec3 center = {
targetPos[0] + cam->targetOffset[0],
targetPos[1] + cam->targetOffset[1],
targetPos[2] + cam->targetOffset[2]
};
componentid_t posComp = entityGetComponent(entityId, COMPONENT_TYPE_POSITION);
componentid_t camComp = entityGetComponent(entityId, COMPONENT_TYPE_CAMERA);
entityCameraLookAtPixelPerfect(
entityId, posComp, camComp, center, cam->eyeOffset, cam->scale
);
errorOk();
}
@@ -1,69 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entity/entitybase.h"
#include "error/error.h"
typedef struct {
entityid_t targetEntityId;
componentid_t targetPosCompId;
vec3 targetOffset;
vec3 eyeOffset;
float_t scale;
} entityoverworldcamera_t;
/**
* Initializes the overworld camera component.
*
* @param entityId The owning entity.
* @param componentId This component's ID.
*/
void entityOverworldCameraInit(
const entityid_t entityId,
const componentid_t componentId
);
/**
* Returns a pointer to the overworld camera component data.
*
* @param entityId The owning entity.
* @param componentId This component's ID.
* @return Pointer to the entityoverworldcamera_t data.
*/
entityoverworldcamera_t * entityOverworldCameraGet(
const entityid_t entityId,
const componentid_t componentId
);
/**
* Sets the entity and position component the camera will follow.
*
* @param entityId The owning entity.
* @param componentId This component's ID.
* @param targetEntityId Entity to follow.
* @param targetPosCompId Position component on the target entity.
*/
void entityOverworldCameraSetTarget(
const entityid_t entityId,
const componentid_t componentId,
const entityid_t targetEntityId,
const componentid_t targetPosCompId
);
/**
* Render callback: updates the camera position to track the target entity.
* Called automatically each frame via componentRenderAll.
*
* @param entityId The owning entity.
* @param componentId This component's ID.
* @return Error state.
*/
errorret_t entityOverworldCameraRender(
const entityid_t entityId,
const componentid_t componentId
);
@@ -1,83 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entityoverworldtrigger.h"
#include "entity/entitymanager.h"
#include "entity/component/display/entityposition.h"
static void entityOverworldTriggerUpdate(
const entityid_t entityId,
const componentid_t componentId,
void *user
) {
entityoverworldtrigger_t *t = entityOverworldTriggerGet(entityId, componentId);
// Find the player position.
vec3 playerPos;
bool_t playerFound = false;
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
if((ENTITY_MANAGER.entities[i].state & ENTITY_STATE_ACTIVE) == 0) continue;
if(entityGetComponent(i, COMPONENT_TYPE_PLAYER) == COMPONENT_ID_INVALID) continue;
componentid_t posComp = entityGetComponent(i, COMPONENT_TYPE_POSITION);
if(posComp == COMPONENT_ID_INVALID) continue;
entityPositionGetWorldPosition(i, posComp, playerPos);
playerFound = true;
break;
}
bool_t wasInside = t->playerInside;
bool_t nowInside = playerFound && (
playerPos[0] >= t->min[0] && playerPos[0] <= t->max[0] &&
playerPos[1] >= t->min[1] && playerPos[1] <= t->max[1] &&
playerPos[2] >= t->min[2] && playerPos[2] <= t->max[2]
);
t->playerInside = nowInside;
if(nowInside) {
if(!wasInside && t->onEnter) t->onEnter(entityId, componentId, t->user);
if(t->onStay) t->onStay(entityId, componentId, t->user);
} else {
if(wasInside && t->onExit) t->onExit(entityId, componentId, t->user);
if(t->onOutside) t->onOutside(entityId, componentId, t->user);
}
}
void entityOverworldTriggerInit(
const entityid_t entityId,
const componentid_t componentId
) {
entityoverworldtrigger_t *t = entityOverworldTriggerGet(entityId, componentId);
glm_vec3_zero(t->min);
glm_vec3_zero(t->max);
t->playerInside = false;
t->onEnter = NULL;
t->onExit = NULL;
t->onStay = NULL;
t->onOutside = NULL;
t->user = NULL;
entityUpdateAdd(entityId, entityOverworldTriggerUpdate, componentId, NULL);
}
entityoverworldtrigger_t * entityOverworldTriggerGet(
const entityid_t entityId,
const componentid_t componentId
) {
return componentGetData(
entityId, componentId, COMPONENT_TYPE_OVERWORLD_TRIGGER
);
}
void entityOverworldTriggerSetBounds(
const entityid_t entityId,
const componentid_t componentId,
const vec3 min,
const vec3 max
) {
entityoverworldtrigger_t *t = entityOverworldTriggerGet(entityId, componentId);
glm_vec3_copy((float_t*)min, t->min);
glm_vec3_copy((float_t*)max, t->max);
}
@@ -1,54 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entity/entitybase.h"
typedef void (*entityoverworldtriggercallback_t)(
const entityid_t entityId,
const componentid_t componentId,
void *user
);
typedef struct {
vec3 min;
vec3 max;
bool_t playerInside;
entityoverworldtriggercallback_t onEnter;
entityoverworldtriggercallback_t onExit;
entityoverworldtriggercallback_t onStay;
entityoverworldtriggercallback_t onOutside;
void *user;
} entityoverworldtrigger_t;
/**
* Initializes the overworld trigger component and registers its update
* callback with the entity manager. The trigger is entirely self-contained
* after init — no scene-level wiring required.
*/
void entityOverworldTriggerInit(
const entityid_t entityId,
const componentid_t componentId
);
/**
* Returns a pointer to the overworld trigger component data.
*/
entityoverworldtrigger_t * entityOverworldTriggerGet(
const entityid_t entityId,
const componentid_t componentId
);
/**
* Sets the trigger AABB bounds.
*/
void entityOverworldTriggerSetBounds(
const entityid_t entityId,
const componentid_t componentId,
const vec3 min,
const vec3 max
);
@@ -1,103 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entityplayer.h"
#include "entity/entitymanager.h"
#include "entity/component/display/entityposition.h"
#include "entity/component/physics/entityphysics.h"
#include "entity/component/overworld/entityoverworld.h"
#include "entity/component/overworld/entityinteractable.h"
#include "input/input.h"
void entityPlayerInit(
const entityid_t entityId,
const componentid_t componentId
) {
entityplayer_t *player = entityPlayerGet(entityId, componentId);
player->speed = ENTITY_PLAYER_SPEED;
player->runSpeed = ENTITY_PLAYER_RUN_SPEED;
}
entityplayer_t * entityPlayerGet(
const entityid_t entityId,
const componentid_t componentId
) {
return componentGetData(entityId, componentId, COMPONENT_TYPE_PLAYER);
}
void entityPlayerUpdate(
const entityid_t entityId,
const componentid_t componentId
) {
entityplayer_t *player = entityPlayerGet(entityId, componentId);
vec2 dir;
inputAngle2D(
INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT,
INPUT_ACTION_UP, INPUT_ACTION_DOWN,
dir
);
float_t speed = (
inputIsDown(INPUT_ACTION_CANCEL) ? player->runSpeed : player->speed
);
componentid_t owCompId = entityGetComponent(entityId, COMPONENT_TYPE_OVERWORLD);
entityoverworld_t *ow = entityOverworldGet(entityId, owCompId);
if(ow && glm_vec2_norm(dir) > 0.0f) {
if(fabsf(dir[0]) >= fabsf(dir[1])) {
ow->facing = dir[0] > 0.0f ? FACING_DIR_RIGHT : FACING_DIR_LEFT;
} else {
ow->facing = dir[1] > 0.0f ? FACING_DIR_DOWN : FACING_DIR_UP;
}
}
vec3 vel;
entityPhysicsGetVelocity(entityId, ow->physCompId, vel);
vel[0] = dir[0] * speed;
vel[2] = dir[1] * speed;
entityPhysicsSetVelocity(entityId, ow->physCompId, vel);
if(!inputPressed(INPUT_ACTION_ACCEPT)) return;
vec3 playerPos;
componentid_t playerPosCompId = entityGetComponent(entityId, COMPONENT_TYPE_POSITION);
if(playerPosCompId == COMPONENT_ID_INVALID) return;
entityPositionGetWorldPosition(entityId, playerPosCompId, playerPos);
vec2 facingDir;
facingDirToVec2(ow ? ow->facing : FACING_DIR_DOWN, facingDir);
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
if((ENTITY_MANAGER.entities[i].state & ENTITY_STATE_ACTIVE) == 0) continue;
if(i == entityId) continue;
componentid_t interComp = entityGetComponent(i, COMPONENT_TYPE_INTERACTABLE);
if(interComp == COMPONENT_ID_INVALID) continue;
componentid_t posComp = entityGetComponent(i, COMPONENT_TYPE_POSITION);
if(posComp == COMPONENT_ID_INVALID) continue;
vec3 targetPos;
entityPositionGetWorldPosition(i, posComp, targetPos);
vec2 toTarget = {
targetPos[0] - playerPos[0],
targetPos[2] - playerPos[2],
};
float_t forward = glm_vec2_dot(facingDir, toTarget);
if(forward <= 0.0f || forward > ENTITY_PLAYER_INTERACT_RANGE) continue;
float_t lateral = fabsf(
facingDir[0] * toTarget[1] - facingDir[1] * toTarget[0]
);
if(lateral > ENTITY_PLAYER_INTERACT_LATERAL) continue;
entityInteractableTrigger(i, interComp);
}
}
@@ -1,52 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "entity/entitybase.h"
#define ENTITY_PLAYER_SPEED 4.0f
#define ENTITY_PLAYER_RUN_SPEED 8.0f
#define ENTITY_PLAYER_INTERACT_RANGE 1.5f
#define ENTITY_PLAYER_INTERACT_LATERAL 0.6f
typedef struct {
float_t speed;
float_t runSpeed;
} entityplayer_t;
/**
* Initializes the player component.
*
* @param entityId The owning entity.
* @param componentId This component's ID.
*/
void entityPlayerInit(const entityid_t entityId, const componentid_t componentId);
/**
* Returns a pointer to the player component data.
*
* @param entityId The owning entity.
* @param componentId This component's ID.
* @return Pointer to the entityplayer_t data.
*/
entityplayer_t * entityPlayerGet(
const entityid_t entityId,
const componentid_t componentId
);
/**
* Reads input, moves the player, updates facing direction, and checks for
* interactable entities in front of the player when accept is pressed.
*
* @param entityId The owning entity.
* @param componentId This component's ID.
*/
void entityPlayerUpdate(
const entityid_t entityId,
const componentid_t componentId
);
-10
View File
@@ -10,11 +10,6 @@
#include "entity/component/display/entityrenderable.h" #include "entity/component/display/entityrenderable.h"
#include "entity/component/physics/entityphysics.h" #include "entity/component/physics/entityphysics.h"
#include "entity/component/trigger/entitytrigger.h" #include "entity/component/trigger/entitytrigger.h"
#include "entity/component/overworld/entityoverworld.h"
#include "entity/component/overworld/entityoverworldtrigger.h"
#include "entity/component/overworld/entityplayer.h"
#include "entity/component/overworld/entityinteractable.h"
#include "entity/component/overworld/entityoverworldcamera.h"
// Name (Uppercase) // Name (Uppercase)
// Structure // Structure
@@ -28,8 +23,3 @@ X(CAMERA, entitycamera_t, camera, entityCameraInit, NULL, NULL)
X(RENDERABLE, entityrenderable_t, renderable, entityRenderableInit, entityRenderableDispose, NULL) X(RENDERABLE, entityrenderable_t, renderable, entityRenderableInit, entityRenderableDispose, NULL)
X(PHYSICS, entityphysics_t, physics, entityPhysicsInit, entityPhysicsDispose, NULL) X(PHYSICS, entityphysics_t, physics, entityPhysicsInit, entityPhysicsDispose, NULL)
X(TRIGGER, entitytrigger_t, trigger, entityTriggerInit, NULL, NULL) X(TRIGGER, entitytrigger_t, trigger, entityTriggerInit, NULL, NULL)
X(OVERWORLD, entityoverworld_t, overworld, entityOverworldInit, NULL, NULL)
X(PLAYER, entityplayer_t, player, entityPlayerInit, NULL, NULL)
X(INTERACTABLE, entityinteractable_t, interactable, entityInteractableInit, NULL, NULL)
X(OVERWORLD_CAMERA, entityoverworldcamera_t, overworldCamera, entityOverworldCameraInit, NULL, entityOverworldCameraRender)
X(OVERWORLD_TRIGGER, entityoverworldtrigger_t, overworldTrigger, entityOverworldTriggerInit, NULL, NULL)
+14 -2
View File
@@ -22,8 +22,20 @@ errorret_t inputInit(void) {
INPUT.actions[i].action = (inputaction_t)i; INPUT.actions[i].action = (inputaction_t)i;
INPUT.actions[i].lastValue = 0.0f; INPUT.actions[i].lastValue = 0.0f;
INPUT.actions[i].currentValue = 0.0f; INPUT.actions[i].currentValue = 0.0f;
eventInit(&INPUT.actions[i].onPressed, INPUT.actions[i].onPressedCallbacks, INPUT.actions[i].onPressedUsers, 4);
eventInit(&INPUT.actions[i].onReleased, INPUT.actions[i].onReleasedCallbacks, INPUT.actions[i].onReleasedUsers, 4); eventInit(
&INPUT.actions[i].onPressed,
INPUT.actions[i].onPressedCallbacks,
INPUT.actions[i].onPressedUsers,
INPUT_ACTION_CALLBACK_COUNT_MAX
);
eventInit(
&INPUT.actions[i].onReleased,
INPUT.actions[i].onReleasedCallbacks,
INPUT.actions[i].onReleasedUsers,
INPUT_ACTION_CALLBACK_COUNT_MAX
);
} }
#ifdef inputInitPlatform #ifdef inputInitPlatform
+6 -4
View File
@@ -10,6 +10,8 @@
#include "input/inputactiondefs.h" #include "input/inputactiondefs.h"
#include "event/event.h" #include "event/event.h"
#define INPUT_ACTION_CALLBACK_COUNT_MAX 4
typedef struct { typedef struct {
inputaction_t action; inputaction_t action;
float_t lastValue; float_t lastValue;
@@ -20,11 +22,11 @@ typedef struct {
float_t currentDynamicValue; float_t currentDynamicValue;
#endif #endif
eventcallback_t onPressedCallbacks[4]; eventcallback_t onPressedCallbacks[INPUT_ACTION_CALLBACK_COUNT_MAX];
void *onPressedUsers[4]; void *onPressedUsers[INPUT_ACTION_CALLBACK_COUNT_MAX];
event_t onPressed; event_t onPressed;
eventcallback_t onReleasedCallbacks[4]; eventcallback_t onReleasedCallbacks[INPUT_ACTION_CALLBACK_COUNT_MAX];
void *onReleasedUsers[4]; void *onReleasedUsers[INPUT_ACTION_CALLBACK_COUNT_MAX];
event_t onReleased; event_t onReleased;
} inputactiondata_t; } inputactiondata_t;
+6 -7
View File
@@ -1,16 +1,15 @@
# Copyright (c) 2025 Dominic Masters # Copyright (c) 2026 Dominic Masters
# #
# This software is released under the MIT License. # This software is released under the MIT License.
# https://opensource.org/licenses/MIT # https://opensource.org/licenses/MIT
# Sources include(duskjs2c)
dusk_embed_js(${DUSK_LIBRARY_TARGET_NAME}
${CMAKE_CURRENT_SOURCE_DIR}/scene.js
)
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
scene.c scene.c
scenerenderpipeline.c scenerenderpipeline.c
) )
# Subdirectories
add_subdirectory(initial)
add_subdirectory(test)
add_subdirectory(overworld)
-35
View File
@@ -1,35 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "initialscene.h"
#include "console/console.h"
#include "scene/scene.h"
#include "time/time.h"
#include "ui/uiloading.h"
void initialSceneInit(void) {
consolePrint("Initial scene initialized");
SCENE.data.initial.timer = 0.0f;
SCENE.data.initial.hiding = false;
uiLoadingShow(NULL, NULL);
}
errorret_t initialSceneUpdate(void) {
initialscene_t *scene = &SCENE.data.initial;
if(scene->hiding) errorOk();
scene->timer += TIME.delta;
if(scene->timer >= INITIAL_SCENE_WAIT) {
scene->hiding = true;
uiLoadingHide(NULL, NULL);
}
errorOk();
}
void initialSceneDispose(void) {
}
-20
View File
@@ -1,20 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#define INITIAL_SCENE_WAIT 2.0f
typedef struct {
float_t timer;
bool_t hiding;
} initialscene_t;
void initialSceneInit(void);
errorret_t initialSceneUpdate(void);
void initialSceneDispose(void);
@@ -1,49 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "overworldground.h"
#include "entity/entitymanager.h"
#include "entity/component/physics/entityphysics.h"
#define OVERWORLD_GROUND_SIZE 20.0f
void overworldGroundAdd(overworldground_t *ground) {
ground->entityId = entityManagerAdd();
ground->posCompId = entityAddComponent(
ground->entityId, COMPONENT_TYPE_POSITION
);
entityAddComponent(ground->entityId, COMPONENT_TYPE_RENDERABLE);
vec3 pos = { -OVERWORLD_GROUND_SIZE, 0.0f, -OVERWORLD_GROUND_SIZE };
vec3 scale = { OVERWORLD_GROUND_SIZE * 2.0f, 1.0f, OVERWORLD_GROUND_SIZE * 2.0f };
entityPositionSetLocalPosition(ground->entityId, ground->posCompId, pos);
entityPositionSetLocalScale(ground->entityId, ground->posCompId, scale);
// Separate physics entity centered on the finite ground area.
// The visual entity's position is its corner {-size, 0, -size}, not its
// center, so a box collider on it would be misplaced. This entity sits at
// the true center {0, -0.5, 0} with halfExtents matching the visual extent.
ground->floorEntityId = entityManagerAdd();
componentid_t floorPosComp = entityAddComponent(
ground->floorEntityId, COMPONENT_TYPE_POSITION
);
componentid_t floorPhysComp = entityAddComponent(
ground->floorEntityId, COMPONENT_TYPE_PHYSICS
);
vec3 floorPos = { 0.0f, -0.5f, 0.0f };
entityPositionSetLocalPosition(ground->floorEntityId, floorPosComp, floorPos);
entityPhysicsSetBodyType(ground->floorEntityId, floorPhysComp, PHYSICS_BODY_STATIC);
physicsshape_t shape = {
.type = PHYSICS_SHAPE_CUBE,
.data.cube = { .halfExtents = {
OVERWORLD_GROUND_SIZE,
0.5f,
OVERWORLD_GROUND_SIZE
}}
};
entityPhysicsSetShape(ground->floorEntityId, floorPhysComp, shape);
}
@@ -1,22 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entity/entitybase.h"
typedef struct {
entityid_t entityId;
componentid_t posCompId;
entityid_t floorEntityId;
} overworldground_t;
/**
* Creates the ground entity and adds it to the world.
*
* @param ground The ground state to initialize.
*/
void overworldGroundAdd(overworldground_t *ground);
-47
View File
@@ -1,47 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "overworldnpc.h"
#include "entity/entitymanager.h"
#include "entity/component/physics/entityphysics.h"
#include "entity/component/trigger/entitytrigger.h"
#include "entity/component/overworld/entityoverworld.h"
#include "entity/component/overworld/entityinteractable.h"
void overworldNpcAdd(overworldnpc_t *npc, vec3 position) {
npc->entityId = entityManagerAdd();
npc->posCompId = entityAddComponent(npc->entityId, COMPONENT_TYPE_POSITION);
entityAddComponent(npc->entityId, COMPONENT_TYPE_RENDERABLE);
entityAddComponent(npc->entityId, COMPONENT_TYPE_PHYSICS);
npc->overworldCompId = entityAddComponent(npc->entityId, COMPONENT_TYPE_OVERWORLD);
npc->triggerCompId = entityAddComponent(npc->entityId, COMPONENT_TYPE_TRIGGER);
npc->interactableCompId = entityAddComponent(
npc->entityId, COMPONENT_TYPE_INTERACTABLE
);
entityPositionSetLocalPosition(npc->entityId, npc->posCompId, position);
componentid_t physCompId = entityOverworldGet(npc->entityId, npc->overworldCompId)->physCompId;
entityPhysicsSetBodyType(npc->entityId, physCompId, PHYSICS_BODY_STATIC);
physicsshape_t shape = {
.type = PHYSICS_SHAPE_CAPSULE,
.data.capsule = { .radius = 0.4f, .halfHeight = 0.1f }
};
entityPhysicsSetShape(npc->entityId, physCompId, shape);
vec3 min = {
position[0] - OVERWORLD_NPC_INTERACT_RANGE,
position[1] - OVERWORLD_NPC_INTERACT_RANGE,
position[2] - OVERWORLD_NPC_INTERACT_RANGE
};
vec3 max = {
position[0] + OVERWORLD_NPC_INTERACT_RANGE,
position[1] + OVERWORLD_NPC_INTERACT_RANGE,
position[2] + OVERWORLD_NPC_INTERACT_RANGE
};
entityTriggerSetBounds(npc->entityId, npc->triggerCompId, min, max);
}
-27
View File
@@ -1,27 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entity/entitybase.h"
#define OVERWORLD_NPC_INTERACT_RANGE 1.5f
typedef struct {
entityid_t entityId;
componentid_t posCompId;
componentid_t overworldCompId;
componentid_t triggerCompId;
componentid_t interactableCompId;
} overworldnpc_t;
/**
* Creates the NPC entity at the given world position.
*
* @param npc The NPC state to initialize.
* @param position World-space position to spawn the NPC at.
*/
void overworldNpcAdd(overworldnpc_t *npc, vec3 position);
@@ -1,38 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "overworldplayer.h"
#include "entity/entitymanager.h"
#include "entity/component/physics/entityphysics.h"
#include "entity/component/overworld/entityoverworld.h"
#include "entity/component/overworld/entityplayer.h"
void overworldPlayerAdd(overworldplayer_t *player) {
player->entityId = entityManagerAdd();
player->posCompId = entityAddComponent(player->entityId, COMPONENT_TYPE_POSITION);
entityAddComponent(player->entityId, COMPONENT_TYPE_RENDERABLE);
entityAddComponent(player->entityId, COMPONENT_TYPE_PHYSICS);
componentid_t owCompId = entityAddComponent(player->entityId, COMPONENT_TYPE_OVERWORLD);
entityOverworldSetType(player->entityId, owCompId, OVERWORLD_ENTITY_TYPE_PLAYER);
player->playerCompId = entityAddComponent(player->entityId, COMPONENT_TYPE_PLAYER);
vec3 pos = { 0.0f, 0.5f, 0.0f };
entityPositionSetLocalPosition(player->entityId, player->posCompId, pos);
componentid_t physCompId = entityOverworldGet(player->entityId, owCompId)->physCompId;
entityPhysicsSetBodyType(player->entityId, physCompId, PHYSICS_BODY_DYNAMIC);
physicsshape_t shape = {
.type = PHYSICS_SHAPE_CAPSULE,
.data.capsule = { .radius = 0.4f, .halfHeight = 0.1f }
};
entityPhysicsSetShape(player->entityId, physCompId, shape);
entityPhysicsGet(player->entityId, physCompId)->gravityScale = 1.0f;
}
void overworldPlayerUpdate(overworldplayer_t *player) {
entityPlayerUpdate(player->entityId, player->playerCompId);
}
@@ -1,29 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entity/entitybase.h"
typedef struct {
entityid_t entityId;
componentid_t posCompId;
componentid_t playerCompId;
} overworldplayer_t;
/**
* Creates the player entity and adds it to the world.
*
* @param player The player state to initialize.
*/
void overworldPlayerAdd(overworldplayer_t *player);
/**
* Updates the player entity, reading input and moving on the XZ plane.
*
* @param player The player state to update.
*/
void overworldPlayerUpdate(overworldplayer_t *player);
-173
View File
@@ -1,173 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "overworldscene.h"
#include "overworldplayer.h"
#include "overworldground.h"
#include "overworldnpc.h"
#include "console/console.h"
#include "entity/entitymanager.h"
#include "entity/component/display/entityrenderable.h"
#include "display/shader/shaderlist.h"
#include "entity/component/overworld/entityinteractable.h"
#include "entity/component/overworld/entityoverworldcamera.h"
#include "entity/component/overworld/entityoverworldtrigger.h"
#include "display/mesh/cube.h"
#include "display/mesh/plane.h"
#include "display/displaystate.h"
#include "scene/scene.h"
#include "assert/assert.h"
#define OVERWORLD (SCENE.data.overworld)
static void overworldSceneTestTriggerEnter(
const entityid_t entityId,
const componentid_t componentId,
void *user
) {
consolePrint("Test trigger: player entered");
}
static void overworldSceneTestTriggerExit(
const entityid_t entityId,
const componentid_t componentId,
void *user
) {
consolePrint("Test trigger: player exited");
}
static void overworldSceneNpcInteract(
const entityid_t entityId,
const componentid_t componentId,
void *user
) {
consolePrint("NPC interacted with!");
}
void overworldSceneConfigureShaderMaterial(
const entityid_t entityId,
const color_t color,
mesh_t *mesh
) {
assertNotNull(mesh, "Mesh cannot be null");
componentid_t renderComp = entityGetComponent(
entityId, COMPONENT_TYPE_RENDERABLE
);
entityRenderableSetType(entityId, renderComp, ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL);
entityrenderable_t *r = componentGetData(
entityId, renderComp, COMPONENT_TYPE_RENDERABLE
);
r->data.material.shaderType = SHADER_LIST_SHADER_UNLIT;
r->data.material.material.unlit.color = color;
r->data.material.material.unlit.texture = NULL;
r->data.material.state.flags = DISPLAY_STATE_FLAG_DEPTH_TEST;
r->data.material.meshes[0] = mesh;
r->data.material.meshOffsets[0] = 0;
r->data.material.meshCounts[0] = -1;
r->data.material.meshCount = 1;
}
void overworldSceneConfigureSprite(
const entityid_t entityId,
const color_t color,
texture_t *texture
) {
componentid_t renderComp = entityGetComponent(
entityId, COMPONENT_TYPE_RENDERABLE
);
entityRenderableSetType(entityId, renderComp, ENTITY_RENDERABLE_TYPE_SPRITEBATCH);
entityrenderable_t *r = componentGetData(
entityId, renderComp, COMPONENT_TYPE_RENDERABLE
);
r->data.spritebatch.texture = texture;
r->data.spritebatch.spriteCount = 1;
r->data.spritebatch.sprites[0].min[0] = -0.5f;
r->data.spritebatch.sprites[0].min[1] = -0.0f;
r->data.spritebatch.sprites[0].min[2] = -0.5f;
r->data.spritebatch.sprites[0].max[0] = 0.5f;
r->data.spritebatch.sprites[0].max[1] = 0.0f;
r->data.spritebatch.sprites[0].max[2] = 0.5f;
r->data.spritebatch.sprites[0].uvMin[0] = 0.0f;
r->data.spritebatch.sprites[0].uvMin[1] = 0.0f;
r->data.spritebatch.sprites[0].uvMax[0] = 1.0f;
r->data.spritebatch.sprites[0].uvMax[1] = 1.0f;
}
void overworldSceneInit(void) {
consolePrint("Overworld scene initialized");
overworldGroundAdd(&OVERWORLD.ground);
overworldSceneConfigureShaderMaterial(
OVERWORLD.ground.entityId, COLOR_MAGENTA, &PLANE_MESH_SIMPLE
);
overworldPlayerAdd(&OVERWORLD.player);
overworldSceneConfigureSprite(OVERWORLD.player.entityId, COLOR_GREEN, NULL);
vec3 npcPos = { 3.0f, 0.5f, 3.0f };
overworldNpcAdd(&OVERWORLD.npc, npcPos);
overworldSceneConfigureSprite(OVERWORLD.npc.entityId, COLOR_BLUE, NULL);
OVERWORLD.cameraEntityId = entityManagerAdd();
entityAddComponent(OVERWORLD.cameraEntityId, COMPONENT_TYPE_POSITION);
entityAddComponent(OVERWORLD.cameraEntityId, COMPONENT_TYPE_CAMERA);
OVERWORLD.cameraOverworldCompId = entityAddComponent(
OVERWORLD.cameraEntityId, COMPONENT_TYPE_OVERWORLD_CAMERA
);
entityOverworldCameraSetTarget(
OVERWORLD.cameraEntityId, OVERWORLD.cameraOverworldCompId,
OVERWORLD.player.entityId, OVERWORLD.player.posCompId
);
entityoverworldcamera_t *camData = entityOverworldCameraGet(
OVERWORLD.cameraEntityId, OVERWORLD.cameraOverworldCompId
);
glm_vec3_zero(camData->targetOffset);
glm_vec3_copy((vec3){ 0.0f, 0.0f, 5.0f }, camData->eyeOffset);
camData->scale = 32.0f;
entityInteractableSetCallback(
OVERWORLD.npc.entityId, OVERWORLD.npc.interactableCompId,
overworldSceneNpcInteract, NULL
);
OVERWORLD.testTriggerId = entityManagerAdd();
OVERWORLD.testTriggerCompId = entityAddComponent(
OVERWORLD.testTriggerId, COMPONENT_TYPE_OVERWORLD_TRIGGER
);
entityOverworldTriggerSetBounds(
OVERWORLD.testTriggerId, OVERWORLD.testTriggerCompId,
(vec3){ -2.0f, -1.0f, -2.0f },
(vec3){ 2.0f, 1.0f, 2.0f }
);
entityoverworldtrigger_t *testTrigger = entityOverworldTriggerGet(
OVERWORLD.testTriggerId, OVERWORLD.testTriggerCompId
);
testTrigger->onEnter = overworldSceneTestTriggerEnter;
testTrigger->onExit = overworldSceneTestTriggerExit;
}
errorret_t overworldSceneUpdate(void) {
overworldPlayerUpdate(&OVERWORLD.player);
errorOk();
}
void overworldSceneDispose(void) {
OVERWORLD.cameraEntityId = ENTITY_ID_INVALID;
OVERWORLD.cameraOverworldCompId = COMPONENT_ID_INVALID;
OVERWORLD.ground.entityId = ENTITY_ID_INVALID;
OVERWORLD.ground.posCompId = COMPONENT_ID_INVALID;
OVERWORLD.ground.floorEntityId = ENTITY_ID_INVALID;
OVERWORLD.player.entityId = ENTITY_ID_INVALID;
OVERWORLD.player.posCompId = COMPONENT_ID_INVALID;
OVERWORLD.npc.entityId = ENTITY_ID_INVALID;
OVERWORLD.npc.posCompId = COMPONENT_ID_INVALID;
OVERWORLD.npc.overworldCompId = COMPONENT_ID_INVALID;
OVERWORLD.npc.triggerCompId = COMPONENT_ID_INVALID;
OVERWORLD.npc.interactableCompId = COMPONENT_ID_INVALID;
}
-72
View File
@@ -1,72 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "entity/entitybase.h"
#include "display/color.h"
#include "display/mesh/mesh.h"
#include "display/texture/texture.h"
#include "scene/overworld/overworldplayer.h"
#include "scene/overworld/overworldground.h"
#include "scene/overworld/overworldnpc.h"
typedef struct {
entityid_t cameraEntityId;
componentid_t cameraOverworldCompId;
overworldplayer_t player;
overworldground_t ground;
overworldnpc_t npc;
entityid_t testTriggerId;
componentid_t testTriggerCompId;
} overworldscene_t;
/**
* Configures a SHADER_MATERIAL renderable on an entity with the unlit shader,
* depth testing enabled, no blending, and a single mesh.
*
* @param entityId The entity to configure.
* @param color Unlit diffuse color.
* @param mesh Mesh to render.
*/
void overworldSceneConfigureShaderMaterial(
const entityid_t entityId,
const color_t color,
mesh_t *mesh
);
/**
* Configures a SPRITEBATCH renderable on an entity as a single 1x1 billboard
* sprite centered at the entity's origin.
*
* @param entityId The entity to configure.
* @param color Per-vertex sprite tint color.
* @param texture Texture to use, or NULL for untextured.
*/
void overworldSceneConfigureSprite(
const entityid_t entityId,
const color_t color,
texture_t *texture
);
/**
* Initializes the overworld scene, spawning all entities and configuring
* the camera, player, ground, and NPC.
*/
void overworldSceneInit(void);
/**
* Updates the overworld scene each frame.
*
* @return Error state.
*/
errorret_t overworldSceneUpdate(void);
/**
* Disposes the overworld scene, invalidating all entity and component IDs.
*/
void overworldSceneDispose(void);
+14 -52
View File
@@ -6,65 +6,41 @@
#include "scene.h" #include "scene.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
#include "log/log.h"
#include "time/time.h" #include "time/time.h"
#include "display/screen/screen.h" #include "display/screen/screen.h"
#include "entity/entitymanager.h" #include "entity/entitymanager.h"
#include "display/shader/shaderunlit.h" #include "display/shader/shaderunlit.h"
#include "display/display.h" #include "display/display.h"
#include "console/console.h"
#include "util/string.h"
#include "ui/ui.h" #include "ui/ui.h"
#include "scene/scenerenderpipeline.h" #include "scene/scenerenderpipeline.h"
#include "entity/component.h" #include "entity/component.h"
#include "asset/asset.h"
scenefuncs_t SCENE_FUNCTIONS[SCENE_TYPE_COUNT] = { #include "asset/loader/assetloader.h"
{ 0 }, #include "script/module/modulebase.h"
#define X(structName, varName, varNameUpper, initFunc, updateFunc, disposeFunc) \ #include "console/console.h"
{ initFunc, updateFunc, disposeFunc }, #include "script/script.h"
#include "scene/scenelist.h" #include "scene_js.h"
#undef X
};
scene_t SCENE; scene_t SCENE;
errorret_t sceneInit(void) { errorret_t sceneInit(void) {
memoryZero(&SCENE, sizeof(scene_t)); memoryZero(&SCENE, sizeof(scene_t));
errorChain(scriptExecString(SCENE_JS));
errorOk(); errorOk();
} }
errorret_t sceneUpdate(void) { errorret_t sceneUpdate(void) {
// Handle scene change
if(SCENE.nextType != SCENE_TYPE_NULL) {
// Dispose current scene.
if(SCENE.type != SCENE_TYPE_NULL) {
if(SCENE_FUNCTIONS[SCENE.type].dispose) {
SCENE_FUNCTIONS[SCENE.type].dispose();
}
}
// Init new scene
SCENE.type = SCENE.nextType;
SCENE.nextType = SCENE_TYPE_NULL;
if(SCENE.type != SCENE_TYPE_NULL) {
if(SCENE_FUNCTIONS[SCENE.type].init) {
SCENE_FUNCTIONS[SCENE.type].init();
}
}
}
// Update scene
#ifdef DUSK_TIME_DYNAMIC #ifdef DUSK_TIME_DYNAMIC
if(TIME.dynamicUpdate) { if(TIME.dynamicUpdate) {
errorOk(); errorChain(scriptExecString("Scene.dynamicUpdate();"));
} else {
errorChain(scriptExecString("Scene.update();"));
} }
#else
errorChain(scriptExecString("Scene.update();"));
errorChain(scriptExecString("Scene.dynamicUpdate();"));
#endif #endif
if(SCENE.type != SCENE_TYPE_NULL && SCENE_FUNCTIONS[SCENE.type].update) {
errorChain(SCENE_FUNCTIONS[SCENE.type].update());
}
errorOk(); errorOk();
} }
@@ -103,21 +79,7 @@ errorret_t sceneRender(void) {
errorOk(); errorOk();
} }
void sceneSet(const scenetype_t type) {
assertTrue(
type > SCENE_TYPE_NULL && type < SCENE_TYPE_COUNT,
"Invalid scene type"
);
SCENE.nextType = type;
}
errorret_t sceneDispose(void) { errorret_t sceneDispose(void) {
if(SCENE.type != SCENE_TYPE_NULL) { errorChain(scriptExecString("Scene.dispose();"));
if(SCENE_FUNCTIONS[SCENE.type].dispose) {
SCENE_FUNCTIONS[SCENE.type].dispose();
}
}
SCENE.type = SCENE_TYPE_NULL;
SCENE.nextType = SCENE_TYPE_NULL;
errorOk(); errorOk();
} }
+11 -47
View File
@@ -6,75 +6,39 @@
*/ */
#pragma once #pragma once
#include "asset/assetfile.h" #include "error/error.h"
#include "scene/initial/initialscene.h"
#include "scene/test/testscene.h"
#include "scene/overworld/overworldscene.h"
#define SCENE_EVENT_UPDATE_MAX 16
typedef enum {
SCENE_TYPE_NULL,
#define X(structName, varName, varNameUpper, initFunc, updateFunc, disposeFunc) \
SCENE_TYPE_##varNameUpper,
#include "scene/scenelist.h"
#undef X
SCENE_TYPE_COUNT
} scenetype_t;
typedef union {
#define X(structName, varName, varNameUpper, initFunc, updateFunc, disposeFunc) \
structName varName;
#include "scene/scenelist.h"
#undef X
} scenedata_t;
typedef struct { typedef struct {
void (*init)(void); void *nothing;
errorret_t (*update)(void);
void (*dispose)(void);
} scenefuncs_t;
typedef struct {
scenedata_t data;
scenetype_t type;
scenetype_t nextType;
} scene_t; } scene_t;
extern scenefuncs_t SCENE_FUNCTIONS[SCENE_TYPE_COUNT];
extern scene_t SCENE; extern scene_t SCENE;
/** /**
* Initializes the scene manager. * Initialises the scene manager.
* *
* @return Any error state that happened. * @return An error if the init failed, or errorOk() if it succeeded.
*/ */
errorret_t sceneInit(void); errorret_t sceneInit(void);
/** /**
* Ticks the scene manager; may call the scene's update method. * Ticks the scene manager. Processes any pending scene transition, then
* calls scriptSceneUpdate on the active scene.
* *
* @return Any error state that happened. * @return An error if the update failed, or errorOk() if it succeeded.
*/ */
errorret_t sceneUpdate(void); errorret_t sceneUpdate(void);
/** /**
* Renders the scene. * Renders the current scene (entities, render pipeline, UI).
* *
* @return Any error state that happened. * @return An error if the render failed, or errorOk() if it succeeded.
*/ */
errorret_t sceneRender(void); errorret_t sceneRender(void);
/** /**
* Requests a scene change on the next safe opportunity. * Disposes the active scene immediately.
* *
* @param type The type of scene to change to. * @return An error if the dispose failed, or errorOk() if it succeeded.
*/
void sceneSet(const scenetype_t type);
/**
* Disposes of the current scene.
*
* @return Any error state that happened.
*/ */
errorret_t sceneDispose(void); errorret_t sceneDispose(void);
+36
View File
@@ -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;
}
-15
View File
@@ -1,15 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#ifndef X
#define X(structName, varName, varNameUpper, initFunc, updateFunc, disposeFunc) \
((void))
#endif
X(initialscene_t, initial, INITIAL, initialSceneInit, initialSceneUpdate, initialSceneDispose)
X(testscene_t, test, TEST, testSceneInit, testSceneUpdate, testSceneDispose)
X(overworldscene_t, overworld, OVERWORLD, overworldSceneInit, overworldSceneUpdate, overworldSceneDispose)
-110
View File
@@ -1,110 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "testscene.h"
#include "console/console.h"
#include "entity/entitymanager.h"
#include "input/input.h"
#define TEST_SCENE_ENTITY_MAX (ENTITY_COUNT_MAX - 1)
static entityid_t cameraEntityId;
static componentid_t cameraCompId;
static entityid_t testEntities[TEST_SCENE_ENTITY_MAX];
static uint8_t testEntityCount = 0;
static void testSceneSpawnTestEntity(void) {
if(testEntityCount >= TEST_SCENE_ENTITY_MAX) return;
entityid_t entity = entityManagerAdd();
componentid_t posComp = entityAddComponent(entity, COMPONENT_TYPE_POSITION);
entityAddComponent(entity, COMPONENT_TYPE_RENDERABLE);
const int32_t cols = 20;
const float_t spacing = 1.5f;
int32_t col = testEntityCount % cols;
int32_t row = testEntityCount / cols;
vec3 pos = {
((float_t)col - (cols - 1) * 0.5f) * spacing,
0.0f,
(float_t)row * spacing
};
entityPositionSetLocalPosition(entity, posComp, pos);
testEntities[testEntityCount++] = entity;
}
static void testSceneUpdateCamera(void) {
if(testEntityCount == 0) return;
float_t minX = FLT_MAX, maxX = -FLT_MAX;
float_t minZ = FLT_MAX, maxZ = -FLT_MAX;
for(entityid_t i = 0; i < testEntityCount; i++) {
componentid_t posComp = entityGetComponent(
testEntities[i], COMPONENT_TYPE_POSITION
);
if(posComp == COMPONENT_ID_INVALID) continue;
vec3 pos;
entityPositionGetLocalPosition(testEntities[i], posComp, pos);
if(pos[0] < minX) minX = pos[0];
if(pos[0] > maxX) maxX = pos[0];
if(pos[2] < minZ) minZ = pos[2];
if(pos[2] > maxZ) maxZ = pos[2];
}
float_t centerX = (minX + maxX) * 0.5f;
float_t centerZ = (minZ + maxZ) * 0.5f;
float_t extentX = (maxX - minX) * 0.5f + 0.5f;
float_t extentZ = (maxZ - minZ) * 0.5f + 0.5f;
float_t extent = extentX > extentZ ? extentX : extentZ;
float_t dist = extent * 1.5f + 2.0f;
vec3 target = { centerX, 0.0f, centerZ };
vec3 eye = { centerX + dist, dist, centerZ + dist };
vec3 up = { 0.0f, 1.0f, 0.0f };
entityPositionLookAt(cameraEntityId, cameraCompId, eye, target, up);
}
void testSceneInit(void) {
consolePrint("Test scene initialized");
testEntityCount = 0;
cameraEntityId = entityManagerAdd();
cameraCompId = entityAddComponent(cameraEntityId, COMPONENT_TYPE_POSITION);
entityAddComponent(cameraEntityId, COMPONENT_TYPE_CAMERA);
vec3 eye, target, up;
glm_vec3_zero(target);
glm_vec3_copy((vec3){ 3.0f, 3.0f, 3.0f }, eye);
glm_vec3_copy((vec3){ 0.0f, 1.0f, 0.0f }, up);
entityPositionLookAt(cameraEntityId, cameraCompId, eye, target, up);
for(int i = 0; i < 5; i++) testSceneSpawnTestEntity();
}
errorret_t testSceneUpdate(void) {
if(inputPressed(INPUT_ACTION_ACCEPT)) {
for(int i = 0; i < 5; i++) testSceneSpawnTestEntity();
}
if(inputPressed(INPUT_ACTION_CANCEL)) {
for(int i = 0; i < 5 && testEntityCount > 0; i++) {
entityDispose(testEntities[--testEntityCount]);
}
}
testSceneUpdateCamera();
errorOk();
}
void testSceneDispose(void) {
testEntityCount = 0;
cameraEntityId = ENTITY_ID_INVALID;
cameraCompId = COMPONENT_ID_INVALID;
}
+14
View File
@@ -6,4 +6,18 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
modulebase.c modulebase.c
modulelist.c
) )
# Subdirs
add_subdirectory(asset)
add_subdirectory(console)
add_subdirectory(display)
add_subdirectory(engine)
add_subdirectory(entity)
add_subdirectory(event)
add_subdirectory(input)
add_subdirectory(math)
add_subdirectory(require)
add_subdirectory(scene)
add_subdirectory(system)
@@ -5,8 +5,7 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
overworldground.c moduleassetentry.c
overworldnpc.c moduleassetbatch.c
overworldplayer.c moduleasset.c
overworldscene.c
) )
+132
View File
@@ -0,0 +1,132 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduleasset.h"
scriptproto_t MODULE_ASSET_PROTO;
moduleBaseFunction(moduleAssetExists) {
moduleBaseRequireArgs(1);
moduleBaseRequireString(0);
char_t buf[256];
moduleBaseToString(args[0], buf, sizeof(buf));
return jerry_boolean(assetFileExists(buf));
}
moduleBaseFunction(moduleAssetLock) {
moduleBaseRequireArgs(2);
moduleBaseRequireString(0);
moduleBaseRequireNumber(1);
char_t buf[256];
moduleBaseToString(args[0], buf, sizeof(buf));
assetloadertype_t type = (assetloadertype_t)moduleBaseArgInt(1);
assetloaderinput_t input;
assetloaderinput_t *inputPtr = NULL;
if(argc >= 3 && jerry_value_is_number(args[2])) {
int32_t inputVal = moduleBaseArgInt(2);
switch(type) {
case ASSET_LOADER_TYPE_TEXTURE:
input.texture = (textureformat_t)inputVal;
inputPtr = &input;
break;
case ASSET_LOADER_TYPE_MESH:
input.mesh = (assetmeshinputaxis_t)inputVal;
inputPtr = &input;
break;
default:
break;
}
}
assetentry_t *entry = assetLock(buf, type, inputPtr);
if(!entry) return moduleBaseThrow("Asset.lock: failed to lock asset");
jsassetentry_t e = { .entry = entry };
return scriptProtoCreateValue(&MODULE_ASSET_ENTRY_PROTO, &e);
}
moduleBaseFunction(moduleAssetRequireLoaded) {
moduleBaseRequireArgs(1);
jsassetentry_t *e = (jsassetentry_t *)scriptProtoGetValue(
&MODULE_ASSET_ENTRY_PROTO, args[0]
);
if(!e || !e->entry) {
return moduleBaseThrow("Asset.requireLoaded: expected AssetEntry");
}
errorret_t err = assetRequireLoaded(e->entry);
if(errorIsNotOk(err)) return moduleBaseThrowError(err);
return jerry_value_copy(args[0]);
}
moduleBaseFunction(moduleAssetUnlock) {
moduleBaseRequireArgs(1);
moduleBaseRequireString(0);
char_t buf[256];
moduleBaseToString(args[0], buf, sizeof(buf));
assetUnlock(buf);
return jerry_undefined();
}
void moduleAssetInit(void) {
moduleAssetEntryInit();
moduleAssetBatchInit();
scriptProtoInit(&MODULE_ASSET_PROTO, "Asset", sizeof(uint8_t), NULL);
scriptProtoDefineStaticFunc(
&MODULE_ASSET_PROTO, "exists", moduleAssetExists
);
scriptProtoDefineStaticFunc(
&MODULE_ASSET_PROTO, "lock", moduleAssetLock
);
scriptProtoDefineStaticFunc(
&MODULE_ASSET_PROTO, "unlock", moduleAssetUnlock
);
scriptProtoDefineStaticFunc(
&MODULE_ASSET_PROTO, "requireLoaded", moduleAssetRequireLoaded
);
jerry_value_t global = MODULE_ASSET_PROTO.prototype;
struct { const char_t *name; int val; } types[] = {
{ "TYPE_MESH", ASSET_LOADER_TYPE_MESH },
{ "TYPE_TEXTURE", ASSET_LOADER_TYPE_TEXTURE },
{ "TYPE_TILESET", ASSET_LOADER_TYPE_TILESET },
{ "TYPE_LOCALE", ASSET_LOADER_TYPE_LOCALE },
{ "TYPE_JSON", ASSET_LOADER_TYPE_JSON },
{ "TYPE_SCRIPT", ASSET_LOADER_TYPE_SCRIPT },
};
for(int i = 0; i < 6; i++) {
jerry_value_t k = jerry_string_sz(types[i].name);
jerry_value_t v = jerry_number((double)types[i].val);
jerry_object_set(global, k, v);
jerry_value_free(v);
jerry_value_free(k);
}
struct { const char_t *name; int val; } axes[] = {
{ "MESH_AXIS_Y_UP", MESH_INPUT_AXIS_Y_UP },
{ "MESH_AXIS_Z_UP", MESH_INPUT_AXIS_Z_UP },
{ "MESH_AXIS_X_UP", MESH_INPUT_AXIS_X_UP },
{ "MESH_AXIS_Y_DOWN", MESH_INPUT_AXIS_Y_DOWN },
{ "MESH_AXIS_Z_DOWN", MESH_INPUT_AXIS_Z_DOWN },
{ "MESH_AXIS_X_DOWN", MESH_INPUT_AXIS_X_DOWN },
};
for(int i = 0; i < 6; i++) {
jerry_value_t k = jerry_string_sz(axes[i].name);
jerry_value_t v = jerry_number((double)axes[i].val);
jerry_object_set(global, k, v);
jerry_value_free(v);
jerry_value_free(k);
}
}
void moduleAssetDispose(void) {
scriptProtoDispose(&MODULE_ASSET_PROTO);
moduleAssetBatchDispose();
moduleAssetEntryDispose();
}
@@ -0,0 +1,61 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/module/modulebase.h"
#include "script/scriptproto.h"
#include "script/module/display/moduletexture.h"
#include "script/module/asset/moduleassetentry.h"
#include "script/module/asset/moduleassetbatch.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
extern scriptproto_t MODULE_ASSET_PROTO;
/**
* Asset.exists(path) — returns true if the path exists in the asset archive.
* @param args[0] Archive-relative path string.
*/
moduleBaseFunction(moduleAssetExists);
/**
* Asset.lock(path, type, input?) — locks an asset entry and returns an
* AssetEntry. The entry begins loading in the background.
*
* @param args[0] Path string.
* @param args[1] Loader type constant (Asset.TYPE_*).
* @param args[2] Optional loader input (Texture.FORMAT_* or Asset.MESH_AXIS_*).
*/
moduleBaseFunction(moduleAssetLock);
/**
* Asset.requireLoaded(entry) — blocks until the entry is fully loaded.
* @param args[0] An AssetEntry object.
* @return The same AssetEntry for chaining.
* @throws If the load fails.
*/
moduleBaseFunction(moduleAssetRequireLoaded);
/**
* Asset.unlock(path) — releases the asset lock for the given path.
* Prefer entry.unlock() on the AssetEntry object directly.
* @param args[0] Path string originally passed to Asset.lock().
*/
moduleBaseFunction(moduleAssetUnlock);
/**
* Initializes the Asset module. Also calls moduleAssetEntryInit() and
* moduleAssetBatchInit(), then registers the global Asset object with
* exists/lock/unlock/requireLoaded and all TYPE_* / MESH_AXIS_* constants.
*/
void moduleAssetInit(void);
/**
* Disposes the Asset module and calls moduleAssetBatchDispose() and
* moduleAssetEntryDispose().
*/
void moduleAssetDispose(void);
@@ -0,0 +1,394 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduleassetbatch.h"
#define ASSET_BATCH_LOADED_MAX 8
typedef struct {
assetbatch_t *batch;
jerry_value_t promise;
} assetbatchloadedpend_t;
scriptproto_t MODULE_ASSET_BATCH_PROTO;
static assetbatchloadedpend_t ASSET_BATCH_LOADED[ASSET_BATCH_LOADED_MAX];
static uint32_t ASSET_BATCH_LOADED_COUNT = 0;
static void assetBatchOnLoadedFire(void *params, void *user);
static void assetBatchOnErrorFire(void *params, void *user);
static void assetBatchOnLoadedFire(void *params, void *user) {
assetbatch_t *batch = (assetbatch_t *)user;
uint32_t i = 0;
while(i < ASSET_BATCH_LOADED_COUNT) {
if(ASSET_BATCH_LOADED[i].batch != batch) { i++; continue; }
jerry_value_t undef = jerry_undefined();
jerry_value_t r = jerry_promise_resolve(
ASSET_BATCH_LOADED[i].promise, undef
);
jerry_value_free(undef);
jerry_value_free(r);
jerry_value_free(ASSET_BATCH_LOADED[i].promise);
ASSET_BATCH_LOADED_COUNT--;
if(i < ASSET_BATCH_LOADED_COUNT) {
ASSET_BATCH_LOADED[i] =
ASSET_BATCH_LOADED[ASSET_BATCH_LOADED_COUNT];
}
}
eventUnsubscribe(&batch->onError, assetBatchOnErrorFire);
}
static void assetBatchOnErrorFire(void *params, void *user) {
assetbatch_t *batch = (assetbatch_t *)user;
uint32_t i = 0;
while(i < ASSET_BATCH_LOADED_COUNT) {
if(ASSET_BATCH_LOADED[i].batch != batch) { i++; continue; }
jerry_value_t err = jerry_string_sz("Asset batch failed to load");
jerry_value_t r = jerry_promise_reject(
ASSET_BATCH_LOADED[i].promise, err
);
jerry_value_free(err);
jerry_value_free(r);
jerry_value_free(ASSET_BATCH_LOADED[i].promise);
ASSET_BATCH_LOADED_COUNT--;
if(i < ASSET_BATCH_LOADED_COUNT) {
ASSET_BATCH_LOADED[i] =
ASSET_BATCH_LOADED[ASSET_BATCH_LOADED_COUNT];
}
}
eventUnsubscribe(&batch->onLoaded, assetBatchOnLoadedFire);
}
void moduleAssetBatchFree(void *ptr, jerry_object_native_info_t *info) {
jsassetbatch_t *b = (jsassetbatch_t *)ptr;
if(b && b->batch) {
assetBatchDispose(b->batch);
memoryFree(b->batch);
}
memoryFree(ptr);
}
jsassetbatch_t *moduleAssetBatchSelf(const jerry_call_info_t *callInfo) {
return (jsassetbatch_t *)scriptProtoGetValue(
&MODULE_ASSET_BATCH_PROTO, callInfo->this_value
);
}
moduleBaseFunction(moduleAssetBatchCtor) {
if(argc < 1 || !jerry_value_is_array(args[0])) {
return moduleBaseThrow("AssetBatch: expected an array of descriptors");
}
uint32_t count = jerry_array_length(args[0]);
if(count == 0 || count > (uint32_t)ASSET_BATCH_COUNT_MAX) {
return moduleBaseThrow("AssetBatch: descriptor count out of range");
}
assetbatchdesc_t descs[ASSET_BATCH_COUNT_MAX];
char_t paths[ASSET_BATCH_COUNT_MAX][ASSET_FILE_NAME_MAX];
for(uint32_t i = 0; i < count; i++) {
jerry_value_t desc = jerry_object_get_index(args[0], i);
if(!jerry_value_is_object(desc)) {
jerry_value_free(desc);
return moduleBaseThrow(
"AssetBatch: each descriptor must be an object"
);
}
jerry_value_t pathProp = moduleBaseGetProp(desc, "path");
if(!jerry_value_is_string(pathProp)) {
jerry_value_free(pathProp);
jerry_value_free(desc);
return moduleBaseThrow(
"AssetBatch: descriptor.path must be a string"
);
}
jerry_size_t pathLen = jerry_string_to_buffer(
pathProp, JERRY_ENCODING_UTF8,
(jerry_char_t *)paths[i], ASSET_FILE_NAME_MAX - 1
);
paths[i][pathLen] = '\0';
jerry_value_free(pathProp);
descs[i].path = paths[i];
jerry_value_t typeProp = moduleBaseGetProp(desc, "type");
if(!jerry_value_is_number(typeProp)) {
jerry_value_free(typeProp);
jerry_value_free(desc);
return moduleBaseThrow(
"AssetBatch: descriptor.type must be a number"
);
}
descs[i].type = (assetloadertype_t)moduleBaseValueInt(typeProp);
jerry_value_free(typeProp);
memoryZero(&descs[i].input, sizeof(assetloaderinput_t));
jerry_value_t inputProp = moduleBaseGetProp(desc, "format");
if(jerry_value_is_undefined(inputProp)) {
jerry_value_free(inputProp);
inputProp = moduleBaseGetProp(desc, "input");
}
if(jerry_value_is_undefined(inputProp)) {
jerry_value_free(inputProp);
inputProp = moduleBaseGetProp(desc, "axis");
}
if(jerry_value_is_number(inputProp)) {
int32_t v = moduleBaseValueInt(inputProp);
switch(descs[i].type) {
case ASSET_LOADER_TYPE_TEXTURE:
descs[i].input.texture = (textureformat_t)v;
break;
case ASSET_LOADER_TYPE_MESH:
descs[i].input.mesh = (assetmeshinputaxis_t)v;
break;
default:
break;
}
}
jerry_value_free(inputProp);
jerry_value_free(desc);
}
assetbatch_t *batch = (assetbatch_t *)memoryAllocate(sizeof(assetbatch_t));
assetBatchInit(batch, (uint16_t)count, descs);
jsassetbatch_t init = { .batch = batch };
return scriptProtoCreateValue(&MODULE_ASSET_BATCH_PROTO, &init);
}
moduleBaseFunction(moduleAssetBatchGetCount) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_number(0.0);
return jerry_number((double)b->batch->count);
}
moduleBaseFunction(moduleAssetBatchGetIsLoaded) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_boolean(false);
return jerry_boolean(assetBatchIsLoaded(b->batch));
}
moduleBaseFunction(moduleAssetBatchGetHasError) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_boolean(false);
return jerry_boolean(assetBatchHasError(b->batch));
}
moduleBaseFunction(moduleAssetBatchRequireLoaded) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) {
return moduleBaseThrow(
"AssetBatch.requireLoaded: batch already disposed"
);
}
errorret_t err = assetBatchRequireLoaded(b->batch);
if(errorIsNotOk(err)) return moduleBaseThrowError(err);
return jerry_value_copy(callInfo->this_value);
}
moduleBaseFunction(moduleAssetBatchLoaded) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) {
return moduleBaseThrow("AssetBatch.loaded: batch disposed");
}
jerry_value_t promise = jerry_promise();
if(assetBatchIsLoaded(b->batch)) {
jerry_value_t undef = jerry_undefined();
jerry_value_t r = jerry_promise_resolve(promise, undef);
jerry_value_free(undef);
jerry_value_free(r);
return promise;
}
if(assetBatchHasError(b->batch)) {
jerry_value_t err = jerry_string_sz("Asset batch failed to load");
jerry_value_t r = jerry_promise_reject(promise, err);
jerry_value_free(err);
jerry_value_free(r);
return promise;
}
if(ASSET_BATCH_LOADED_COUNT >= ASSET_BATCH_LOADED_MAX) {
jerry_value_free(promise);
return moduleBaseThrow("AssetBatch.loaded: too many pending");
}
bool_t subscribed = false;
for(uint32_t i = 0; i < ASSET_BATCH_LOADED_COUNT; i++) {
if(ASSET_BATCH_LOADED[i].batch == b->batch) {
subscribed = true; break;
}
}
if(!subscribed) {
eventSubscribe(
&b->batch->onLoaded, assetBatchOnLoadedFire, b->batch
);
eventSubscribe(
&b->batch->onError, assetBatchOnErrorFire, b->batch
);
}
ASSET_BATCH_LOADED[ASSET_BATCH_LOADED_COUNT].batch = b->batch;
ASSET_BATCH_LOADED[ASSET_BATCH_LOADED_COUNT].promise =
jerry_value_copy(promise);
ASSET_BATCH_LOADED_COUNT++;
return promise;
}
moduleBaseFunction(moduleAssetBatchLock) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_undefined();
assetBatchLock(b->batch);
return jerry_value_copy(callInfo->this_value);
}
moduleBaseFunction(moduleAssetBatchUnlock) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_undefined();
assetBatchDispose(b->batch);
memoryFree(b->batch);
b->batch = NULL;
return jerry_undefined();
}
moduleBaseFunction(moduleAssetBatchEntry) {
moduleBaseRequireArgs(1);
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_undefined();
uint32_t idx = (uint32_t)moduleBaseArgInt(0);
if(idx >= (uint32_t)b->batch->count) return jerry_undefined();
assetentry_t *entry = b->batch->entries[idx];
if(!entry) return jerry_undefined();
assetEntryLock(entry);
jsassetentry_t e = { .entry = entry };
return scriptProtoCreateValue(&MODULE_ASSET_ENTRY_PROTO, &e);
}
moduleBaseFunction(moduleAssetBatchGetOnLoaded) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_undefined();
return moduleEventGetOrCreate(
callInfo, &b->batch->onLoaded, "_onLoaded"
);
}
moduleBaseFunction(moduleAssetBatchGetOnEntryLoaded) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_undefined();
return moduleEventGetOrCreate(
callInfo, &b->batch->onEntryLoaded, "_onEntryLoaded"
);
}
moduleBaseFunction(moduleAssetBatchGetOnError) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_undefined();
return moduleEventGetOrCreate(
callInfo, &b->batch->onError, "_onError"
);
}
moduleBaseFunction(moduleAssetBatchGetOnEntryError) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_undefined();
return moduleEventGetOrCreate(
callInfo, &b->batch->onEntryError, "_onEntryError"
);
}
moduleBaseFunction(moduleAssetBatchToString) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_string_sz("AssetBatch:invalid");
char_t buf[32];
snprintf(buf, sizeof(buf), "AssetBatch(%u)", (unsigned)b->batch->count);
return jerry_string_sz(buf);
}
void moduleAssetBatchInit(void) {
ASSET_BATCH_LOADED_COUNT = 0;
scriptProtoInit(
&MODULE_ASSET_BATCH_PROTO, "AssetBatch",
sizeof(jsassetbatch_t), moduleAssetBatchCtor
);
MODULE_ASSET_BATCH_PROTO.info.free_cb = moduleAssetBatchFree;
scriptProtoDefineProp(
&MODULE_ASSET_BATCH_PROTO, "count",
moduleAssetBatchGetCount, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_BATCH_PROTO, "isLoaded",
moduleAssetBatchGetIsLoaded, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_BATCH_PROTO, "hasError",
moduleAssetBatchGetHasError, NULL
);
scriptProtoDefineFunc(
&MODULE_ASSET_BATCH_PROTO, "requireLoaded",
moduleAssetBatchRequireLoaded
);
scriptProtoDefineFunc(
&MODULE_ASSET_BATCH_PROTO, "loaded", moduleAssetBatchLoaded
);
scriptProtoDefineFunc(
&MODULE_ASSET_BATCH_PROTO, "lock", moduleAssetBatchLock
);
scriptProtoDefineFunc(
&MODULE_ASSET_BATCH_PROTO, "unlock", moduleAssetBatchUnlock
);
scriptProtoDefineFunc(
&MODULE_ASSET_BATCH_PROTO, "entry", moduleAssetBatchEntry
);
scriptProtoDefineProp(
&MODULE_ASSET_BATCH_PROTO, "onLoaded",
moduleAssetBatchGetOnLoaded, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_BATCH_PROTO, "onEntryLoaded",
moduleAssetBatchGetOnEntryLoaded, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_BATCH_PROTO, "onError",
moduleAssetBatchGetOnError, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_BATCH_PROTO, "onEntryError",
moduleAssetBatchGetOnEntryError, NULL
);
scriptProtoDefineToString(
&MODULE_ASSET_BATCH_PROTO, moduleAssetBatchToString
);
}
void moduleAssetBatchDispose(void) {
for(uint32_t i = 0; i < ASSET_BATCH_LOADED_COUNT; i++) {
bool_t seen = false;
for(uint32_t j = 0; j < i; j++) {
if(ASSET_BATCH_LOADED[j].batch == ASSET_BATCH_LOADED[i].batch) {
seen = true; break;
}
}
if(!seen) {
eventUnsubscribe(
&ASSET_BATCH_LOADED[i].batch->onLoaded,
assetBatchOnLoadedFire
);
eventUnsubscribe(
&ASSET_BATCH_LOADED[i].batch->onError,
assetBatchOnErrorFire
);
}
jerry_value_free(ASSET_BATCH_LOADED[i].promise);
}
ASSET_BATCH_LOADED_COUNT = 0;
scriptProtoDispose(&MODULE_ASSET_BATCH_PROTO);
}
@@ -0,0 +1,114 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/module/modulebase.h"
#include "script/scriptproto.h"
#include "script/module/asset/moduleassetentry.h"
#include "asset/assetbatch.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
#include "util/memory.h"
extern scriptproto_t MODULE_ASSET_BATCH_PROTO;
/** C struct wrapped by every AssetBatch JS instance. */
typedef struct {
assetbatch_t *batch;
} jsassetbatch_t;
/**
* GC free callback — disposes and frees the batch when the JS object is
* garbage collected.
*
* @param ptr Native jsassetbatch_t pointer.
* @param info Native info (unused).
*/
void moduleAssetBatchFree(void *ptr, jerry_object_native_info_t *info);
/**
* Returns the jsassetbatch_t pointer from the current this_value.
*
* @param callInfo The call info.
* @return Pointer to the jsassetbatch_t, or NULL if invalid.
*/
jsassetbatch_t *moduleAssetBatchSelf(const jerry_call_info_t *callInfo);
/**
* AssetBatch(descriptors[]) constructor. Parses an array of
* {path, type, format?/input?/axis?} descriptor objects, locks all assets,
* and returns an AssetBatch JS object. Works with and without `new`.
*
* @param args[0] Array of AssetBatchDescriptor objects.
*/
moduleBaseFunction(moduleAssetBatchCtor);
/** @return Number of entries in the batch. */
moduleBaseFunction(moduleAssetBatchGetCount);
/** @return True when every entry has reached LOADED. */
moduleBaseFunction(moduleAssetBatchGetIsLoaded);
/** @return True if any entry is in an ERROR state. */
moduleBaseFunction(moduleAssetBatchGetHasError);
/**
* requireLoaded() — blocks until every entry is loaded.
* @return this for chaining.
* @throws If any entry fails to load.
*/
moduleBaseFunction(moduleAssetBatchRequireLoaded);
/**
* loaded() — returns a Promise that resolves when all entries load, or
* rejects if any entry errors. Resolves immediately if already loaded.
*/
moduleBaseFunction(moduleAssetBatchLoaded);
/**
* lock() — acquires one additional lock on every entry.
* @return this for chaining.
*/
moduleBaseFunction(moduleAssetBatchLock);
/**
* unlock() — releases all locks and disposes the batch. The object is
* invalid after this call.
*/
moduleBaseFunction(moduleAssetBatchUnlock);
/**
* entry(index) — returns the AssetEntry at index, adding an independent
* lock.
* @param args[0] Index (number).
* @return AssetEntry, or undefined if out of range.
*/
moduleBaseFunction(moduleAssetBatchEntry);
/** @return The onLoaded Event (fires once when all entries load). */
moduleBaseFunction(moduleAssetBatchGetOnLoaded);
/** @return The onEntryLoaded Event (fires per entry). */
moduleBaseFunction(moduleAssetBatchGetOnEntryLoaded);
/** @return The onError Event (fires once when any entry errors). */
moduleBaseFunction(moduleAssetBatchGetOnError);
/** @return The onEntryError Event (fires per errored entry). */
moduleBaseFunction(moduleAssetBatchGetOnEntryError);
/** @return "AssetBatch(n)" string. */
moduleBaseFunction(moduleAssetBatchToString);
/**
* Initializes the AssetBatch module and registers the global AssetBatch
* class.
*/
void moduleAssetBatchInit(void);
/** Disposes the AssetBatch module. */
void moduleAssetBatchDispose(void);
@@ -0,0 +1,311 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduleassetentry.h"
#define ASSET_ENTRY_LOADED_MAX 16
typedef struct {
assetentry_t *entry;
jerry_value_t promise;
} assetentryloadedpend_t;
scriptproto_t MODULE_ASSET_ENTRY_PROTO;
static assetentryloadedpend_t ASSET_ENTRY_LOADED[ASSET_ENTRY_LOADED_MAX];
static uint32_t ASSET_ENTRY_LOADED_COUNT = 0;
static void assetEntryOnLoadedFire(void *params, void *user);
static void assetEntryOnErrorFire(void *params, void *user);
static void assetEntryOnLoadedFire(void *params, void *user) {
assetentry_t *entry = (assetentry_t *)user;
uint32_t i = 0;
while(i < ASSET_ENTRY_LOADED_COUNT) {
if(ASSET_ENTRY_LOADED[i].entry != entry) { i++; continue; }
jerry_value_t undef = jerry_undefined();
jerry_value_t r = jerry_promise_resolve(
ASSET_ENTRY_LOADED[i].promise, undef
);
jerry_value_free(undef);
jerry_value_free(r);
jerry_value_free(ASSET_ENTRY_LOADED[i].promise);
ASSET_ENTRY_LOADED_COUNT--;
if(i < ASSET_ENTRY_LOADED_COUNT) {
ASSET_ENTRY_LOADED[i] =
ASSET_ENTRY_LOADED[ASSET_ENTRY_LOADED_COUNT];
}
}
eventUnsubscribe(&entry->onError, assetEntryOnErrorFire);
}
static void assetEntryOnErrorFire(void *params, void *user) {
assetentry_t *entry = (assetentry_t *)user;
uint32_t i = 0;
while(i < ASSET_ENTRY_LOADED_COUNT) {
if(ASSET_ENTRY_LOADED[i].entry != entry) { i++; continue; }
jerry_value_t err = jerry_string_sz("Asset failed to load");
jerry_value_t r = jerry_promise_reject(
ASSET_ENTRY_LOADED[i].promise, err
);
jerry_value_free(err);
jerry_value_free(r);
jerry_value_free(ASSET_ENTRY_LOADED[i].promise);
ASSET_ENTRY_LOADED_COUNT--;
if(i < ASSET_ENTRY_LOADED_COUNT) {
ASSET_ENTRY_LOADED[i] =
ASSET_ENTRY_LOADED[ASSET_ENTRY_LOADED_COUNT];
}
}
eventUnsubscribe(&entry->onLoaded, assetEntryOnLoadedFire);
}
void moduleAssetEntryFree(void *ptr, jerry_object_native_info_t *info) {
jsassetentry_t *e = (jsassetentry_t *)ptr;
if(e && e->entry) {
assetUnlockEntry(e->entry);
e->entry = NULL;
}
memoryFree(ptr);
}
jsassetentry_t *moduleAssetEntrySelf(const jerry_call_info_t *callInfo) {
return (jsassetentry_t *)scriptProtoGetValue(
&MODULE_ASSET_ENTRY_PROTO, callInfo->this_value
);
}
moduleBaseFunction(moduleAssetEntryCtor) {
return moduleBaseThrow("AssetEntry cannot be instantiated with new");
}
moduleBaseFunction(moduleAssetEntryGetName) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) return jerry_undefined();
return jerry_string_sz(e->entry->name);
}
moduleBaseFunction(moduleAssetEntryGetState) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) return jerry_undefined();
return jerry_number((double)e->entry->state);
}
moduleBaseFunction(moduleAssetEntryGetType) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) return jerry_undefined();
return jerry_number((double)e->entry->type);
}
moduleBaseFunction(moduleAssetEntryGetIsLoaded) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) return jerry_boolean(false);
return jerry_boolean(e->entry->state == ASSET_ENTRY_STATE_LOADED);
}
moduleBaseFunction(moduleAssetEntryGetTexture) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) return jerry_undefined();
if(e->entry->type != ASSET_LOADER_TYPE_TEXTURE) return jerry_undefined();
if(e->entry->state != ASSET_ENTRY_STATE_LOADED) return jerry_undefined();
assetEntryLock(e->entry);
jstexture_t tex = { .entry = e->entry };
return scriptProtoCreateValue(&MODULE_TEXTURE_PROTO, &tex);
}
moduleBaseFunction(moduleAssetEntryRequireLoaded) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) {
return moduleBaseThrow("AssetEntry.requireLoaded: invalid entry");
}
errorret_t err = assetRequireLoaded(e->entry);
if(errorIsNotOk(err)) return moduleBaseThrowError(err);
return jerry_value_copy(callInfo->this_value);
}
moduleBaseFunction(moduleAssetEntryUnlock) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) return jerry_undefined();
assetUnlockEntry(e->entry);
e->entry = NULL;
return jerry_undefined();
}
moduleBaseFunction(moduleAssetEntryGetOnLoaded) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) return jerry_undefined();
return moduleEventGetOrCreate(
callInfo, &e->entry->onLoaded, "_onLoaded"
);
}
moduleBaseFunction(moduleAssetEntryGetOnUnloaded) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) return jerry_undefined();
return moduleEventGetOrCreate(
callInfo, &e->entry->onUnloaded, "_onUnloaded"
);
}
moduleBaseFunction(moduleAssetEntryGetOnError) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) return jerry_undefined();
return moduleEventGetOrCreate(
callInfo, &e->entry->onError, "_onError"
);
}
moduleBaseFunction(moduleAssetEntryLoaded) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) {
return moduleBaseThrow("AssetEntry.loaded: invalid entry");
}
jerry_value_t promise = jerry_promise();
if(e->entry->state == ASSET_ENTRY_STATE_LOADED) {
jerry_value_t undef = jerry_undefined();
jerry_value_t r = jerry_promise_resolve(promise, undef);
jerry_value_free(undef);
jerry_value_free(r);
return promise;
}
if(e->entry->state == ASSET_ENTRY_STATE_ERROR) {
jerry_value_t err = jerry_string_sz("Asset failed to load");
jerry_value_t r = jerry_promise_reject(promise, err);
jerry_value_free(err);
jerry_value_free(r);
return promise;
}
if(ASSET_ENTRY_LOADED_COUNT >= ASSET_ENTRY_LOADED_MAX) {
jerry_value_free(promise);
return moduleBaseThrow("AssetEntry.loaded: too many pending");
}
bool_t subscribed = false;
for(uint32_t i = 0; i < ASSET_ENTRY_LOADED_COUNT; i++) {
if(ASSET_ENTRY_LOADED[i].entry == e->entry) {
subscribed = true; break;
}
}
if(!subscribed) {
eventSubscribe(
&e->entry->onLoaded, assetEntryOnLoadedFire, e->entry
);
eventSubscribe(
&e->entry->onError, assetEntryOnErrorFire, e->entry
);
}
ASSET_ENTRY_LOADED[ASSET_ENTRY_LOADED_COUNT].entry = e->entry;
ASSET_ENTRY_LOADED[ASSET_ENTRY_LOADED_COUNT].promise =
jerry_value_copy(promise);
ASSET_ENTRY_LOADED_COUNT++;
return promise;
}
moduleBaseFunction(moduleAssetEntryToString) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) return jerry_string_sz("AssetEntry:invalid");
char_t buf[64];
snprintf(buf, sizeof(buf), "AssetEntry(%s)", e->entry->name);
return jerry_string_sz(buf);
}
void moduleAssetEntryInit(void) {
ASSET_ENTRY_LOADED_COUNT = 0;
scriptProtoInit(
&MODULE_ASSET_ENTRY_PROTO, "AssetEntry",
sizeof(jsassetentry_t), moduleAssetEntryCtor
);
MODULE_ASSET_ENTRY_PROTO.info.free_cb = moduleAssetEntryFree;
scriptProtoDefineProp(
&MODULE_ASSET_ENTRY_PROTO, "name", moduleAssetEntryGetName, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_ENTRY_PROTO, "state", moduleAssetEntryGetState, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_ENTRY_PROTO, "type", moduleAssetEntryGetType, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_ENTRY_PROTO, "isLoaded",
moduleAssetEntryGetIsLoaded, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_ENTRY_PROTO, "texture",
moduleAssetEntryGetTexture, NULL
);
scriptProtoDefineFunc(
&MODULE_ASSET_ENTRY_PROTO, "requireLoaded",
moduleAssetEntryRequireLoaded
);
scriptProtoDefineFunc(
&MODULE_ASSET_ENTRY_PROTO, "unlock", moduleAssetEntryUnlock
);
scriptProtoDefineProp(
&MODULE_ASSET_ENTRY_PROTO, "onLoaded",
moduleAssetEntryGetOnLoaded, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_ENTRY_PROTO, "onUnloaded",
moduleAssetEntryGetOnUnloaded, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_ENTRY_PROTO, "onError",
moduleAssetEntryGetOnError, NULL
);
scriptProtoDefineFunc(
&MODULE_ASSET_ENTRY_PROTO, "loaded", moduleAssetEntryLoaded
);
scriptProtoDefineToString(
&MODULE_ASSET_ENTRY_PROTO, moduleAssetEntryToString
);
jerry_value_t ctor = MODULE_ASSET_ENTRY_PROTO.constructor;
struct { const char_t *name; int val; } states[] = {
{ "NOT_STARTED", ASSET_ENTRY_STATE_NOT_STARTED },
{ "PENDING", ASSET_ENTRY_STATE_PENDING_ASYNC },
{ "LOADING", ASSET_ENTRY_STATE_LOADING_ASYNC },
{ "LOADED", ASSET_ENTRY_STATE_LOADED },
{ "ERROR", ASSET_ENTRY_STATE_ERROR },
};
for(int i = 0; i < 5; i++) {
jerry_value_t k = jerry_string_sz(states[i].name);
jerry_value_t v = jerry_number((double)states[i].val);
jerry_object_set(ctor, k, v);
jerry_value_free(v);
jerry_value_free(k);
}
}
void moduleAssetEntryDispose(void) {
for(uint32_t i = 0; i < ASSET_ENTRY_LOADED_COUNT; i++) {
bool_t seen = false;
for(uint32_t j = 0; j < i; j++) {
if(ASSET_ENTRY_LOADED[j].entry == ASSET_ENTRY_LOADED[i].entry) {
seen = true; break;
}
}
if(!seen) {
eventUnsubscribe(
&ASSET_ENTRY_LOADED[i].entry->onLoaded,
assetEntryOnLoadedFire
);
eventUnsubscribe(
&ASSET_ENTRY_LOADED[i].entry->onError,
assetEntryOnErrorFire
);
}
jerry_value_free(ASSET_ENTRY_LOADED[i].promise);
}
ASSET_ENTRY_LOADED_COUNT = 0;
scriptProtoDispose(&MODULE_ASSET_ENTRY_PROTO);
}
@@ -0,0 +1,96 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/module/modulebase.h"
#include "script/scriptproto.h"
#include "script/module/display/moduletexture.h"
#include "script/module/event/moduleevent.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
#include "asset/loader/assetentry.h"
#include "util/memory.h"
extern scriptproto_t MODULE_ASSET_ENTRY_PROTO;
/** C struct wrapped by every AssetEntry JS instance. */
typedef struct {
assetentry_t *entry;
} jsassetentry_t;
/**
* GC free callback — releases the asset lock when the AssetEntry JS object
* is garbage collected.
*
* @param ptr Native jsassetentry_t pointer.
* @param info Native info (unused).
*/
void moduleAssetEntryFree(void *ptr, jerry_object_native_info_t *info);
/**
* Returns the jsassetentry_t pointer from the current this_value.
*
* @param callInfo The call info.
* @return Pointer to the jsassetentry_t, or NULL if invalid.
*/
jsassetentry_t *moduleAssetEntrySelf(const jerry_call_info_t *callInfo);
/** AssetEntry() constructor — always throws; not directly instantiable. */
moduleBaseFunction(moduleAssetEntryCtor);
/** @return Archive-relative path used as the cache key. */
moduleBaseFunction(moduleAssetEntryGetName);
/** @return Current loading state as a number (AssetEntry.* constants). */
moduleBaseFunction(moduleAssetEntryGetState);
/** @return Loader type constant. */
moduleBaseFunction(moduleAssetEntryGetType);
/** @return True when the entry has fully loaded. */
moduleBaseFunction(moduleAssetEntryGetIsLoaded);
/**
* Returns a Texture wrapping this entry's texture data. The Texture holds
* its own asset lock independently of this AssetEntry.
* @return A Texture JS object, or undefined if not a loaded texture.
*/
moduleBaseFunction(moduleAssetEntryGetTexture);
/**
* requireLoaded() — blocks until the entry is LOADED or ERROR.
* @return this for chaining.
* @throws If the load fails.
*/
moduleBaseFunction(moduleAssetEntryRequireLoaded);
/** unlock() — releases the asset lock immediately. */
moduleBaseFunction(moduleAssetEntryUnlock);
/** @return The onLoaded Event for this entry. */
moduleBaseFunction(moduleAssetEntryGetOnLoaded);
/** @return The onUnloaded Event for this entry. */
moduleBaseFunction(moduleAssetEntryGetOnUnloaded);
/** @return The onError Event for this entry. */
moduleBaseFunction(moduleAssetEntryGetOnError);
/**
* loaded() — returns a Promise that resolves when the entry loads, or
* rejects on error. Resolves immediately if already loaded.
*/
moduleBaseFunction(moduleAssetEntryLoaded);
/** @return "AssetEntry(name)" string. */
moduleBaseFunction(moduleAssetEntryToString);
/** Initializes the AssetEntry module. */
void moduleAssetEntryInit(void);
/** Disposes the AssetEntry module. */
void moduleAssetEntryDispose(void);
@@ -0,0 +1,202 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduleeventproxy.h"
#include "util/memory.h"
scriptproto_t MODULE_EVENT_PROXY_PROTO;
void moduleEventProxyTrampoline0(void *params, void *user) {
jerry_value_t fn = (jerry_value_t)(uintptr_t)user;
jerry_value_t ret = jerry_call(fn, jerry_undefined(), NULL, 0);
jerry_value_free(ret);
}
void moduleEventProxyTrampoline1(void *params, void *user) {
jerry_value_t fn = (jerry_value_t)(uintptr_t)user;
jerry_value_t ret = jerry_call(fn, jerry_undefined(), NULL, 0);
jerry_value_free(ret);
}
void moduleEventProxyTrampoline2(void *params, void *user) {
jerry_value_t fn = (jerry_value_t)(uintptr_t)user;
jerry_value_t ret = jerry_call(fn, jerry_undefined(), NULL, 0);
jerry_value_free(ret);
}
void moduleEventProxyTrampoline3(void *params, void *user) {
jerry_value_t fn = (jerry_value_t)(uintptr_t)user;
jerry_value_t ret = jerry_call(fn, jerry_undefined(), NULL, 0);
jerry_value_free(ret);
}
eventcallback_t MODULE_EVENT_PROXY_TRAMPOLINES[MODULE_EVENT_PROXY_MAX_SLOTS] = {
moduleEventProxyTrampoline0,
moduleEventProxyTrampoline1,
moduleEventProxyTrampoline2,
moduleEventProxyTrampoline3,
};
void moduleEventProxyFree(void *ptr, jerry_object_native_info_t *info) {
jseventproxy_t *ep = (jseventproxy_t *)ptr;
if(ep) {
for(uint32_t i = 0; i < MODULE_EVENT_PROXY_MAX_SLOTS; i++) {
if(jerry_value_is_function(ep->fns[i])) {
if(ep->event) {
eventUnsubscribe(ep->event, MODULE_EVENT_PROXY_TRAMPOLINES[i]);
}
jerry_value_free(ep->fns[i]);
}
}
}
memoryFree(ptr);
}
jseventproxy_t *moduleEventProxySelf(const jerry_call_info_t *callInfo) {
return (jseventproxy_t *)scriptProtoGetValue(
&MODULE_EVENT_PROXY_PROTO, callInfo->this_value
);
}
static jerry_value_t moduleEventProxyGetSlot(
const jerry_call_info_t *callInfo,
const uint32_t slot
) {
jseventproxy_t *ep = moduleEventProxySelf(callInfo);
if(!ep || !ep->event || slot >= ep->event->size) return jerry_null();
return jerry_value_is_function(ep->fns[slot])
? jerry_value_copy(ep->fns[slot])
: jerry_null();
}
static jerry_value_t moduleEventProxySetSlot(
const jerry_call_info_t *callInfo,
const jerry_value_t args[],
const jerry_length_t argc,
const uint32_t slot
) {
jseventproxy_t *ep = moduleEventProxySelf(callInfo);
if(!ep || !ep->event || slot >= ep->event->size) return jerry_undefined();
if(jerry_value_is_function(ep->fns[slot])) {
eventUnsubscribe(ep->event, MODULE_EVENT_PROXY_TRAMPOLINES[slot]);
jerry_value_free(ep->fns[slot]);
ep->fns[slot] = jerry_undefined();
}
jerry_value_t val = (argc > 0) ? args[0] : jerry_undefined();
if(jerry_value_is_function(val)) {
ep->fns[slot] = jerry_value_copy(val);
eventSubscribe(
ep->event,
MODULE_EVENT_PROXY_TRAMPOLINES[slot],
(void *)(uintptr_t)ep->fns[slot]
);
}
return jerry_undefined();
}
moduleBaseFunction(moduleEventProxyGet0) {
return moduleEventProxyGetSlot(callInfo, 0);
}
moduleBaseFunction(moduleEventProxySet0) {
return moduleEventProxySetSlot(callInfo, args, argc, 0);
}
moduleBaseFunction(moduleEventProxyGet1) {
return moduleEventProxyGetSlot(callInfo, 1);
}
moduleBaseFunction(moduleEventProxySet1) {
return moduleEventProxySetSlot(callInfo, args, argc, 1);
}
moduleBaseFunction(moduleEventProxyGet2) {
return moduleEventProxyGetSlot(callInfo, 2);
}
moduleBaseFunction(moduleEventProxySet2) {
return moduleEventProxySetSlot(callInfo, args, argc, 2);
}
moduleBaseFunction(moduleEventProxyGet3) {
return moduleEventProxyGetSlot(callInfo, 3);
}
moduleBaseFunction(moduleEventProxySet3) {
return moduleEventProxySetSlot(callInfo, args, argc, 3);
}
moduleBaseFunction(moduleEventProxyGetLength) {
jseventproxy_t *ep = moduleEventProxySelf(callInfo);
if(!ep || !ep->event) return jerry_number(0.0);
return jerry_number((double)ep->event->size);
}
moduleBaseFunction(moduleEventProxyToString) {
return jerry_string_sz("EventProxy");
}
jerry_value_t moduleEventProxyGetOrCreate(
const jerry_call_info_t *callInfo,
event_t *event,
const char_t *pinKey
) {
jerry_value_t keyStr = jerry_string_sz(pinKey);
jerry_value_t existing = jerry_object_get(callInfo->this_value, keyStr);
if(!jerry_value_is_undefined(existing)) {
jerry_value_free(keyStr);
return existing;
}
jerry_value_free(existing);
jseventproxy_t ep;
ep.event = event;
for(uint32_t i = 0; i < MODULE_EVENT_PROXY_MAX_SLOTS; i++) {
ep.fns[i] = jerry_undefined();
}
jerry_value_t proxy = scriptProtoCreateValue(
&MODULE_EVENT_PROXY_PROTO, &ep
);
jerry_object_set(callInfo->this_value, keyStr, proxy);
jerry_value_free(keyStr);
return proxy;
}
void moduleEventProxyInit(void) {
scriptProtoInit(
&MODULE_EVENT_PROXY_PROTO, NULL,
sizeof(jseventproxy_t), NULL
);
MODULE_EVENT_PROXY_PROTO.info.free_cb = moduleEventProxyFree;
scriptProtoDefineProp(
&MODULE_EVENT_PROXY_PROTO, "0",
moduleEventProxyGet0, moduleEventProxySet0
);
scriptProtoDefineProp(
&MODULE_EVENT_PROXY_PROTO, "1",
moduleEventProxyGet1, moduleEventProxySet1
);
scriptProtoDefineProp(
&MODULE_EVENT_PROXY_PROTO, "2",
moduleEventProxyGet2, moduleEventProxySet2
);
scriptProtoDefineProp(
&MODULE_EVENT_PROXY_PROTO, "3",
moduleEventProxyGet3, moduleEventProxySet3
);
scriptProtoDefineProp(
&MODULE_EVENT_PROXY_PROTO, "length",
moduleEventProxyGetLength, NULL
);
scriptProtoDefineToString(
&MODULE_EVENT_PROXY_PROTO, moduleEventProxyToString
);
}
void moduleEventProxyDispose(void) {
scriptProtoDispose(&MODULE_EVENT_PROXY_PROTO);
}
@@ -0,0 +1,10 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
/* Merged into script/module/event/moduleevent.h */
#pragma once
#include "script/module/event/moduleevent.h"
@@ -3,8 +3,7 @@
# This software is released under the MIT License. # This software is released under the MIT License.
# https://opensource.org/licenses/MIT # https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
initialscene.c moduleconsole.c
) )
@@ -0,0 +1,66 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduleconsole.h"
#include "util/string.h"
#include <string.h>
scriptproto_t MODULE_CONSOLE_PROTO;
moduleBaseFunction(moduleConsolePrint) {
char_t buf[512];
char_t msg[4096];
size_t msgLen = 0;
for(jerry_length_t i = 0; i < argc; ++i) {
jerry_value_t strVal = jerry_value_to_string(args[i]);
moduleBaseToString(strVal, buf, sizeof(buf));
jerry_value_free(strVal);
size_t partLen = strlen(buf);
if(msgLen + partLen + 1 < sizeof(msg)) {
stringCopy(msg + msgLen, buf, sizeof(msg) - msgLen);
msgLen += partLen;
}
if(i + 1 < argc && msgLen + 1 < sizeof(msg)) {
msg[msgLen++] = '\t';
msg[msgLen] = '\0';
}
}
consolePrint("%s", msg);
return jerry_undefined();
}
moduleBaseFunction(moduleConsoleGetVisible) {
return jerry_boolean(CONSOLE.visible);
}
moduleBaseFunction(moduleConsoleSetVisible) {
moduleBaseRequireArgs(1);
CONSOLE.visible = moduleBaseArgBool(0);
return jerry_undefined();
}
void moduleConsoleInit(void) {
scriptProtoInit(
&MODULE_CONSOLE_PROTO, "Console",
sizeof(uint8_t), NULL
);
scriptProtoDefineStaticFunc(
&MODULE_CONSOLE_PROTO, "print", moduleConsolePrint
);
scriptProtoDefineStaticProp(
&MODULE_CONSOLE_PROTO, "visible",
moduleConsoleGetVisible, moduleConsoleSetVisible
);
}
void moduleConsoleDispose(void) {
scriptProtoDispose(&MODULE_CONSOLE_PROTO);
}
+19 -51
View File
@@ -10,59 +10,27 @@
#include "script/scriptproto.h" #include "script/scriptproto.h"
#include "console/console.h" #include "console/console.h"
static scriptproto_t MODULE_CONSOLE_PROTO; extern scriptproto_t MODULE_CONSOLE_PROTO;
moduleBaseFunction(moduleConsolePrint) { /**
char_t buf[512]; * Console.print(...args) — concatenates all arguments tab-separated and prints
char_t msg[4096]; * to the engine console.
size_t msgLen = 0; */
moduleBaseFunction(moduleConsolePrint);
for(jerry_length_t i = 0; i < argc; ++i) { /** @return Whether the console overlay is currently visible. */
jerry_value_t strVal = jerry_value_to_string(args[i]); moduleBaseFunction(moduleConsoleGetVisible);
moduleBaseToString(strVal, buf, sizeof(buf));
jerry_value_free(strVal);
size_t partLen = strlen(buf); /** Sets console visibility. @param args[0] Boolean visible state. */
if(msgLen + partLen + 1 < sizeof(msg)) { moduleBaseFunction(moduleConsoleSetVisible);
stringCopy(msg + msgLen, buf, sizeof(msg) - msgLen);
msgLen += partLen;
}
if(i + 1 < argc && msgLen + 1 < sizeof(msg)) { /**
msg[msgLen++] = '\t'; * Initializes the Console module and registers the global Console object with
msg[msgLen] = '\0'; * print() and the visible property.
} */
} void moduleConsoleInit(void);
consolePrint("%s", msg); /**
return jerry_undefined(); * Disposes the Console module.
} */
void moduleConsoleDispose(void);
moduleBaseFunction(moduleConsoleGetVisible) {
return jerry_boolean(CONSOLE.visible);
}
moduleBaseFunction(moduleConsoleSetVisible) {
moduleBaseRequireArgs(1);
CONSOLE.visible = moduleBaseArgBool(0);
return jerry_undefined();
}
static void moduleConsoleInit(void) {
scriptProtoInit(
&MODULE_CONSOLE_PROTO, "Console",
sizeof(uint8_t), NULL
);
scriptProtoDefineStaticFunc(
&MODULE_CONSOLE_PROTO, "print", moduleConsolePrint
);
scriptProtoDefineStaticProp(
&MODULE_CONSOLE_PROTO, "visible",
moduleConsoleGetVisible, moduleConsoleSetVisible
);
}
static void moduleConsoleDispose(void) {
scriptProtoDispose(&MODULE_CONSOLE_PROTO);
}
@@ -5,9 +5,7 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
entityinteractable.c modulecolor.c
entityoverworld.c modulescreen.c
entityoverworldcamera.c moduletexture.c
entityoverworldtrigger.c
entityplayer.c
) )
@@ -0,0 +1,149 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "modulecolor.h"
scriptproto_t MODULE_COLOR_PROTO;
color_t *moduleColorFrom(const jerry_value_t val) {
return (color_t *)scriptProtoGetValue(&MODULE_COLOR_PROTO, val);
}
jerry_value_t moduleColorPush(const color_t c) {
return scriptProtoCreateValue(&MODULE_COLOR_PROTO, &c);
}
moduleBaseFunction(moduleColorConstructor) {
color_t *ptr = (color_t *)memoryAllocate(sizeof(color_t));
ptr->r = (uint8_t)moduleBaseOptInt(0, 0);
ptr->g = (uint8_t)moduleBaseOptInt(1, 0);
ptr->b = (uint8_t)moduleBaseOptInt(2, 0);
ptr->a = (uint8_t)moduleBaseOptInt(3, 255);
jerry_object_set_native_ptr(
callInfo->this_value, &MODULE_COLOR_PROTO.info, ptr
);
return jerry_undefined();
}
moduleBaseFunction(moduleColorGetR) {
color_t *c = moduleColorFrom(callInfo->this_value);
if(!c) return jerry_undefined();
return jerry_number((double)c->r);
}
moduleBaseFunction(moduleColorSetR) {
moduleBaseRequireArgs(1);
color_t *c = moduleColorFrom(callInfo->this_value);
if(!c) return jerry_undefined();
c->r = (uint8_t)moduleBaseArgInt(0);
return jerry_undefined();
}
moduleBaseFunction(moduleColorGetG) {
color_t *c = moduleColorFrom(callInfo->this_value);
if(!c) return jerry_undefined();
return jerry_number((double)c->g);
}
moduleBaseFunction(moduleColorSetG) {
moduleBaseRequireArgs(1);
color_t *c = moduleColorFrom(callInfo->this_value);
if(!c) return jerry_undefined();
c->g = (uint8_t)moduleBaseArgInt(0);
return jerry_undefined();
}
moduleBaseFunction(moduleColorGetB) {
color_t *c = moduleColorFrom(callInfo->this_value);
if(!c) return jerry_undefined();
return jerry_number((double)c->b);
}
moduleBaseFunction(moduleColorSetB) {
moduleBaseRequireArgs(1);
color_t *c = moduleColorFrom(callInfo->this_value);
if(!c) return jerry_undefined();
c->b = (uint8_t)moduleBaseArgInt(0);
return jerry_undefined();
}
moduleBaseFunction(moduleColorGetA) {
color_t *c = moduleColorFrom(callInfo->this_value);
if(!c) return jerry_undefined();
return jerry_number((double)c->a);
}
moduleBaseFunction(moduleColorSetA) {
moduleBaseRequireArgs(1);
color_t *c = moduleColorFrom(callInfo->this_value);
if(!c) return jerry_undefined();
c->a = (uint8_t)moduleBaseArgInt(0);
return jerry_undefined();
}
moduleBaseFunction(moduleColorToString) {
color_t *c = moduleColorFrom(callInfo->this_value);
if(!c) return jerry_string_sz("Color:invalid");
char_t buf[32];
snprintf(buf, sizeof(buf), "Color(%u,%u,%u,%u)",
(unsigned)c->r, (unsigned)c->g,
(unsigned)c->b, (unsigned)c->a
);
return jerry_string_sz(buf);
}
void moduleColorInit(void) {
scriptProtoInit(
&MODULE_COLOR_PROTO, "Color",
sizeof(color_t), moduleColorConstructor
);
scriptProtoDefineProp(
&MODULE_COLOR_PROTO, "r", moduleColorGetR, moduleColorSetR
);
scriptProtoDefineProp(
&MODULE_COLOR_PROTO, "g", moduleColorGetG, moduleColorSetG
);
scriptProtoDefineProp(
&MODULE_COLOR_PROTO, "b", moduleColorGetB, moduleColorSetB
);
scriptProtoDefineProp(
&MODULE_COLOR_PROTO, "a", moduleColorGetA, moduleColorSetA
);
scriptProtoDefineToString(&MODULE_COLOR_PROTO, moduleColorToString);
struct { const char_t *name; color_t val; } constants[] = {
{ "WHITE", COLOR_WHITE },
{ "BLACK", COLOR_BLACK },
{ "RED", COLOR_RED },
{ "GREEN", COLOR_GREEN },
{ "BLUE", COLOR_BLUE },
{ "YELLOW", COLOR_YELLOW },
{ "CYAN", COLOR_CYAN },
{ "MAGENTA", COLOR_MAGENTA },
{ "TRANSPARENT", COLOR_TRANSPARENT },
{ "GRAY", COLOR_GRAY },
{ "LIGHT_GRAY", COLOR_LIGHT_GRAY },
{ "DARK_GRAY", COLOR_DARK_GRAY },
{ "ORANGE", COLOR_ORANGE },
{ "PURPLE", COLOR_PURPLE },
{ "PINK", COLOR_PINK },
{ "TEAL", COLOR_TEAL },
{ "CORNFLOWER_BLUE", COLOR_CORNFLOWER_BLUE },
};
jerry_value_t ctor = MODULE_COLOR_PROTO.constructor;
for(int i = 0; i < (int)(sizeof(constants)/sizeof(constants[0])); i++) {
jerry_value_t k = jerry_string_sz(constants[i].name);
jerry_value_t v = moduleColorPush(constants[i].val);
jerry_object_set(ctor, k, v);
jerry_value_free(v);
jerry_value_free(k);
}
}
void moduleColorDispose(void) {
scriptProtoDispose(&MODULE_COLOR_PROTO);
}
@@ -0,0 +1,67 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/module/modulebase.h"
#include "script/scriptproto.h"
#include "util/memory.h"
#include "display/color.h"
extern scriptproto_t MODULE_COLOR_PROTO;
/**
* Returns the native color_t pointer from a Color JS value.
*
* @param val The JS value to extract from.
* @return Pointer to the color data, or NULL if not a Color.
*/
color_t *moduleColorFrom(const jerry_value_t val);
/**
* Creates a Color JS object wrapping a copy of a C color_t.
*
* @param c The source color.
* @return A new Color JS object.
*/
jerry_value_t moduleColorPush(const color_t c);
/** Color(r?, g?, b?, a?) constructor. */
moduleBaseFunction(moduleColorConstructor);
/** @return The red channel as a number. */
moduleBaseFunction(moduleColorGetR);
/** Sets the red channel. @param args[0] New r value (0-255). */
moduleBaseFunction(moduleColorSetR);
/** @return The green channel as a number. */
moduleBaseFunction(moduleColorGetG);
/** Sets the green channel. @param args[0] New g value (0-255). */
moduleBaseFunction(moduleColorSetG);
/** @return The blue channel as a number. */
moduleBaseFunction(moduleColorGetB);
/** Sets the blue channel. @param args[0] New b value (0-255). */
moduleBaseFunction(moduleColorSetB);
/** @return The alpha channel as a number. */
moduleBaseFunction(moduleColorGetA);
/** Sets the alpha channel. @param args[0] New a value (0-255). */
moduleBaseFunction(moduleColorSetA);
/** @return "Color(r,g,b,a)" string. */
moduleBaseFunction(moduleColorToString);
/**
* Initializes the Color module and registers the global Color class with named
* color constants (Color.WHITE, Color.RED, etc.).
*/
void moduleColorInit(void);
/**
* Disposes the Color module.
*/
void moduleColorDispose(void);
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "modulescreen.h"
scriptproto_t MODULE_SCREEN_PROTO;
moduleBaseFunction(moduleScreenGetWidth) {
return jerry_number((double)SCREEN.width);
}
moduleBaseFunction(moduleScreenGetHeight) {
return jerry_number((double)SCREEN.height);
}
moduleBaseFunction(moduleScreenGetAspect) {
return jerry_number((double)SCREEN.aspect);
}
void moduleScreenInit(void) {
scriptProtoInit(&MODULE_SCREEN_PROTO, "Screen", sizeof(uint8_t), NULL);
scriptProtoDefineStaticProp(
&MODULE_SCREEN_PROTO, "width", moduleScreenGetWidth, NULL
);
scriptProtoDefineStaticProp(
&MODULE_SCREEN_PROTO, "height", moduleScreenGetHeight, NULL
);
scriptProtoDefineStaticProp(
&MODULE_SCREEN_PROTO, "aspect", moduleScreenGetAspect, NULL
);
}
void moduleScreenDispose(void) {
scriptProtoDispose(&MODULE_SCREEN_PROTO);
}
+16 -29
View File
@@ -10,37 +10,24 @@
#include "script/scriptproto.h" #include "script/scriptproto.h"
#include "display/screen/screen.h" #include "display/screen/screen.h"
static scriptproto_t MODULE_SCREEN_PROTO; extern scriptproto_t MODULE_SCREEN_PROTO;
moduleBaseFunction(moduleScreenGetWidth) { /** @return Current screen width in pixels. */
return jerry_number((double)SCREEN.width); moduleBaseFunction(moduleScreenGetWidth);
}
moduleBaseFunction(moduleScreenGetHeight) { /** @return Current screen height in pixels. */
return jerry_number((double)SCREEN.height); moduleBaseFunction(moduleScreenGetHeight);
}
moduleBaseFunction(moduleScreenGetAspect) { /** @return Current screen aspect ratio (width / height). */
return jerry_number((double)SCREEN.aspect); moduleBaseFunction(moduleScreenGetAspect);
}
static void moduleScreenInit(void) { /**
scriptProtoInit(&MODULE_SCREEN_PROTO, "Screen", sizeof(uint8_t), NULL); * Initializes the Screen module and registers read-only width/height/aspect
* properties on the global Screen object.
*/
void moduleScreenInit(void);
scriptProtoDefineStaticProp( /**
&MODULE_SCREEN_PROTO, "width", * Disposes the Screen module.
moduleScreenGetWidth, NULL */
); void moduleScreenDispose(void);
scriptProtoDefineStaticProp(
&MODULE_SCREEN_PROTO, "height",
moduleScreenGetHeight, NULL
);
scriptProtoDefineStaticProp(
&MODULE_SCREEN_PROTO, "aspect",
moduleScreenGetAspect, NULL
);
}
static void moduleScreenDispose(void) {
scriptProtoDispose(&MODULE_SCREEN_PROTO);
}
@@ -0,0 +1,85 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduletexture.h"
scriptproto_t MODULE_TEXTURE_PROTO;
void moduleTextureFree(void *ptr, jerry_object_native_info_t *info) {
jstexture_t *tex = (jstexture_t *)ptr;
if(tex && tex->entry) {
assetUnlockEntry(tex->entry);
tex->entry = NULL;
}
memoryFree(ptr);
}
jstexture_t *moduleTextureSelf(const jerry_call_info_t *callInfo) {
return (jstexture_t *)scriptProtoGetValue(
&MODULE_TEXTURE_PROTO, callInfo->this_value
);
}
moduleBaseFunction(moduleTextureCtor) {
return moduleBaseThrow("Texture cannot be instantiated with new");
}
moduleBaseFunction(moduleTextureGetWidth) {
jstexture_t *t = moduleTextureSelf(callInfo);
if(!t || !t->entry) return jerry_undefined();
return jerry_number((double)t->entry->data.texture.width);
}
moduleBaseFunction(moduleTextureGetHeight) {
jstexture_t *t = moduleTextureSelf(callInfo);
if(!t || !t->entry) return jerry_undefined();
return jerry_number((double)t->entry->data.texture.height);
}
moduleBaseFunction(moduleTextureToString) {
jstexture_t *t = moduleTextureSelf(callInfo);
if(!t || !t->entry) return jerry_string_sz("Texture:invalid");
char_t buf[64];
snprintf(buf, sizeof(buf), "Texture(%dx%d)",
t->entry->data.texture.width,
t->entry->data.texture.height
);
return jerry_string_sz(buf);
}
void moduleTextureInit(void) {
scriptProtoInit(
&MODULE_TEXTURE_PROTO, "Texture",
sizeof(jstexture_t), moduleTextureCtor
);
MODULE_TEXTURE_PROTO.info.free_cb = moduleTextureFree;
scriptProtoDefineProp(
&MODULE_TEXTURE_PROTO, "width", moduleTextureGetWidth, NULL
);
scriptProtoDefineProp(
&MODULE_TEXTURE_PROTO, "height", moduleTextureGetHeight, NULL
);
scriptProtoDefineToString(&MODULE_TEXTURE_PROTO, moduleTextureToString);
jerry_value_t ctor = MODULE_TEXTURE_PROTO.constructor;
struct { const char_t *name; int val; } formats[] = {
{ "FORMAT_RGBA", TEXTURE_FORMAT_RGBA },
{ "FORMAT_PALETTE", TEXTURE_FORMAT_PALETTE },
};
for(int i = 0; i < 2; i++) {
jerry_value_t k = jerry_string_sz(formats[i].name);
jerry_value_t v = jerry_number((double)formats[i].val);
jerry_object_set(ctor, k, v);
jerry_value_free(v);
jerry_value_free(k);
}
}
void moduleTextureDispose(void) {
scriptProtoDispose(&MODULE_TEXTURE_PROTO);
}
@@ -0,0 +1,61 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/module/modulebase.h"
#include "script/scriptproto.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
#include "display/texture/texture.h"
#include "util/memory.h"
extern scriptproto_t MODULE_TEXTURE_PROTO;
/** C struct wrapped by every Texture JS instance. */
typedef struct {
assetentry_t *entry;
} jstexture_t;
/**
* GC free callback — unlocks the asset entry when the Texture JS object is
* garbage collected.
*
* @param ptr Native jstexture_t pointer.
* @param info Native info (unused).
*/
void moduleTextureFree(void *ptr, jerry_object_native_info_t *info);
/**
* Returns the jstexture_t pointer from the current this_value.
*
* @param callInfo The call info.
* @return Pointer to the jstexture_t, or NULL if invalid.
*/
jstexture_t *moduleTextureSelf(const jerry_call_info_t *callInfo);
/** Texture() constructor — always throws; not directly instantiable. */
moduleBaseFunction(moduleTextureCtor);
/** @return Texture width in pixels, or undefined if not loaded. */
moduleBaseFunction(moduleTextureGetWidth);
/** @return Texture height in pixels, or undefined if not loaded. */
moduleBaseFunction(moduleTextureGetHeight);
/** @return "Texture(WxH)" string. */
moduleBaseFunction(moduleTextureToString);
/**
* Initializes the Texture module and registers the global Texture class with
* FORMAT_RGBA and FORMAT_PALETTE constants.
*/
void moduleTextureInit(void);
/**
* Disposes the Texture module.
*/
void moduleTextureDispose(void);
@@ -0,0 +1,11 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
moduleengine.c
moduleframe.c
moduletimeout.c
)
@@ -0,0 +1,34 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduleengine.h"
scriptproto_t MODULE_ENGINE_PROTO;
moduleBaseFunction(moduleEngineGetRunning) {
return jerry_boolean(ENGINE.running);
}
moduleBaseFunction(moduleEngineExit) {
ENGINE.running = false;
return jerry_undefined();
}
void moduleEngineInit(void) {
scriptProtoInit(&MODULE_ENGINE_PROTO, "Engine", sizeof(uint8_t), NULL);
scriptProtoDefineStaticProp(
&MODULE_ENGINE_PROTO, "running",
moduleEngineGetRunning, NULL
);
scriptProtoDefineStaticFunc(
&MODULE_ENGINE_PROTO, "exit", moduleEngineExit
);
}
void moduleEngineDispose(void) {
scriptProtoDispose(&MODULE_ENGINE_PROTO);
}
+14 -22
View File
@@ -10,29 +10,21 @@
#include "script/scriptproto.h" #include "script/scriptproto.h"
#include "engine/engine.h" #include "engine/engine.h"
static scriptproto_t MODULE_ENGINE_PROTO; extern scriptproto_t MODULE_ENGINE_PROTO;
moduleBaseFunction(moduleEngineGetRunning) { /** @return True if the engine main loop is still running. */
return jerry_boolean(ENGINE.running); moduleBaseFunction(moduleEngineGetRunning);
}
moduleBaseFunction(moduleEngineExit) { /** Signals the engine to stop on the next frame. */
ENGINE.running = false; moduleBaseFunction(moduleEngineExit);
return jerry_undefined();
}
static void moduleEngineInit(void) { /**
scriptProtoInit(&MODULE_ENGINE_PROTO, "Engine", sizeof(uint8_t), NULL); * Initializes the Engine module and registers the global Engine object with the
* running property and exit() method.
*/
void moduleEngineInit(void);
scriptProtoDefineStaticProp( /**
&MODULE_ENGINE_PROTO, "running", * Disposes the Engine module.
moduleEngineGetRunning, NULL */
); void moduleEngineDispose(void);
scriptProtoDefineStaticFunc(
&MODULE_ENGINE_PROTO, "exit", moduleEngineExit
);
}
static void moduleEngineDispose(void) {
scriptProtoDispose(&MODULE_ENGINE_PROTO);
}
@@ -0,0 +1,45 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduleframe.h"
jerry_value_t MODULE_FRAME_PENDING[MODULE_FRAME_PENDING_MAX];
uint32_t MODULE_FRAME_PENDING_COUNT = 0;
moduleBaseFunction(moduleFrameFrame) {
if(MODULE_FRAME_PENDING_COUNT >= MODULE_FRAME_PENDING_MAX) {
return moduleBaseThrow("Too many pending frame() calls");
}
jerry_value_t promise = jerry_promise();
MODULE_FRAME_PENDING[MODULE_FRAME_PENDING_COUNT++] =
jerry_value_copy(promise);
return promise;
}
void moduleFrameFlush(void) {
uint32_t count = MODULE_FRAME_PENDING_COUNT;
MODULE_FRAME_PENDING_COUNT = 0;
for(uint32_t i = 0; i < count; i++) {
jerry_value_t ret = jerry_promise_resolve(
MODULE_FRAME_PENDING[i], jerry_undefined()
);
jerry_value_free(ret);
jerry_value_free(MODULE_FRAME_PENDING[i]);
}
}
void moduleFrameInit(void) {
MODULE_FRAME_PENDING_COUNT = 0;
moduleBaseDefineGlobalMethod("frame", moduleFrameFrame);
}
void moduleFrameDispose(void) {
for(uint32_t i = 0; i < MODULE_FRAME_PENDING_COUNT; i++) {
jerry_value_free(MODULE_FRAME_PENDING[i]);
}
MODULE_FRAME_PENDING_COUNT = 0;
}
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/module/modulebase.h"
/** Maximum number of concurrent frame() awaits. */
#define MODULE_FRAME_PENDING_MAX 64
extern jerry_value_t MODULE_FRAME_PENDING[MODULE_FRAME_PENDING_MAX];
extern uint32_t MODULE_FRAME_PENDING_COUNT;
/**
* frame() — returns a Promise that resolves at the start of the next frame.
* Used as `await frame()` inside an async script loop.
*/
moduleBaseFunction(moduleFrameFrame);
/**
* Resolves all pending frame() promises. Must be called once per frame before
* jerry_run_jobs() so that awaiting scripts resume in the same tick.
*/
void moduleFrameFlush(void);
/**
* Initializes the frame module and registers the global frame() function.
*/
void moduleFrameInit(void);
/**
* Disposes the frame module, releasing any unresolved pending promises.
*/
void moduleFrameDispose(void);
@@ -0,0 +1,61 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduletimeout.h"
moduletimeoutentry_t MODULE_TIMEOUT_PENDING[MODULE_TIMEOUT_PENDING_MAX];
uint32_t MODULE_TIMEOUT_PENDING_COUNT = 0;
moduleBaseFunction(moduleTimeoutTimeout) {
moduleBaseRequireArgs(1);
moduleBaseRequireNumber(0);
if(MODULE_TIMEOUT_PENDING_COUNT >= MODULE_TIMEOUT_PENDING_MAX) {
return moduleBaseThrow("Too many pending timeout() calls");
}
float_t ms = moduleBaseArgFloat(0);
jerry_value_t promise = jerry_promise();
MODULE_TIMEOUT_PENDING[MODULE_TIMEOUT_PENDING_COUNT].promise =
jerry_value_copy(promise);
MODULE_TIMEOUT_PENDING[MODULE_TIMEOUT_PENDING_COUNT].targetTime =
TIME.time + ms / 1000.0f;
MODULE_TIMEOUT_PENDING_COUNT++;
return promise;
}
void moduleTimeoutFlush(void) {
uint32_t i = 0;
while(i < MODULE_TIMEOUT_PENDING_COUNT) {
if(TIME.time >= MODULE_TIMEOUT_PENDING[i].targetTime) {
jerry_value_t ret = jerry_promise_resolve(
MODULE_TIMEOUT_PENDING[i].promise, jerry_undefined()
);
jerry_value_free(ret);
jerry_value_free(MODULE_TIMEOUT_PENDING[i].promise);
MODULE_TIMEOUT_PENDING_COUNT--;
if(i < MODULE_TIMEOUT_PENDING_COUNT) {
MODULE_TIMEOUT_PENDING[i] =
MODULE_TIMEOUT_PENDING[MODULE_TIMEOUT_PENDING_COUNT];
}
} else {
i++;
}
}
}
void moduleTimeoutInit(void) {
MODULE_TIMEOUT_PENDING_COUNT = 0;
moduleBaseDefineGlobalMethod("timeout", moduleTimeoutTimeout);
}
void moduleTimeoutDispose(void) {
for(uint32_t i = 0; i < MODULE_TIMEOUT_PENDING_COUNT; i++) {
jerry_value_free(MODULE_TIMEOUT_PENDING[i].promise);
}
MODULE_TIMEOUT_PENDING_COUNT = 0;
}
@@ -0,0 +1,46 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/module/modulebase.h"
#include "time/time.h"
/** Maximum number of concurrent timeout() awaits. */
#define MODULE_TIMEOUT_PENDING_MAX 64
/** Entry tracking one pending timeout() promise. */
typedef struct {
jerry_value_t promise;
float_t targetTime;
} moduletimeoutentry_t;
extern moduletimeoutentry_t MODULE_TIMEOUT_PENDING[MODULE_TIMEOUT_PENDING_MAX];
extern uint32_t MODULE_TIMEOUT_PENDING_COUNT;
/**
* timeout(ms) — returns a Promise that resolves after at least `ms`
* milliseconds have elapsed. Used as `await timeout(500)`.
*
* @param args[0] Delay in milliseconds (number).
*/
moduleBaseFunction(moduleTimeoutTimeout);
/**
* Resolves any pending timeout() promises whose target time has passed.
* Must be called once per frame before jerry_run_jobs().
*/
void moduleTimeoutFlush(void);
/**
* Initializes the timeout module and registers the global timeout() function.
*/
void moduleTimeoutInit(void);
/**
* Disposes the timeout module, releasing any unresolved pending promises.
*/
void moduleTimeoutDispose(void);
@@ -0,0 +1,13 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
modulecomponent.c
moduleentity.c
)
# Subdirs
add_subdirectory(component)
@@ -0,0 +1,14 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
modulecomponentlist.c
)
# Subdirs
add_subdirectory(display)
add_subdirectory(physics)
add_subdirectory(trigger)
@@ -0,0 +1,11 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
modulecamera.c
moduleposition.c
modulerenderable.c
)
@@ -0,0 +1,169 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "modulecamera.h"
scriptproto_t MODULE_CAMERA_PROTO;
jscomponent_t *moduleCameraSelf(const jerry_call_info_t *callInfo) {
return (jscomponent_t *)scriptProtoGetValue(
&MODULE_CAMERA_PROTO, callInfo->this_value
);
}
entitycamera_t *moduleCameraData(const jscomponent_t *c) {
return (entitycamera_t *)componentGetData(
c->entityId, c->componentId, COMPONENT_TYPE_CAMERA
);
}
moduleBaseFunction(moduleCameraCtor) {
return moduleBaseThrow("Camera cannot be instantiated with new");
}
moduleBaseFunction(moduleCameraGetEntity) {
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_undefined();
return jerry_number((double)c->entityId);
}
moduleBaseFunction(moduleCameraGetId) {
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_undefined();
return jerry_number((double)c->componentId);
}
moduleBaseFunction(moduleCameraGetFov) {
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_undefined();
entitycamera_t *cam = moduleCameraData(c);
if(!cam) return jerry_undefined();
return jerry_number((double)cam->perspective.fov);
}
moduleBaseFunction(moduleCameraSetFov) {
moduleBaseRequireArgs(1);
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_undefined();
entitycamera_t *cam = moduleCameraData(c);
if(!cam) return jerry_undefined();
cam->perspective.fov = moduleBaseArgFloat(0);
return jerry_undefined();
}
moduleBaseFunction(moduleCameraGetNearClip) {
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_undefined();
entitycamera_t *cam = moduleCameraData(c);
if(!cam) return jerry_undefined();
return jerry_number((double)cam->nearClip);
}
moduleBaseFunction(moduleCameraSetNearClip) {
moduleBaseRequireArgs(1);
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_undefined();
entitycamera_t *cam = moduleCameraData(c);
if(!cam) return jerry_undefined();
cam->nearClip = moduleBaseArgFloat(0);
return jerry_undefined();
}
moduleBaseFunction(moduleCameraGetFarClip) {
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_undefined();
entitycamera_t *cam = moduleCameraData(c);
if(!cam) return jerry_undefined();
return jerry_number((double)cam->farClip);
}
moduleBaseFunction(moduleCameraSetFarClip) {
moduleBaseRequireArgs(1);
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_undefined();
entitycamera_t *cam = moduleCameraData(c);
if(!cam) return jerry_undefined();
cam->farClip = moduleBaseArgFloat(0);
return jerry_undefined();
}
moduleBaseFunction(moduleCameraGetProjType) {
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_undefined();
entitycamera_t *cam = moduleCameraData(c);
if(!cam) return jerry_undefined();
return jerry_number((double)cam->projType);
}
moduleBaseFunction(moduleCameraSetProjType) {
moduleBaseRequireArgs(1);
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_undefined();
entitycamera_t *cam = moduleCameraData(c);
if(!cam) return jerry_undefined();
cam->projType = (entitycameraprojectiontype_t)moduleBaseArgInt(0);
return jerry_undefined();
}
moduleBaseFunction(moduleCameraToString) {
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_string_sz("Camera:invalid");
char_t buf[32];
snprintf(buf, sizeof(buf), "Camera(%u)", (unsigned)c->componentId);
return jerry_string_sz(buf);
}
void moduleCameraInit(void) {
scriptProtoInit(
&MODULE_CAMERA_PROTO, "Camera",
sizeof(jscomponent_t), moduleCameraCtor
);
scriptProtoDefineProp(
&MODULE_CAMERA_PROTO, "entity", moduleCameraGetEntity, NULL
);
scriptProtoDefineProp(
&MODULE_CAMERA_PROTO, "id", moduleCameraGetId, NULL
);
scriptProtoDefineProp(
&MODULE_CAMERA_PROTO, "fov", moduleCameraGetFov, moduleCameraSetFov
);
scriptProtoDefineProp(
&MODULE_CAMERA_PROTO, "nearClip",
moduleCameraGetNearClip, moduleCameraSetNearClip
);
scriptProtoDefineProp(
&MODULE_CAMERA_PROTO, "farClip",
moduleCameraGetFarClip, moduleCameraSetFarClip
);
scriptProtoDefineProp(
&MODULE_CAMERA_PROTO, "projType",
moduleCameraGetProjType, moduleCameraSetProjType
);
scriptProtoDefineToString(&MODULE_CAMERA_PROTO, moduleCameraToString);
jerry_value_t ctor = MODULE_CAMERA_PROTO.constructor;
struct { const char_t *name; int val; } projtypes[] = {
{ "PERSPECTIVE", ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE },
{
"PERSPECTIVE_FLIPPED",
ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED
},
{ "ORTHOGRAPHIC", ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC },
};
for(int i = 0; i < 3; i++) {
jerry_value_t k = jerry_string_sz(projtypes[i].name);
jerry_value_t v = jerry_number((double)projtypes[i].val);
jerry_object_set(ctor, k, v);
jerry_value_free(v);
jerry_value_free(k);
}
}
void moduleCameraDispose(void) {
scriptProtoDispose(&MODULE_CAMERA_PROTO);
}
@@ -0,0 +1,62 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/module/modulebase.h"
#include "script/scriptproto.h"
#include "script/module/entity/modulecomponent.h"
#include "entity/component/display/entitycamera.h"
extern scriptproto_t MODULE_CAMERA_PROTO;
/** Camera() constructor — always throws; not directly instantiable. */
moduleBaseFunction(moduleCameraCtor);
/** @return Entity ID that owns this camera component. */
moduleBaseFunction(moduleCameraGetEntity);
/** @return This component's ID. */
moduleBaseFunction(moduleCameraGetId);
/** @return Field of view in degrees. */
moduleBaseFunction(moduleCameraGetFov);
/** Sets field of view. @param args[0] FOV in degrees. */
moduleBaseFunction(moduleCameraSetFov);
/** @return Near clip plane distance. */
moduleBaseFunction(moduleCameraGetNearClip);
/** Sets near clip plane. @param args[0] Near clip distance. */
moduleBaseFunction(moduleCameraSetNearClip);
/** @return Far clip plane distance. */
moduleBaseFunction(moduleCameraGetFarClip);
/** Sets far clip plane. @param args[0] Far clip distance. */
moduleBaseFunction(moduleCameraSetFarClip);
/** @return Projection type constant (Camera.PERSPECTIVE etc.). */
moduleBaseFunction(moduleCameraGetProjType);
/** Sets projection type. @param args[0] entitycameraprojectiontype_t value. */
moduleBaseFunction(moduleCameraSetProjType);
/** @return "Camera(id)" string. */
moduleBaseFunction(moduleCameraToString);
/**
* Initializes the Camera module and registers the global Camera class with
* fov/nearClip/farClip/projType properties and PERSPECTIVE/ORTHOGRAPHIC
* constants.
*/
void moduleCameraInit(void);
/**
* Disposes the Camera module.
*/
void moduleCameraDispose(void);

Some files were not shown because too many files have changed in this diff Show More