19 Commits

Author SHA1 Message Date
YourWishes 88903fee94 No need for asset batching on text.c 2026-06-01 22:36:02 -05:00
YourWishes 1e8311fc04 Add asset batch 2026-06-01 22:34:44 -05:00
YourWishes 2b78370cb8 Add asset reaping 2026-06-01 22:20:57 -05:00
YourWishes 8f78bba9e9 Restoring JerryScript a bit cleaner 2026-06-01 21:52:36 -05:00
YourWishes 41a4be678e Added a tiny sleep on assets to stop pegging the CPU 2026-06-01 15:48:10 -05:00
YourWishes 8b2b4b7c3d Fixed JSON loader, added some tests 2026-06-01 15:31:22 -05:00
YourWishes 1f3a29f89d Asyncify other loaders 2026-06-01 15:10:58 -05:00
YourWishes c4c93097cd Add async texture loading 2026-06-01 14:53:18 -05:00
YourWishes eedb7769e6 Add some extra tests 2026-06-01 13:48:29 -05:00
YourWishes 98db62a4bc Add some more tests, prepping for asset testing 2026-06-01 13:37:14 -05:00
YourWishes df48c8e500 Consistency and fixing thread unit tests 2026-06-01 11:33:27 -05:00
YourWishes db9cc0f4c6 Add thread tests 2026-06-01 10:59:56 -05:00
YourWishes a79ee429b4 Prepping for async 2026-06-01 10:57:40 -05:00
YourWishes 6acfca6d48 Consistent 2026-05-30 20:30:13 -05:00
YourWishes 1cd6f4cb72 First refactor of new asset system 2026-05-30 08:21:58 -05:00
YourWishes 3271e8c7d6 FInished porting last asset loader types 2026-05-30 07:59:06 -05:00
YourWishes 0bcde064af Asset refactor, phase one. 2026-05-29 14:27:40 -05:00
YourWishes 957980b3c5 Updating event handler 2026-05-28 14:22:13 -05:00
YourWishes 03eb328d81 Allow dynamic trace on any platform that can support it. 2026-05-28 11:21:36 -05:00
111 changed files with 5983 additions and 1212 deletions
+12
View File
@@ -0,0 +1,12 @@
Console.print('This is called from JavaScript');
const platformNames = {
[System.PLATFORM_LINUX]: 'Linux',
[System.PLATFORM_KNULLI]: 'Knulli',
[System.PLATFORM_PSP]: 'PSP',
[System.PLATFORM_GAMECUBE]: 'GameCube',
[System.PLATFORM_WII]: 'Wii',
};
const platformName = platformNames[System.platform] || 'Unknown';
Console.print('Platform: ' + platformName);
+96
View File
@@ -0,0 +1,96 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Turn things off we don't need
set(JERRY_CMDLINE OFF CACHE BOOL "" FORCE)
set(JERRY_EXT ON CACHE BOOL "" FORCE)
set(JERRY_DEBUGGER OFF CACHE BOOL "" FORCE)
set(JERRY_BUILTIN_DATE OFF CACHE BOOL "" FORCE)
set(ENABLE_LTO OFF CACHE BOOL "" FORCE)
# Fetch Jerry
include(FetchContent)
FetchContent_Declare(
jerryscript
GIT_REPOSITORY https://git.wish.moe/YourWishes/jerryscript
GIT_TAG float32-fix
)
FetchContent_MakeAvailable(jerryscript)
# Mark found
set(jerryscript_FOUND ON)
# Define targets
if(TARGET jerryscript-core)
set(JERRY_CORE_TARGET jerryscript-core)
elseif(TARGET jerry-core)
set(JERRY_CORE_TARGET jerry-core)
endif()
if(TARGET jerryscript-ext)
set(JERRY_EXT_TARGET jerryscript-ext)
elseif(TARGET jerry-ext)
set(JERRY_EXT_TARGET jerry-ext)
endif()
if(TARGET jerryscript-port-default)
set(JERRY_PORT_TARGET jerryscript-port-default)
elseif(TARGET jerry-port-default)
set(JERRY_PORT_TARGET jerry-port-default)
elseif(TARGET jerryscript-port)
set(JERRY_PORT_TARGET jerryscript-port)
elseif(TARGET jerry-port)
set(JERRY_PORT_TARGET jerry-port)
endif()
if(NOT JERRY_CORE_TARGET)
message(FATAL_ERROR "JerryScript core target not found")
endif()
if(NOT JERRY_EXT_TARGET)
message(FATAL_ERROR "JerryScript ext target not found")
endif()
if(NOT JERRY_PORT_TARGET)
message(FATAL_ERROR "JerryScript port target not found")
endif()
foreach(tgt IN ITEMS
${JERRY_CORE_TARGET}
${JERRY_EXT_TARGET}
${JERRY_PORT_TARGET}
)
if(TARGET ${tgt})
set_property(TARGET ${tgt} PROPERTY INTERPROCEDURAL_OPTIMIZATION OFF)
target_compile_definitions(${JERRY_CORE_TARGET} PRIVATE
JERRY_NUMBER_TYPE_FLOAT64=0
JERRY_BUILTIN_DATE=0
)
endif()
endforeach()
# Export include dirs through the targets
target_include_directories(${JERRY_CORE_TARGET} INTERFACE
${jerryscript_SOURCE_DIR}/jerry-core/include
)
target_include_directories(${JERRY_EXT_TARGET} INTERFACE
${jerryscript_SOURCE_DIR}/jerry-ext/include
)
target_include_directories(${JERRY_PORT_TARGET} INTERFACE
${jerryscript_SOURCE_DIR}/jerry-port/default/include
)
# Suppress JerryScript-only warning
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(${JERRY_CORE_TARGET} PRIVATE
-Wno-error
)
endif()
add_library(jerryscript::core ALIAS ${JERRY_CORE_TARGET})
add_library(jerryscript::ext ALIAS ${JERRY_EXT_TARGET})
add_library(jerryscript::port ALIAS ${JERRY_PORT_TARGET})
+1
View File
@@ -34,6 +34,7 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_OPENGL
DUSK_OPENGL_ES
DUSK_LINUX
DUSK_KNULLI
DUSK_DISPLAY_SIZE_DYNAMIC
DUSK_DISPLAY_WIDTH_DEFAULT=640
DUSK_DISPLAY_HEIGHT_DEFAULT=480
+2 -3
View File
@@ -16,9 +16,6 @@ else()
)
endif()
# Export symbols so backtrace_symbols() can resolve function names.
target_link_options(${DUSK_LIBRARY_TARGET_NAME} PUBLIC -rdynamic)
# Link required libraries.
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
SDL2
@@ -29,6 +26,8 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
# CURL::libcurl
)
set(DUSK_BACKTRACE ON CACHE BOOL "Enable backtrace support for assert failures.")
# Define platform-specific macros.
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_SDL2
+2
View File
@@ -1,4 +1,6 @@
#!/bin/bash
set -e
rm -rf build-tests
cmake -S . -B build-tests -DDUSK_BUILD_TESTS=ON -DDUSK_TARGET_SYSTEM=linux
cmake --build build-tests -- -j$(nproc)
ctest --output-on-failure --test-dir build-tests
+17
View File
@@ -32,6 +32,22 @@ if(NOT yyjson_FOUND)
endif()
endif()
if(NOT jerryscript_FOUND)
find_package(jerryscript REQUIRED)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
jerryscript::core
jerryscript::ext
jerryscript::port
)
endif()
if(DUSK_BACKTRACE)
target_link_options(${DUSK_LIBRARY_TARGET_NAME} PUBLIC -rdynamic)
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
DUSK_BACKTRACE
)
endif()
# Includes
target_include_directories(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
@@ -62,6 +78,7 @@ add_subdirectory(input)
add_subdirectory(locale)
add_subdirectory(physics)
add_subdirectory(scene)
add_subdirectory(script)
add_subdirectory(system)
add_subdirectory(time)
add_subdirectory(ui)
-1
View File
@@ -7,5 +7,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
easing.c
animation.c
animationproperty.c
)
+34 -75
View File
@@ -8,86 +8,45 @@
#include "util/memory.h"
#include "util/math.h"
void animationInit(animation_t *anim) {
memoryZero(anim, sizeof(animation_t));
eventInit(&anim->onStart);
eventInit(&anim->onComplete);
void animationInit(
animation_t *anim,
keyframe_t *keyframes,
uint16_t keyframeCount
) {
assertNotNull(anim, "Animation pointer cannot be null.");
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
anim->keyframes = keyframes;
anim->keyframeCount = keyframeCount;
}
animationproperty_t *animationAddProperty(animation_t *anim, float_t *target) {
assertTrue(
anim->propertyCount < ANIMATION_PROPERTY_COUNT_MAX,
"Property count exceeds ANIMATION_PROPERTY_COUNT_MAX"
);
animationproperty_t *prop = &anim->properties[anim->propertyCount++];
prop->keyframeCount = 0;
prop->target = target;
return prop;
}
float_t animationGetValue(animation_t *anim, const float_t time) {
assertNotNull(anim, "Animation pointer cannot be null.");
assertNotNull(anim->keyframes, "Keyframes pointer cannot be null.");
assertTrue(anim->keyframeCount > 0, "Keyframe count invalid.");
assertTrue(time >= 0, "Time must be non-negative.");
keyframe_t *start;
keyframe_t *end;
keyframe_t *last = anim->keyframes + anim->keyframeCount - 1;
keyframe_t *current = anim->keyframes;
start = current;
void animationUpdate(animation_t *anim, float_t delta) {
assertNotNull(anim, "Animation cannot be null");
if(
(anim->flags & ANIMATION_FLAG_FINISHED) &&
!(anim->flags & ANIMATION_FLAG_LOOP_ENABLED)
) return;
bool_t wasStarted = (anim->flags & ANIMATION_FLAG_STARTED) != 0;
anim->flags |= ANIMATION_FLAG_STARTED;
anim->time += delta;
float_t duration = animationGetDuration(anim);
bool_t justFinished = false;
if(anim->time >= duration) {
if(anim->flags & ANIMATION_FLAG_LOOP_ENABLED) {
anim->time = mathModFloat(anim->time, duration);
} else {
anim->time = duration;
if(!(anim->flags & ANIMATION_FLAG_FINISHED)) {
anim->flags |= ANIMATION_FLAG_FINISHED;
justFinished = true;
}
do {
if(current->time > time) {
end = current;
break;
}
}
start = current;
current++;
for(uint8_t i = 0; i < anim->propertyCount; i++) {
animationproperty_t *prop = &anim->properties[i];
if(prop->target != NULL) {
*prop->target = animationPropertyGetValue(prop, anim->time);
if(current > last) {
end = start;
break;
}
}
} while(true);
if(!wasStarted) eventInvoke(&anim->onStart, anim);
if(justFinished) eventInvoke(&anim->onComplete, anim);
}
void animationReset(animation_t *anim) {
anim->time = 0.0f;
anim->flags &= ~(ANIMATION_FLAG_FINISHED | ANIMATION_FLAG_STARTED);
}
bool_t animationIsFinished(const animation_t *anim) {
return (anim->flags & ANIMATION_FLAG_FINISHED) != 0;
}
bool_t animationIsStarted(const animation_t *anim) {
return (anim->flags & ANIMATION_FLAG_STARTED) != 0;
}
bool_t animationIsLooping(const animation_t *anim) {
return (anim->flags & ANIMATION_FLAG_LOOP_ENABLED) != 0;
}
float_t animationGetDuration(const animation_t *anim) {
float_t duration = 0.0f;
for(uint8_t i = 0; i < anim->propertyCount; i++) {
animationproperty_t *prop = &anim->properties[i];
if(prop->keyframeCount == 0) continue;
float_t lastKeyframeTime =
prop->keyframes[prop->keyframeCount - 1].time;
if(lastKeyframeTime > duration) duration = lastKeyframeTime;
}
return duration;
float_t t = (time - start->time) / (end->time - start->time);
return mathLerp(start->value, end->value, easingApply(start->easing, t));
}
+15 -71
View File
@@ -4,87 +4,31 @@
// https://opensource.org/licenses/MIT
#pragma once
#include "animationproperty.h"
#include "event/event.h"
#define ANIMATION_PROPERTY_COUNT_MAX 8
#define ANIMATION_FLAG_FINISHED (1 << 0)
#define ANIMATION_FLAG_STARTED (1 << 1)
#define ANIMATION_FLAG_LOOP_ENABLED (1 << 2)
#include "keyframe.h"
typedef struct {
animationproperty_t properties[ANIMATION_PROPERTY_COUNT_MAX];
uint8_t propertyCount;
float_t time;
uint8_t flags;
event_t onStart;
event_t onComplete;
keyframe_t *keyframes;
uint16_t keyframeCount;
} animation_t;
/**
* Initializes an animation.
*
* @param anim The animation to initialize.
* @param keyframes The keyframes to use for the animation.
* @param keyframeCount The number of keyframes in the animation.
*/
void animationInit(animation_t *anim);
void animationInit(
animation_t *anim,
keyframe_t *keyframes,
uint16_t keyframeCount
);
/**
* Adds a new animated property. The caller owns target and must keep it valid
* for the lifetime of the animation.
* Gets the value of the animation at a given time.
*
* @param anim The animation to add the property to.
* @param target Pointer to the float to write interpolated values into.
* @return A pointer to the new property.
* @param anim The animation to get the value from.
* @param time The time at which to get the value, in seconds.
* @return The value of the animation at the given time.
*/
animationproperty_t *animationAddProperty(animation_t *anim, float_t *target);
/**
* Advances the animation by delta seconds and writes interpolated values to
* each property's target pointer.
*
* @param anim The animation to update.
* @param delta The time to advance the animation by, in seconds.
*/
void animationUpdate(animation_t *anim, float_t delta);
/**
* Resets the animation to the beginning, clearing Started and Finished flags.
*
* @param anim The animation to reset.
*/
void animationReset(animation_t *anim);
/**
* Returns true if the animation has finished (i.e. reached the end of its
* duration and is not looping).
*
* @param anim The animation to check.
* @return true if the animation has finished, false otherwise.
*/
bool_t animationIsFinished(const animation_t *anim);
/**
* Returns true if the animation has been started (i.e. animationUpdate has
* been called at least once).
*
* @param anim The animation to check.
* @return true if the animation has been started, false otherwise.
*/
bool_t animationIsStarted(const animation_t *anim);
/**
* Returns true if the animation is set to loop.
*
* @param anim The animation to check.
* @return true if the animation is set to loop, false otherwise.
*/
bool_t animationIsLooping(const animation_t *anim);
/**
* Gets the total duration of the animation (based on the keyframes).
*
* @param anim The animation to get the duration of.
* @return The total duration of the animation, in seconds.
*/
float_t animationGetDuration(const animation_t *anim);
float_t animationGetValue(animation_t *anim, const float_t time);
-55
View File
@@ -1,55 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "animationproperty.h"
#include "assert/assert.h"
void animationPropertyAddKeyframe(
animationproperty_t *prop,
const float_t time,
const float_t value,
const easingtype_t easing
) {
assertNotNull(prop, "Property cannot be null");
assertTrue(
prop->keyframeCount < ANIMATION_PROPERTY_KEYFRAME_COUNT_MAX,
"Too many keyframes added to property"
);
assertTrue(time >= 0.0f, "Keyframe time cannot be negative");
keyframe_t *frame = &prop->keyframes[prop->keyframeCount++];
frame->time = time;
frame->value = value;
frame->easing = easing;
}
float_t animationPropertyGetValue(
const animationproperty_t *prop,
const float_t time
) {
assertNotNull(prop, "Property cannot be null");
assertTrue(time >= 0.0f, "Time cannot be negative");
if(prop->keyframeCount == 0) return 0.0f;
uint8_t last = prop->keyframeCount - 1;
if(prop->keyframeCount == 1) return prop->keyframes[0].value;
if(time <= prop->keyframes[0].time) return prop->keyframes[0].value;
if(time >= prop->keyframes[last].time) return prop->keyframes[last].value;
for(uint8_t i = 0; i < last; i++) {
const keyframe_t *a = &prop->keyframes[i];
const keyframe_t *b = &prop->keyframes[i + 1];
if(time < a->time || time >= b->time) continue;
float_t t = (time - a->time) / (b->time - a->time);
t = easingApply(a->easing, t);
return a->value + (b->value - a->value) * t;
}
return prop->keyframes[last].value;
}
-45
View File
@@ -1,45 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "keyframe.h"
#define ANIMATION_PROPERTY_KEYFRAME_COUNT_MAX 16
typedef struct {
keyframe_t keyframes[ANIMATION_PROPERTY_KEYFRAME_COUNT_MAX];
uint8_t keyframeCount;
float_t *target;
} animationproperty_t;
/**
* Appends a keyframe to a property. Keyframes must be added in ascending time
* order. Updates the animation's duration.
*
* @param property The property to add the keyframe to.
* @param time The time of the keyframe, in seconds.
* @param value The value of the keyframe.
* @param easing The easing type to use when interpolating to the next keyframe.
*/
void animationPropertyAddKeyframe(
animationproperty_t *property,
const float_t time,
const float_t value,
const easingtype_t easing
);
/**
* Gets the property's value at a given time.
*
* @param prop The property to get the value from.
* @param time The time at which to get the value, in seconds.
* @return The value of the property at the given time.
*/
float_t animationPropertyGetValue(
const animationproperty_t *prop,
const float_t time
);
+1 -1
View File
@@ -10,4 +10,4 @@ typedef struct {
float_t time;
float_t value;
easingtype_t easing;
} keyframe_t;
} keyframe_t;
+21 -19
View File
@@ -8,24 +8,7 @@
#include "assert.h"
#include "log/log.h"
#include "util/string.h"
#ifdef DUSK_LINUX
#include <execinfo.h>
#include <stdlib.h>
static void assertLogBacktrace(void) {
void *frames[64];
int count = backtrace(frames, 64);
char **symbols = backtrace_symbols(frames, count);
logError("Stack trace:\n");
if(symbols) {
for(int i = 0; i < count; i++) {
logError(" %s\n", symbols[i]);
}
free(symbols);
}
}
#endif
#include "util/memory.h"
#ifndef DUSK_ASSERTIONS_FAKED
#ifdef DUSK_TEST_ASSERT
@@ -43,6 +26,25 @@
);
}
#else
#ifdef DUSK_BACKTRACE
#include <execinfo.h>
#include <stdlib.h>
static void assertLogBacktrace(void) {
void *frames[64];
int count = backtrace(frames, 64);
char **symbols = backtrace_symbols(frames, count);
memoryTrack(symbols);
logError("Stack trace:\n");
if(symbols) {
for(int i = 0; i < count; i++) {
logError(" %s\n", symbols[i]);
}
memoryFree(symbols);
}
}
#endif
void assertTrueImpl(
const char *file,
const int32_t line,
@@ -56,7 +58,7 @@
line,
message
);
#ifdef DUSK_LINUX
#ifdef DUSK_BACKTRACE
assertLogBacktrace();
#endif
abort();
+1 -2
View File
@@ -7,9 +7,8 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
asset.c
assetfile.c
assetcache.c
assetbatch.c
assetfile.c
)
# Subdirs
+300 -15
View File
@@ -11,17 +11,24 @@
#include "assert/assert.h"
#include "engine/engine.h"
#include "util/string.h"
#include "console/console.h"
#include <unistd.h>
asset_t ASSET;
errorret_t assetInit(void) {
memoryZero(&ASSET, sizeof(asset_t));
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
threadMutexInit(&ASSET.loading[i].mutex);
}
// assetInitPlatform must either define ASSET.zip or throw an error.
errorChain(assetInitPlatform());
assertNotNull(ASSET.zip, "Asset zip null without error.");
threadInit(&ASSET.loadThread, assetUpdateAsync);
threadStart(&ASSET.loadThread);
assetCacheInit(&ASSET.cache);
errorOk();
}
@@ -33,26 +40,304 @@ bool_t assetFileExists(const char_t *filename) {
return true;
}
errorret_t assetLoad(
const char_t *filename,
assetfileloader_t loader,
void *params,
void *output
assetentry_t * assetGetEntry(
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
) {
assertStrLenMax(filename, ASSET_FILE_NAME_MAX, "Filename too long.");
assertNotNull(output, "Output pointer cannot be NULL.");
assertNotNull(loader, "Asset file loader cannot be NULL.");
// Is there an existing asset?
assetentry_t *entry = ASSET.entries;
do {
if(entry->type == ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
if(stringEquals(entry->name, name)) {
assertTrue(entry->type == type, "Asset entry type mismatch.");
return entry;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
// We did not find one existing, Find first available slot.
entry = ASSET.entries;
do {
if(entry->type != ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
if(entry->state == ASSET_ENTRY_STATE_NOT_STARTED) {
assetEntryInit(entry, name, type, input);
return entry;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
assertUnreachable("No available asset entry slots.");
return NULL;
}
errorret_t assetRequireLoaded(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL.");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
// Already loaded?
if(entry->state == ASSET_ENTRY_STATE_LOADED) {
errorOk();
}
// Not loaded, just spin the wheel
while(entry->state != ASSET_ENTRY_STATE_LOADED) {
usleep(1000);
errorChain(assetUpdate());
}
assetfile_t file;
errorChain(assetFileInit(&file, filename, params, output));
errorChain(loader(&file));
errorChain(assetFileDispose(&file));
errorOk();
}
errorret_t assetDispose(void) {
assetCacheDispose(&ASSET.cache);
assetentry_t * assetLock(
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
) {
assetentry_t *entry = assetGetEntry(name, type, input);
assetEntryLock(entry);
return entry;
}
void assetUnlock(const char_t *name) {
assertNotNull(name, "Name cannot be NULL.");
assetentry_t *entry = ASSET.entries;
do {
if(entry->type != ASSET_LOADER_TYPE_NULL && stringEquals(entry->name, name)) {
assetEntryUnlock(entry);
return;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
assertUnreachable("Asset entry not found for unlock.");
}
void assetUnlockEntry(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL.");
assetEntryUnlock(entry);
}
errorret_t assetUpdate(void) {
// Determine how many available loading slots we have.
assetloading_t *availableLoading[ASSET_LOADING_COUNT_MAX];
uint8_t availableLoadingCount = 0;
assetloading_t *loading = ASSET.loading;
assetentry_t *entry;
do {
// We only care about NULL entry references. Nothing async touches this so
// it's fine to use raw here.
if(loading->entry != NULL) {
loading++;
continue;
}
availableLoading[availableLoadingCount++] = loading;
loading++;
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
// Now we can check for pending asset entries, we can't do anything if there
// is no available slots though.
if(availableLoadingCount > 0) {
entry = ASSET.entries;
do {
// Is this asset "ready to start loading" ?
if(entry->type == ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
// We only care about assets not started.
if(entry->state != ASSET_ENTRY_STATE_NOT_STARTED) {
entry++;
continue;
}
// Pop a loading slot for this asset entry.
loading = availableLoading[--availableLoadingCount];
// Start loading this asset.
assetEntryStartLoading(entry, loading);
entry++;
// Did we run out of loading slots?
if(availableLoadingCount == 0) {
break;
}
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
}
// Now walk over all the loading slots and see what needs to be done.
loading = ASSET.loading;
do {
// Is the loading slot in use? Entry can only be modified synchronously.
if(loading->entry == NULL) {
loading++;
continue;
}
// Lock the loading slot. This will prevent any async modifications.
threadMutexLock(&loading->mutex);
// Check the state of the entry.
switch(loading->entry->state) {
// This thing is pending synchronous loading.
case ASSET_ENTRY_STATE_PENDING_SYNC:
// Perform sync load.
loading->entry->state = ASSET_ENTRY_STATE_LOADING_SYNC;
errorret_t ret = (
ASSET_LOADER_CALLBACKS[loading->type].loadSync(loading)
);
// After a sync load, these are the only valid states.
assertTrue(
loading->entry->state == ASSET_ENTRY_STATE_LOADED ||
loading->entry->state == ASSET_ENTRY_STATE_ERROR ||
loading->entry->state == ASSET_ENTRY_STATE_PENDING_SYNC ||
loading->entry->state == ASSET_ENTRY_STATE_PENDING_ASYNC,
"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)) {
errorCatch(errorPrint(ret));
assertTrue(
loading->entry->state == ASSET_ENTRY_STATE_ERROR,
"Loader did not set entry state to error on failed load."
);
}
threadMutexUnlock(&loading->mutex);
loading++;
break;
case ASSET_ENTRY_STATE_LOADING_SYNC:
assertUnreachable(
"Entry is in a pending sync state still?"
);
break;
// Done loading, we can just free it up.
case ASSET_ENTRY_STATE_LOADED:
loading->entry = NULL;
threadMutexUnlock(&loading->mutex);
loading++;
break;
case ASSET_ENTRY_STATE_ERROR:
threadMutexUnlock(&loading->mutex);
errorThrow("Failed to load asset asynchronously.");
break;
default:
threadMutexUnlock(&loading->mutex);
loading++;
continue;
}
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
// Reap entries that have no external locks (refs.count == 1 means only the
// system hold remains). Only safe to reap LOADED and NOT_STARTED states —
// mid-load entries are left for the next cycle.
entry = ASSET.entries;
do {
if(entry->state != ASSET_ENTRY_STATE_LOADED) {
entry++;
continue;
}
if(entry->type == ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
if(entry->refs.count > 0) {
entry++;
continue;
}
consolePrint("Reaping asset %s", entry->name);
errorChain(assetEntryDispose(entry));
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
errorOk();
}
void assetUpdateAsync(thread_t *thread) {
while(!threadShouldStop(thread)) {
// Walk over each asset
assetloading_t *loading;
loading = ASSET.loading;
do {
threadMutexLock(&loading->mutex);
if(loading->entry == NULL) {
threadMutexUnlock(&loading->mutex);
loading++;
continue;
}
switch(loading->entry->state) {
case ASSET_ENTRY_STATE_PENDING_ASYNC:
loading->entry->state = ASSET_ENTRY_STATE_LOADING_ASYNC;
assertNotNull(
ASSET_LOADER_CALLBACKS[loading->type].loadAsync,
"Loader does not support async loading."
);
errorret_t ret = (
ASSET_LOADER_CALLBACKS[loading->type].loadAsync(loading)
);
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
assertTrue(
loading->entry->state == ASSET_ENTRY_STATE_ERROR,
"Loader did not set entry state to error on failed load."
);
}
threadMutexUnlock(&loading->mutex);
loading++;
break;
case ASSET_ENTRY_STATE_LOADING_ASYNC:
assertUnreachable(
"Entry is in a pending async state still?"
);
break;
default:
threadMutexUnlock(&loading->mutex);
loading++;
continue;
}
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
if(threadShouldStop(thread)) break;
usleep(1000);
}
}
errorret_t assetDispose(void) {
threadStop(&ASSET.loadThread);
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
threadMutexDispose(&ASSET.loading[i].mutex);
}
// Cleanup zip file.
if(ASSET.zip != NULL) {
if(zip_close(ASSET.zip) != 0) {
errorThrow("Failed to close asset zip archive.");
+80 -14
View File
@@ -9,7 +9,9 @@
#include "error/error.h"
#include "asset/assetplatform.h"
#include "assetfile.h"
#include "assetcache.h"
#include "thread/thread.h"
#include "asset/loader/assetentry.h"
#include "asset/loader/assetloading.h"
#ifndef assetInitPlatform
#error "Platform must define assetInitPlatform function."
@@ -21,16 +23,27 @@
#define ASSET_FILE_NAME "dusk.dsk"
#define ASSET_HEADER_SIZE 3
#define ASSET_LOADING_COUNT_MAX 4
#define ASSET_ENTRY_COUNT_MAX 128
typedef struct asset_s {
zip_t *zip;
assetplatform_t platform;
assetcache_t cache;
// Background loading thread.
thread_t loadThread;
// Assets that are mid loading.
assetloading_t loading[ASSET_LOADING_COUNT_MAX];
assetentry_t entries[ASSET_ENTRY_COUNT_MAX];
} asset_t;
extern asset_t ASSET;
/**
* Initializes the asset system.
*
* @return An error code if the asset system could not be initialized properly.
*/
errorret_t assetInit(void);
@@ -43,21 +56,74 @@ errorret_t assetInit(void);
bool_t assetFileExists(const char_t *filename);
/**
* Loads an asset by its filename,
*
* @param filename The filename of the asset to retrieve.
* @param loader Loader to use for loading the asset file.
* @param params Parameters to pass to the loader.
* @param output The output pointer to store the loaded asset data.
* @return An error code if the asset could not be loaded.
* Gets, or creates, a new asset entry. Internal — prefer assetLock.
*
* @param name Filename of the asset.
* @param type Type of the asset.
* @param input Loader-specific parameters.
*/
errorret_t assetLoad(
const char_t *filename,
assetfileloader_t loader,
void *params,
void *output
assetentry_t * assetGetEntry(
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
);
/**
* 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
* reclaimed.
*
* @param name Filename of the asset.
* @param type Type of the asset.
* @param input Loader-specific parameters.
* @return The locked asset entry.
*/
assetentry_t * assetLock(
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
);
/**
* Releases a lock on an asset entry by name. When all locks are released the
* entry will be reclaimed at the start of the next assetUpdate.
*
* @param name Filename of the asset to unlock.
*/
void assetUnlock(const char_t *name);
/**
* Releases a lock on an asset entry by pointer. When all locks are released
* the entry will be reclaimed at the start of the next assetUpdate.
*
* @param entry The asset entry to unlock.
*/
void assetUnlockEntry(assetentry_t *entry);
/**
* Requires an asset entry to be loaded. This will block until the asset entry
* is fully loaded.
*
* @param entry The asset entry to require.
* @return An error code if the asset entry could not be loaded properly.
*/
errorret_t assetRequireLoaded(assetentry_t *entry);
/**
* Updates the asset system.
*
* @return An error code if the asset system could not be updated properly.
*/
errorret_t assetUpdate(void);
/**
* Starts the background asset loading thread. The thread runs assetUpdate
* in a loop with a short sleep until stopped.
*
* @param thread The thread runner.
*/
void assetUpdateAsync(thread_t *thread);
/**
* Disposes/cleans up the asset system.
*
+68 -55
View File
@@ -7,75 +7,88 @@
#include "assetbatch.h"
#include "asset.h"
#include "util/memory.h"
#include "util/string.h"
#include "assert/assert.h"
#include "util/memory.h"
#include <unistd.h>
void assetBatchInit(
assetbatch_t *batch,
assetbatchcomplete_t onComplete,
assetbatcherror_t onError,
void *context
const uint16_t count,
const assetbatchdesc_t *descs
) {
assertNotNull(batch, "Batch cannot be NULL.");
assertNotNull(descs, "Descs cannot be NULL.");
assertTrue(count > 0, "Count must be greater than 0.");
assertTrue(count <= ASSET_BATCH_COUNT_MAX, "Count exceeds ASSET_BATCH_COUNT_MAX.");
memoryZero(batch, sizeof(assetbatch_t));
batch->onComplete = onComplete;
batch->onError = onError;
batch->context = context;
}
batch->count = count;
void assetBatchAdd(
assetbatch_t *batch,
const char_t *path,
assetfileloader_t loader,
const void *params,
size_t paramsSize,
size_t dataSize,
assetcachedisposer_t disposer,
void **outPtr
) {
assertTrue(batch->count < ASSET_BATCH_MAX, "Asset batch is full.");
assertTrue(paramsSize <= ASSET_BATCH_PARAMS_SIZE, "Asset batch params too large.");
assertNotNull(outPtr, "Batch output pointer cannot be NULL.");
assetbatchitem_t *item = &batch->items[batch->count++];
stringCopy(item->path, path, ASSET_FILE_NAME_MAX);
item->loader = loader;
if (params != NULL && paramsSize > 0) {
memoryCopy(item->params, params, paramsSize);
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->entries[i] = assetLock(descs[i].path, descs[i].type, &batch->inputs[i]);
}
item->dataSize = dataSize;
item->disposer = disposer;
item->outPtr = outPtr;
}
errorret_t assetBatchLoad(assetbatch_t *batch) {
for (uint8_t i = 0; i < batch->count; i++) {
assetbatchitem_t *item = &batch->items[i];
void assetBatchLock(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
assetEntryLock(batch->entries[i]);
}
}
void *cached = assetCacheLookup(&ASSET.cache, item->path);
if (cached != NULL) {
assetCacheRetain(&ASSET.cache, item->path);
*item->outPtr = cached;
continue;
}
void assetBatchUnlock(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
assetEntryUnlock(batch->entries[i]);
}
}
void *data = memoryAllocate(item->dataSize);
errorret_t err = assetLoad(item->path, item->loader, item->params, data);
if (err.code != ERROR_OK) {
memoryFree(data);
if (batch->onError != NULL) {
batch->onError(batch, batch->context, err);
bool_t assetBatchIsLoaded(const assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
if(batch->entries[i]->state != ASSET_ENTRY_STATE_LOADED) return false;
}
return true;
}
bool_t assetBatchHasError(const assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
if(batch->entries[i]->state == ASSET_ENTRY_STATE_ERROR) return true;
}
return false;
}
errorret_t assetBatchRequireLoaded(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
bool_t allDone;
do {
allDone = true;
for(uint16_t i = 0; i < batch->count; i++) {
const assetentrystate_t state = batch->entries[i]->state;
if(state == ASSET_ENTRY_STATE_ERROR) {
errorThrow("Asset '%s' failed to load.", batch->entries[i]->name);
}
if(state != ASSET_ENTRY_STATE_LOADED) {
allDone = false;
}
return errorChainImpl(err, __FILE__, __func__, __LINE__);
}
assetCacheInsert(&ASSET.cache, item->path, data, item->disposer);
*item->outPtr = data;
}
if (batch->onComplete != NULL) {
batch->onComplete(batch, batch->context);
}
if(!allDone) {
usleep(1000);
errorChain(assetUpdate());
}
} while(!allDone);
errorOk();
}
void assetBatchDispose(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
assetUnlockEntry(batch->entries[i]);
}
memoryZero(batch, sizeof(assetbatch_t));
}
+55 -63
View File
@@ -6,85 +6,77 @@
*/
#pragma once
#include "assetcache.h"
#include "error/error.h"
#include "asset/loader/assetentry.h"
#include "asset/loader/assetloader.h"
#ifndef ASSET_BATCH_MAX
#define ASSET_BATCH_MAX 16
#endif
#define ASSET_BATCH_PARAMS_SIZE 16
typedef struct assetbatch_s assetbatch_t;
typedef void (*assetbatchcomplete_t)(assetbatch_t *batch, void *context);
typedef void (*assetbatcherror_t)(
assetbatch_t *batch,
void *context,
errorret_t error
);
#define ASSET_BATCH_COUNT_MAX 64
typedef struct {
char_t path[ASSET_FILE_NAME_MAX];
assetfileloader_t loader;
uint8_t params[ASSET_BATCH_PARAMS_SIZE];
size_t dataSize;
assetcachedisposer_t disposer;
void **outPtr;
} assetbatchitem_t;
const char_t *path;
assetloadertype_t type;
assetloaderinput_t input;
} assetbatchdesc_t;
struct assetbatch_s {
assetbatchitem_t items[ASSET_BATCH_MAX];
uint8_t count;
assetbatchcomplete_t onComplete;
assetbatcherror_t onError;
void *context;
};
typedef struct {
assetentry_t *entries[ASSET_BATCH_COUNT_MAX];
assetloaderinput_t inputs[ASSET_BATCH_COUNT_MAX];
uint16_t count;
} assetbatch_t;
/**
* Initializes a batch, optionally with completion and error callbacks.
* Initialises the batch from an array of descriptors. Each entry is locked
* and queued for loading immediately.
*
* @param batch The batch to initialize.
* @param onComplete Called when all items have loaded successfully. May be NULL.
* @param onError Called if any item fails to load. May be NULL.
* @param context Passed through to both callbacks.
* @param batch Batch to initialise.
* @param descs Array of entry descriptors (need not outlive this call).
* @param count Number of descriptors (must be <= ASSET_BATCH_COUNT_MAX).
*/
void assetBatchInit(
assetbatch_t *batch,
assetbatchcomplete_t onComplete,
assetbatcherror_t onError,
void *context
uint16_t count,
const assetbatchdesc_t *descs
);
/**
* Adds an item to the batch. Params are copied into inline storage.
* Acquires one additional lock on every entry in the batch.
*
* @param batch The batch to add to.
* @param path Asset path, used as the cache key.
* @param loader The loader function for this asset type.
* @param params Loader-specific parameters. Copied — safe to pass stack address.
* @param paramsSize Size of params in bytes. Must be <= ASSET_BATCH_PARAMS_SIZE.
* @param dataSize Size in bytes to allocate for the output struct.
* @param disposer Called on the data before freeing when released. May be NULL.
* @param outPtr Caller's pointer variable. Set to the cache-owned data on load.
* @param batch Batch to lock.
*/
void assetBatchAdd(
assetbatch_t *batch,
const char_t *path,
assetfileloader_t loader,
const void *params,
size_t paramsSize,
size_t dataSize,
assetcachedisposer_t disposer,
void **outPtr
);
void assetBatchLock(assetbatch_t *batch);
/**
* Loads all items in the batch, checking the cache for each. Fires onComplete
* on success or onError on the first failure. Also returns the error for
* synchronous callers.
* Releases one lock from every entry in the batch. When an entry's lock
* count reaches zero it will be reaped on the next assetUpdate.
*
* @param batch The batch to execute.
* @return Error if any item failed to load.
* @param batch Batch to unlock.
*/
errorret_t assetBatchLoad(assetbatch_t *batch);
void assetBatchUnlock(assetbatch_t *batch);
/**
* Returns true if every entry in the batch has finished loading.
*
* @param batch Batch to query.
*/
bool_t assetBatchIsLoaded(const assetbatch_t *batch);
/**
* Returns true if any entry in the batch is in an error state.
*
* @param batch Batch to query.
*/
bool_t assetBatchHasError(const assetbatch_t *batch);
/**
* Blocks until every entry is loaded. Returns an error if any entry fails.
*
* @param batch Batch to wait on.
*/
errorret_t assetBatchRequireLoaded(assetbatch_t *batch);
/**
* Releases the batch's lock on every entry and clears the batch. After this
* call the batch struct may be reused with assetBatchInit.
*
* @param batch Batch to dispose.
*/
void assetBatchDispose(assetbatch_t *batch);
-81
View File
@@ -1,81 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetcache.h"
#include "util/memory.h"
#include "util/string.h"
#include "assert/assert.h"
void assetCacheInit(assetcache_t *cache) {
memoryZero(cache, sizeof(assetcache_t));
}
void *assetCacheLookup(assetcache_t *cache, const char_t *path) {
for(uint8_t i = 0; i < cache->count; i++) {
if(stringCompare(cache->entries[i].path, path) == 0) {
return cache->entries[i].data;
}
}
return NULL;
}
void assetCacheInsert(
assetcache_t *cache,
const char_t *path,
void *data,
assetcachedisposer_t disposer
) {
assertTrue(cache->count < ASSET_CACHE_MAX, "Asset cache is full.");
assetcacheentry_t *entry = &cache->entries[cache->count++];
stringCopy(entry->path, path, ASSET_FILE_NAME_MAX);
entry->data = data;
entry->refcount = 1;
entry->disposer = disposer;
}
void assetCacheRetain(assetcache_t *cache, const char_t *path) {
for (uint8_t i = 0; i < cache->count; i++) {
if (stringCompare(cache->entries[i].path, path) == 0) {
cache->entries[i].refcount++;
return;
}
}
assertTrue(false, "Asset not found in cache for retain.");
}
static void assetCacheEntryDispose(assetcacheentry_t *entry) {
if (entry->disposer != NULL) {
entry->disposer(entry->data);
}
memoryFree(entry->data);
}
void assetCacheRelease(assetcache_t *cache, const char_t *path) {
for (uint8_t i = 0; i < cache->count; i++) {
if (stringCompare(cache->entries[i].path, path) == 0) {
assetcacheentry_t *entry = &cache->entries[i];
entry->refcount--;
if (entry->refcount == 0) {
assetCacheEntryDispose(entry);
for (uint8_t j = i; j < cache->count - 1; j++) {
cache->entries[j] = cache->entries[j + 1];
}
cache->count--;
}
return;
}
}
assertTrue(false, "Asset not found in cache for release.");
}
void assetCacheDispose(assetcache_t *cache) {
for (uint8_t i = 0; i < cache->count; i++) {
assetCacheEntryDispose(&cache->entries[i]);
}
memoryZero(cache, sizeof(assetcache_t));
}
-81
View File
@@ -1,81 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "assetfile.h"
#ifndef ASSET_CACHE_MAX
#define ASSET_CACHE_MAX 64
#endif
typedef void (*assetcachedisposer_t)(void *data);
typedef struct {
char_t path[ASSET_FILE_NAME_MAX];
void *data;
uint32_t refcount;
assetcachedisposer_t disposer;
} assetcacheentry_t;
typedef struct {
assetcacheentry_t entries[ASSET_CACHE_MAX];
uint8_t count;
} assetcache_t;
/**
* Initializes the asset cache.
*
* @param cache The cache to initialize.
*/
void assetCacheInit(assetcache_t *cache);
/**
* Looks up a cached asset by path.
*
* @param cache The cache to search.
* @param path The asset path to look up.
* @return Pointer to the cached data, or NULL if not found.
*/
void *assetCacheLookup(assetcache_t *cache, const char_t *path);
/**
* Inserts a newly loaded asset into the cache with refcount 1.
*
* @param cache The cache to insert into.
* @param path The asset path key.
* @param data Heap-allocated asset data. The cache takes ownership.
* @param disposer Called with data before freeing when refcount reaches 0.
*/
void assetCacheInsert(
assetcache_t *cache,
const char_t *path,
void *data,
assetcachedisposer_t disposer
);
/**
* Increments the refcount for a cached asset.
*
* @param cache The cache containing the asset.
* @param path The asset path.
*/
void assetCacheRetain(assetcache_t *cache, const char_t *path);
/**
* Decrements the refcount. Disposes and frees the asset when it reaches 0.
*
* @param cache The cache containing the asset.
* @param path The asset path.
*/
void assetCacheRelease(assetcache_t *cache, const char_t *path);
/**
* Disposes all remaining cache entries and resets the cache.
*
* @param cache The cache to dispose.
*/
void assetCacheDispose(assetcache_t *cache);
+1
View File
@@ -15,6 +15,7 @@ typedef struct assetfile_s assetfile_t;
typedef errorret_t (*assetfileloader_t)(assetfile_t *file);
// Describes a file not yet loaded.
typedef struct assetfile_s {
char_t filename[ASSET_FILE_NAME_MAX];
void *params;
+8 -1
View File
@@ -4,8 +4,15 @@
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
assetentry.c
assetloader.c
)
# Subdirs
add_subdirectory(display)
add_subdirectory(locale)
add_subdirectory(json)
add_subdirectory(json)
add_subdirectory(script)
+73
View File
@@ -0,0 +1,73 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetentry.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
void assetEntryInit(
assetentry_t *entry,
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
) {
assertNotNull(entry, "Entry cannot be NULL");
assertStrLenMin(name, 1, "Name cannot be empty");
assertStrLenMax(name, ASSET_FILE_NAME_MAX - 1, "Name too long");
assertTrue(type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertTrue(type < ASSET_LOADER_TYPE_COUNT, "Invalid loader type.");
memoryZero(entry, sizeof(assetentry_t));
stringCopy(entry->name, name, ASSET_FILE_NAME_MAX);
entry->type = type;
entry->input = input;
entry->state = ASSET_ENTRY_STATE_NOT_STARTED;
refInit(&entry->refs, entry, NULL, NULL, NULL);
}
void assetEntryLock(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
refLock(&entry->refs);
}
void assetEntryUnlock(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
refUnlock(&entry->refs);
}
void assetEntryStartLoading(
assetentry_t *entry,
assetloading_t *loading
) {
assertNotNull(entry, "Entry cannot be NULL");
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertTrue(
entry->state == ASSET_ENTRY_STATE_NOT_STARTED,
"Can only start loading from NOT_STARTED state."
);
entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
memoryZero(&loading->loading, sizeof(assetloaderloading_t));
loading->type = entry->type;
loading->entry = entry;
// At this point the asset manager will manage this thing's loading
}
errorret_t assetEntryDispose(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertTrue(entry->type < ASSET_LOADER_TYPE_COUNT, "Invalid loader type.");
errorChain(ASSET_LOADER_CALLBACKS[entry->type].dispose(entry));
memoryZero(entry, sizeof(assetentry_t));
errorOk();
}
+86
View File
@@ -0,0 +1,86 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/loader/assetloading.h"
#include "util/ref.h"
typedef enum {
ASSET_ENTRY_STATE_NOT_STARTED,
ASSET_ENTRY_STATE_PENDING_ASYNC,
ASSET_ENTRY_STATE_LOADING_ASYNC,
ASSET_ENTRY_STATE_PENDING_SYNC,
ASSET_ENTRY_STATE_LOADING_SYNC,
ASSET_ENTRY_STATE_LOADED,
ASSET_ENTRY_STATE_ERROR
} assetentrystate_t;
typedef struct assetentry_s {
// Filename and cache key
char_t name[ASSET_FILE_NAME_MAX];
// What type of asset is this?
assetloadertype_t type;
// Data
assetloaderoutput_t data;
// What state is this asset entry in currently?
assetentrystate_t state;
// What is referencing this asset entry.
ref_t refs;
// Data that will be passed to the loader about how it should load.
assetloaderinput_t *input;
} assetentry_t;
/**
* Initializes an asset entry with the given name and type. This does not load
* the asset.
*
* @param entry The asset entry to initialize.
* @param name The name of the asset, used as a key for loading and caching.
* @param type The type of asset this entry represents.
* @param input Data that will be passed to the loader about how it should load.
*/
void assetEntryInit(
assetentry_t *entry,
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
);
/**
* Locks an asset entry, preventing it from being freed until it is unlocked.
*
* @param entry The asset entry to lock.
*/
void assetEntryLock(assetentry_t *entry);
/**
* Unlocks an asset entry, allowing it to be freed if there are no more locks.
*
* @param entry The asset entry to unlock.
*/
void assetEntryUnlock(assetentry_t *entry);
/**
* Starts loading the given asset entry using an assetloading slot. This will
* be called by the asset manager when it deems it's a good time to begin the
* loading of this asset entry.
*
* Currently we return the error but in future this will not be returned.
*
* @param entry The asset entry to start loading.
* @param loading The assetloading slot to use for loading this asset entry.
* @return Any error that occurs during loading.
*/
void assetEntryStartLoading(assetentry_t *entry, assetloading_t *loading);
/**
* Disposes an asset entry, freeing any resources it holds.
*
* @param entry The asset entry to dispose.
* @return Any error that occurs during disposal.
*/
errorret_t assetEntryDispose(assetentry_t *entry);
+48
View File
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetloader.h"
assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
[ASSET_LOADER_TYPE_NULL] = { 0 },
[ASSET_LOADER_TYPE_MESH] = {
.loadSync = assetMeshLoaderSync,
.loadAsync = assetMeshLoaderAsync,
.dispose = assetMeshDispose
},
[ASSET_LOADER_TYPE_TEXTURE] = {
.loadSync = assetTextureLoaderSync,
.loadAsync = assetTextureLoaderAsync,
.dispose = assetTextureDispose
},
[ASSET_LOADER_TYPE_TILESET] = {
.loadSync = assetTilesetLoaderSync,
.loadAsync = assetTilesetLoaderAsync,
.dispose = assetTilesetDispose
},
[ASSET_LOADER_TYPE_LOCALE] = {
.loadSync = assetLocaleLoaderSync,
.loadAsync = assetLocaleLoaderAsync,
.dispose = assetLocaleDispose
},
[ASSET_LOADER_TYPE_JSON] = {
.loadSync = assetJsonLoaderSync,
.loadAsync = assetJsonLoaderAsync,
.dispose = assetJsonDispose
},
[ASSET_LOADER_TYPE_SCRIPT] = {
.loadSync = assetScriptLoaderSync,
.loadAsync = assetScriptLoaderAsync,
.dispose = assetScriptDispose
},
};
+96
View File
@@ -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 "asset/loader/display/assetmeshloader.h"
#include "asset/loader/display/assettextureloader.h"
#include "asset/loader/display/assettilesetloader.h"
#include "asset/loader/locale/assetlocaleloader.h"
#include "asset/loader/json/assetjsonloader.h"
#include "asset/loader/script/assetscriptloader.h"
typedef enum {
ASSET_LOADER_TYPE_NULL,
ASSET_LOADER_TYPE_MESH,
ASSET_LOADER_TYPE_TEXTURE,
ASSET_LOADER_TYPE_TILESET,
ASSET_LOADER_TYPE_LOCALE,
ASSET_LOADER_TYPE_JSON,
ASSET_LOADER_TYPE_SCRIPT,
ASSET_LOADER_TYPE_COUNT
} assetloadertype_t;
typedef union {
assetmeshloaderinput_t mesh;
assettextureloaderinput_t texture;
assettilesetloaderinput_t tileset;
assetlocaleloaderinput_t locale;
assetjsonloaderinput_t json;
assetscriptloaderinput_t script;
} assetloaderinput_t;
typedef union {
assetmeshloaderloading_t mesh;
assettextureloaderloading_t texture;
assettilesetloaderloading_t tileset;
assetlocaleloaderloading_t locale;
assetjsonloaderloading_t json;
assetscriptloaderloading_t script;
} assetloaderloading_t;
typedef union {
assetmeshoutput_t mesh;
assettextureoutput_t texture;
assettilesetoutput_t tileset;
assetlocaleoutput_t locale;
assetjsonoutput_t json;
assetscriptoutput_t script;
} assetloaderoutput_t;
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef errorret_t (assetloadersynccallback_t)(assetloading_t *loading);
typedef errorret_t (assetloaderasynccallback_t)(assetloading_t *loading);
typedef errorret_t (assetloaderdisposecallback_t)(assetentry_t *entry);
typedef struct {
assetloadersynccallback_t *loadSync;
assetloaderasynccallback_t *loadAsync;
assetloaderdisposecallback_t *dispose;
} assetloadercallbacks_t;
extern assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT];
/**
* Shorthand method to both chain an error (against the loader state) and to
* set the asset entry state to error.
*
* @param loading The asset loading slot.
* @param ret The error return value to check and chain if it's an error.
*/
#define assetLoaderErrorChain(loading, _expr) {\
errorret_t _alec = (_expr); \
if(errorIsNotOk(_alec)) { \
(loading)->entry->state = ASSET_ENTRY_STATE_ERROR; \
errorChain(_alec); \
} \
}
/**
* Shorthand method to both throw an error (against the loader state) and to
* set the asset entry state to error.
*
* @param loading The asset loading slot.
* @param ... Format string and arguments for the error message.
*/
#define assetLoaderErrorThrow(loading, ...) {\
loading->entry->state = ASSET_ENTRY_STATE_ERROR; \
errorThrow(__VA_ARGS__); \
}
+30
View File
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "assetloader.h"
#include "asset/assetfile.h"
#include "thread/threadmutex.h"
typedef struct assetentry_s assetentry_t;
typedef struct assetloading_s {
// Protects entry pointer and entry->state from concurrent access.
threadmutex_t mutex;
// What type of asset is being loaded.
assetloadertype_t type;
// Referral back to the asset entry that will be kept alive after load done.
assetentry_t *entry;
// Information used during the load operation only.
assetloaderloading_t loading;
} assetloading_t;
typedef errorret_t (assetloadingcallback_t)(assetloading_t *loading);
typedef struct {
assetloadingcallback_t *loadSync;
} assetloadingcallbacks_t;
+117 -125
View File
@@ -1,182 +1,174 @@
/**
* Copyright (c) 2026 Dominic Masters
*
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "asset/loader/display/assetmeshloader.h"
#include "asset/asset.h"
#include "assetmeshloader.h"
#include "assert/assert.h"
#include "util/endian.h"
#include "util/memory.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
errorret_t assetMeshLoader(assetfile_t *file) {
assertNotNull(file, "Asset file cannot be null");
assetmeshoutput_t *output = (assetmeshoutput_t *)file->output;
assertNotNull(output, "Output cannot be null");
assertNotNull(output->outMesh, "Output mesh cannot be null");
assertNotNull(output->outVertices, "Output vertices cannot be null");
errorret_t assetMeshLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
// STL file loading
errorChain(assetFileOpen(file));
if(loading->loading.mesh.state != ASSET_MESH_LOADING_STATE_READ_FILE) {
errorOk();
}
// Skip the 80 byte header
errorChain(assetFileRead(file, NULL, 80));
if(file->lastRead != 80) errorThrow("Failed to skip STL header");
assetmeshoutput_t *out = &loading->entry->data.mesh;
assetfile_t *file = &loading->loading.mesh.file;
assetmeshinputaxis_t axis = loading->entry->input->mesh;
assetLoaderErrorChain(loading, assetFileInit(file, loading->entry->name, NULL, NULL));
assetLoaderErrorChain(loading, assetFileOpen(file));
// Skip the 80-byte STL header.
assetLoaderErrorChain(loading, assetFileRead(file, NULL, 80));
if(file->lastRead != 80) {
assetLoaderErrorThrow(loading, "Failed to skip STL header.");
}
uint32_t triangleCount;
errorChain(assetFileRead(file, &triangleCount, sizeof(uint32_t)));
if(file->lastRead != sizeof(uint32_t)) errorThrow("Failed read tri count");
// normalize
assetLoaderErrorChain(loading, assetFileRead(file, &triangleCount, sizeof(uint32_t)));
if(file->lastRead != sizeof(uint32_t)) {
assetLoaderErrorThrow(loading, "Failed to read tri count");
}
triangleCount = endianLittleToHost32(triangleCount);
// Allocate mesh and vertex data
errorret_t ret;
meshvertex_t *verts = memoryAllocate(
sizeof(meshvertex_t) * triangleCount * 3
);
*output->outVertices = verts;
// Read triangle data
out->vertices = memoryAllocate(sizeof(meshvertex_t) * triangleCount * 3);
meshvertex_t *verts = out->vertices;
errorret_t ret;
for(uint32_t i = 0; i < triangleCount; i++) {
assetmeshstltriangle_t triData;
ret = assetFileRead(file, &triData, sizeof(triData));
if(ret.code != ERROR_OK) {
if(errorIsNotOk(ret)) {
memoryFree(verts);
errorChain(ret);
out->vertices = NULL;
assetLoaderErrorChain(loading, ret);
}
if(file->lastRead != sizeof(triData)) {
memoryFree(verts);
errorThrow("Failed to read triangle data");
out->vertices = NULL;
assetLoaderErrorThrow(loading, "Failed to read triangle data");
}
// Skip normals, we don't use them
// Fix endianess of of data
for(uint8_t j = 0; j < 3; j++) {
#if MESH_ENABLE_COLOR
verts[i * 3 + j].color.r = (uint8_t)(endianLittleToHostFloat(
triData.normal[0]
) * 255.0f);
verts[i * 3 + j].color.g = (uint8_t)(endianLittleToHostFloat(
triData.normal[1]
) * 255.0f);
verts[i * 3 + j].color.b = (uint8_t)(endianLittleToHostFloat(
triData.normal[2]
) * 255.0f);
verts[i * 3 + j].color.r = (
(uint8_t)(endianLittleToHostFloat(triData.normal[0]) * 255.0f)
);
verts[i * 3 + j].color.g = (
(uint8_t)(endianLittleToHostFloat(triData.normal[1]) * 255.0f)
);
verts[i * 3 + j].color.b = (
(uint8_t)(endianLittleToHostFloat(triData.normal[2]) * 255.0f)
);
verts[i * 3 + j].color.a = 0xFF;
#endif
verts[i * 3 + j].uv[0] = 0.0f; // No UV data in STL, just set to 0
verts[i * 3 + j].uv[0] = 0.0f;
verts[i * 3 + j].uv[1] = 0.0f;
for(uint8_t k = 0; k < 3; k++) {
verts[i * 3 + j].pos[k] = endianLittleToHostFloat(
triData.positions[j][k]
);
verts[i * 3 + j].pos[k] = endianLittleToHostFloat(triData.positions[j][k]);
}
switch(output->inputAxis) {
case MESH_INPUT_AXIS_Z_UP:
// Convert Z-Up to Y-Up
{
float_t temp = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = verts[i * 3 + j].pos[2];
verts[i * 3 + j].pos[2] = temp;
}
switch(axis) {
case MESH_INPUT_AXIS_Z_UP: {
float_t temp = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = verts[i * 3 + j].pos[2];
verts[i * 3 + j].pos[2] = temp;
break;
case MESH_INPUT_AXIS_X_UP:
// Convert X-Up to Y-Up
{
float_t temp = verts[i * 3 + j].pos[0];
verts[i * 3 + j].pos[0] = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = temp;
}
}
case MESH_INPUT_AXIS_X_UP: {
float_t temp = verts[i * 3 + j].pos[0];
verts[i * 3 + j].pos[0] = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = temp;
break;
}
case MESH_INPUT_AXIS_Y_DOWN:
// Invert Y axis
verts[i * 3 + j].pos[1] = -verts[i * 3 + j].pos[1];
break;
case MESH_INPUT_AXIS_Z_DOWN:
// Convert Z-Up to Y-Up and invert Y axis
{
float_t temp = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = -verts[i * 3 + j].pos[2];
verts[i * 3 + j].pos[2] = temp;
}
case MESH_INPUT_AXIS_Z_DOWN: {
float_t temp = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = -verts[i * 3 + j].pos[2];
verts[i * 3 + j].pos[2] = temp;
break;
case MESH_INPUT_AXIS_X_DOWN:
// Convert X-Up to Y-Up and invert Y axis
{
float_t temp = verts[i * 3 + j].pos[0];
verts[i * 3 + j].pos[0] = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = -temp;
}
}
case MESH_INPUT_AXIS_X_DOWN: {
float_t temp = verts[i * 3 + j].pos[0];
verts[i * 3 + j].pos[0] = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = -temp;
break;
}
case MESH_INPUT_AXIS_Y_UP:
default:
// No covnersion possible / Needed
break;
}
}
}
// Finally, init mesh
ret = meshInit(
output->outMesh,
MESH_PRIMITIVE_TYPE_TRIANGLES,
triangleCount * 3,
verts
);
if(ret.code != ERROR_OK) {
memoryFree(verts);
errorChain(ret);
}
ret = assetFileClose(file);
if(ret.code != ERROR_OK) {
errorCatch(errorPrint(meshDispose(output->outMesh)));
if(errorIsNotOk(ret)) {
memoryFree(verts);
errorChain(ret);
out->vertices = NULL;
assetLoaderErrorChain(loading, ret);
}
assetFileDispose(file);
loading->loading.mesh.triangleCount = triangleCount;
loading->loading.mesh.state = ASSET_MESH_LOADING_STATE_CREATE_MESH;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
errorret_t assetMeshLoadToOutput(
const char_t *path,
assetmeshoutput_t *output
) {
assertNotNull(path, "Path cannot be null");
assertNotNull(output, "Output cannot be null");
assertNotNull(output->outMesh, "Output mesh cannot be null");
assertNotNull(output->outVertices, "Output vertices cannot be null");
return assetLoad(path, &assetMeshLoader, NULL, output);
errorret_t assetMeshLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_MESH, "Invalid type.");
switch(loading->loading.mesh.state) {
case ASSET_MESH_LOADING_STATE_INITIAL:
loading->loading.mesh.state = ASSET_MESH_LOADING_STATE_READ_FILE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_MESH_LOADING_STATE_CREATE_MESH:
break;
default:
errorOk();
}
assetmeshoutput_t *out = &loading->entry->data.mesh;
assertNotNull(out->vertices, "Mesh vertices should have been loaded by now.");
errorret_t ret = meshInit(
&out->mesh,
MESH_PRIMITIVE_TYPE_TRIANGLES,
loading->loading.mesh.triangleCount * 3,
out->vertices
);
if(errorIsNotOk(ret)) {
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
memoryFree(out->vertices);
out->vertices = NULL;
assetLoaderErrorChain(loading, ret);
}
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t assetMeshLoad(
const char_t *path,
mesh_t *outMesh,
meshvertex_t **outVertices,
const assetmeshinputaxis_t inputAxis
) {
assertNotNull(path, "Path cannot be null");
assertNotNull(outMesh, "Output mesh cannot be null");
assertNotNull(outVertices, "Output vertices cannot be null");
assetmeshoutput_t output = {
outMesh,
outVertices,
inputAxis
};
return assetMeshLoadToOutput(path, &output);
}
errorret_t assetMeshDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_MESH, "Invalid type.");
errorChain(meshDispose(&entry->data.mesh.mesh));
memoryFree(entry->data.mesh.vertices);
errorOk();
}
+26 -29
View File
@@ -1,17 +1,20 @@
/**
* Copyright (c) 2026 Dominic Masters
*
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/asset.h"
#include "asset/assetfile.h"
#include "display/mesh/mesh.h"
#include "assert/assert.h"
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef enum {
MESH_INPUT_AXIS_Y_UP,// Default
MESH_INPUT_AXIS_Y_UP,
MESH_INPUT_AXIS_Z_UP,
MESH_INPUT_AXIS_X_UP,
@@ -20,10 +23,24 @@ typedef enum {
MESH_INPUT_AXIS_X_DOWN,
} assetmeshinputaxis_t;
typedef assetmeshinputaxis_t assetmeshloaderinput_t;
typedef enum {
ASSET_MESH_LOADING_STATE_INITIAL,
ASSET_MESH_LOADING_STATE_READ_FILE,
ASSET_MESH_LOADING_STATE_CREATE_MESH,
ASSET_MESH_LOADING_STATE_DONE
} assetmeshloadingstate_t;
typedef struct {
mesh_t *outMesh;
meshvertex_t **outVertices;
assetmeshinputaxis_t inputAxis;
assetfile_t file;
assetmeshloadingstate_t state;
uint32_t triangleCount;
} assetmeshloaderloading_t;
typedef struct {
mesh_t mesh;
meshvertex_t *vertices;
} assetmeshoutput_t;
#pragma pack(push, 1)
@@ -36,26 +53,6 @@ typedef struct {
assertStructSize(assetmeshstltriangle_t, 50);
/**
* Loader for mesh assets.
*
* @param file Asset file to load the mesh from.
* @return Any error that occurs during loading.
*/
errorret_t assetMeshLoader(assetfile_t *file);
/**
* Handler for mesh assets.
*
* @param file Asset file to load the mesh from.
* @param outMesh Output mesh to load the data into.
* @param outVertices Output pointer to the vertex data, used for cleanup.
* @param inputAxis The axis orientation of the input mesh data.
* @return Any error that occurs during loading.
*/
errorret_t assetMeshLoad(
const char_t *path,
mesh_t *outMesh,
meshvertex_t **outVertices,
const assetmeshinputaxis_t inputAxis
);
errorret_t assetMeshLoaderAsync(assetloading_t *loading);
errorret_t assetMeshLoaderSync(assetloading_t *loading);
errorret_t assetMeshDispose(assetentry_t *entry);
@@ -6,12 +6,13 @@
*/
#include "assettextureloader.h"
#include "asset/assetbatch.h"
#include "assert/assert.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include "log/log.h"
#include "util/endian.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
stbi_io_callbacks ASSET_TEXTURE_STB_CALLBACKS = {
.read = assetTextureReader,
@@ -26,7 +27,7 @@ int assetTextureReader(void *user, char *data, int size) {
assertNotNull(file, "Asset file in stb_image callbacks cannot be NULL.");
errorret_t ret = assetFileRead(file, data, (size_t)size);
if(ret.code != ERROR_OK) {
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
return -1;
}
@@ -39,7 +40,7 @@ void assetTextureSkipper(void *user, int n) {
assertNotNull(file, "Asset file in stb_image callbacks cannot be NULL.");
errorret_t ret = assetFileRead(file, NULL, (size_t)n);
if(ret.code != ERROR_OK) {
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
}
}
@@ -51,88 +52,114 @@ int assetTextureEOF(void *user) {
return file->position >= file->size;
}
errorret_t assetTextureLoader(assetfile_t *file) {
assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(file->params, "Asset file parameters cannot be NULL.");
assertNotNull(file->output, "Asset file output cannot be NULL.");
errorret_t assetTextureLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assettextureloaderparams_t *p = (assettextureloaderparams_t*)file->params;
assertNotNull(p, "Asset texture loader parameters cannot be NULL.");
// Only care about loading pixels.
if(loading->loading.texture.state != ASSET_TEXTURE_LOADING_STATE_LOAD_PIXELS){
errorOk();
}
// Init the file
assertNull(
loading->loading.texture.data, "Pixels already defined?"
);
assetfile_t *file = &loading->loading.texture.file;
assetLoaderErrorChain(loading, assetFileInit(
file,
loading->entry->name,
NULL,
&loading->entry->data.texture
));
assetLoaderErrorChain(loading, assetFileOpen(file));
// Determine channels
int channelsDesired;
switch(p->format) {
switch(loading->entry->input->texture) {
case TEXTURE_FORMAT_RGBA:
channelsDesired = 4;
break;
default:
errorThrow("Bad texture format.");
assetLoaderErrorThrow(loading, "Bad texture format.");
}
int width, height, channels;
errorChain(assetFileOpen(file));
uint8_t *data = stbi_load_from_callbacks(
// Load image pixels.
loading->loading.texture.data = stbi_load_from_callbacks(
&ASSET_TEXTURE_STB_CALLBACKS,
file,
&width,
&height,
&channels,
&loading->loading.texture.width,
&loading->loading.texture.height,
&loading->loading.texture.channels,
channelsDesired
);
errorChain(assetFileClose(file));
if(data == NULL) {
// Close out the file.
assetLoaderErrorChain(loading, assetFileClose(file));
assetLoaderErrorChain(loading, assetFileDispose(file));
// Ensure we loaded correctly.
if(loading->loading.texture.data == NULL) {
const char_t *errorStr = stbi_failure_reason();
errorThrow("Failed to load texture from file %s.", errorStr);
assetLoaderErrorThrow(loading, "Failed to load texture from file %s.", errorStr);
}
// Fixes a specific bug probably with Dolphin but for now just assuming endian
if(!isHostLittleEndian()) {
stbi__vertical_flip(data, width, height, channelsDesired);
stbi__vertical_flip(
loading->loading.texture.data,
loading->loading.texture.width,
loading->loading.texture.height,
loading->loading.texture.channels
);
}
errorChain(textureInit(
(texture_t*)file->output,
(int32_t)width, (int32_t)height,
p->format,
(texturedata_t){
.rgbaColors = (color_t*)data
}
));
stbi_image_free(data);
loading->loading.texture.state = ASSET_TEXTURE_LOADING_STATE_CREATE_TEXTURE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
errorret_t assetTextureLoad(
const char_t *path,
texture_t *out,
const textureformat_t format
) {
assettextureloaderparams_t params = {
.format = format
};
return assetLoad(path, assetTextureLoader, &params, out);
}
errorret_t assetTextureLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
static void assetTextureCacheDispose(void *data) {
errorCatch(textureDispose((texture_t*)data));
}
switch(loading->loading.texture.state) {
case ASSET_TEXTURE_LOADING_STATE_INITIAL:
loading->loading.texture.state = ASSET_TEXTURE_LOADING_STATE_LOAD_PIXELS;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_TEXTURE_LOADING_STATE_CREATE_TEXTURE:
break;
void assetBatchTexture(
assetbatch_t *batch,
const char_t *path,
textureformat_t format,
texture_t **out
) {
assettextureloaderparams_t params = { .format = format };
assetBatchAdd(
batch,
path,
assetTextureLoader,
&params, sizeof(params),
sizeof(texture_t),
assetTextureCacheDispose,
(void**)out
default:
errorOk();
}
// Create the texture.
assertNotNull(
loading->loading.texture.data, "Pixels should have been loaded by now."
);
assetLoaderErrorChain(loading, textureInit(
(texture_t*)&loading->entry->data.texture,
loading->loading.texture.width,
loading->loading.texture.height,
loading->entry->input->texture,
(texturedata_t){
.rgbaColors = (color_t*)loading->loading.texture.data
}
));
// Free the pixels.
stbi_image_free(loading->loading.texture.data);
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t assetTextureDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL");
return textureDispose(&entry->data.texture);
}
@@ -6,13 +6,29 @@
*/
#pragma once
#include "asset/asset.h"
#include "asset/assetbatch.h"
#include "asset/assetfile.h"
#include "display/texture/texture.h"
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef textureformat_t assettextureloaderinput_t;
typedef enum {
ASSET_TEXTURE_LOADING_STATE_INITIAL,
ASSET_TEXTURE_LOADING_STATE_LOAD_PIXELS,
ASSET_TEXTURE_LOADING_STATE_CREATE_TEXTURE,
ASSET_TEXTURE_LOADING_STATE_DONE
} assettextureloadingstate_t;
typedef struct {
textureformat_t format;
} assettextureloaderparams_t;
assetfile_t file;
assettextureloadingstate_t state;
int channels, width, height;
uint8_t *data;
} assettextureloaderloading_t;
typedef texture_t assettextureoutput_t;
/**
* STB image read callback for asset files.
@@ -24,44 +40,42 @@ typedef struct {
*/
int assetTextureReader(void *user, char *data, int size);
/**
* STB image skip callback for asset files.
*
* @param user User data passed to the callback, should be an assetfile_t*.
* @param n Number of bytes to skip in the file.
*/
void assetTextureSkipper(void *user, int n);
/**
* STB image EOF callback for asset files.
*
* @param user User data passed to the callback, should be an assetfile_t*.
* @return Non-zero if end of file has been reached, zero otherwise.
*/
int assetTextureEOF(void *user);
/**
* Handler for texture assets.
* Synchronous loader for texture assets.
*
* @param file Asset file to load the texture from.
* @return Any error that occurs during loading.
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetTextureLoader(assetfile_t *file);
errorret_t assetTextureLoaderAsync(assetloading_t *loading);
/**
* Loads a texture from the specified path.
* Synchronous loader for texture assets.
*
* @param path Path to the texture asset.
* @param out Output texture to load into.
* @param format Format of the texture to load.
* @return Any error that occurs during loading.
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetTextureLoad(
const char_t *path,
texture_t *out,
const textureformat_t format
);
errorret_t assetTextureLoaderSync(assetloading_t *loading);
/**
* Adds a texture load request to a batch. The cache-owned texture_t* is
* written to *out when the batch completes.
*
* @param batch The batch to add to.
* @param path Path to the texture asset.
* @param format Format of the texture to load.
* @param out Receives a pointer to the loaded texture on completion.
* Disposer for texture assets.
*
* @param entry Asset entry containing the texture to dispose.
* @return Error code indicating success or failure of the dispose operation.
*/
void assetBatchTexture(
assetbatch_t *batch,
const char_t *path,
textureformat_t format,
texture_t **out
);
errorret_t assetTextureDispose(assetentry_t *entry);
@@ -1,97 +1,115 @@
/**
* Copyright (c) 2026 Dominic Masters
*
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assettilesetloader.h"
#include "asset/assetbatch.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/endian.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
errorret_t assetTilesetLoader(assetfile_t *file) {
assertNotNull(file, "Asset file pointer for tileset loader is null.");
errorret_t assetTilesetLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
tileset_t *out = (tileset_t *)file->output;
assertNotNull(out, "Output pointer for tileset loader is null.");
uint8_t *entire = memoryAllocate(file->size);
errorChain(assetFileOpen(file));
errorChain(assetFileRead(file, entire, file->size));
errorChain(assetFileClose(file));
assertTrue(file->lastRead == file->size, "Failed to read entire file.");
if(
entire[0] != 'D' ||
entire[1] != 'T' ||
entire[2] != 'F'
) {
errorThrow("Invalid tileset header");
if(loading->loading.tileset.state != ASSET_TILESET_LOADING_STATE_READ_FILE) {
errorOk();
}
if(entire[3] != 0x00) {
errorThrow("Unsupported tileset version");
}
assertNull(loading->loading.tileset.data, "Data already defined?");
// Fix endianness
assetfile_t *file = &loading->loading.tileset.file;
assetLoaderErrorChain(loading, assetFileInit(file, loading->entry->name, NULL, NULL));
out->tileWidth = endianLittleToHost16(*(uint16_t *)(entire + 4));
out->tileHeight = endianLittleToHost16(*(uint16_t *)(entire + 6));
out->columns = endianLittleToHost16(*(uint16_t *)(entire + 8));
out->rows = endianLittleToHost16(*(uint16_t *)(entire + 10));
// out->right = endianLittleToHost16(*(uint16_t *)(entire + 12));
// out->bottom = endianLittleToHost16(*(uint16_t *)(entire + 14));
if(out->tileWidth == 0) {
errorThrow("Tile width cannot be 0");
}
if(out->tileHeight == 0) {
errorThrow("Tile height cannot be 0");
}
if(out->columns == 0) {
errorThrow("Column count cannot be 0");
}
if(out->rows == 0) {
errorThrow("Row count cannot be 0");
}
out->uv[0] = endianLittleToHostFloat(*(float *)(entire + 16));
out->uv[1] = endianLittleToHostFloat(*(float *)(entire + 20));
if(out->uv[1] < 0.0f || out->uv[1] > 1.0f) {
errorThrow("Invalid v0 value in tileset");
}
// Setup tileset
out->tileCount = out->columns * out->rows;
memoryFree(entire);
uint8_t *data = memoryAllocate(file->size);
assetLoaderErrorChain(loading, assetFileOpen(file));
assetLoaderErrorChain(loading, assetFileRead(file, data, file->size));
assetLoaderErrorChain(loading, assetFileClose(file));
assetLoaderErrorChain(loading, assetFileDispose(file));
assertTrue(file->lastRead == file->size, "Failed to read entire tileset file.");
loading->loading.tileset.data = data;
loading->loading.tileset.state = ASSET_TILESET_LOADING_STATE_PARSE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
errorret_t assetTilesetLoad(
const char_t *path,
tileset_t *out
) {
return assetLoad(path, assetTilesetLoader, NULL, out);
errorret_t assetTilesetLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_TILESET, "Invalid type.");
switch(loading->loading.tileset.state) {
case ASSET_TILESET_LOADING_STATE_INITIAL:
loading->loading.tileset.state = ASSET_TILESET_LOADING_STATE_READ_FILE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_TILESET_LOADING_STATE_PARSE:
break;
default:
errorOk();
}
uint8_t *data = loading->loading.tileset.data;
assertNotNull(data, "Tileset data should have been loaded by now.");
tileset_t *out = &loading->entry->data.tileset;
if(data[0] != 'D' || data[1] != 'T' || data[2] != 'F') {
memoryFree(data);
assetLoaderErrorThrow(loading, "Invalid tileset header");
}
if(data[3] != 0x00) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Unsupported tileset version");
}
out->tileWidth = endianLittleToHost16(*(uint16_t *)(data + 4));
out->tileHeight = endianLittleToHost16(*(uint16_t *)(data + 6));
out->columns = endianLittleToHost16(*(uint16_t *)(data + 8));
out->rows = endianLittleToHost16(*(uint16_t *)(data + 10));
if(out->tileWidth == 0) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Tile width cannot be 0");
}
if(out->tileHeight == 0) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Tile height cannot be 0");
}
if(out->columns == 0) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Column count cannot be 0");
}
if(out->rows == 0) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Row count cannot be 0");
}
out->uv[0] = endianLittleToHostFloat(*(float *)(data + 16));
out->uv[1] = endianLittleToHostFloat(*(float *)(data + 20));
if(out->uv[1] < 0.0f || out->uv[1] > 1.0f) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Invalid v0 value in tileset");
}
out->tileCount = out->columns * out->rows;
memoryFree(data);
loading->loading.tileset.data = NULL;
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
void assetBatchTileset(
assetbatch_t *batch,
const char_t *path,
tileset_t **out
) {
assetBatchAdd(
batch,
path,
assetTilesetLoader,
NULL, 0,
sizeof(tileset_t),
NULL,
(void**)out
);
}
errorret_t assetTilesetDispose(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_TILESET, "Invalid type.");
errorOk();
}
@@ -1,45 +1,60 @@
/**
* Copyright (c) 2026 Dominic Masters
*
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/asset.h"
#include "asset/assetbatch.h"
#include "asset/assetfile.h"
#include "display/texture/tileset.h"
/**
* Handler for tileset assets.
*
* @param file Asset file to load the tileset from.
* @return Any error that occurs during loading.
*/
errorret_t assetTilesetLoader(assetfile_t *file);
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef struct {
void *nothing;
} assettilesetloaderinput_t;
typedef enum {
ASSET_TILESET_LOADING_STATE_INITIAL,
ASSET_TILESET_LOADING_STATE_READ_FILE,
ASSET_TILESET_LOADING_STATE_PARSE,
ASSET_TILESET_LOADING_STATE_DONE
} assettilesetloadingstate_t;
typedef struct {
assetfile_t file;
assettilesetloadingstate_t state;
uint8_t *data;
} assettilesetloaderloading_t;
typedef tileset_t assettilesetoutput_t;
/**
* Loads a tileset from the specified path.
*
* @param path Path to the tileset asset.
* @param out Output tileset to load into.
* @return Any error that occurs during loading.
*/
errorret_t assetTilesetLoad(
const char_t *path,
tileset_t *out
);
/**
* Adds a tileset load request to a batch. The cache-owned tileset_t* is
* written to *out when the batch completes.
* Asynchronous loader for tileset assets. Reads the raw DTF file bytes into
* the loading buffer so the sync phase can parse without blocking the main
* thread on I/O.
*
* @param batch The batch to add to.
* @param path Path to the tileset asset.
* @param out Receives a pointer to the loaded tileset on completion.
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
void assetBatchTileset(
assetbatch_t *batch,
const char_t *path,
tileset_t **out
);
errorret_t assetTilesetLoaderAsync(assetloading_t *loading);
/**
* Synchronous loader for tileset assets. Parses the DTF binary previously
* read by the async phase and populates the output tileset_t.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetTilesetLoaderSync(assetloading_t *loading);
/**
* Disposer for tileset assets.
*
* @param entry Asset entry containing the tileset to dispose.
* @return Error code indicating success or failure of the dispose operation.
*/
errorret_t assetTilesetDispose(assetentry_t *entry);
+66 -31
View File
@@ -1,6 +1,6 @@
/**
* Copyright (c) 2026 Dominic Masters
*
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
@@ -8,46 +8,81 @@
#include "assetjsonloader.h"
#include "util/memory.h"
#include "assert/assert.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
errorret_t assetJsonLoadFileToDoc(assetfile_t *file, yyjson_doc **outDoc) {
assertNotNull(file, "Asset file pointer for JSON loader is null.");
assertNotNull(outDoc, "Output pointer for JSON loader is null.");
errorret_t assetJsonLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
if(file->size > ASSET_JSON_FILE_SIZE_MAX) {
errorThrow("JSON exceeds maximum allowed size");
if(loading->loading.json.state != ASSET_JSON_LOADING_STATE_READ_FILE) {
errorOk();
}
// Create buffer
uint8_t *buffer = memoryAllocate(file->size);
assertNull(loading->loading.json.buffer, "Buffer already defined?");
errorChain(assetFileOpen(file));
assetfile_t *file = &loading->loading.json.file;
assetLoaderErrorChain(loading, assetFileInit(file, loading->entry->name, NULL, NULL));
// Read entire file
errorChain(assetFileRead(file, buffer, file->size));
if(file->size > ASSET_JSON_FILE_SIZE_MAX) {
assetLoaderErrorThrow(loading, "JSON exceeds maximum allowed size");
}
size_t fileSize = (size_t)file->size;
uint8_t *buffer = memoryAllocate(fileSize);
assetLoaderErrorChain(loading, assetFileOpen(file));
assetLoaderErrorChain(loading, assetFileRead(file, buffer, fileSize));
assertTrue(file->lastRead == file->size, "Failed to read entire JSON file.");
assetLoaderErrorChain(loading, assetFileClose(file));
assetLoaderErrorChain(loading, assetFileDispose(file));
errorChain(assetFileClose(file));
*outDoc = yyjson_read(
buffer,
file->size,
YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS
);
memoryFree(buffer);
if(!*outDoc) errorThrow("Failed to parse JSON");
loading->loading.json.buffer = buffer;
loading->loading.json.size = fileSize;
loading->loading.json.state = ASSET_JSON_LOADING_STATE_PARSE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
errorret_t assetJsonLoader(assetfile_t *file) {
yyjson_doc **outDoc = (yyjson_doc **)file->output;
assertNotNull(outDoc, "Output pointer for JSON loader is null.");
return assetJsonLoadFileToDoc(file, outDoc);
errorret_t assetJsonLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_JSON, "Invalid type.");
switch(loading->loading.json.state) {
case ASSET_JSON_LOADING_STATE_INITIAL:
loading->loading.json.state = ASSET_JSON_LOADING_STATE_READ_FILE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_JSON_LOADING_STATE_PARSE:
break;
default:
errorOk();
}
uint8_t *buffer = loading->loading.json.buffer;
assertNotNull(buffer, "JSON buffer should have been loaded by now.");
loading->entry->data.json = yyjson_read(
(char *)buffer,
loading->loading.json.size,
YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS
);
memoryFree(buffer);
loading->loading.json.buffer = NULL;
if(!loading->entry->data.json) {
assetLoaderErrorThrow(loading, "Failed to parse JSON");
}
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t assetJsonLoad(
const char_t *path,
yyjson_doc **outDoc
) {
return assetLoad(path, assetJsonLoader, NULL, outDoc);
}
errorret_t assetJsonDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_JSON, "Invalid type.");
yyjson_doc_free(entry->data.json);
entry->data.json = NULL;
errorOk();
}
+23 -31
View File
@@ -1,45 +1,37 @@
/**
* Copyright (c) 2026 Dominic Masters
*
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/asset.h"
#include "asset/assetfile.h"
#include "yyjson.h"
#define ASSET_JSON_FILE_SIZE_MAX 1024*256
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef struct { void *nothing; } assetjsonloaderinput_t;
typedef enum {
ASSET_JSON_LOADING_STATE_INITIAL,
ASSET_JSON_LOADING_STATE_READ_FILE,
ASSET_JSON_LOADING_STATE_PARSE,
ASSET_JSON_LOADING_STATE_DONE
} assetjsonloadingstate_t;
typedef struct {
void *nothing;
} assetjsonloaderparams_t;
assetfile_t file;
assetjsonloadingstate_t state;
uint8_t *buffer;
size_t size;
} assetjsonloaderloading_t;
/**
* Loads a JSON document from the specified asset file.
*
* @param file Asset file to load the JSON document from.
* @param outDoc Pointer to store the loaded JSON document.
* @return Any error that occurs during loading.
*/
errorret_t assetJsonLoadFileToDoc(assetfile_t *file, yyjson_doc **outDoc);
typedef yyjson_doc * assetjsonoutput_t;
/**
* Handler for locale assets.
*
* @param file Asset file to load the locale from.
* @return Any error that occurs during loading.
*/
errorret_t assetJsonLoader(assetfile_t *file);
/**
* Loads a locale from the specified path.
*
* @param path Path to the locale asset.
* @param outDoc Pointer to store the loaded JSON document.
* @return Any error that occurs during loading.
*/
errorret_t assetJsonLoad(
const char_t *path,
yyjson_doc **outDoc
);
errorret_t assetJsonLoaderAsync(assetloading_t *loading);
errorret_t assetJsonLoaderSync(assetloading_t *loading);
errorret_t assetJsonDispose(assetentry_t *entry);
@@ -11,38 +11,60 @@
#include "util/string.h"
#include "assert/assert.h"
errorret_t assetLocaleFileInit(
assetlocalefile_t *localeFile,
const char_t *path
) {
assertNotNull(localeFile, "Locale file cannot be NULL.");
assertNotNull(path, "Locale file path cannot be NULL.");
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
errorret_t assetLocaleLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
if(loading->loading.locale.state != ASSET_LOCALE_LOADER_STATE_LOAD_HEADER) {
errorOk();
}
assetlocalefile_t *localeFile = &loading->entry->data.locale;
memoryZero(localeFile, sizeof(assetlocalefile_t));
assetLoaderErrorChain(loading, assetFileInit(&localeFile->file, loading->entry->name, NULL, NULL));
assetLoaderErrorChain(loading, assetFileOpen(&localeFile->file));
// Init the asset file.
errorChain(assetFileInit(&localeFile->file, path, NULL, NULL));
// Open the file handle
errorChain(assetFileOpen(&localeFile->file));
// Get the blank key, this is basically the header info for po files
char_t buffer[1024];
errorChain(assetLocaleGetString(localeFile, "", 0, buffer, sizeof(buffer)));
errorChain(assetLocaleParseHeader(localeFile, buffer, sizeof(buffer)));
assetLoaderErrorChain(loading, assetLocaleGetString(localeFile, "", 0, buffer, sizeof(buffer)));
assetLoaderErrorChain(loading, assetLocaleParseHeader(localeFile, buffer, sizeof(buffer)));
loading->loading.locale.state = ASSET_LOCALE_LOADER_STATE_DONE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
errorret_t assetLocaleFileDispose(assetlocalefile_t *localeFile) {
assertNotNull(localeFile, "Locale file cannot be NULL.");
errorret_t assetLocaleLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_LOCALE, "Invalid type.");
errorChain(assetFileClose(&localeFile->file));
errorChain(assetFileDispose(&localeFile->file));
switch(loading->loading.locale.state) {
case ASSET_LOCALE_LOADER_STATE_INITIAL:
loading->loading.locale.state = ASSET_LOCALE_LOADER_STATE_LOAD_HEADER;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_LOCALE_LOADER_STATE_DONE:
break;
default:
errorOk();
}
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t assetLocaleDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_LOCALE, "Invalid type.");
assetlocalefile_t *localeFile = &entry->data.locale;
errorChain(assetFileClose(&localeFile->file));
return assetFileDispose(&localeFile->file);
}
errorret_t assetLocaleParseHeader(
assetlocalefile_t *localeFile,
char_t *headerBuffer,
@@ -307,16 +329,24 @@ errorret_t assetLocaleLineSkipBlanks(
while(!reader->eof) {
// Skip blank lines
if(lineBuffer[0] == '\0') {
errorChain(assetFileLineReaderNext(reader));
errorret_t r = assetFileLineReaderNext(reader);
if(errorIsNotOk(r)) {
errorCatch(r);
break;
}
continue;
}
// Skip comment lines
if(lineBuffer[0] == '#') {
errorChain(assetFileLineReaderNext(reader));
errorret_t r = assetFileLineReaderNext(reader);
if(errorIsNotOk(r)) {
errorCatch(r);
break;
}
continue;
}
// Is line only spaces?
size_t lineLength = strlen((char_t *)lineBuffer);
size_t i;
@@ -329,7 +359,11 @@ errorret_t assetLocaleLineSkipBlanks(
}
if(onlySpaces) {
errorChain(assetFileLineReaderNext(reader));
errorret_t r = assetFileLineReaderNext(reader);
if(errorIsNotOk(r)) {
errorCatch(r);
break;
}
continue;
}
break;
@@ -365,10 +399,18 @@ errorret_t assetLocaleLineUnbuffer(
// Now start buffering lines out
while(!reader->eof) {
errorChain(assetFileLineReaderNext(reader));
errorret_t r = assetFileLineReaderNext(reader);
if(errorIsNotOk(r)) {
errorCatch(r);
break;
}
// Skip blank lines
errorChain(assetLocaleLineSkipBlanks(reader, lineBuffer));
r = assetLocaleLineSkipBlanks(reader, lineBuffer);
if(errorIsNotOk(r)) {
errorCatch(r);
break;
}
// Skip starting spaces
char_t *ptr = (char_t *)lineBuffer;
@@ -593,7 +635,7 @@ errorret_t assetLocaleGetStringWithVA(
tempBuffer,
bufferSize
);
if(ret.code != ERROR_OK) {
if(errorIsNotOk(ret)) {
memoryFree(tempBuffer);
return ret;
}
@@ -641,7 +683,7 @@ errorret_t assetLocaleGetStringWithArgs(
format,
bufferSize
);
if(ret.code != ERROR_OK) {
if(errorIsNotOk(ret)) {
memoryFree(format);
return ret;
}
+181 -61
View File
@@ -1,15 +1,40 @@
/**
* Copyright (c) 2026 Dominic Masters
*
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/asset.h"
#include "asset/assetfile.h"
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
/** Input passed to the locale loader — currently unused. */
typedef struct { void *nothing; } assetlocaleloaderinput_t;
typedef enum {
ASSET_LOCALE_LOADER_STATE_INITIAL,
ASSET_LOCALE_LOADER_STATE_LOAD_HEADER,
ASSET_LOCALE_LOADER_STATE_DONE
} assetlocaleloaderstate_t;
/** Per-slot scratch data used while the locale file is loading. */
typedef struct {
assetlocaleloaderstate_t state;
} assetlocaleloaderloading_t;
/** Maximum number of distinct plural forms a locale file may declare. */
#define ASSET_LOCALE_FILE_PLURAL_FORM_COUNT 6
/**
* Comparison operator used in a plural-form expression.
*
* Each condition in the plural-form header is evaluated as
* `n <op> value`
* where `n` is the runtime plural count.
*/
typedef enum {
ASSET_LOCALE_PLURAL_OP_EQUAL,
ASSET_LOCALE_PLURAL_OP_NOT_EQUAL,
@@ -19,13 +44,24 @@ typedef enum {
ASSET_LOCALE_PLURAL_OP_GREATER_EQUAL
} assetlocalepluraloperation_t;
/**
* Discriminator tag for a locale string argument.
* @see assetlocalearg_t
*/
typedef enum {
ASSET_LOCALE_ARG_STRING,
ASSET_LOCALE_ARG_INT,
ASSET_LOCALE_ARG_FLOAT
} assetlocaleargtype_t;
/**
* A single typed argument for locale string substitution.
*
* Used with @ref assetLocaleGetStringWithArgs to fill `%s`, `%d`, or `%f`
* placeholders inside a translated string.
*/
typedef struct {
/** Which union member is active. */
assetlocaleargtype_t type;
union {
const char_t *stringValue;
@@ -34,35 +70,81 @@ typedef struct {
};
} assetlocalearg_t;
/**
* Runtime state for an open locale file.
*
* Loaded once by @ref assetLocaleLoaderSync and kept alive for the lifetime
* of the asset entry. The plural-form fields are populated from the PO header
* by @ref assetLocaleParseHeader.
*
* Plural evaluation works as a linear scan over `pluralStateCount - 1`
* conditions; if none match, `pluralDefaultIndex` is returned.
*/
typedef struct {
/** Underlying file handle used to rewind and re-read the PO data. */
assetfile_t file;
/** Comparison operator for each conditional plural clause. */
assetlocalepluraloperation_t pluralOps[ASSET_LOCALE_FILE_PLURAL_FORM_COUNT];
/** Right-hand value for each conditional plural clause. */
int32_t pluralValues[ASSET_LOCALE_FILE_PLURAL_FORM_COUNT];
/** Form index returned when the corresponding condition is true. */
int32_t pluralIndices[ASSET_LOCALE_FILE_PLURAL_FORM_COUNT];
/** Total number of plural forms declared by `nplurals=`. */
uint8_t pluralStateCount;
/** Form index used when no conditional clause matches. */
uint8_t pluralDefaultIndex;
} assetlocalefile_t;
/**
* Initialize a locale asset file.
*
* @param localeFile The locale file to initialize.
* @param path The path to the locale file.
* @return An error code if a failure occurs.
*/
errorret_t assetLocaleFileInit(
assetlocalefile_t *localeFile,
const char_t *path
);
/** Convenience alias — the loaded output type of a locale asset entry. */
typedef assetlocalefile_t assetlocaleoutput_t;
/**
* Dispose of a locale asset file.
*
* @param localeFile The locale file to dispose of.
* @return An error code if a failure occurs.
* Asynchronous loader callback. Opens the locale file, reads the PO header,
* and parses plural-form rules. All I/O happens here so the main thread is
* not blocked. Sets entry state to `ASSET_ENTRY_STATE_PENDING_SYNC` on
* success or `ASSET_ENTRY_STATE_ERROR` on failure.
*
* @param loading The loading slot for this asset entry.
* @return OK on success, error otherwise.
*/
errorret_t assetLocaleFileDispose(assetlocalefile_t *localeFile);
errorret_t assetLocaleLoaderAsync(assetloading_t *loading);
/**
* Synchronous loader callback. Confirms the async phase completed and marks
* the entry as `ASSET_ENTRY_STATE_LOADED`.
*
* @param loading The loading slot for this asset entry.
* @return OK on success, error otherwise.
*/
errorret_t assetLocaleLoaderSync(assetloading_t *loading);
/**
* Dispose callback. Closes the open file handle and zeros the locale data.
*
* @param entry The asset entry to dispose.
* @return OK on success, error otherwise.
*/
errorret_t assetLocaleDispose(assetentry_t *entry);
/**
* Parses the `Plural-Forms:` line from a PO header string and populates the
* plural-form fields of `localeFile`. If no `Plural-Forms:` key is present
* the function returns OK without modifying the struct.
*
* Supports the `nplurals=N; plural=(<expr>);` syntax where `<expr>` is a
* chain of `n <op> value ? index : ...` ternary conditions ending with a
* fallback index.
*
* @param localeFile Struct whose plural fields will be filled in.
* @param headerBuffer The raw PO header string (msgstr of msgid "").
* @param headerBufferSize Size of `headerBuffer` in bytes.
* @return OK on success, error if the plural expression is malformed.
*/
errorret_t assetLocaleParseHeader(
assetlocalefile_t *localeFile,
char_t *headerBuffer,
@@ -70,11 +152,29 @@ errorret_t assetLocaleParseHeader(
);
/**
* Skips blank lines and comment lines in the line reader.
*
* @param reader Line reader to read from.
* @param lineBuffer Buffer to use for reading lines.
* @return Any error that occurs during skipping.
* Evaluates which plural form index to use for a given count.
*
* Walks the conditions stored in `file` in order; returns the index for the
* first condition that matches `pluralCount`. Falls back to
* `file->pluralDefaultIndex` if none match.
*
* @param file Locale file with parsed plural rules.
* @param pluralCount The runtime count (e.g. number of items).
* @return Zero-based plural form index.
*/
uint8_t assetLocaleEvaluatePlural(
assetlocalefile_t *file,
const int32_t pluralCount
);
/**
* Advances the line reader past blank lines, comment lines (starting with
* `#`), and lines containing only spaces. Stops at the first content line or
* end of file.
*
* @param reader Line reader positioned at the current line.
* @param lineBuffer Output buffer that the reader writes each line into.
* @return OK on success, error if reading fails.
*/
errorret_t assetLocaleLineSkipBlanks(
assetfilelinereader_t *reader,
@@ -82,16 +182,19 @@ errorret_t assetLocaleLineSkipBlanks(
);
/**
* Unbuffers a potentially multi-line quoted string from the line reader.
*
* This will read lines until it finds a line that starts with a quote, then
* read until the closing quote.
*
* @param reader Line reader to read from.
* @param lineBuffer Buffer to use for reading lines.
* @param stringBuffer Buffer to write the unbuffered string to.
* @param stringBufferSize Size of the string buffer.
* @return Any error that occurs during unbuffering.
* Reads a PO quoted string value from the current line and any immediately
* following continuation lines that begin with `"`.
*
* Handles standard C escape sequences (`\n`, `\t`, `\\`, `\"`).
*
* @param reader Line reader positioned at the line containing the opening
* quote (e.g. `msgstr "..."`).
* @param lineBuffer Buffer the reader fills on each @ref assetFileLineReaderNext
* call; also used to detect continuation lines.
* @param stringBuffer Destination for the unescaped string content.
* @param stringBufferSize Capacity of `stringBuffer` in bytes.
* @return OK on success, error if a quote or escape sequence is malformed or
* the buffer would overflow.
*/
errorret_t assetLocaleLineUnbuffer(
assetfilelinereader_t *reader,
@@ -101,14 +204,19 @@ errorret_t assetLocaleLineUnbuffer(
);
/**
* Test function for locale asset loading.
*
* @param file Asset file to test loading from.
* @param messageId The message ID to retrieve.
* @param pluralCount Count for formulating the plural variant.
* @param stringBuffer Buffer to write the retrieved string to.
* @param stringBufferSize Size of the string buffer.
* @return Any error that occurs during testing.
* Looks up a translated string by message ID from the open locale file.
*
* Rewinds the file and scans from the beginning on every call. For plural
* entries (`msgid_plural`) the `pluralCount` is evaluated against the loaded
* plural rules to select the correct `msgstr[N]` form.
*
* @param file Locale file to search. Must be open.
* @param messageId PO message ID to find (`""` retrieves the header entry).
* @param pluralCount Count used to select the plural form (ignored for
* singular entries).
* @param stringBuffer Buffer to receive the translated string.
* @param stringBufferSize Capacity of `stringBuffer` in bytes.
* @return OK on success, error if the message ID is not found or I/O fails.
*/
errorret_t assetLocaleGetString(
assetlocalefile_t *file,
@@ -119,15 +227,21 @@ errorret_t assetLocaleGetString(
);
/**
* Test function for locale asset loading with a variable argument list.
*
* @param file Asset file to test loading from.
* @param messageId The message ID to retrieve.
* @param pluralCount Count for formulating the plural variant.
* @param buffer Buffer to write the retrieved string to.
* @param bufferSize Size of the buffer.
* @param ... Additional arguments for formatting the string.
* @return Any error that occurs during testing.
* Looks up a translated string and formats it with a `printf`-style variadic
* argument list.
*
* Retrieves the raw format string via @ref assetLocaleGetString then applies
* `vsnprintf` with the provided arguments.
*
* @param file Locale file to search. Must be open.
* @param messageId PO message ID to find.
* @param pluralCount Count used to select the plural form.
* @param buffer Buffer to receive the formatted string.
* @param bufferSize Capacity of `buffer` in bytes.
* @param ... Format arguments matching the specifiers in the translated
* string.
* @return OK on success, error if the message is not found or formatting
* fails.
*/
errorret_t assetLocaleGetStringWithVA(
assetlocalefile_t *file,
@@ -139,16 +253,22 @@ errorret_t assetLocaleGetStringWithVA(
);
/**
* Test function for locale asset loading with a list of arguments.
*
* @param file Asset file to test loading from.
* @param messageId The message ID to retrieve.
* @param pluralCount Count for formulating the plural variant.
* @param buffer Buffer to write the retrieved string to.
* @param bufferSize Size of the buffer.
* @param args List of arguments for formatting the string.
* @param argCount Number of arguments in the list.
* @return Any error that occurs during testing.
* Looks up a translated string and substitutes typed arguments into its
* `%s`, `%d`, and `%f` placeholders.
*
* Unlike @ref assetLocaleGetStringWithVA this variant accepts an explicit
* array of @ref assetlocalearg_t values, which is safer to use from
* generated or scripted call sites.
*
* @param file Locale file to search. Must be open.
* @param messageId PO message ID to find.
* @param pluralCount Count used to select the plural form.
* @param buffer Buffer to receive the formatted string.
* @param bufferSize Capacity of `buffer` in bytes.
* @param args Array of typed arguments; may be NULL when `argCount` is 0.
* @param argCount Number of elements in `args`.
* @return OK on success, error if the message is not found, an argument type
* mismatches the specifier, or the buffer would overflow.
*/
errorret_t assetLocaleGetStringWithArgs(
assetlocalefile_t *file,
@@ -158,4 +278,4 @@ errorret_t assetLocaleGetStringWithArgs(
const size_t bufferSize,
const assetlocalearg_t *args,
const size_t argCount
);
);
@@ -0,0 +1,9 @@
# 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
assetscriptloader.c
)
@@ -0,0 +1,116 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetscriptloader.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
#include "asset/loader/assetloader.h"
#include "util/memory.h"
#include "assert/assert.h"
#include <jerryscript.h>
errorret_t assetScriptLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
if(loading->loading.script.state != ASSET_SCRIPT_LOADING_STATE_READ_FILE) {
errorOk();
}
assertNull(loading->loading.script.buffer, "Buffer already defined?");
assetfile_t *file = &loading->loading.script.file;
assetLoaderErrorChain(loading, assetFileInit(file, loading->entry->name, NULL, NULL));
assetLoaderErrorChain(loading, assetFileOpen(file));
size_t capacity = ASSET_SCRIPT_CHUNK_SIZE;
uint8_t *buffer = memoryAllocate(capacity + 1);
size_t offset = 0;
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));
loading->loading.script.buffer = buffer;
loading->loading.script.size = offset;
loading->loading.script.state = ASSET_SCRIPT_LOADING_STATE_EXEC;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
errorret_t assetScriptLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_SCRIPT, "Invalid type.");
switch(loading->loading.script.state) {
case ASSET_SCRIPT_LOADING_STATE_INITIAL:
loading->loading.script.state = ASSET_SCRIPT_LOADING_STATE_READ_FILE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_SCRIPT_LOADING_STATE_EXEC:
break;
default:
errorOk();
}
uint8_t *buffer = loading->loading.script.buffer;
assertNotNull(buffer, "Script buffer should have been loaded by now.");
jerry_value_t result = jerry_eval(
(const jerry_char_t *)buffer,
loading->loading.script.size,
JERRY_PARSE_NO_OPTS
);
memoryFree(buffer);
loading->loading.script.buffer = NULL;
if(jerry_value_is_exception(result)) {
jerry_value_t errVal = jerry_exception_value(result, false);
jerry_value_t errStr = jerry_value_to_string(errVal);
char_t buf[256];
jerry_size_t len = jerry_string_to_buffer(
errStr, JERRY_ENCODING_UTF8, (jerry_char_t *)buf, sizeof(buf) - 1
);
buf[len] = '\0';
jerry_value_free(errStr);
jerry_value_free(errVal);
jerry_value_free(result);
assetLoaderErrorThrow(loading, "Script error in '%s': %s",
loading->entry->name, buf
);
}
loading->entry->data.script = (assetscriptoutput_t)result;
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t assetScriptDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_SCRIPT, "Invalid type.");
if(entry->data.script != 0) {
jerry_value_free((jerry_value_t)entry->data.script);
entry->data.script = 0;
}
errorOk();
}
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/assetfile.h"
#define ASSET_SCRIPT_CHUNK_SIZE 1024
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef struct { void *nothing; } assetscriptloaderinput_t;
typedef uint32_t assetscriptoutput_t;
typedef enum {
ASSET_SCRIPT_LOADING_STATE_INITIAL,
ASSET_SCRIPT_LOADING_STATE_READ_FILE,
ASSET_SCRIPT_LOADING_STATE_EXEC,
ASSET_SCRIPT_LOADING_STATE_DONE
} assetscriptloadingstate_t;
typedef struct {
assetfile_t file;
assetscriptloadingstate_t state;
uint8_t *buffer;
size_t size;
} assetscriptloaderloading_t;
errorret_t assetScriptLoaderAsync(assetloading_t *loading);
errorret_t assetScriptLoaderSync(assetloading_t *loading);
errorret_t assetScriptDispose(assetentry_t *entry);
+17 -9
View File
@@ -10,7 +10,6 @@
#include "util/memory.h"
#include "display/spritebatch/spritebatch.h"
#include "asset/asset.h"
#include "asset/assetbatch.h"
#include "asset/loader/display/assettextureloader.h"
#include "asset/loader/display/assettilesetloader.h"
#include "display/shader/shaderunlit.h"
@@ -18,19 +17,26 @@
font_t FONT_DEFAULT;
errorret_t textInit(void) {
assetbatch_t batch;
assetBatchInit(&batch, NULL, NULL, NULL);
assetBatchTexture(&batch, "ui/minogram.png", TEXTURE_FORMAT_RGBA, &FONT_DEFAULT.texture);
assetBatchTileset(&batch, "ui/minogram.dtf", &FONT_DEFAULT.tileset);
errorChain(assetBatchLoad(&batch));
assetloaderinput_t input = { .texture = TEXTURE_FORMAT_RGBA };
assetentry_t *entryTexture = assetLock(
"ui/minogram.png", ASSET_LOADER_TYPE_TEXTURE, &input
);
assetentry_t *entryTileset = assetLock(
"ui/minogram.dtf", ASSET_LOADER_TYPE_TILESET, NULL
);
errorChain(assetRequireLoaded(entryTexture));
errorChain(assetRequireLoaded(entryTileset));
FONT_DEFAULT.texture = &entryTexture->data.texture;
FONT_DEFAULT.tileset = &entryTileset->data.tileset;
errorOk();
}
errorret_t textDispose(void) {
assetCacheRelease(&ASSET.cache, "ui/minogram.png");
assetCacheRelease(&ASSET.cache, "ui/minogram.dtf");
FONT_DEFAULT.texture = NULL;
FONT_DEFAULT.tileset = NULL;
assetUnlock("ui/minogram.png");
assetUnlock("ui/minogram.dtf");
errorOk();
}
@@ -87,7 +93,9 @@ errorret_t textDraw(
float_t posX = x;
float_t posY = y;
errorChain(shaderSetTexture(&SHADER_UNLIT, SHADER_UNLIT_TEXTURE, font->texture));
errorChain(shaderSetTexture(
&SHADER_UNLIT, SHADER_UNLIT_TEXTURE, font->texture
));
#if MESH_ENABLE_COLOR
#else
+6 -2
View File
@@ -23,6 +23,7 @@
#include "network/network.h"
#include "system/system.h"
#include "console/console.h"
#include "script/script.h"
#include "item/backpack.h"
#include "save/save.h"
@@ -54,9 +55,10 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
physicsManagerInit();
errorChain(networkInit());
/* Run the init script. */
errorChain(scriptInit());
errorChain(scriptExecFile("init.js"));
consolePrint("Engine initialized");
sceneSet(SCENE_TYPE_OVERWORLD);
errorOk();
}
@@ -74,6 +76,7 @@ errorret_t engineUpdate(void) {
errorChain(displayUpdate());
errorChain(cutsceneUpdate());
errorChain(sceneUpdate());
errorChain(assetUpdate());
if(inputPressed(INPUT_ACTION_RAGEQUIT)) ENGINE.running = false;
errorOk();
@@ -95,6 +98,7 @@ errorret_t engineDispose(void) {
errorChain(displayDispose());
// errorChain(saveDispose());
errorChain(assetDispose());
errorChain(scriptDispose());
errorOk();
}
+6 -5
View File
@@ -11,7 +11,7 @@
#include "util/string.h"
#include "log/log.h"
errorstate_t ERROR_STATE = { 0 };
THREAD_LOCAL errorstate_t ERROR_STATE = { 0 };
errorret_t errorThrowImpl(
errorstate_t *state,
@@ -64,7 +64,7 @@ errorret_t errorOkImpl() {
ERROR_STATE.code == ERROR_OK,
"Global error state is not OK (Likely missing errorCatch)"
);
return (errorret_t) {
.code = ERROR_OK,
.state = NULL
@@ -77,7 +77,7 @@ errorret_t errorChainImpl(
const char_t *function,
const int32_t line
) {
if(retval.code == ERROR_OK) return retval;
if(errorIsOk(retval)) return retval;
assertNotNull(retval.state, "Error state NULL (Likely missing errorOk)");
assertNotNull(retval.state->message, "Message cannot be NULL");
@@ -111,18 +111,19 @@ errorret_t errorChainImpl(
}
void errorCatch(errorret_t retval) {
if(retval.code == ERROR_OK) return;
if(errorIsOk(retval)) return;
assertNotNull(retval.state, "Error state cannot be NULL");
assertNotNull(retval.state->message, "Message cannot be NULL");
// Clear the error state
memoryFree((void*)retval.state->message);
memoryFree((void*)retval.state->lines);
retval.state->code = ERROR_OK;
}
errorret_t errorPrint(const errorret_t retval) {
if(retval.code == ERROR_OK) return retval;
if(errorIsOk(retval)) return retval;
assertNotNull(retval.state, "Error state cannot be NULL");
assertNotNull(retval.state->message, "Message cannot be NULL");
+19 -31
View File
@@ -7,6 +7,7 @@
#pragma once
#include "dusk.h"
#include "thread/threadlocal.h"
typedef uint8_t errorcode_t;
@@ -26,7 +27,7 @@ static const errorcode_t ERROR_NOT_OK = 1;
static const char_t *ERROR_PRINT_FORMAT = "Error (%d): %s\n%s";
static const char_t *ERROR_LINE_FORMAT = " at %s:%d in function %s\n";
extern errorstate_t ERROR_STATE;
extern THREAD_LOCAL errorstate_t ERROR_STATE;
/**
* Sets the error state with the provided code and message.
@@ -52,7 +53,7 @@ errorret_t errorThrowImpl(
/**
* Returns an error state with no error.
*
*
* @return An error state with code ERROR_OK.
*/
errorret_t errorOkImpl();
@@ -88,34 +89,6 @@ void errorCatch(errorret_t retval);
*/
errorret_t errorPrint(const errorret_t retval);
/**
* Creates an error with a specific error state.
*
* @param state The error state to set.
* @param message The format string for the error message.
* @param ... Additional arguments for the format string.
* @return The error code.
*/
#define errorCreate(state, message, ... ) \
errorThrowImpl(\
(state), ERROR_NOT_OK, __FILE__, __func__, __LINE__, (message), \
##__VA_ARGS__ \
)
/**
* Throws an error with a specific error state.
*
* @param state The error state to set.
* @param message The format string for the error message.
* @param ... Additional arguments for the format string.
* @return The error code.
*/
#define errorThrowState(state, message, ... ) \
return errorThrowImpl(\
(state), ERROR_NOT_OK, __FILE__, __func__, __LINE__, (message), \
##__VA_ARGS__ \
)
/**
* Throws an error with a formatted message.
*
@@ -154,12 +127,27 @@ errorret_t errorPrint(const errorret_t retval);
return errorChainImpl(errorChainRetval, __FILE__, __func__, __LINE__); \
} \
}
/**
* Returns without an error.
*
* @return An error state with code ERROR_OK.
*/
#define errorOk() \
return errorOkImpl()
/**
* Returns non-zero if retval indicates success.
*
* @param retval errorret_t to test.
*/
#define errorIsOk(retval) ((retval).code == ERROR_OK)
/**
* Returns non-zero if retval indicates failure.
*
* @param retval errorret_t to test.
*/
#define errorIsNotOk(retval) ((retval).code != ERROR_OK)
// EOF
+31 -19
View File
@@ -9,43 +9,53 @@
#include "assert/assert.h"
#include "util/memory.h"
void eventInit(event_t *event) {
void eventInit(event_t *event, eventcallback_t *callbacks, void **users, size_t size) {
assertNotNull(event, "event must not be NULL");
memoryZero(event, sizeof(event_t));
assertNotNull((void *)callbacks, "callbacks must not be NULL");
assertTrue(size > 0, "size must be greater than 0");
event->callbacks = callbacks;
event->users = users;
event->size = size;
event->count = 0;
memoryZero(callbacks, sizeof(eventcallback_t) * size);
if(users) memoryZero(users, sizeof(void *) * size);
}
void eventSubscribe(event_t *event, eventcallback_t callback, void *user) {
assertNotNull(event, "event must not be NULL");
assertNotNull((void *)callback, "callback must not be NULL");
assertNotNull(callback, "callback must not be NULL");
for(uint8_t i = 0; i < event->count; i++) {
if(event->callbacks[i] == callback && event->users[i] == user) return;
// Ensure callback isn't already susbcribed
for(uint32_t i = 0; i < event->count; i++) {
if(event->callbacks[i] != callback) continue;
assertUnreachable("Callback already registered, cannot subscribe twice.");
}
assertTrue(
event->count < EVENT_SUBSCRIBER_COUNT_MAX,
"EVENT_SUBSCRIBER_COUNT_MAX exceeded"
);
assertTrue(event->count < event->size, "event subscriber capacity exceeded");
event->callbacks[event->count] = callback;
event->users[event->count] = user;
if(user) {
assertNotNull(event->users, "Cannot add user pointer.");
event->users[event->count] = user;
}
event->count++;
}
void eventUnsubscribe(event_t *event, eventcallback_t callback, void *user) {
void eventUnsubscribe(event_t *event, eventcallback_t callback) {
assertNotNull(event, "event must not be NULL");
assertNotNull((void *)callback, "callback must not be NULL");
assertNotNull(callback, "callback must not be NULL");
for(uint8_t i = 0; i < event->count; i++) {
if(event->callbacks[i] != callback || event->users[i] != user) continue;
for(uint32_t i = 0; i < event->count; i++) {
if(event->callbacks[i] != callback) continue;
uint8_t last = event->count - 1;
uint32_t last = event->count - 1;
if(i != last) {
event->callbacks[i] = event->callbacks[last];
event->users[i] = event->users[last];
if(event->users) event->users[i] = event->users[last];
}
event->callbacks[last] = NULL;
event->users[last] = NULL;
if(event->users) event->users[last] = NULL;
event->count--;
return;
}
@@ -53,7 +63,9 @@ void eventUnsubscribe(event_t *event, eventcallback_t callback, void *user) {
void eventInvoke(const event_t *event, void *params) {
assertNotNull(event, "event must not be NULL");
for(uint8_t i = 0; i < event->count; i++) {
event->callbacks[i](params, event->users[i]);
for(uint32_t i = 0; i < event->count; i++) {
void *u = event->users ? event->users[i] : NULL;
event->callbacks[i](params, u);
}
}
+21 -13
View File
@@ -8,31 +8,40 @@
#pragma once
#include "dusk.h"
#define EVENT_SUBSCRIBER_COUNT_MAX 8
typedef void (*eventcallback_t)(void *params, void *user);
typedef struct {
eventcallback_t callbacks[EVENT_SUBSCRIBER_COUNT_MAX];
void *users[EVENT_SUBSCRIBER_COUNT_MAX];
uint8_t count;
eventcallback_t *callbacks;
void **users;
size_t size;
uint32_t count;
} event_t;
/**
* Initializes an event, clearing all subscribers.
* Initializes an event, binding it to the provided backing arrays and clearing
* all subscribers. May also be called to reset an event (re-clears subscribers
* without changing the backing arrays or size).
*
* @param event The event to initialize.
* @param callbacks Caller-owned array of at least `size` callback slots.
* @param users Array of user pointers, matching each callback, or NULL.
* @param size Capacity of both arrays, must match.
*/
void eventInit(event_t *event);
void eventInit(
event_t *event,
eventcallback_t *callbacks,
void **users,
size_t size
);
/**
* Subscribes a callback to an event. The callback is invoked with params and
* the provided user pointer each time the event fires. The same (callback,
* user) pair may only be subscribed once.
*
* @param event The event to subscribe to.
* @param event The event to subscribe to.
* @param callback The function to call when the event fires.
* @param user Arbitrary pointer forwarded to the callback unchanged.
* @param user Arbitrary pointer forwarded to the callback unchanged.
*/
void eventSubscribe(event_t *event, eventcallback_t callback, void *user);
@@ -40,17 +49,16 @@ void eventSubscribe(event_t *event, eventcallback_t callback, void *user);
* Removes a previously subscribed (callback, user) pair. Does nothing if the
* pair is not currently subscribed.
*
* @param event The event to unsubscribe from.
* @param event The event to unsubscribe from.
* @param callback The callback that was passed to eventSubscribe.
* @param user The user pointer that was passed to eventSubscribe.
*/
void eventUnsubscribe(event_t *event, eventcallback_t callback, void *user);
void eventUnsubscribe(event_t *event, eventcallback_t callback);
/**
* Invokes all subscribed callbacks, passing params and each subscriber's user
* pointer.
*
* @param event The event to invoke.
* @param event The event to invoke.
* @param params Arbitrary pointer forwarded to every callback unchanged.
*/
void eventInvoke(const event_t *event, void *params);
+2 -2
View File
@@ -22,8 +22,8 @@ errorret_t inputInit(void) {
INPUT.actions[i].action = (inputaction_t)i;
INPUT.actions[i].lastValue = 0.0f;
INPUT.actions[i].currentValue = 0.0f;
eventInit(&INPUT.actions[i].onPressed);
eventInit(&INPUT.actions[i].onReleased);
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);
}
#ifdef inputInitPlatform
+4
View File
@@ -20,7 +20,11 @@ typedef struct {
float_t currentDynamicValue;
#endif
eventcallback_t onPressedCallbacks[4];
void *onPressedUsers[4];
event_t onPressed;
eventcallback_t onReleasedCallbacks[4];
void *onReleasedUsers[4];
event_t onReleased;
} inputactiondata_t;
+14 -11
View File
@@ -13,28 +13,31 @@ localemanager_t LOCALE;
errorret_t localeManagerInit() {
memoryZero(&LOCALE, sizeof(localemanager_t));
errorChain(localeManagerSetLocale(&LOCALE_EN_US));
errorOk();
}
errorret_t localeManagerSetLocale(const localeinfo_t *locale) {
if(LOCALE.fileOpen) {
errorChain(assetLocaleFileDispose(&LOCALE.file));
LOCALE.fileOpen = false;
assertNotNull(locale, "Locale cannot be NULL");
if(LOCALE.entry != NULL) {
assetEntryUnlock(LOCALE.entry);
LOCALE.entry = NULL;
}
// Init the asset file
errorChain(assetLocaleFileInit(&LOCALE.file, locale->file));
LOCALE.fileOpen = true;
LOCALE.locale = locale;
LOCALE.entry = assetGetEntry(locale->file, ASSET_LOADER_TYPE_LOCALE, NULL);
assetEntryLock(LOCALE.entry);
errorChain(assetRequireLoaded(LOCALE.entry));
errorOk();
}
void localeManagerDispose() {
if(LOCALE.fileOpen) {
errorCatch(errorPrint(assetLocaleFileDispose(&LOCALE.file)));
LOCALE.fileOpen = false;
if(LOCALE.entry != NULL) {
assetEntryUnlock(LOCALE.entry);
LOCALE.entry = NULL;
}
LOCALE.locale = NULL;
}
+4 -5
View File
@@ -9,12 +9,11 @@
#include "error/error.h"
#include "localemanager.h"
#include "locale/localeinfo.h"
#include "asset/loader/locale/assetlocaleloader.h"
#include "asset/asset.h"
typedef struct {
const localeinfo_t *locale;
assetlocalefile_t file;
bool_t fileOpen;
assetentry_t *entry;
} localemanager_t;
extern localemanager_t LOCALE;
@@ -46,7 +45,7 @@ errorret_t localeManagerSetLocale(const localeinfo_t *locale);
*/
#define localeManagerGetText(id, buffer, bufferSize, plural, ...) \
assetLocaleGetStringWithVA( \
&LOCALE.file, \
&LOCALE.entry->data.locale, \
id, \
plural, \
buffer, \
@@ -69,7 +68,7 @@ errorret_t localeManagerSetLocale(const localeinfo_t *locale);
id, buffer, bufferSize, plural, args, argCount \
) \
assetLocaleGetStringWithArgs( \
&LOCALE.file, \
&LOCALE.entry->data.locale, \
id, \
plural, \
buffer, \
+3 -3
View File
@@ -15,7 +15,7 @@ int main(int argc, char **argv) {
// Init engine
ret = engineInit(argc, (const char_t **)argv);
if(ret.code != ERROR_OK) {
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
return ret.code;
}
@@ -23,14 +23,14 @@ int main(int argc, char **argv) {
// Begin main loop
do {
ret = engineUpdate();
if(ret.code != ERROR_OK) {
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
return ret.code;
}
} while(ENGINE.running);
ret = engineDispose();
if(ret.code != ERROR_OK) {
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
return ret.code;
}
+3 -3
View File
@@ -50,7 +50,7 @@ errorret_t saveLoad(const uint8_t slot) {
saveStreamClosePlatform(&stream);
#endif
if(ret.code != ERROR_OK) return ret;
if(errorIsNotOk(ret)) return ret;
errorChain(saveStreamVerifyChecksumImpl(&stream, slot));
@@ -72,7 +72,7 @@ errorret_t saveWrite(const uint8_t slot) {
errorret_t ret = saveFileWrite(&stream, file);
if(ret.code == ERROR_OK) {
if(errorIsOk(ret)) {
ret = saveStreamFinalizeWriteImpl(&stream);
}
@@ -80,7 +80,7 @@ errorret_t saveWrite(const uint8_t slot) {
saveStreamClosePlatform(&stream);
#endif
if(ret.code != ERROR_OK) return ret;
if(errorIsNotOk(ret)) return ret;
file->exists = true;
errorOk();
+2 -1
View File
@@ -464,4 +464,5 @@ errorret_t saveFileWrite(savestream_t *stream, savefile_t *file);
#define saveFileReadDate(stream, out) \
errorChain(saveStreamReadDateImpl(stream, out))
#define saveFileWriteDate(stream, input) \
errorChain(saveStreamWriteDateImpl(stream, input))
errorChain(saveStreamWriteDateImpl(stream, input))
+14
View File
@@ -0,0 +1,14 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
script.c
scriptproto.c
)
# Subdirectories
add_subdirectory(module)
+9
View File
@@ -0,0 +1,9 @@
# 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
modulebase.c
)
@@ -0,0 +1,68 @@
/**
* 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 "console/console.h"
static 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();
}
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);
}
@@ -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 "script/scriptproto.h"
#include "display/screen/screen.h"
static 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);
}
static 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
);
}
static void moduleScreenDispose(void) {
scriptProtoDispose(&MODULE_SCREEN_PROTO);
}
@@ -0,0 +1,38 @@
/**
* 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 "engine/engine.h"
static scriptproto_t MODULE_ENGINE_PROTO;
moduleBaseFunction(moduleEngineGetRunning) {
return jerry_boolean(ENGINE.running);
}
moduleBaseFunction(moduleEngineExit) {
ENGINE.running = false;
return jerry_undefined();
}
static 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
);
}
static void moduleEngineDispose(void) {
scriptProtoDispose(&MODULE_ENGINE_PROTO);
}
@@ -0,0 +1,16 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
/* Include component modules here as they are added. */
static void moduleComponentListInit(void) {
}
static void moduleComponentListDispose(void) {
}
@@ -0,0 +1,93 @@
/**
* 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 "entity/component.h"
/** C struct wrapped by every Component JS instance. */
typedef struct {
entityid_t entityId;
componentid_t componentId;
} jscomponent_t;
static scriptproto_t MODULE_COMPONENT_PROTO;
moduleBaseFunction(moduleComponentCtor) {
(void)callInfo; (void)args; (void)argc;
return moduleBaseThrow("Component cannot be instantiated with new");
}
moduleBaseFunction(moduleComponentGetEntity) {
jscomponent_t *comp = scriptProtoGetValue(
&MODULE_COMPONENT_PROTO, callInfo->this_value
);
if(!comp) return jerry_undefined();
return jerry_number((double)comp->entityId);
}
moduleBaseFunction(moduleComponentGetId) {
jscomponent_t *comp = scriptProtoGetValue(
&MODULE_COMPONENT_PROTO, callInfo->this_value
);
if(!comp) return jerry_undefined();
return jerry_number((double)comp->componentId);
}
moduleBaseFunction(moduleComponentToString) {
jscomponent_t *comp = scriptProtoGetValue(
&MODULE_COMPONENT_PROTO, callInfo->this_value
);
if(!comp) return jerry_string_sz("Component:invalid");
jerry_value_t num = jerry_number((double)comp->componentId);
jerry_value_t str = jerry_value_to_string(num);
jerry_value_free(num);
return str;
}
static void moduleComponentInit(void) {
scriptProtoInit(
&MODULE_COMPONENT_PROTO, "Component",
sizeof(jscomponent_t), moduleComponentCtor
);
/* Instance properties */
scriptProtoDefineProp(
&MODULE_COMPONENT_PROTO, "entity",
moduleComponentGetEntity, NULL
);
scriptProtoDefineProp(
&MODULE_COMPONENT_PROTO, "id",
moduleComponentGetId, NULL
);
scriptProtoDefineToString(&MODULE_COMPONENT_PROTO, moduleComponentToString);
/* Component.POSITION, Component.CAMERA, etc. from componentlist.h */
jerry_value_t ctor = MODULE_COMPONENT_PROTO.constructor;
#define X(enumName, type, field, init, dispose, render) \
do { \
jerry_value_t _key = jerry_string_sz(#enumName); \
jerry_value_t _val = jerry_number((double)COMPONENT_TYPE_##enumName); \
jerry_object_set(ctor, _key, _val); \
jerry_value_free(_val); \
jerry_value_free(_key); \
} while(0);
#include "entity/componentlist.h"
#undef X
/* Component.INVALID */
jerry_value_t _key = jerry_string_sz("INVALID");
jerry_value_t _val = jerry_number((double)COMPONENT_ID_INVALID);
jerry_object_set(ctor, _key, _val);
jerry_value_free(_val);
jerry_value_free(_key);
}
static void moduleComponentDispose(void) {
scriptProtoDispose(&MODULE_COMPONENT_PROTO);
}
@@ -0,0 +1,105 @@
/**
* 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/entitymanager.h"
/** C struct wrapped by every Entity JS instance. */
typedef struct {
entityid_t id;
} jsentity_t;
static scriptproto_t MODULE_ENTITY_PROTO;
moduleBaseFunction(moduleEntityCtor) {
(void)callInfo; (void)args; (void)argc;
return moduleBaseThrow("Entity cannot be instantiated with new");
}
moduleBaseFunction(moduleEntityCreate) {
entityid_t id = entityManagerAdd();
if(id == ENTITY_ID_INVALID) {
return moduleBaseThrow("Entity.create: no entity slots available");
}
jsentity_t ent = { .id = id };
return scriptProtoCreateValue(&MODULE_ENTITY_PROTO, &ent);
}
moduleBaseFunction(moduleEntityDisposeEntity) {
moduleBaseRequireArgs(1);
jsentity_t *ent = scriptProtoGetValue(&MODULE_ENTITY_PROTO, args[0]);
if(!ent) return moduleBaseThrow("Entity.dispose: expected Entity object");
entityDispose(ent->id);
return jerry_undefined();
}
moduleBaseFunction(moduleEntityAdd) {
jsentity_t *ent = scriptProtoGetValue(
&MODULE_ENTITY_PROTO, callInfo->this_value
);
if(!ent) return moduleBaseThrow("Entity.add: invalid this");
moduleBaseRequireArgs(1);
moduleBaseRequireNumber(0);
const componenttype_t type = (componenttype_t)moduleBaseArgInt(0);
if(type <= COMPONENT_TYPE_NULL || type >= COMPONENT_TYPE_COUNT) {
return moduleBaseThrow("Entity.add: invalid component type");
}
componentid_t cid = entityAddComponent(ent->id, type);
if(cid == COMPONENT_ID_INVALID) {
return moduleBaseThrow("Entity.add: failed to add component");
}
jscomponent_t comp = { .entityId = ent->id, .componentId = cid };
return scriptProtoCreateValue(&MODULE_COMPONENT_PROTO, &comp);
}
moduleBaseFunction(moduleEntityToString) {
jsentity_t *ent = scriptProtoGetValue(
&MODULE_ENTITY_PROTO, callInfo->this_value
);
if(!ent) return jerry_string_sz("Entity:invalid");
jerry_value_t num = jerry_number((double)ent->id);
jerry_value_t str = jerry_value_to_string(num);
jerry_value_free(num);
return str;
}
static void moduleEntityInit(void) {
scriptProtoInit(
&MODULE_ENTITY_PROTO, "Entity",
sizeof(jsentity_t), moduleEntityCtor
);
/* Static methods */
scriptProtoDefineStaticFunc(
&MODULE_ENTITY_PROTO, "create", moduleEntityCreate
);
scriptProtoDefineStaticFunc(
&MODULE_ENTITY_PROTO, "dispose", moduleEntityDisposeEntity
);
/* Entity.INVALID */
jerry_value_t ctor = MODULE_ENTITY_PROTO.constructor;
jerry_value_t _key = jerry_string_sz("INVALID");
jerry_value_t _val = jerry_number((double)ENTITY_ID_INVALID);
jerry_object_set(ctor, _key, _val);
jerry_value_free(_val);
jerry_value_free(_key);
/* Instance methods */
scriptProtoDefineFunc(&MODULE_ENTITY_PROTO, "add", moduleEntityAdd);
scriptProtoDefineToString(&MODULE_ENTITY_PROTO, moduleEntityToString);
}
static void moduleEntityDispose(void) {
scriptProtoDispose(&MODULE_ENTITY_PROTO);
}
+138
View File
@@ -0,0 +1,138 @@
/**
* 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 "input/input.h"
#include "input/inputbutton.h"
static scriptproto_t MODULE_INPUT_PROTO;
/**
* Validates an inputaction_t argument and returns a type error if bad.
*
* @param i Argument index.
* @param ctx Context string for error messages.
*/
#define moduleInputRequireAction(i, ctx) do { \
if(!jerry_value_is_number(args[(i)])) { \
return moduleBaseThrow(ctx ": action must be a number"); \
} \
const inputaction_t _act = (inputaction_t)moduleBaseArgInt(i); \
if(_act <= INPUT_ACTION_NULL || _act >= INPUT_ACTION_COUNT) { \
return moduleBaseThrow(ctx ": invalid action"); \
} \
} while(0)
moduleBaseFunction(moduleInputIsDown) {
moduleInputRequireAction(0, "Input.isDown");
return jerry_boolean(
inputIsDown((inputaction_t)moduleBaseArgInt(0))
);
}
moduleBaseFunction(moduleInputWasDown) {
moduleInputRequireAction(0, "Input.wasDown");
return jerry_boolean(
inputWasDown((inputaction_t)moduleBaseArgInt(0))
);
}
moduleBaseFunction(moduleInputPressed) {
moduleInputRequireAction(0, "Input.pressed");
return jerry_boolean(
inputPressed((inputaction_t)moduleBaseArgInt(0))
);
}
moduleBaseFunction(moduleInputReleased) {
moduleInputRequireAction(0, "Input.released");
return jerry_boolean(
inputReleased((inputaction_t)moduleBaseArgInt(0))
);
}
moduleBaseFunction(moduleInputGetValue) {
moduleInputRequireAction(0, "Input.getValue");
return jerry_number(
inputGetCurrentValue((inputaction_t)moduleBaseArgInt(0))
);
}
moduleBaseFunction(moduleInputAxis) {
moduleInputRequireAction(0, "Input.axis");
moduleInputRequireAction(1, "Input.axis");
return jerry_number(inputAxis(
(inputaction_t)moduleBaseArgInt(0),
(inputaction_t)moduleBaseArgInt(1)
));
}
moduleBaseFunction(moduleInputBind) {
moduleBaseRequireArgs(2);
moduleBaseRequireString(0);
if(!jerry_value_is_number(args[1])) {
return moduleBaseThrow("Input.bind: action must be a number");
}
const inputaction_t action = (inputaction_t)moduleBaseArgInt(1);
if(action <= INPUT_ACTION_NULL || action >= INPUT_ACTION_COUNT) {
return moduleBaseThrow("Input.bind: invalid action");
}
char_t name[128];
moduleBaseToString(args[0], name, sizeof(name));
if(name[0] == '\0') {
return moduleBaseThrow("Input.bind: button name cannot be empty");
}
inputbutton_t btn = inputButtonGetByName(name);
if(btn.type == INPUT_BUTTON_TYPE_NONE) {
return moduleBaseThrow("Input.bind: unknown button name");
}
inputBind(btn, action);
return jerry_undefined();
}
static void moduleInputInit(void) {
scriptProtoInit(&MODULE_INPUT_PROTO, "Input", sizeof(uint8_t), NULL);
scriptProtoDefineStaticFunc(
&MODULE_INPUT_PROTO, "isDown", moduleInputIsDown
);
scriptProtoDefineStaticFunc(
&MODULE_INPUT_PROTO, "wasDown", moduleInputWasDown
);
scriptProtoDefineStaticFunc(
&MODULE_INPUT_PROTO, "pressed", moduleInputPressed
);
scriptProtoDefineStaticFunc(
&MODULE_INPUT_PROTO, "released", moduleInputReleased
);
scriptProtoDefineStaticFunc(
&MODULE_INPUT_PROTO, "getValue", moduleInputGetValue
);
scriptProtoDefineStaticFunc(
&MODULE_INPUT_PROTO, "axis", moduleInputAxis
);
scriptProtoDefineStaticFunc(
&MODULE_INPUT_PROTO, "bind", moduleInputBind
);
/* Register INPUT_ACTION_* integer constants into the global scope. */
jerry_value_t result = jerry_eval(
(const jerry_char_t *)INPUT_ACTION_SCRIPT,
strlen(INPUT_ACTION_SCRIPT),
JERRY_PARSE_NO_OPTS
);
jerry_value_free(result);
}
static void moduleInputDispose(void) {
scriptProtoDispose(&MODULE_INPUT_PROTO);
}
+127
View File
@@ -0,0 +1,127 @@
/**
* 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 "cglm/cglm.h"
static scriptproto_t MODULE_VEC3_PROTO;
/**
* Returns the native float[3] pointer from the Vec3 instance that owns
* the current call (this_value). Returns NULL if not a Vec3.
*/
static inline float_t *moduleVec3Get(const jerry_call_info_t *callInfo) {
return (float_t *)scriptProtoGetValue(
&MODULE_VEC3_PROTO, callInfo->this_value
);
}
/**
* Returns the native float[3] pointer from any jerry value.
* Returns NULL if the value is not a Vec3 instance.
*/
static inline float_t *moduleVec3From(const jerry_value_t val) {
return (float_t *)scriptProtoGetValue(&MODULE_VEC3_PROTO, val);
}
/**
* Creates a Vec3 JS object from a C vec3 array.
*
* @param v Source vec3 to copy.
* @return A new Vec3 JS instance owning a copy of the data.
*/
static inline jerry_value_t moduleVec3Push(const vec3 v) {
return scriptProtoCreateValue(&MODULE_VEC3_PROTO, v);
}
moduleBaseFunction(moduleVec3Constructor) {
float_t *ptr = (float_t *)memoryAllocate(sizeof(vec3));
ptr[0] = moduleBaseOptFloat(0, 0.0f);
ptr[1] = moduleBaseOptFloat(1, 0.0f);
ptr[2] = moduleBaseOptFloat(2, 0.0f);
jerry_object_set_native_ptr(
callInfo->this_value, &MODULE_VEC3_PROTO.info, ptr
);
return jerry_undefined();
}
moduleBaseFunction(moduleVec3GetX) {
float_t *v = moduleVec3Get(callInfo);
if(!v) return jerry_undefined();
return jerry_number((double)v[0]);
}
moduleBaseFunction(moduleVec3SetX) {
moduleBaseRequireArgs(1);
float_t *v = moduleVec3Get(callInfo);
if(!v) return jerry_undefined();
v[0] = moduleBaseArgFloat(0);
return jerry_undefined();
}
moduleBaseFunction(moduleVec3GetY) {
float_t *v = moduleVec3Get(callInfo);
if(!v) return jerry_undefined();
return jerry_number((double)v[1]);
}
moduleBaseFunction(moduleVec3SetY) {
moduleBaseRequireArgs(1);
float_t *v = moduleVec3Get(callInfo);
if(!v) return jerry_undefined();
v[1] = moduleBaseArgFloat(0);
return jerry_undefined();
}
moduleBaseFunction(moduleVec3GetZ) {
float_t *v = moduleVec3Get(callInfo);
if(!v) return jerry_undefined();
return jerry_number((double)v[2]);
}
moduleBaseFunction(moduleVec3SetZ) {
moduleBaseRequireArgs(1);
float_t *v = moduleVec3Get(callInfo);
if(!v) return jerry_undefined();
v[2] = moduleBaseArgFloat(0);
return jerry_undefined();
}
moduleBaseFunction(moduleVec3ToString) {
float_t *v = moduleVec3Get(callInfo);
if(!v) return jerry_string_sz("Vec3:invalid");
char_t buf[64];
snprintf(buf, sizeof(buf), "Vec3(%g, %g, %g)",
(double)v[0], (double)v[1], (double)v[2]
);
return jerry_string_sz(buf);
}
static void moduleVec3Init(void) {
scriptProtoInit(
&MODULE_VEC3_PROTO, "Vec3",
sizeof(vec3), moduleVec3Constructor
);
scriptProtoDefineProp(
&MODULE_VEC3_PROTO, "x", moduleVec3GetX, moduleVec3SetX
);
scriptProtoDefineProp(
&MODULE_VEC3_PROTO, "y", moduleVec3GetY, moduleVec3SetY
);
scriptProtoDefineProp(
&MODULE_VEC3_PROTO, "z", moduleVec3GetZ, moduleVec3SetZ
);
scriptProtoDefineToString(&MODULE_VEC3_PROTO, moduleVec3ToString);
}
static void moduleVec3Dispose(void) {
scriptProtoDispose(&MODULE_VEC3_PROTO);
}
+127
View File
@@ -0,0 +1,127 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "modulebase.h"
#include "util/memory.h"
jerry_object_native_info_t MODULE_BASE_NATIVE_INFO = {
.free_cb = moduleBaseFreeProto,
.number_of_references = 0,
.offset_of_references = 0
};
void moduleBaseFreeProto(void *ptr, jerry_object_native_info_t *info) {
assertNotNull(ptr, "Pointer must not be null");
assertNotNull(info, "Native info must not be null");
memoryFree(ptr);
}
jerry_value_t moduleBaseThrow(const char_t *message) {
assertStrLenMin(message, 1, "Error message must not be empty");
return jerry_throw_sz(JERRY_ERROR_TYPE, message);
}
jerry_value_t moduleBaseThrowError(const errorret_t err) {
assertNotNull(err.state, "Error state must not be NULL");
assertNotNull(err.state->message, "Error message must not be NULL");
jerry_value_t jsErr = jerry_throw_sz(JERRY_ERROR_TYPE, err.state->message);
errorCatch(err);
return jsErr;
}
void moduleBaseToString(jerry_value_t val, char_t *buf, jerry_size_t buflen) {
jerry_size_t len = jerry_string_to_buffer(
val, JERRY_ENCODING_UTF8, (jerry_char_t *)buf, buflen - 1
);
buf[len] = '\0';
}
void moduleBaseExceptionMessage(
jerry_value_t exception,
char_t *buf,
size_t buflen
) {
jerry_value_t errVal = jerry_exception_value(exception, false);
jerry_value_t errStr = jerry_value_to_string(errVal);
jerry_size_t len = jerry_string_to_buffer(
errStr, JERRY_ENCODING_UTF8, (jerry_char_t *)buf, (jerry_size_t)(buflen - 1)
);
buf[len] = '\0';
jerry_value_free(errStr);
jerry_value_free(errVal);
}
void moduleBaseDefineMethod(
jerry_value_t obj,
const char_t *name,
jerry_external_handler_t fn
) {
jerry_value_t key = jerry_string_sz(name);
jerry_value_t func = jerry_function_external(fn);
jerry_object_set(obj, key, func);
jerry_value_free(func);
jerry_value_free(key);
}
void moduleBaseDefineGlobalMethod(
const char_t *name,
jerry_external_handler_t fn
) {
jerry_value_t global = jerry_current_realm();
moduleBaseDefineMethod(global, name, fn);
jerry_value_free(global);
}
void moduleBaseSetValue(const char_t *name, jerry_value_t value) {
jerry_value_t global = jerry_current_realm();
jerry_value_t key = jerry_string_sz(name);
jerry_object_set(global, key, value);
jerry_value_free(key);
jerry_value_free(global);
}
jerry_value_t moduleBaseWrapPointer(void *ptr) {
jerry_value_t obj = jerry_object();
jerry_object_set_native_ptr(obj, &MODULE_BASE_NATIVE_INFO, ptr);
return obj;
}
void *moduleBaseUnwrapPointer(jerry_value_t val) {
if(!jerry_value_is_object(val)) return NULL;
return jerry_object_get_native_ptr(val, &MODULE_BASE_NATIVE_INFO);
}
jerry_value_t moduleBaseGetProp(
jerry_value_t obj, const char_t *name
) {
jerry_value_t key = jerry_string_sz(name);
jerry_value_t val = jerry_object_get(obj, key);
jerry_value_free(key);
return val;
}
float_t moduleBaseValueFloat(jerry_value_t val) {
return (float_t)jerry_value_as_number(val);
}
int32_t moduleBaseValueInt(jerry_value_t val) {
return (int32_t)jerry_value_as_number(val);
}
void moduleBaseSetNumber(const char_t *name, double value) {
jerry_value_t global = jerry_current_realm();
jerry_value_t key = jerry_string_sz(name);
jerry_value_t val = jerry_number(value);
jerry_object_set(global, key, val);
jerry_value_free(val);
jerry_value_free(key);
jerry_value_free(global);
}
void moduleBaseSetInt(const char_t *name, int32_t value) {
moduleBaseSetNumber(name, (double)value);
}
+190
View File
@@ -0,0 +1,190 @@
/**
* 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 "assert/assert.h"
#include "util/string.h"
#include <jerryscript.h>
#include <stdlib.h>
extern jerry_object_native_info_t MODULE_BASE_NATIVE_INFO;
/**
* Frees a native pointer associated with a JavaScript object, uses the internal
* memory functions.
*
* @param ptr The native pointer to free. Must not be NULL.
* @param info The native info associated with the pointer. Must not be NULL.
*/
void moduleBaseFreeProto(void *ptr, jerry_object_native_info_t *info);
/**
* Throws a JavaScript error with the given message.
*
* @param message The error message to throw. Must not be empty.
* @return A jerry_value_t representing the thrown error.
*/
jerry_value_t moduleBaseThrow(const char_t *message);
/**
* "Converts" a dusk error to a jerry type error.
*
* @param err Native dusk error to convert.
* @return A jerry_value_t representing the thrown error.
*/
jerry_value_t moduleBaseThrowError(const errorret_t err);
/**
* Converts a jerry value to a string and writes it to the provided buffer.
*
* @param val The jerry value to convert.
* @param buf The buffer to write the string to.
* @param buflen The length of the buffer.
*/
void moduleBaseToString(jerry_value_t val, char_t *buf, jerry_size_t buflen);
/**
* Converts a jerry exception value to a string and writes it to the provided
* buffer.
*
* @param exception The jerry exception value to convert.
* @param buf The buffer to write the string to.
* @param buflen The length of the buffer.
*/
void moduleBaseExceptionMessage(
jerry_value_t exception,
char_t *buf,
size_t buflen
);
/**
* Defines a method on a given object.
*
* @param obj The object to define the method on.
* @param name The name of the method.
* @param fn The function pointer to the method implementation.
*/
void moduleBaseDefineMethod(
jerry_value_t obj,
const char_t *name,
jerry_external_handler_t fn
);
/**
* Defines a global method.
*
* @param name The name of the method.
* @param fn The function pointer to the method implementation.
*/
void moduleBaseDefineGlobalMethod(
const char_t *name,
jerry_external_handler_t fn
);
/**
* Sets a global variable to a given jerry value.
*
* @param name The name of the global variable.
* @param value The jerry value to set the variable to.
*/
void moduleBaseSetValue(const char_t *name, jerry_value_t value);
/**
* Wraps a native pointer in a jerry object.
*
* @param ptr The native pointer to wrap.
* @return A jerry_value_t representing the wrapped pointer.
*/
jerry_value_t moduleBaseWrapPointer(void *ptr);
/**
* Unwraps a native pointer from a jerry object.
*
* @param val The jerry value to unwrap the pointer from.
* @return The unwrapped native pointer.
*/
void *moduleBaseUnwrapPointer(jerry_value_t val);
/**
* Gets a property from a jerry object as a jerry value.
*
* @param obj The object to get the property from.
* @param name The name of the property to get.
* @return The value of the property.
*/
jerry_value_t moduleBaseGetProp(
jerry_value_t obj, const char_t *name
);
/**
* Converts a jerry value to a float.
*
* @param val The jerry value to convert.
* @return The converted float value.
*/
float_t moduleBaseValueFloat(jerry_value_t val);
/**
* Converts a jerry value to an int.
*
* @param val The jerry value to convert.
* @return The converted int value.
*/
int32_t moduleBaseValueInt(jerry_value_t val);
/**
* Sets a global variable to a given number value.
*
* @param name The name of the global variable.
* @param value The number value to set the variable to.
*/
void moduleBaseSetNumber(const char_t *name, double value);
/**
* Sets a global variable to a given int value.
*
* @param name The name of the global variable.
* @param value The int value to set the variable to.
*/
void moduleBaseSetInt(const char_t *name, int32_t value);
#define moduleBaseFunction(name) \
static jerry_value_t name( \
const jerry_call_info_t *callInfo, \
const jerry_value_t args[], \
const jerry_length_t argc)
#define moduleBaseRequireArgs(n) do { \
if(argc < (jerry_length_t)(n)) { \
return moduleBaseThrow("Expected at least " #n " argument(s)"); \
} \
} while(0)
#define moduleBaseRequireNumber(i) do { \
if(!jerry_value_is_number(args[(i)])) { \
return moduleBaseThrow("Expected number argument"); \
} \
} while(0)
#define moduleBaseRequireString(i) do { \
if(!jerry_value_is_string(args[(i)])) { \
return moduleBaseThrow("Expected string argument"); \
} \
} while(0)
#define moduleBaseArgFloat(i) ((float_t)jerry_value_as_number(args[(i)]))
#define moduleBaseArgInt(i) ((int32_t)jerry_value_as_number(args[(i)]))
#define moduleBaseArgBool(i) (jerry_value_is_true(args[(i)]))
#define moduleBaseOptFloat(i, def) \
((jerry_length_t)(i) < argc && jerry_value_is_number(args[(i)]) \
? (float_t)jerry_value_as_number(args[(i)]) : (def))
#define moduleBaseOptInt(i, def) \
((jerry_length_t)(i) < argc && jerry_value_is_number(args[(i)]) \
? (int32_t)jerry_value_as_number(args[(i)]) : (def))
+44
View File
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/module/console/moduleconsole.h"
#include "script/module/display/modulescreen.h"
#include "script/module/engine/moduleengine.h"
#include "script/module/entity/component/modulecomponentlist.h"
#include "script/module/entity/modulecomponent.h"
#include "script/module/entity/moduleentity.h"
#include "script/module/input/moduleinput.h"
#include "script/module/math/modulevec3.h"
#include "script/module/scene/modulescene.h"
#include "script/module/system/modulesystem.h"
static void moduleListInit(void) {
moduleConsoleInit();
moduleScreenInit();
moduleEngineInit();
moduleVec3Init();
moduleComponentInit();
moduleEntityInit();
moduleComponentListInit();
moduleInputInit();
moduleSceneInit();
moduleSystemInit();
}
static void moduleListDispose(void) {
moduleSystemDispose();
moduleSceneDispose();
moduleInputDispose();
moduleComponentListDispose();
moduleEntityDispose();
moduleComponentDispose();
moduleVec3Dispose();
moduleEngineDispose();
moduleScreenDispose();
moduleConsoleDispose();
}
@@ -0,0 +1,21 @@
/**
* 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 "scene/scene.h"
static scriptproto_t MODULE_SCENE_PROTO;
static void moduleSceneInit(void) {
scriptProtoInit(&MODULE_SCENE_PROTO, "Scene", sizeof(uint8_t), NULL);
}
static void moduleSceneDispose(void) {
scriptProtoDispose(&MODULE_SCENE_PROTO);
}
@@ -0,0 +1,44 @@
/**
* 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 "system/system.h"
static scriptproto_t MODULE_SYSTEM_PROTO;
moduleBaseFunction(moduleSystemGetPlatform) {
return jerry_number((double)systemGetPlatform());
}
static void moduleSystemInit(void) {
scriptProtoInit(&MODULE_SYSTEM_PROTO, "System", sizeof(uint8_t), NULL);
scriptProtoDefineStaticProp(
&MODULE_SYSTEM_PROTO, "platform",
moduleSystemGetPlatform, NULL
);
/* Register PLATFORM_* integer constants on the System global. */
jerry_value_t target = MODULE_SYSTEM_PROTO.prototype;
#define SYSTEM_PLATFORM(name, value) \
do { \
jerry_value_t key = jerry_string_sz("PLATFORM_" #name); \
jerry_value_t val = jerry_number((double)(value)); \
jerry_object_set(target, key, val); \
jerry_value_free(val); \
jerry_value_free(key); \
} while(0);
SYSTEM_PLATFORM_LIST
#undef SYSTEM_PLATFORM
}
static void moduleSystemDispose(void) {
scriptProtoDispose(&MODULE_SYSTEM_PROTO);
}
+76
View File
@@ -0,0 +1,76 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "script.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
#include "asset/loader/assetentry.h"
#include "script/module/modulelist.h"
#include <jerryscript.h>
script_t SCRIPT;
errorret_t scriptInit(void) {
memoryZero(&SCRIPT, sizeof(script_t));
jerry_init(JERRY_INIT_EMPTY);
SCRIPT.initialized = true;
moduleListInit();
errorOk();
}
errorret_t scriptExecString(const char_t *source) {
assertNotNull(source, "Source cannot be NULL");
assertTrue(SCRIPT.initialized, "Script system not initialized");
jerry_value_t result = jerry_eval(
(const jerry_char_t *)source,
strlen(source),
JERRY_PARSE_NO_OPTS
);
if(jerry_value_is_exception(result)) {
char_t buf[256];
jerry_value_t errVal = jerry_exception_value(result, false);
jerry_value_t errStr = jerry_value_to_string(errVal);
jerry_size_t len = jerry_string_to_buffer(
errStr, JERRY_ENCODING_UTF8, (jerry_char_t *)buf, sizeof(buf) - 1
);
buf[len] = '\0';
jerry_value_free(errStr);
jerry_value_free(errVal);
jerry_value_free(result);
errorThrow("Script error: %s", buf);
}
jerry_value_free(result);
errorOk();
}
errorret_t scriptExecFile(const char_t *path) {
assertNotNull(path, "Path cannot be NULL");
assertTrue(SCRIPT.initialized, "Script system not initialized");
assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_SCRIPT, NULL);
assertNotNull(entry, "Failed to get asset entry for script");
errorChain(assetRequireLoaded(entry));
assetUnlockEntry(entry);
errorOk();
}
errorret_t scriptDispose(void) {
if(!SCRIPT.initialized) errorOk();
moduleListDispose();
jerry_cleanup();
SCRIPT.initialized = false;
errorOk();
}
+47
View File
@@ -0,0 +1,47 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
typedef struct {
bool_t initialized;
} script_t;
extern script_t SCRIPT;
/**
* Initializes the script system, starting up JerryScript and registering all
* built-in modules.
*
* @return Any error that occurred.
*/
errorret_t scriptInit(void);
/**
* Evaluates a JS source string in the global scope.
*
* @param source Null-terminated JS source to evaluate.
* @return Any error that occurred.
*/
errorret_t scriptExecString(const char_t *source);
/**
* Loads and evaluates a script asset from the archive. The result is cached
* by the asset system; repeated calls with the same path do not re-execute.
*
* @param path Path of the script inside the asset archive.
* @return Any error that occurred.
*/
errorret_t scriptExecFile(const char_t *path);
/**
* Disposes of the script system and shuts down JerryScript.
*
* @return Any error that occurred.
*/
errorret_t scriptDispose(void);
+186
View File
@@ -0,0 +1,186 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "scriptproto.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "script/module/modulebase.h"
void scriptProtoInit(
scriptproto_t *proto,
const char_t *name,
const size_t size,
jerry_external_handler_t constructor
) {
assertNotNull(proto, "Script prototype struct must not be null");
memoryZero(proto, sizeof(scriptproto_t));
proto->info = (jerry_object_native_info_t){
.free_cb = moduleBaseFreeProto,
.number_of_references = 0,
.offset_of_references = 0
};
proto->prototype = jerry_object();
proto->size = size;
if(constructor != NULL) {
proto->constructor = jerry_function_external(constructor);
jerry_value_t protoKey = jerry_string_sz("prototype");
jerry_object_set(proto->constructor, protoKey, proto->prototype);
jerry_value_free(protoKey);
jerry_value_t ctorKey = jerry_string_sz("constructor");
jerry_object_set(proto->prototype, ctorKey, proto->constructor);
jerry_value_free(ctorKey);
}
if(name != NULL) {
jerry_value_t global = jerry_current_realm();
jerry_value_t key = jerry_string_sz(name);
jerry_value_t val = proto->constructor ? proto->constructor : proto->prototype;
jerry_object_set(global, key, val);
jerry_value_free(key);
jerry_value_free(global);
}
}
void scriptProtoDefineProp(
scriptproto_t *proto,
const char_t *name,
jerry_external_handler_t getter,
jerry_external_handler_t setter
) {
assertNotNull(proto, "Script prototype struct must not be null");
assertStrLenMin(name, 1, "Property name must not be empty");
assertNotNull(getter, "Getter must not be null");
jerry_property_descriptor_t desc;
memoryZero(&desc, sizeof(desc));
desc.flags = (uint16_t)(
JERRY_PROP_IS_GET_DEFINED |
JERRY_PROP_IS_ENUMERABLE_DEFINED | JERRY_PROP_IS_ENUMERABLE |
JERRY_PROP_IS_CONFIGURABLE_DEFINED | JERRY_PROP_IS_CONFIGURABLE
);
desc.getter = jerry_function_external(getter);
if(setter != NULL) {
desc.flags |= JERRY_PROP_IS_SET_DEFINED;
desc.setter = jerry_function_external(setter);
}
jerry_value_t key = jerry_string_sz(name);
jerry_value_t result = jerry_object_define_own_prop(proto->prototype, key, &desc);
jerry_value_free(result);
jerry_value_free(key);
jerry_value_free(desc.getter);
if(setter != NULL) jerry_value_free(desc.setter);
}
void scriptProtoDefineFunc(
scriptproto_t *proto,
const char_t *name,
jerry_external_handler_t fn
) {
assertNotNull(proto, "Script prototype struct must not be null");
assertStrLenMin(name, 1, "Method name must not be empty");
assertNotNull(fn, "Function handler must not be null");
jerry_value_t key = jerry_string_sz(name);
jerry_value_t func = jerry_function_external(fn);
jerry_object_set(proto->prototype, key, func);
jerry_value_free(func);
jerry_value_free(key);
}
void scriptProtoDefineStaticProp(
scriptproto_t *proto,
const char_t *name,
jerry_external_handler_t getter,
jerry_external_handler_t setter
) {
assertNotNull(proto, "Script prototype struct must not be null");
assertStrLenMin(name, 1, "Property name must not be empty");
assertNotNull(getter, "Getter must not be null");
jerry_value_t target = (
proto->constructor ? proto->constructor : proto->prototype
);
jerry_property_descriptor_t desc;
memoryZero(&desc, sizeof(desc));
desc.flags = (uint16_t)(
JERRY_PROP_IS_GET_DEFINED |
JERRY_PROP_IS_ENUMERABLE_DEFINED | JERRY_PROP_IS_ENUMERABLE |
JERRY_PROP_IS_CONFIGURABLE_DEFINED | JERRY_PROP_IS_CONFIGURABLE
);
desc.getter = jerry_function_external(getter);
if(setter != NULL) {
desc.flags |= JERRY_PROP_IS_SET_DEFINED;
desc.setter = jerry_function_external(setter);
}
jerry_value_t key = jerry_string_sz(name);
jerry_value_t result = jerry_object_define_own_prop(target, key, &desc);
jerry_value_free(result);
jerry_value_free(key);
jerry_value_free(desc.getter);
if(setter != NULL) jerry_value_free(desc.setter);
}
void scriptProtoDefineStaticFunc(
scriptproto_t *proto,
const char_t *name,
jerry_external_handler_t fn
) {
assertNotNull(proto, "Script prototype struct must not be null");
assertStrLenMin(name, 1, "Method name must not be empty");
assertNotNull(fn, "Function handler must not be null");
jerry_value_t target = (
proto->constructor ? proto->constructor : proto->prototype
);
jerry_value_t key = jerry_string_sz(name);
jerry_value_t func = jerry_function_external(fn);
jerry_object_set(target, key, func);
jerry_value_free(func);
jerry_value_free(key);
}
jerry_value_t scriptProtoCreateValue(
const scriptproto_t *proto,
const void *value
) {
assertNotNull(proto, "Script prototype struct must not be null");
assertNotNull(value, "Value pointer must not be null");
void *ptr = memoryAllocate(proto->size);
memoryCopy(ptr, value, proto->size);
jerry_value_t obj = jerry_object();
jerry_object_set_native_ptr(obj, &proto->info, ptr);
jerry_object_set_proto(obj, proto->prototype);
return obj;
}
void *scriptProtoGetValue(
const scriptproto_t *proto,
const jerry_value_t obj
) {
assertNotNull(proto, "Script prototype struct must not be null");
if(!jerry_value_is_object(obj)) return NULL;
return jerry_object_get_native_ptr(obj, &proto->info);
}
void scriptProtoDefineToString(
scriptproto_t *proto,
jerry_external_handler_t fn
) {
scriptProtoDefineFunc(proto, "toString", fn);
}
void scriptProtoDispose(scriptproto_t *proto) {
assertNotNull(proto, "Script prototype struct must not be null");
jerry_value_free(proto->prototype);
if(proto->constructor) jerry_value_free(proto->constructor);
}
+156
View File
@@ -0,0 +1,156 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#include <jerryscript.h>
/**
* Represents a JavaScript class prototype backed by a C struct.
* Use scriptProtoInit() to populate, scriptProtoDispose() to clean up.
*/
typedef struct {
/** Native-info tag used to identify instances of this class. */
jerry_object_native_info_t info;
/** The JS prototype object shared by all instances. */
jerry_value_t prototype;
/** The JS constructor function, or 0 if no constructor was given. */
jerry_value_t constructor;
/** sizeof the C struct wrapped by each instance. */
size_t size;
} scriptproto_t;
/**
* Initializes a JS class prototype.
*
* If name is non-NULL the class is registered as a global. When ctor is
* also non-NULL the global is the constructor function (enabling
* `new Name(...)`); otherwise the prototype object itself becomes the
* global.
*
* @param proto The struct to initialize.
* @param name JS global name, or NULL to skip global registration.
* @param size sizeof the C struct this class wraps.
* @param ctor Constructor handler, or NULL for no constructor.
*/
void scriptProtoInit(
scriptproto_t *proto,
const char_t *name,
const size_t size,
jerry_external_handler_t ctor
);
/**
* Defines an instance property with a getter and optional setter.
*
* @param proto The class prototype.
* @param name Property name.
* @param getter Getter handler (must not be NULL).
* @param setter Setter handler, or NULL for a read-only property.
*/
void scriptProtoDefineProp(
scriptproto_t *proto,
const char_t *name,
jerry_external_handler_t getter,
jerry_external_handler_t setter
);
/**
* Defines an instance method on the class prototype.
*
* @param proto The class prototype.
* @param name Method name.
* @param fn C handler called when the method is invoked.
*/
void scriptProtoDefineFunc(
scriptproto_t *proto,
const char_t *name,
jerry_external_handler_t fn
);
/**
* Defines a static property on the class (e.g. Console.visible).
*
* Attaches to the constructor when one exists, otherwise attaches
* directly to the prototype object (which is the global in that case).
*
* @param proto The class prototype.
* @param name Property name.
* @param getter Getter handler (must not be NULL).
* @param setter Setter handler, or NULL for a read-only property.
*/
void scriptProtoDefineStaticProp(
scriptproto_t *proto,
const char_t *name,
jerry_external_handler_t getter,
jerry_external_handler_t setter
);
/**
* Defines a static method on the class (e.g. Console.print).
*
* Attaches to the constructor when one exists, otherwise attaches
* directly to the prototype object.
*
* @param proto The class prototype.
* @param name Method name.
* @param fn C handler called when the static method is invoked.
*/
void scriptProtoDefineStaticFunc(
scriptproto_t *proto,
const char_t *name,
jerry_external_handler_t fn
);
/**
* Creates a JS instance wrapping a copy of a C value.
*
* Allocates a copy of the value, sets it as native data on a new JS
* object, and applies the class prototype.
*
* @param proto The class prototype.
* @param value Pointer to the C value to copy into the new JS object.
* @return A new JS object with the class prototype and native data set.
*/
jerry_value_t scriptProtoCreateValue(
const scriptproto_t *proto,
const void *value
);
/**
* Unwraps the native C pointer from a JS object instance.
*
* @param proto The class prototype.
* @param obj The JS object to inspect.
* @return Pointer to the wrapped C value, or NULL if not an instance.
*/
void *scriptProtoGetValue(
const scriptproto_t *proto,
const jerry_value_t obj
);
/**
* Defines a toString() method on the class prototype.
*
* @param proto The class prototype.
* @param fn C handler called when toString() is invoked on an
* instance.
*/
void scriptProtoDefineToString(
scriptproto_t *proto,
jerry_external_handler_t fn
);
/**
* Releases all JerryScript resources held by the prototype.
*
* Must be called before jerry_cleanup() to avoid heap finalizer
* crashes from live jerry_value_t GC roots.
*
* @param proto The class prototype to dispose.
*/
void scriptProtoDispose(scriptproto_t *proto);
+14
View File
@@ -22,4 +22,18 @@ errorret_t systemInit() {
systemdialogtype_t systemGetActiveDialogType() {
return systemGetActiveDialogTypePlatform();
}
systemplatform_t systemGetPlatform(void) {
#if defined(DUSK_KNULLI)
return SYSTEM_PLATFORM_KNULLI;
#elif defined(DUSK_PSP)
return SYSTEM_PLATFORM_PSP;
#elif defined(DUSK_GAMECUBE)
return SYSTEM_PLATFORM_GAMECUBE;
#elif defined(DUSK_WII)
return SYSTEM_PLATFORM_WII;
#else
return SYSTEM_PLATFORM_LINUX;
#endif
}
+13 -1
View File
@@ -7,6 +7,11 @@
#pragma once
#include "error/error.h"
#include "system/systemplatformlist.h"
#define SYSTEM_PLATFORM(name, value) SYSTEM_PLATFORM_##name = value,
typedef enum { SYSTEM_PLATFORM_LIST } systemplatform_t;
#undef SYSTEM_PLATFORM
typedef enum {
SYSTEM_DIALOG_TYPE_NONE,
@@ -35,4 +40,11 @@ errorret_t systemInit(void);
*
* @return Dialog type currently open.
*/
systemdialogtype_t systemGetActiveDialogType();
systemdialogtype_t systemGetActiveDialogType();
/**
* Returns the platform the engine is currently running on.
*
* @return The current platform.
*/
systemplatform_t systemGetPlatform(void);
+23
View File
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
/**
* X-macro list of all supported platforms.
* Usage: SYSTEM_PLATFORM(name, value)
*
* Example:
* #define SYSTEM_PLATFORM(name, value) \
* case SYSTEM_PLATFORM_##name: return #name;
* SYSTEM_PLATFORM_LIST
* #undef SYSTEM_PLATFORM
*/
#define SYSTEM_PLATFORM_LIST \
SYSTEM_PLATFORM(LINUX, 0) \
SYSTEM_PLATFORM(KNULLI, 1) \
SYSTEM_PLATFORM(PSP, 2) \
SYSTEM_PLATFORM(GAMECUBE, 3) \
SYSTEM_PLATFORM(WII, 4)
+4 -2
View File
@@ -14,6 +14,7 @@ void threadInit(thread_t *thread, const threadcallback_t callback) {
assertNotNull(callback, "Thread callback cannot be NULL.");
memoryZero(thread, sizeof(thread_t));
threadMutexInit(&thread->stateMutex);
thread->state = THREAD_STATE_STOPPED;
thread->callback = callback;
@@ -59,9 +60,10 @@ void threadStart(thread_t *thread) {
threadStartRequest(thread);
threadMutexLock(&thread->stateMutex);
threadMutexWaitLock(&thread->stateMutex);
while(thread->state == THREAD_STATE_STARTING) {
threadMutexWaitLock(&thread->stateMutex);
}
threadMutexUnlock(&thread->stateMutex);
printf("Thread is now running.\n");
}
void threadStop(thread_t *thread) {
+1
View File
@@ -6,6 +6,7 @@
*/
#pragma once
#include "thread/threadlocal.h"
#include "thread/threadmutex.h"
typedef struct thread_s thread_t;
+17
View File
@@ -0,0 +1,17 @@
/**
* Copyright (c) 2025 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#ifdef DUSK_THREAD_PTHREAD
#define THREAD_LOCAL __thread
#endif
#ifndef THREAD_LOCAL
#error "No threading implementation found, cannot define THREAD_LOCAL."
#endif
+2
View File
@@ -10,6 +10,7 @@
void threadMutexInit(threadmutex_t *lock) {
#ifdef DUSK_THREAD_PTHREAD
pthread_mutex_init(&lock->mutex, NULL);
pthread_cond_init(&lock->cond, NULL);
#endif
}
@@ -46,5 +47,6 @@ void threadMutexSignal(threadmutex_t *lock) {
void threadMutexDispose(threadmutex_t *lock) {
#ifdef DUSK_THREAD_PTHREAD
pthread_mutex_destroy(&lock->mutex);
pthread_cond_destroy(&lock->cond);
#endif
}
+1 -1
View File
@@ -19,7 +19,7 @@ uifullbox_t UI_FULLBOX_OVER;
void uiFullboxInit(uifullbox_t *fullbox) {
assertNotNull(fullbox, "fullbox must not be NULL");
memoryZero(fullbox, sizeof(uifullbox_t));
eventInit(&fullbox->onTransitionEnd);
eventInit(&fullbox->onTransitionEnd, fullbox->onTransitionEndCallbacks, fullbox->onTransitionEndUsers, 4);
}
void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta) {
+2
View File
@@ -17,6 +17,8 @@ typedef struct {
float_t duration;
float_t time;
easingtype_t easing;
eventcallback_t onTransitionEndCallbacks[4];
void *onTransitionEndUsers[4];
event_t onTransitionEnd;
} uifullbox_t;
+2 -2
View File
@@ -19,7 +19,7 @@ uiloading_t UI_LOADING;
void uiLoadingInit(void) {
memoryZero(&UI_LOADING, sizeof(uiloading_t));
eventInit(&UI_LOADING.onTransitionEnd);
eventInit(&UI_LOADING.onTransitionEnd, UI_LOADING.onTransitionEndCallbacks, UI_LOADING.onTransitionEndUsers, 4);
}
void uiLoadingUpdate(float_t delta) {
@@ -65,7 +65,7 @@ static void uiLoadingTransition(
UI_LOADING.toAlpha = to;
UI_LOADING.duration = UI_LOADING_FADE_DURATION;
UI_LOADING.time = 0.0f;
eventInit(&UI_LOADING.onTransitionEnd);
eventInit(&UI_LOADING.onTransitionEnd, UI_LOADING.onTransitionEndCallbacks, UI_LOADING.onTransitionEndUsers, 4);
if(callback) eventSubscribe(&UI_LOADING.onTransitionEnd, callback, user);
}
+2
View File
@@ -17,6 +17,8 @@ typedef struct {
float_t toAlpha;
float_t duration;
float_t time;
eventcallback_t onTransitionEndCallbacks[4];
void *onTransitionEndUsers[4];
event_t onTransitionEnd;
} uiloading_t;
+2 -2
View File
@@ -46,8 +46,8 @@ errorret_t uiTextboxInit(void) {
UI_TEXTBOX.frame.tileset.uv[1] = 1.0f / 3.0f;
UI_TEXTBOX.frame.texture = &TEXTURE_WHITE;
eventInit(&UI_TEXTBOX.onPageComplete);
eventInit(&UI_TEXTBOX.onLastPage);
eventInit(&UI_TEXTBOX.onPageComplete, UI_TEXTBOX.onPageCompleteCallbacks, UI_TEXTBOX.onPageCompleteUsers, 4);
eventInit(&UI_TEXTBOX.onLastPage, UI_TEXTBOX.onLastPageCallbacks, UI_TEXTBOX.onLastPageUsers, 4);
errorOk();
}
+4
View File
@@ -42,7 +42,11 @@ typedef struct {
int32_t scroll;
inputaction_t advanceAction;
eventcallback_t onPageCompleteCallbacks[4];
void *onPageCompleteUsers[4];
event_t onPageComplete;
eventcallback_t onLastPageCallbacks[4];
void *onLastPageUsers[4];
event_t onLastPage;
} uitextbox_t;
+1
View File
@@ -13,4 +13,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
string.c
math.c
sort.c
ref.c
)
+4
View File
@@ -24,4 +24,8 @@ float_t mathModFloat(float_t x, float_t y) {
float_t result = fmodf(x, y);
if(result < 0) result += y;
return result;
}
float_t mathLerp(float_t a, float_t b, float_t t) {
return a + t * (b - a);
}
+11 -1
View File
@@ -60,4 +60,14 @@ uint32_t mathNextPowTwo(uint32_t value);
* @param y The divisor.
* @return The result of the modulus operation.
*/
float_t mathModFloat(float_t x, float_t y);
float_t mathModFloat(float_t x, float_t y);
/**
* Linearly interpolates between two values.
*
* @param a The start value.
* @param b The end value.
* @param t The interpolation factor, between 0 and 1.
* @return The interpolated value.
*/
float_t mathLerp(float_t a, float_t b, float_t t);
+8 -3
View File
@@ -13,11 +13,16 @@ size_t memoryGetAllocatedCount(void) {
return MEMORY_POINTERS_IN_USE;
}
void memoryTrack(void *ptr) {
assertNotNull(ptr, "Cannot track NULL pointer.");
MEMORY_POINTERS_IN_USE++;
}
void * memoryAllocate(const size_t size) {
assertTrue(size > 0, "Cannot allocate 0 bytes of memory.");
void *ptr = malloc(size);
assertNotNull(ptr, "Memory allocation failed.");
MEMORY_POINTERS_IN_USE++;
memoryTrack(ptr);
return ptr;
}
@@ -26,7 +31,7 @@ void * memoryAlign(size_t alignment, size_t size) {
assertTrue(size > 0, "Cannot allocate 0 bytes of memory.");
void *ptr = memalign(alignment, size);
assertNotNull(ptr, "Aligned memory allocation failed.");
MEMORY_POINTERS_IN_USE++;
memoryTrack(ptr);
return ptr;
}
@@ -124,7 +129,7 @@ void memoryCopyInterleaved(
uint8_t *d = (uint8_t *)dest;
const uint8_t *s = (const uint8_t *)src;
for(size_t i = 0; i < count; i++) {
memcpy(d, s, elementSize);
memoryCopy(d, s, elementSize);
d += destStride;
s += srcStride;
}
+7
View File
@@ -17,6 +17,13 @@ static size_t MEMORY_POINTERS_IN_USE = 0;
*/
size_t memoryGetAllocatedCount(void);
/**
* Track a pointer that was malloc'd outside of the dusk engine.
*
* @param ptr The pointer to track.
*/
void memoryTrack(void *ptr);
/**
* Allocates memory.
*
+52
View File
@@ -0,0 +1,52 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "ref.h"
#include "memory.h"
#include "assert/assert.h"
void refInit(
ref_t *ref,
void *data,
refcallback_t onLock,
refcallback_t onUnlock,
refcallback_t onAllUnlocked
) {
assertNotNull(ref, "Ref cannot be NULL.");
memoryZero(ref, sizeof(ref_t));
ref->count = 0;
ref->data = data;
ref->onLock = onLock;
ref->onUnlock = onUnlock;
ref->onAllUnlocked = onAllUnlocked;
}
void refLock(ref_t *ref) {
assertNotNull(ref, "Ref cannot be NULL.");
assertTrue(ref->count >= 0, "Cannot lock a ref with negative count.");
ref->count++;
if(ref->onLock != NULL) ref->onLock(ref);
}
bool_t refUnlock(ref_t *ref) {
assertNotNull(ref, "Ref cannot be NULL.");
assertTrue(ref->count >= 0, "Cannot unlock a ref with negative count.");
ref->count--;
if(ref->count > 0) {
if(ref->onUnlock != NULL) ref->onUnlock(ref);
return false;
}
void *data = ref->data;
refcallback_t onAllUnlocked = ref->onAllUnlocked;
memoryZero(ref, sizeof(ref_t));
ref->data = data;
if(onAllUnlocked != NULL) onAllUnlocked(ref);
return true;
}
+54
View File
@@ -0,0 +1,54 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct ref_s ref_t;
typedef void (*refcallback_t)(ref_t *ref);
typedef struct ref_s {
uint32_t count;
void *data;
refcallback_t onLock;
refcallback_t onUnlock;
refcallback_t onAllUnlocked;
} ref_t;
/**
* Initializes a ref with a count of 1.
*
* @param ref The ref to initialize.
* @param data Opaque context pointer accessible to all callbacks.
* @param onLock Called each time the ref is locked. May be NULL.
* @param onUnlock Called each time the ref is unlocked (count still > 0). May be NULL.
* @param onAllUnlocked Called when the count reaches zero. Responsible for
* any cleanup. May be NULL.
*/
void refInit(
ref_t *ref,
void *data,
refcallback_t onLock,
refcallback_t onUnlock,
refcallback_t onAllUnlocked
);
/**
* Increments the lock count.
*
* @param ref The ref to lock.
*/
void refLock(ref_t *ref);
/**
* Decrements the lock count. When it reaches zero onAllUnlocked is called
* (if set) and the ref struct is zeroed.
*
* @param ref The ref to unlock.
* @return true if the count reached zero.
*/
bool_t refUnlock(ref_t *ref);
+1 -1
View File
@@ -10,6 +10,6 @@
#include "error/errorgl.h"
#define assertNoGLError(message) \
assertTrue(errorGLCheck().code == ERROR_OK, message)
assertTrue(errorIsOk(errorGLCheck()), message)
// EOF
+9 -9
View File
@@ -40,14 +40,14 @@ errorret_t shaderInitGL(shadergl_t *shader, const shaderdefinitiongl_t *def) {
glShaderSource(shader->vertexShaderId, 1, &def->vert, NULL);
err = errorGLCheck();
if(err.code != ERROR_OK) {
if(errorIsNotOk(err)) {
glDeleteShader(shader->vertexShaderId);
errorChain(err);
}
glCompileShader(shader->vertexShaderId);
err = errorGLCheck();
if(err.code != ERROR_OK) {
if(errorIsNotOk(err)) {
glDeleteShader(shader->vertexShaderId);
errorChain(err);
}
@@ -64,14 +64,14 @@ errorret_t shaderInitGL(shadergl_t *shader, const shaderdefinitiongl_t *def) {
// Create fragment shader
shader->fragmentShaderId = glCreateShader(GL_FRAGMENT_SHADER);
err = errorGLCheck();
if(err.code != ERROR_OK) {
if(errorIsNotOk(err)) {
glDeleteShader(shader->vertexShaderId);
errorChain(err);
}
glShaderSource(shader->fragmentShaderId, 1, &def->frag, NULL);
err = errorGLCheck();
if(err.code != ERROR_OK) {
if(errorIsNotOk(err)) {
glDeleteShader(shader->vertexShaderId);
glDeleteShader(shader->fragmentShaderId);
errorChain(err);
@@ -79,7 +79,7 @@ errorret_t shaderInitGL(shadergl_t *shader, const shaderdefinitiongl_t *def) {
glCompileShader(shader->fragmentShaderId);
err = errorGLCheck();
if(err.code != ERROR_OK) {
if(errorIsNotOk(err)) {
glDeleteShader(shader->vertexShaderId);
glDeleteShader(shader->fragmentShaderId);
errorChain(err);
@@ -97,7 +97,7 @@ errorret_t shaderInitGL(shadergl_t *shader, const shaderdefinitiongl_t *def) {
// Create shader program
shader->shaderProgramId = glCreateProgram();
err = errorGLCheck();
if(err.code != ERROR_OK) {
if(errorIsNotOk(err)) {
glDeleteShader(shader->vertexShaderId);
glDeleteShader(shader->fragmentShaderId);
errorChain(err);
@@ -105,7 +105,7 @@ errorret_t shaderInitGL(shadergl_t *shader, const shaderdefinitiongl_t *def) {
glAttachShader(shader->shaderProgramId, shader->vertexShaderId);
err = errorGLCheck();
if(err.code != ERROR_OK) {
if(errorIsNotOk(err)) {
glDeleteProgram(shader->shaderProgramId);
glDeleteShader(shader->vertexShaderId);
glDeleteShader(shader->fragmentShaderId);
@@ -114,7 +114,7 @@ errorret_t shaderInitGL(shadergl_t *shader, const shaderdefinitiongl_t *def) {
glAttachShader(shader->shaderProgramId, shader->fragmentShaderId);
err = errorGLCheck();
if(err.code != ERROR_OK) {
if(errorIsNotOk(err)) {
glDeleteProgram(shader->shaderProgramId);
glDeleteShader(shader->vertexShaderId);
glDeleteShader(shader->fragmentShaderId);
@@ -123,7 +123,7 @@ errorret_t shaderInitGL(shadergl_t *shader, const shaderdefinitiongl_t *def) {
glLinkProgram(shader->shaderProgramId);
err = errorGLCheck();
if(err.code != ERROR_OK) {
if(errorIsNotOk(err)) {
glDeleteProgram(shader->shaderProgramId);
glDeleteShader(shader->vertexShaderId);
glDeleteShader(shader->fragmentShaderId);
+2 -2
View File
@@ -101,7 +101,7 @@ errorret_t networkPSPUpdate() {
// Kill the PSP network stack.
errorret_t err = networkPSPTerm();
if(err.code != ERROR_OK) {
if(errorIsNotOk(err)) {
errorCatch(errorPrint(err));
}
@@ -203,7 +203,7 @@ void networkPSPRequestDisconnection(
);
errorret_t err = networkPSPTerm();
if(err.code != ERROR_OK) {
if(errorIsNotOk(err)) {
errorCatch(errorPrint(err));
}
+3
View File
@@ -4,6 +4,9 @@
# https://opensource.org/licenses/MIT
add_subdirectory(assert)
add_subdirectory(asset)
add_subdirectory(error)
add_subdirectory(thread)
add_subdirectory(display)
# add_subdirectory(rpg)
# add_subdirectory(item)
+11
View File
@@ -0,0 +1,11 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(dusktest)
dusktest(test_assetlocale.c)
dusktest(test_asset.c)
dusktest(test_assetjsonloader.c)
dusktest(test_assettilesetloader.c)
+402
View File
@@ -0,0 +1,402 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
#include "asset/loader/assetentry.h"
#include "util/memory.h"
#include "util/string.h"
// ============================================================
// Stub loader callbacks
// ============================================================
static errorret_t stub_load_success(assetloading_t *loading) {
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
static errorret_t stub_load_fail(assetloading_t *loading) {
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
errorThrow("Stub loader failed");
}
static errorret_t stub_dispose(assetentry_t *entry) {
errorOk();
}
// ============================================================
// Per-test setup / teardown
// ============================================================
static assetloadercallbacks_t saved_callbacks[ASSET_LOADER_TYPE_COUNT];
static int asset_setup(void **state) {
// Save real callbacks so we can restore them in teardown.
memoryCopy(saved_callbacks, ASSET_LOADER_CALLBACKS, sizeof(saved_callbacks));
// Manually init ASSET — no thread, no ZIP.
memoryZero(&ASSET, sizeof(ASSET));
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
threadMutexInit(&ASSET.loading[i].mutex);
}
// Replace all loader callbacks with stubs.
for(int i = 0; i < ASSET_LOADER_TYPE_COUNT; i++) {
ASSET_LOADER_CALLBACKS[i].loadSync = stub_load_success;
ASSET_LOADER_CALLBACKS[i].dispose = stub_dispose;
}
return 0;
}
static int asset_teardown(void **state) {
// Dispose any entries that tests left behind.
for(int i = 0; i < ASSET_ENTRY_COUNT_MAX; i++) {
if(ASSET.entries[i].type != ASSET_LOADER_TYPE_NULL) {
errorret_t ret = assetEntryDispose(&ASSET.entries[i]);
if(errorIsNotOk(ret)) errorCatch(ret);
}
}
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
threadMutexDispose(&ASSET.loading[i].mutex);
}
// Restore real callbacks before zeroing state.
memoryCopy(ASSET_LOADER_CALLBACKS, saved_callbacks, sizeof(saved_callbacks));
memoryZero(&ASSET, sizeof(ASSET));
return 0;
}
// ============================================================
// Helper: find which loading slot owns a given entry
// ============================================================
static bool_t loading_slot_has_entry(const assetentry_t *entry) {
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
if(ASSET.loading[i].entry == entry) return true;
}
return false;
}
// ============================================================
// assetGetEntry tests
// ============================================================
static void test_getEntry_creates_new(void **state) {
assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assert_non_null(entry);
assert_int_equal(entry->state, ASSET_ENTRY_STATE_NOT_STARTED);
assert_int_equal(entry->type, ASSET_LOADER_TYPE_LOCALE);
assert_true(stringEquals(entry->name, "test.locale"));
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_getEntry_dedup(void **state) {
assetentry_t *a = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assetentry_t *b = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assert_ptr_equal(a, b);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_getEntry_distinct_names(void **state) {
assetentry_t *a = assetGetEntry("a.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assetentry_t *b = assetGetEntry("b.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assert_ptr_not_equal(a, b);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
// ============================================================
// assetUpdate — state machine tests
// ============================================================
static void test_update_entry_reaches_loaded(void **state) {
assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assert_int_equal(entry->state, ASSET_ENTRY_STATE_NOT_STARTED);
errorret_t ret = assetUpdate();
assert_true(errorIsOk(ret));
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_update_slot_occupied_after_first_update(void **state) {
assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assetUpdate();
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
// Slot not cleared until the second update processes the LOADED case.
assert_true(loading_slot_has_entry(entry));
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_update_slot_cleared_after_second_update(void **state) {
assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assetUpdate();
assetUpdate();
assert_false(loading_slot_has_entry(entry));
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_update_four_slots_fill_independently(void **state) {
// ASSET_LOADING_COUNT_MAX concurrent entries should all load in one pass.
assetentry_t *entries[ASSET_LOADING_COUNT_MAX];
for(int i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
char_t name[ASSET_FILE_NAME_MAX];
snprintf(name, sizeof(name), "asset%d.locale", i);
entries[i] = assetGetEntry(name, ASSET_LOADER_TYPE_LOCALE, NULL);
}
errorret_t ret = assetUpdate();
assert_true(errorIsOk(ret));
for(int i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
assert_int_equal(entries[i]->state, ASSET_ENTRY_STATE_LOADED);
}
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_update_error_state(void **state) {
ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_LOCALE].loadSync = stub_load_fail;
assetentry_t *entry = assetGetEntry("fail.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
// First update: dispatches and calls the failing stub.
// assetUpdate itself returns OK here; the error from loadSync is caught internally.
errorret_t ret = assetUpdate();
assert_true(errorIsOk(ret));
assert_int_equal(entry->state, ASSET_ENTRY_STATE_ERROR);
// Second update: sees ERROR state and propagates it.
ret = assetUpdate();
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_update_noop_on_empty_table(void **state) {
errorret_t ret = assetUpdate();
assert_true(errorIsOk(ret));
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_update_loaded_entry_not_redispatched(void **state) {
assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assetUpdate();
assetUpdate(); // slot freed
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
// Further updates must not re-dispatch or modify a LOADED entry.
assetUpdate();
assetUpdate();
assetUpdate();
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
assert_false(loading_slot_has_entry(entry));
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_update_overflow_queues_entries(void **state) {
// Create one more entry than there are loading slots.
const int TOTAL = ASSET_LOADING_COUNT_MAX + 1;
assetentry_t *entries[ASSET_LOADING_COUNT_MAX + 1];
for(int i = 0; i < TOTAL; i++) {
char_t name[ASSET_FILE_NAME_MAX];
snprintf(name, sizeof(name), "asset%d.locale", i);
entries[i] = assetGetEntry(name, ASSET_LOADER_TYPE_LOCALE, NULL);
}
// Update 1: fills all slots, first ASSET_LOADING_COUNT_MAX entries reach LOADED.
// The overflow entry has no slot yet and stays NOT_STARTED.
errorret_t ret = assetUpdate();
assert_true(errorIsOk(ret));
for(int i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
assert_int_equal(entries[i]->state, ASSET_ENTRY_STATE_LOADED);
}
assert_int_equal(entries[ASSET_LOADING_COUNT_MAX]->state, ASSET_ENTRY_STATE_NOT_STARTED);
// Update 2: LOADED slots are freed. Overflow entry still NOT_STARTED because
// the dispatch phase ran before the slots were cleared this turn.
ret = assetUpdate();
assert_true(errorIsOk(ret));
assert_int_equal(entries[ASSET_LOADING_COUNT_MAX]->state, ASSET_ENTRY_STATE_NOT_STARTED);
// Update 3: now a slot is available; the overflow entry is dispatched and loaded.
ret = assetUpdate();
assert_true(errorIsOk(ret));
assert_int_equal(entries[ASSET_LOADING_COUNT_MAX]->state, ASSET_ENTRY_STATE_LOADED);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_update_error_slot_stays_occupied(void **state) {
ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_LOCALE].loadSync = stub_load_fail;
assetentry_t *entry = assetGetEntry("fail.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assetUpdate();
assert_int_equal(entry->state, ASSET_ENTRY_STATE_ERROR);
// Unlike LOADED, the ERROR case does NOT clear the slot — it throws instead.
assert_true(loading_slot_has_entry(entry));
errorret_t ret = assetUpdate();
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
// ============================================================
// assetGetEntry — dedup against non-NOT_STARTED entries
// ============================================================
static void test_getEntry_returns_loaded_entry(void **state) {
assetentry_t *a = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assetUpdate();
assert_int_equal(a->state, ASSET_ENTRY_STATE_LOADED);
// A second request for the same name must return the same entry even though
// it is already LOADED rather than NOT_STARTED.
assetentry_t *b = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assert_ptr_equal(a, b);
assert_int_equal(b->state, ASSET_ENTRY_STATE_LOADED);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
// ============================================================
// assetEntryDispose tests
// ============================================================
static void test_entry_dispose_clears_entry(void **state) {
assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assetUpdate();
assetUpdate(); // ensure loading slot is freed before disposing
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
errorret_t ret = assetEntryDispose(entry);
assert_true(errorIsOk(ret));
assert_int_equal(entry->type, ASSET_LOADER_TYPE_NULL);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entry_dispose_slot_reusable(void **state) {
assetentry_t *a = assetGetEntry("a.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assetUpdate();
assetUpdate();
assert_false(loading_slot_has_entry(a));
assetEntryDispose(a);
assert_int_equal(a->type, ASSET_LOADER_TYPE_NULL);
// The freed slot should now accept a new entry.
assetentry_t *b = assetGetEntry("b.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assert_non_null(b);
assert_int_equal(b->state, ASSET_ENTRY_STATE_NOT_STARTED);
errorret_t ret = assetUpdate();
assert_true(errorIsOk(ret));
assert_int_equal(b->state, ASSET_ENTRY_STATE_LOADED);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
// ============================================================
// assetRequireLoaded tests
// ============================================================
static void test_requireLoaded_already_loaded(void **state) {
assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assetUpdate();
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
// Should return immediately without calling assetUpdate again.
errorret_t ret = assetRequireLoaded(entry);
assert_true(errorIsOk(ret));
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_requireLoaded_spins_to_loaded(void **state) {
assetentry_t *entry = assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
assert_int_equal(entry->state, ASSET_ENTRY_STATE_NOT_STARTED);
// requireLoaded calls assetUpdate internally until LOADED.
errorret_t ret = assetRequireLoaded(entry);
assert_true(errorIsOk(ret));
assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_requireLoaded_propagates_error(void **state) {
ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_LOCALE].loadSync = stub_load_fail;
assetentry_t *entry = assetGetEntry("fail.locale", ASSET_LOADER_TYPE_LOCALE, NULL);
// requireLoaded spins assetUpdate until LOADED — but the loader always fails,
// so the second assetUpdate sees ERROR and throws, which errorChain propagates.
errorret_t ret = assetRequireLoaded(entry);
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(entry->state, ASSET_ENTRY_STATE_ERROR);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
// ============================================================
// main
// ============================================================
int main(void) {
const struct CMUnitTest tests[] = {
// getEntry
cmocka_unit_test_setup_teardown(test_getEntry_creates_new, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_getEntry_dedup, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_getEntry_distinct_names, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_getEntry_returns_loaded_entry, asset_setup, asset_teardown),
// assetUpdate — state machine
cmocka_unit_test_setup_teardown(test_update_noop_on_empty_table, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_update_entry_reaches_loaded, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_update_slot_occupied_after_first_update, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_update_slot_cleared_after_second_update, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_update_four_slots_fill_independently, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_update_loaded_entry_not_redispatched, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_update_overflow_queues_entries, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_update_error_state, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_update_error_slot_stays_occupied, asset_setup, asset_teardown),
// assetEntryDispose
cmocka_unit_test_setup_teardown(test_entry_dispose_clears_entry, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_entry_dispose_slot_reusable, asset_setup, asset_teardown),
// assetRequireLoaded
cmocka_unit_test_setup_teardown(test_requireLoaded_already_loaded, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_requireLoaded_spins_to_loaded, asset_setup, asset_teardown),
cmocka_unit_test_setup_teardown(test_requireLoaded_propagates_error, asset_setup, asset_teardown),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+222
View File
@@ -0,0 +1,222 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
#include "asset/loader/assetentry.h"
#include "asset/loader/json/assetjsonloader.h"
#include "util/memory.h"
#include <zip.h>
// ============================================================
// Fixtures
// ============================================================
static const char_t *JSON_VALID = "{\"hello\":\"world\",\"count\":42}";
static const char_t *JSON_INVALID = "{ this is definitely not valid json !!!";
// ============================================================
// In-memory ZIP
// ============================================================
static zip_t *g_zip = NULL;
static int zip_setup(void **state) {
zip_error_t err;
zip_error_init(&err);
zip_source_t *write_src = zip_source_buffer_create(NULL, 0, 1, &err);
if(!write_src) return -1;
zip_t *za = zip_open_from_source(write_src, ZIP_TRUNCATE, &err);
if(!za) { zip_source_free(write_src); return -1; }
// valid.json
zip_source_t *s;
s = zip_source_buffer(za, JSON_VALID, strlen(JSON_VALID), 0);
if(zip_file_add(za, "valid.json", s, ZIP_FL_OVERWRITE) < 0) {
zip_close(za); return -1;
}
// invalid.json
s = zip_source_buffer(za, JSON_INVALID, strlen(JSON_INVALID), 0);
if(zip_file_add(za, "invalid.json", s, ZIP_FL_OVERWRITE) < 0) {
zip_close(za); return -1;
}
zip_source_keep(write_src);
if(zip_close(za) != 0) { zip_source_free(write_src); return -1; }
zip_stat_t zs;
memset(&zs, 0, sizeof(zs));
if(zip_source_stat(write_src, &zs) != 0 || !(zs.valid & ZIP_STAT_SIZE)) {
zip_source_free(write_src); return -1;
}
void *zipbuf = malloc((size_t)zs.size);
if(!zipbuf) { zip_source_free(write_src); return -1; }
if(zip_source_open(write_src) != 0) {
free(zipbuf); zip_source_free(write_src); return -1;
}
zip_source_read(write_src, zipbuf, (zip_uint64_t)zs.size);
zip_source_close(write_src);
zip_source_free(write_src);
zip_error_init(&err);
zip_source_t *read_src = zip_source_buffer_create(
zipbuf, (zip_uint64_t)zs.size, 1, &err
);
if(!read_src) { free(zipbuf); return -1; }
g_zip = zip_open_from_source(read_src, 0, &err);
if(!g_zip) { zip_source_free(read_src); return -1; }
ASSET.zip = g_zip;
return 0;
}
static int zip_teardown(void **state) {
if(g_zip) { zip_close(g_zip); g_zip = NULL; }
ASSET.zip = NULL;
return 0;
}
// ============================================================
// Loader pipeline helper
// ============================================================
typedef struct {
assetentry_t entry;
assetloading_t loading;
} loader_ctx_t;
static void loader_ctx_init(loader_ctx_t *ctx, const char_t *name) {
assetEntryInit(&ctx->entry, name, ASSET_LOADER_TYPE_JSON, NULL);
threadMutexInit(&ctx->loading.mutex);
memoryZero(&ctx->loading.loading, sizeof(ctx->loading.loading));
ctx->loading.type = ASSET_LOADER_TYPE_JSON;
ctx->loading.entry = &ctx->entry;
ctx->entry.state = ASSET_ENTRY_STATE_PENDING_SYNC;
}
// Drives sync(INITIAL) -> async(READ) -> sync(PARSE).
static errorret_t loader_ctx_run(loader_ctx_t *ctx) {
errorret_t ret = assetJsonLoaderSync(&ctx->loading);
if(errorIsNotOk(ret)) return ret;
ret = assetJsonLoaderAsync(&ctx->loading);
if(errorIsNotOk(ret)) return ret;
return assetJsonLoaderSync(&ctx->loading);
}
static void loader_ctx_dispose(loader_ctx_t *ctx) {
if(ctx->entry.type != ASSET_LOADER_TYPE_NULL) {
errorret_t ret = assetEntryDispose(&ctx->entry);
if(errorIsNotOk(ret)) errorCatch(ret);
}
threadMutexDispose(&ctx->loading.mutex);
}
// ============================================================
// Tests
// ============================================================
static void test_json_valid_loads(void **state) {
loader_ctx_t ctx;
loader_ctx_init(&ctx, "valid.json");
errorret_t ret = loader_ctx_run(&ctx);
assert_true(errorIsOk(ret));
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_LOADED);
assert_non_null(ctx.entry.data.json);
// Verify content is accessible
yyjson_val *root = yyjson_doc_get_root(ctx.entry.data.json);
assert_non_null(root);
assert_true(yyjson_is_obj(root));
yyjson_val *hello = yyjson_obj_get(root, "hello");
assert_non_null(hello);
assert_string_equal(yyjson_get_str(hello), "world");
yyjson_val *count = yyjson_obj_get(root, "count");
assert_non_null(count);
assert_int_equal((int)yyjson_get_int(count), 42);
loader_ctx_dispose(&ctx);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_json_parse_error(void **state) {
loader_ctx_t ctx;
loader_ctx_init(&ctx, "invalid.json");
// Async read succeeds; sync parse fails because yyjson rejects the content.
errorret_t ret = assetJsonLoaderSync(&ctx.loading);
assert_true(errorIsOk(ret));
ret = assetJsonLoaderAsync(&ctx.loading);
assert_true(errorIsOk(ret));
ret = assetJsonLoaderSync(&ctx.loading);
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR);
assert_null(ctx.entry.data.json);
loader_ctx_dispose(&ctx);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_json_missing_file(void **state) {
loader_ctx_t ctx;
loader_ctx_init(&ctx, "nonexistent.json");
errorret_t ret = assetJsonLoaderSync(&ctx.loading);
assert_true(errorIsOk(ret));
// Async phase stat-fails because the file isn't in the ZIP.
ret = assetJsonLoaderAsync(&ctx.loading);
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR);
loader_ctx_dispose(&ctx);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_json_buffer_cleared_after_load(void **state) {
loader_ctx_t ctx;
loader_ctx_init(&ctx, "valid.json");
loader_ctx_run(&ctx);
// The scratch buffer must be freed by the time sync returns LOADED.
assert_null(ctx.loading.loading.json.buffer);
loader_ctx_dispose(&ctx);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
// ============================================================
// main
// ============================================================
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test_setup_teardown(test_json_valid_loads, zip_setup, zip_teardown),
cmocka_unit_test_setup_teardown(test_json_parse_error, zip_setup, zip_teardown),
cmocka_unit_test_setup_teardown(test_json_missing_file, zip_setup, zip_teardown),
cmocka_unit_test_setup_teardown(test_json_buffer_cleared_after_load, zip_setup, zip_teardown),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+423
View File
@@ -0,0 +1,423 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "asset/loader/locale/assetlocaleloader.h"
#include "asset/asset.h"
#include "util/memory.h"
#include <zip.h>
// ============================================================
// Test locale file (gettext PO format)
// ============================================================
static const char_t *LOCALE_EN =
"msgid \"\"\n"
"msgstr \"\"\n"
"\"Plural-Forms: nplurals=2; plural=(n != 1 ? 1 : 0);\\n\"\n"
"\n"
"msgid \"greeting\"\n"
"msgstr \"Hello, World!\"\n"
"\n"
"msgid \"item\"\n"
"msgid_plural \"items\"\n"
"msgstr[0] \"one item\"\n"
"msgstr[1] \"many items\"\n"
"\n"
"msgid \"score\"\n"
"msgstr \"Score: %d\"\n"
"\n"
"msgid \"player\"\n"
"msgstr \"Player: %s\"\n";
// ============================================================
// In-memory ZIP fixture (shared across all ZIP-based tests)
// ============================================================
static zip_t *g_zip = NULL;
static assetlocalefile_t g_locale;
static int locale_setup(void **state) {
zip_error_t err;
zip_error_init(&err);
// Phase 1: write the zip to a growable buffer source
zip_source_t *write_src = zip_source_buffer_create(NULL, 0, 1, &err);
if(!write_src) return -1;
zip_t *za = zip_open_from_source(write_src, ZIP_TRUNCATE, &err);
if(!za) { zip_source_free(write_src); return -1; }
size_t flen = strlen(LOCALE_EN);
zip_source_t *fs = zip_source_buffer(za, LOCALE_EN, flen, 0);
if(zip_file_add(za, "en.locale", fs, ZIP_FL_OVERWRITE) < 0) {
zip_close(za); return -1;
}
// Keep write_src alive after zip_close so we can read the bytes back out
zip_source_keep(write_src);
if(zip_close(za) != 0) { zip_source_free(write_src); return -1; }
// Phase 2: extract the raw zip bytes from the write buffer.
// zip_source_stat must be called before zip_source_open on a written source.
zip_stat_t zs;
memset(&zs, 0, sizeof(zs));
if(zip_source_stat(write_src, &zs) != 0 || !(zs.valid & ZIP_STAT_SIZE)) {
zip_source_free(write_src); return -1;
}
void *zipbuf = malloc((size_t)zs.size);
if(!zipbuf) { zip_source_free(write_src); return -1; }
if(zip_source_open(write_src) != 0) {
free(zipbuf); zip_source_free(write_src); return -1;
}
zip_source_read(write_src, zipbuf, (zip_uint64_t)zs.size);
zip_source_close(write_src);
zip_source_free(write_src);
// Phase 3: open a fresh read-only archive from the extracted bytes.
// The archive takes ownership of the source (and thus zipbuf via freep=1).
zip_error_init(&err);
zip_source_t *read_src = zip_source_buffer_create(
zipbuf, (zip_uint64_t)zs.size, 1, &err
);
if(!read_src) { free(zipbuf); return -1; }
g_zip = zip_open_from_source(read_src, 0, &err);
if(!g_zip) { zip_source_free(read_src); return -1; }
ASSET.zip = g_zip;
// Init locale file and parse the header
memoryZero(&g_locale, sizeof(g_locale));
errorret_t ret = assetFileInit(&g_locale.file, "en.locale", NULL, NULL);
if(errorIsNotOk(ret)) { errorCatch(ret); goto fail; }
ret = assetFileOpen(&g_locale.file);
if(errorIsNotOk(ret)) { errorCatch(ret); goto fail; }
char_t header[512];
ret = assetLocaleGetString(&g_locale, "", 0, header, sizeof(header));
if(errorIsNotOk(ret)) { errorCatch(ret); assetFileClose(&g_locale.file); goto fail; }
ret = assetLocaleParseHeader(&g_locale, header, sizeof(header));
if(errorIsNotOk(ret)) { errorCatch(ret); assetFileClose(&g_locale.file); goto fail; }
return 0;
fail:
zip_close(g_zip); g_zip = NULL;
ASSET.zip = NULL;
return -1;
}
static int locale_teardown(void **state) {
if(g_locale.file.zipFile != NULL) {
errorret_t ret = assetFileClose(&g_locale.file);
if(errorIsNotOk(ret)) errorCatch(ret);
}
if(g_zip != NULL) {
zip_close(g_zip); // also frees the read_src and zipbuf
g_zip = NULL;
}
ASSET.zip = NULL;
memoryZero(&g_locale, sizeof(g_locale));
return 0;
}
// ============================================================
// assetLocaleParseHeader — pure tests (no ZIP required)
// ============================================================
static void test_parseHeader_english(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n != 1 ? 1 : 0);\n";
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_true(errorIsOk(ret));
assert_int_equal(locale.pluralStateCount, 2);
assert_int_equal(locale.pluralDefaultIndex, 0);
assert_int_equal(locale.pluralOps[0], ASSET_LOCALE_PLURAL_OP_NOT_EQUAL);
assert_int_equal(locale.pluralValues[0], 1);
assert_int_equal(locale.pluralIndices[0], 1);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_parseHeader_singular(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: nplurals=1; plural=(0);\n";
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_true(errorIsOk(ret));
assert_int_equal(locale.pluralStateCount, 1);
assert_int_equal(locale.pluralDefaultIndex, 0);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_parseHeader_less_than(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n < 2 ? 0 : 1);\n";
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_true(errorIsOk(ret));
assert_int_equal(locale.pluralStateCount, 2);
assert_int_equal(locale.pluralOps[0], ASSET_LOCALE_PLURAL_OP_LESS);
assert_int_equal(locale.pluralValues[0], 2);
assert_int_equal(locale.pluralIndices[0], 0);
assert_int_equal(locale.pluralDefaultIndex, 1);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_parseHeader_greater_equal(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n >= 2 ? 1 : 0);\n";
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_true(errorIsOk(ret));
assert_int_equal(locale.pluralStateCount, 2);
assert_int_equal(locale.pluralOps[0], ASSET_LOCALE_PLURAL_OP_GREATER_EQUAL);
assert_int_equal(locale.pluralValues[0], 2);
assert_int_equal(locale.pluralIndices[0], 1);
assert_int_equal(locale.pluralDefaultIndex, 0);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_parseHeader_no_plural_forms(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Content-Type: text/plain; charset=UTF-8\n";
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_true(errorIsOk(ret));
assert_int_equal(locale.pluralStateCount, 0);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_parseHeader_error_nplurals_zero(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: nplurals=0; plural=(0);\n";
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_parseHeader_error_nplurals_too_large(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: nplurals=7; plural=(0);\n";
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_parseHeader_error_missing_nplurals(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: plural=(0);\n";
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
// ============================================================
// assetLocaleEvaluatePlural — pure tests
// ============================================================
static void test_evaluatePlural_english_singular(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n != 1 ? 1 : 0);\n";
assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_int_equal(assetLocaleEvaluatePlural(&locale, 1), 0);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_evaluatePlural_english_plural(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n != 1 ? 1 : 0);\n";
assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_int_equal(assetLocaleEvaluatePlural(&locale, 0), 1);
assert_int_equal(assetLocaleEvaluatePlural(&locale, 2), 1);
assert_int_equal(assetLocaleEvaluatePlural(&locale, 100), 1);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_evaluatePlural_singular_only(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: nplurals=1; plural=(0);\n";
assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_int_equal(assetLocaleEvaluatePlural(&locale, 0), 0);
assert_int_equal(assetLocaleEvaluatePlural(&locale, 1), 0);
assert_int_equal(assetLocaleEvaluatePlural(&locale, 99), 0);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_evaluatePlural_less_than_boundary(void **state) {
assetlocalefile_t locale;
memoryZero(&locale, sizeof(locale));
char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n < 2 ? 0 : 1);\n";
assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
assert_int_equal(assetLocaleEvaluatePlural(&locale, 0), 0);
assert_int_equal(assetLocaleEvaluatePlural(&locale, 1), 0);
assert_int_equal(assetLocaleEvaluatePlural(&locale, 2), 1);
assert_int_equal(assetLocaleEvaluatePlural(&locale, 10), 1);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
// ============================================================
// assetLocaleGetString — ZIP-based tests
// ============================================================
static void test_getString_simple(void **state) {
char_t result[256];
errorret_t ret = assetLocaleGetString(&g_locale, "greeting", 0, result, sizeof(result));
assert_true(errorIsOk(ret));
assert_string_equal(result, "Hello, World!");
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_getString_plural_singular(void **state) {
char_t result[256];
errorret_t ret = assetLocaleGetString(&g_locale, "item", 1, result, sizeof(result));
assert_true(errorIsOk(ret));
assert_string_equal(result, "one item");
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_getString_plural_many(void **state) {
char_t result[256];
errorret_t ret = assetLocaleGetString(&g_locale, "item", 5, result, sizeof(result));
assert_true(errorIsOk(ret));
assert_string_equal(result, "many items");
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_getString_multiple_calls(void **state) {
char_t a[256], b[256];
errorret_t ret = assetLocaleGetString(&g_locale, "greeting", 0, a, sizeof(a));
assert_true(errorIsOk(ret));
// Second call rewinds the file and re-reads from scratch.
ret = assetLocaleGetString(&g_locale, "greeting", 0, b, sizeof(b));
assert_true(errorIsOk(ret));
assert_string_equal(a, b);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_getString_missing_id(void **state) {
char_t result[256];
errorret_t ret = assetLocaleGetString(&g_locale, "nonexistent", 0, result, sizeof(result));
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
// ============================================================
// assetLocaleGetStringWithArgs — ZIP-based tests
// ============================================================
static void test_getStringWithArgs_int(void **state) {
assetlocalearg_t args[] = {
{ .type = ASSET_LOCALE_ARG_INT, .intValue = 42 }
};
char_t result[256];
errorret_t ret = assetLocaleGetStringWithArgs(
&g_locale, "score", 0, result, sizeof(result), args, 1
);
assert_true(errorIsOk(ret));
assert_string_equal(result, "Score: 42");
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_getStringWithArgs_string(void **state) {
assetlocalearg_t args[] = {
{ .type = ASSET_LOCALE_ARG_STRING, .stringValue = "Alice" }
};
char_t result[256];
errorret_t ret = assetLocaleGetStringWithArgs(
&g_locale, "player", 0, result, sizeof(result), args, 1
);
assert_true(errorIsOk(ret));
assert_string_equal(result, "Player: Alice");
assert_int_equal(memoryGetAllocatedCount(), 0);
}
// ============================================================
// main
// ============================================================
int main(void) {
const struct CMUnitTest tests[] = {
// parseHeader — pure
cmocka_unit_test(test_parseHeader_english),
cmocka_unit_test(test_parseHeader_singular),
cmocka_unit_test(test_parseHeader_less_than),
cmocka_unit_test(test_parseHeader_greater_equal),
cmocka_unit_test(test_parseHeader_no_plural_forms),
cmocka_unit_test(test_parseHeader_error_nplurals_zero),
cmocka_unit_test(test_parseHeader_error_nplurals_too_large),
cmocka_unit_test(test_parseHeader_error_missing_nplurals),
// evaluatePlural — pure
cmocka_unit_test(test_evaluatePlural_english_singular),
cmocka_unit_test(test_evaluatePlural_english_plural),
cmocka_unit_test(test_evaluatePlural_singular_only),
cmocka_unit_test(test_evaluatePlural_less_than_boundary),
// getString — in-memory ZIP
cmocka_unit_test_setup_teardown(test_getString_simple, locale_setup, locale_teardown),
cmocka_unit_test_setup_teardown(test_getString_plural_singular, locale_setup, locale_teardown),
cmocka_unit_test_setup_teardown(test_getString_plural_many, locale_setup, locale_teardown),
cmocka_unit_test_setup_teardown(test_getString_multiple_calls, locale_setup, locale_teardown),
cmocka_unit_test_setup_teardown(test_getString_missing_id, locale_setup, locale_teardown),
// getStringWithArgs — in-memory ZIP
cmocka_unit_test_setup_teardown(test_getStringWithArgs_int, locale_setup, locale_teardown),
cmocka_unit_test_setup_teardown(test_getStringWithArgs_string, locale_setup, locale_teardown),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}

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