Remove script

This commit is contained in:
2026-06-17 09:54:02 -05:00
parent 43d0593872
commit 0ea6dd9219
234 changed files with 101 additions and 16537 deletions
-21
View File
@@ -1,21 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
const platformNames = {
[System.PLATFORM_LINUX]: 'Linux',
[System.PLATFORM_KNULLI]: 'Knulli',
[System.PLATFORM_PSP]: 'PSP',
[System.PLATFORM_GAMECUBE]: 'GameCube',
[System.PLATFORM_WII]: 'Wii',
};
Console.print('Platform: ' + (platformNames[System.platform] || 'Unknown'));
UIFullboxOver.setColor(Color.BLACK);
requireAsync('testscene.js').then(Scene.set).catch(err => {
Console.print('Error loading scene: ' + err);
Engine.exit();
});
-63
View File
@@ -1,63 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
const PLAYER_SPEED = 5.0;
// 1 world unit = 16 pixels.
const PIXEL_SCALE = 1.0 / 16.0;
// Player sprite is 32x32 px (test.png dimensions).
const PLAYER_W = 32 * PIXEL_SCALE;
const PLAYER_H = 32 * PIXEL_SCALE;
var player = {};
player.getAssets = () => {
return [
{ path: 'test.png', type: Asset.TYPE_TEXTURE, format: Texture.FORMAT_RGBA }
];
}
player.init = function(scene) {
var texture = scene.assets.getAssetByPath('test.png');
Console.print('Player init: got texture ' + texture);
_entity = Entity.create();
_position = _entity.add(Component.POSITION);
_physics = _entity.add(Component.PHYSICS);
_physics.bodyType = Physics.DYNAMIC;
_physics.shape = Physics.SHAPE_CUBE;
_physics.gravityScale = 1.0;
var r = _entity.add(Component.RENDERABLE);
r.texture = texture.texture;
r.type = Renderable.SPRITEBATCH;
r.color = new Color(220, 80, 80);
// Upright quad centered on X, bottom-aligned on Y.
r.sprites = [[-PLAYER_W/2, 0, 0, PLAYER_W/2, PLAYER_H, 0, 0, 1, 1, 0]];
_position.localPosition = new Vec3(0, PLAYER_H, 0);
};
player.getPosition = function() {
return _position;
};
player.update = function() {
if(!_physics) return;
var vx = Input.axis(INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT) * PLAYER_SPEED;
var vz = Input.axis(INPUT_ACTION_DOWN, INPUT_ACTION_UP) * PLAYER_SPEED;
// Preserve vertical velocity so gravity and landing work correctly.
var vy = _physics.velocity.y;
_physics.velocity = new Vec3(vx, vy, vz);
};
player.dispose = function() {
Entity.dispose(_entity);
_entity = null;
_position = null;
_physics = null;
};
module.exports = player;
-42
View File
@@ -1,42 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
var scene = {};
// Pokemon DS-style camera: ~34 degrees elevation (atan(6/9)).
// CAM_HEIGHT / CAM_DIST ratio controls the tilt - keep it under 0.7 for
// the characteristically shallow DS angle.
const CAM_HEIGHT = 6;
const CAM_DIST = 9;
scene.init = async function() {
// Camera
scene.cam = Entity.create();
var camPos = scene.cam.add(Component.POSITION);
var cam = scene.cam.add(Component.CAMERA);
camPos.localPosition = new Vec3(3, 3, 3);
camPos.lookAt(new Vec3(0, 0, 0));
// Floor - large flat slab, no texture needed.
scene.floor = Entity.create();
var floorPos = scene.floor.add(Component.POSITION);
var floorR = scene.floor.add(Component.RENDERABLE);
floorR.type = Renderable.SHADER_MATERIAL;
floorR.color = Color.BLUE;
// floorPos.localScale = new Vec3(16, 0.2, 16);
// floorPos.localPosition = new Vec3(0, -0.1, 0);
await UIFullboxOver.transition(Color.BLACK, Color.TRANSPARENT, 1.0);
};
scene.update = function() {
};
scene.dispose = function() {
Entity.dispose(scene.floor);
Entity.dispose(scene.cam);
};
module.exports = scene;
-6
View File
@@ -1,6 +0,0 @@
module = {
render() {
Text.draw(0, 0, "Hello World");
SpriteBatch.flush();
}
};
-35
View File
@@ -9,37 +9,6 @@ if(NOT cglm_FOUND)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC cglm) target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC cglm)
endif() endif()
if(NOT libzip_FOUND)
find_package(libzip REQUIRED)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC zip)
endif()
if(NOT stb_image_FOUND)
find_package(stb REQUIRED)
if(STB_IMAGE_FOUND)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC stb_image)
else()
message(FATAL_ERROR "stb_image not found. Please ensure stb is correctly fetched.")
endif()
endif()
if(NOT yyjson_FOUND)
find_package(yyjson REQUIRED)
if(yyjson_FOUND)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC yyjson::yyjson)
else()
message(FATAL_ERROR "yyjson not found. Please ensure yyjson is correctly fetched.")
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) if(DUSK_BACKTRACE)
target_link_options(${DUSK_LIBRARY_TARGET_NAME} PUBLIC -rdynamic) target_link_options(${DUSK_LIBRARY_TARGET_NAME} PUBLIC -rdynamic)
@@ -64,8 +33,6 @@ target_sources(${DUSK_BINARY_TARGET_NAME}
add_subdirectory(animation) add_subdirectory(animation)
add_subdirectory(event) add_subdirectory(event)
add_subdirectory(assert) add_subdirectory(assert)
add_subdirectory(asset)
add_subdirectory(cutscene)
add_subdirectory(item) add_subdirectory(item)
add_subdirectory(story) add_subdirectory(story)
add_subdirectory(console) add_subdirectory(console)
@@ -77,8 +44,6 @@ add_subdirectory(error)
add_subdirectory(input) add_subdirectory(input)
add_subdirectory(locale) add_subdirectory(locale)
add_subdirectory(physics) add_subdirectory(physics)
add_subdirectory(scene)
add_subdirectory(script)
add_subdirectory(system) add_subdirectory(system)
add_subdirectory(time) add_subdirectory(time)
add_subdirectory(ui) add_subdirectory(ui)
-15
View File
@@ -1,15 +0,0 @@
# Copyright (c) 2025 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
asset.c
assetbatch.c
assetfile.c
)
# Subdirs
add_subdirectory(loader)
-450
View File
@@ -1,450 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "asset.h"
#include "util/memory.h"
#include "util/string.h"
#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);
errorOk();
}
bool_t assetFileExists(const char_t *filename) {
assertStrLenMax(filename, ASSET_FILE_NAME_MAX, "Filename too long.");
zip_int64_t idx = zip_name_locate(ASSET.zip, filename, 0);
if(idx < 0) return false;
return true;
}
assetentry_t * assetGetEntry(
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
) {
// 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;
}
uint32_t assetGetEntriesOfType(
assetentry_t **outEntries,
const assetloadertype_t type
) {
assertNotNull(outEntries, "Output entries cannot be NULL.");
uint32_t count = 0;
assetentry_t *entry = ASSET.entries;
do {
if(entry->type == type) {
outEntries[count++] = entry;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
return count;
}
errorret_t assetRequireLoaded(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL.");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertIsMainThread("Currently only works on main thread.");
if(entry->state == ASSET_ENTRY_STATE_LOADED) {
errorOk();
}
// Lock to prevent the reaper from collecting the entry mid-spin.
assetEntryLock(entry);
while(entry->state != ASSET_ENTRY_STATE_LOADED) {
usleep(1000);
errorret_t ret = assetUpdate();
if(errorIsNotOk(ret)) {
assetEntryUnlock(entry);
errorChain(ret);
}
}
assetEntryUnlock(entry);
errorOk();
}
errorret_t assetRequireDisposed(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL.");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertIsMainThread("Currently only works on main thread.");
if(entry->type == ASSET_LOADER_TYPE_NULL) {
errorOk();
}
if(
entry->state == ASSET_ENTRY_STATE_NOT_STARTED ||
entry->state == ASSET_ENTRY_STATE_ERROR
) {
errorOk();
}
assertTrue(
entry->refs.count == 0,
"Cannot require disposal of an entry with active references."
);
// Lock to prevent the reaper from collecting the entry mid-spin.
assetEntryLock(entry);
while(entry->type != ASSET_LOADER_TYPE_NULL) {
usleep(1000);
errorret_t ret = assetUpdate();
if(errorIsNotOk(ret)) {
assetEntryUnlock(entry);
errorChain(ret);
}
}
assetEntryUnlock(entry);
errorOk();
}
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) {
assertIsMainThread("assetUpdate must be called from the main thread.");
// 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:
loading->entry->state = ASSET_ENTRY_STATE_LOADING_SYNC;
// Unlock before calling loadSync. The sync loader may re-enter
// assetUpdate (e.g. a script loading another asset), and the async
// thread never touches LOADING_SYNC entries, so this is safe.
threadMutexUnlock(&loading->mutex);
errorret_t ret = (
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(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
assertTrue(
loading->entry->state == ASSET_ENTRY_STATE_ERROR,
"Loader did not set entry state to error on failed load."
);
} else if(loading->entry->state == ASSET_ENTRY_STATE_LOADED) {
eventInvoke(&loading->entry->onLoaded, loading->entry);
}
loading++;
break;
case ASSET_ENTRY_STATE_LOADING_SYNC:
// A re-entrant assetUpdate call (e.g. from a script loading another
// asset) will see this entry mid-sync-load. Skip it.
threadMutexUnlock(&loading->mutex);
loading++;
continue;
// 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: {
assetentry_t *errEntry = loading->entry;
loading->entry = NULL;
threadMutexUnlock(&loading->mutex);
eventInvoke(&errEntry->onError, errEntry);
errorThrow("Failed to load asset asynchronously.");
break;
}
default:
threadMutexUnlock(&loading->mutex);
loading++;
continue;
}
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
// Reap unused entries.
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) {
assertNotMainThread("assetUpdateAsync must not run on the main 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) {
assertIsMainThread("Must be called from the main thread.");
threadStop(&ASSET.loadThread);
// Free any script read-buffers left behind by an in-flight async load
// that was interrupted before the sync eval phase ran.
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
assetloading_t *loading = &ASSET.loading[i];
if(
loading->entry != NULL &&
loading->type == ASSET_LOADER_TYPE_SCRIPT &&
loading->loading.script.buffer != NULL
) {
memoryFree(loading->loading.script.buffer);
loading->loading.script.buffer = NULL;
}
threadMutexDispose(&loading->mutex);
}
// Dispose every non-null entry so type-specific dispose callbacks
// (e.g. assetScriptDispose freeing jerry values) run before the
// scripting engine is torn down.
assetentry_t *entry = ASSET.entries;
do {
if(entry->type != ASSET_LOADER_TYPE_NULL) {
errorChain(assetEntryDispose(entry));
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
// Cleanup zip file.
if(ASSET.zip != NULL) {
if(zip_close(ASSET.zip) != 0) {
errorThrow("Failed to close asset zip archive.");
}
ASSET.zip = NULL;
}
errorChain(assetDisposePlatform());
errorOk();
}
-153
View File
@@ -1,153 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "asset/assetplatform.h"
#include "assetfile.h"
#include "thread/thread.h"
#include "asset/loader/assetentry.h"
#include "asset/loader/assetloading.h"
#ifndef assetInitPlatform
#error "Platform must define assetInitPlatform function."
#endif
#ifndef assetDisposePlatform
#error "Platform must define assetDisposePlatform function."
#endif
#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;
// 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);
/**
* Checks if an asset file exists.
*
* @param filename The filename of the asset to check.
* @return true if the asset file exists, false otherwise.
*/
bool_t assetFileExists(const char_t *filename);
/**
* 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.
*/
assetentry_t * assetGetEntry(
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
);
/**
* Gets all asset entries of a given type.
*
* @param outEntries Output array to write the entries to.
* @param type Type of the asset entries to get.
* @return The number of entries written to outEntries.
*/
uint32_t assetGetEntriesOfType(
assetentry_t **outEntries,
const assetloadertype_t type
);
/**
* Gets, creates, and locks an asset entry. The asset will begin loading on
* 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);
/**
* Requires an asset entry to be disposed. This will block until the asset entry
* is fully disposed.
*
* @param entry The asset entry to require disposal of.
* @return An error code if the asset entry could not be disposed properly.
*/
errorret_t assetRequireDisposed(assetentry_t *entry);
/**
* Updates the asset system.
*
* @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.
*
* @return An error code if the asset system could not be disposed properly.
*/
errorret_t assetDispose(void);
-165
View File
@@ -1,165 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetbatch.h"
#include "asset.h"
#include "assert/assert.h"
#include "util/memory.h"
#include <unistd.h>
void assetBatchInit(
assetbatch_t *batch,
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->count = count;
eventInit(
&batch->onLoaded,
batch->onLoadedCallbacks, batch->onLoadedUsers, ASSET_BATCH_EVENT_MAX
);
eventInit(
&batch->onEntryLoaded,
batch->onEntryLoadedCallbacks,
batch->onEntryLoadedUsers,
ASSET_BATCH_EVENT_MAX
);
eventInit(
&batch->onError,
batch->onErrorCallbacks, batch->onErrorUsers, ASSET_BATCH_EVENT_MAX
);
eventInit(
&batch->onEntryError,
batch->onEntryErrorCallbacks,
batch->onEntryErrorUsers,
ASSET_BATCH_EVENT_MAX
);
for(uint16_t i = 0; i < count; i++) {
batch->inputs[i] = descs[i].input;
batch->entries[i] = assetLock(
descs[i].path, descs[i].type, &batch->inputs[i]
);
if(batch->entries[i]->state == ASSET_ENTRY_STATE_LOADED) {
// Already loaded (cached) - count it now, no subscription needed.
batch->loadedCount++;
} else if(batch->entries[i]->state == ASSET_ENTRY_STATE_ERROR) {
batch->errorCount++;
} else {
eventSubscribe(
&batch->entries[i]->onLoaded, assetBatchEntryOnLoadedCb, batch
);
eventSubscribe(
&batch->entries[i]->onError, assetBatchEntryOnErrorCb, batch
);
}
}
}
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 assetBatchUnlock(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
assetEntryUnlock(batch->entries[i]);
}
}
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;
}
}
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++) {
if(batch->entries[i]) {
// Unsubscribe while we still hold a lock so the entry is live.
eventUnsubscribe(&batch->entries[i]->onLoaded, assetBatchEntryOnLoadedCb);
eventUnsubscribe(&batch->entries[i]->onError, assetBatchEntryOnErrorCb);
assetUnlockEntry(batch->entries[i]);
}
}
memoryZero(batch, sizeof(assetbatch_t));
}
void assetBatchEntryOnLoadedCb(void *params, void *user) {
assetentry_t *entry = (assetentry_t *)params;
assetbatch_t *batch = (assetbatch_t *)user;
batch->loadedCount++;
eventInvoke(&batch->onEntryLoaded, entry);
if((uint16_t)(batch->loadedCount + batch->errorCount) >= batch->count) {
if(batch->errorCount == 0) {
eventInvoke(&batch->onLoaded, batch);
} else {
eventInvoke(&batch->onError, batch);
}
}
}
void assetBatchEntryOnErrorCb(void *params, void *user) {
assetentry_t *entry = (assetentry_t *)params;
assetbatch_t *batch = (assetbatch_t *)user;
batch->errorCount++;
eventInvoke(&batch->onEntryError, entry);
if((uint16_t)(batch->loadedCount + batch->errorCount) >= batch->count) {
eventInvoke(&batch->onError, batch);
}
}
-124
View File
@@ -1,124 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/loader/assetentry.h"
#include "asset/loader/assetloader.h"
#include "event/event.h"
#define ASSET_BATCH_COUNT_MAX 64
#define ASSET_BATCH_EVENT_MAX 4
typedef struct {
const char_t *path;
assetloadertype_t type;
assetloaderinput_t input;
} assetbatchdesc_t;
typedef struct {
assetentry_t *entries[ASSET_BATCH_COUNT_MAX];
assetloaderinput_t inputs[ASSET_BATCH_COUNT_MAX];
uint16_t count;
uint16_t loadedCount;
uint16_t errorCount;
/** Fires once when every entry loaded. params = assetbatch_t * */
event_t onLoaded;
eventcallback_t onLoadedCallbacks[ASSET_BATCH_EVENT_MAX];
void *onLoadedUsers[ASSET_BATCH_EVENT_MAX];
/** Fires each time a single entry loads. params = assetentry_t * */
event_t onEntryLoaded;
eventcallback_t onEntryLoadedCallbacks[ASSET_BATCH_EVENT_MAX];
void *onEntryLoadedUsers[ASSET_BATCH_EVENT_MAX];
/** Fires when all entries finish (any with errors). params: assetbatch_t * */
event_t onError;
eventcallback_t onErrorCallbacks[ASSET_BATCH_EVENT_MAX];
void *onErrorUsers[ASSET_BATCH_EVENT_MAX];
/** Fires each time a single entry errors. params = assetentry_t * */
event_t onEntryError;
eventcallback_t onEntryErrorCallbacks[ASSET_BATCH_EVENT_MAX];
void *onEntryErrorUsers[ASSET_BATCH_EVENT_MAX];
} assetbatch_t;
/**
* Initialises the batch from an array of descriptors. Each entry is locked
* and queued for loading immediately.
*
* @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,
uint16_t count,
const assetbatchdesc_t *descs
);
/**
* Acquires one additional lock on every entry in the batch.
*
* @param batch Batch to lock.
*/
void assetBatchLock(assetbatch_t *batch);
/**
* 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 Batch to unlock.
*/
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);
/**
* Event trampoline invoked when a batch entry finishes loading.
* Increments the loaded counter and fires batch-level events.
*
* @param params The loaded assetentry_t pointer.
* @param user The owning assetbatch_t pointer.
*/
void assetBatchEntryOnLoadedCb(void *params, void *user);
/**
* Event trampoline invoked when a batch entry fails to load.
* Increments the error counter and fires batch-level events.
*
* @param params The errored assetentry_t pointer.
* @param user The owning assetbatch_t pointer.
*/
void assetBatchEntryOnErrorCb(void *params, void *user);
-380
View File
@@ -1,380 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "asset/asset.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/math.h"
errorret_t assetFileInit(
assetfile_t *file,
const char_t *filename,
void *params,
void *output
) {
assertNotNull(file, "Asset file cannot be NULL.");
assertStrLenMax(filename, ASSET_FILE_NAME_MAX, "Filename too long.");
memoryZero(file, sizeof(assetfile_t));
memoryCopy(file->filename, filename, ASSET_FILE_NAME_MAX);
file->params = params;
file->output = output;
// Stat the file
zip_stat_init(&file->stat);
if(!zip_stat(ASSET.zip, filename, 0, &file->stat) == 0) {
errorThrow("Failed to stat asset file: %s", filename);
}
// Minimum file size.
file->size = (zip_int64_t)file->stat.size;
if(file->size <= 0) {
errorThrow("Invalid asset file size: %s", filename);
}
errorOk();
}
errorret_t assetFileRewind(assetfile_t *file) {
assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(file->zipFile, "Asset file must be opened before rewinding.");
if(file->position == 0) {
errorOk();
}
errorChain(assetFileClose(file));
errorChain(assetFileOpen(file));
errorOk();
}
errorret_t assetFileOpen(assetfile_t *file) {
assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(file->filename, "Asset file filename cannot be NULL.");
assertNotNull(ASSET.zip, "Asset zip cannot be NULL.");
assertNull(file->zipFile, "Asset file already open.");
file->zipFile = zip_fopen(ASSET.zip, file->filename, 0);
if(file->zipFile == NULL) {
errorThrow("Failed to open asset file: %s", file->filename);
}
file->position = 0;
errorOk();
}
errorret_t assetFileRead(
assetfile_t *file,
void *buffer,
const size_t bufferSize
) {
assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(file->zipFile, "Asset file must be opened before reading.");
if(buffer == NULL) {
size_t bytesRemaining = bufferSize;
uint8_t tempBuffer[256];
while(bytesRemaining > 0) {
size_t chunkSize = mathMin(bytesRemaining, sizeof(tempBuffer));
errorChain(assetFileRead(file, tempBuffer, chunkSize));
file->position += chunkSize;
bytesRemaining -= chunkSize;
}
file->lastRead = bufferSize;
errorOk();
}
// I assume zip_fread takes buffer NULL for skipping?
zip_int64_t bytesRead = zip_fread(file->zipFile, buffer, bufferSize);
if(bytesRead < 0) {
errorThrow("Failed to read from asset file: %s", file->filename);
}
file->position += bytesRead;
file->lastRead = bytesRead;
errorOk();
}
errorret_t assetFileClose(assetfile_t *file) {
assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(file->zipFile, "Asset file must be opened before closing.");
if(zip_fclose(file->zipFile) != 0) {
errorThrow("Failed to close asset file: %s", file->filename);
}
file->zipFile = NULL;
file->position = 0;
errorOk();
}
errorret_t assetFileDispose(assetfile_t *file) {
if(file->zipFile != NULL) {
errorChain(assetFileClose(file));
}
memoryZero(file, sizeof(assetfile_t));
errorOk();
}
errorret_t assetFileReadEntire(
assetfile_t *file,
uint8_t **outBuffer,
size_t *outSize
) {
assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(outBuffer, "outBuffer cannot be NULL.");
assertNotNull(outSize, "outSize cannot be NULL.");
assertTrue(
file->size > 0,
"Asset file has no size; call assetFileInit first."
);
// File should be closed currently.
assertNull(file->zipFile, "Asset file must be closed before reading entire.");
// Open file
errorret_t ret = assetFileOpen(file);
if(errorIsNotOk(ret)) {
errorChain(ret);
}
// Set output.
size_t size = (size_t)file->size;
uint8_t *buffer = (uint8_t *)memoryAllocate(size);
// Read entire file.
ret = assetFileRead(file, buffer, size);
if(errorIsNotOk(ret)) {
memoryFree(buffer);
errorChain(ret);
}
// Close the file.
ret = assetFileClose(file);
if(errorIsNotOk(ret)) {
memoryFree(buffer);
errorChain(ret);
}
*outBuffer = buffer;
*outSize = size;
errorOk();
}
// Line Reader;
void assetFileLineReaderInit(
assetfilelinereader_t *reader,
assetfile_t *file,
uint8_t *readBuffer,
const size_t readBufferSize,
uint8_t *outBuffer,
const size_t outBufferSize
) {
assertNotNull(reader, "Line reader cannot be NULL.");
assertNotNull(file, " File cannot be NULL.");
assertNotNull(readBuffer, "Read buffer cannot be NULL.");
assertNotNull(outBuffer, "Output buffer cannot be NULL.");
assertTrue(readBufferSize > 0, "Read buffer size must be greater than 0.");
assertTrue(outBufferSize > 0, "Output buffer size must be greater than 0.");
memoryZero(reader, sizeof(assetfilelinereader_t));
reader->file = file;
reader->readBuffer = readBuffer;
reader->readBufferSize = readBufferSize;
reader->outBuffer = outBuffer;
reader->outBufferSize = outBufferSize;
}
size_t assetFileLineReaderUnreadBytes(const assetfilelinereader_t *reader) {
assertNotNull(reader, "Reader cannot be NULL.");
assertTrue(reader->bufferEnd >= reader->bufferStart, "Invalid buffer state.");
return reader->bufferEnd - reader->bufferStart;
}
const uint8_t *assetFileLineReaderUnreadPtr(
const assetfilelinereader_t *reader
) {
assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->readBuffer, "Read buffer cannot be NULL.");
return reader->readBuffer + reader->bufferStart;
}
static errorret_t assetFileLineReaderAppend(
assetfilelinereader_t *reader,
const uint8_t *src,
size_t srcLength
) {
assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->outBuffer, "Out buffer cannot be NULL.");
if(srcLength == 0) {
errorOk();
}
/* reserve room for optional NUL terminator */
if(reader->lineLength + srcLength >= reader->outBufferSize) {
errorThrow("Line length exceeds output buffer size.");
}
memoryCopy(reader->outBuffer + reader->lineLength, src, srcLength);
reader->lineLength += srcLength;
errorOk();
}
static void assetFileLineReaderTerminate(assetfilelinereader_t *reader) {
assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->outBuffer, "Out buffer cannot be NULL.");
assertTrue(
reader->lineLength < reader->outBufferSize,
"Line length exceeds out buffer."
);
reader->outBuffer[reader->lineLength] = '\0';
}
static ssize_t assetFileLineReaderFindNewline(
const assetfilelinereader_t *reader
) {
size_t i;
assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->readBuffer, "Read buffer cannot be NULL.");
for(i = reader->bufferStart; i < reader->bufferEnd; ++i) {
if(reader->readBuffer[i] == '\n') {
return (ssize_t)i;
}
}
return -1;
}
errorret_t assetFileLineReaderFill(assetfilelinereader_t *reader) {
assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->file, "File cannot be NULL.");
assertNotNull(reader->readBuffer, "Read buffer cannot be NULL.");
if(reader->eof) errorOk();
errorret_t ret;
size_t unreadBytes = assetFileLineReaderUnreadBytes(reader);
/* If buffer is fully consumed, refill from start. */
if(unreadBytes == 0) {
reader->bufferStart = 0;
reader->bufferEnd = 0;
errorChain(assetFileRead(
reader->file,
reader->readBuffer,
reader->readBufferSize
));
if(reader->file->lastRead == 0) {
reader->eof = true;
errorOk();
}
reader->bufferStart = 0;
reader->bufferEnd = reader->file->lastRead;
errorOk();
}
/*
* There are unread bytes left but no newline in them.
* If bufferStart > 0, slide unread bytes to front so we can read more.
* This only happens when necessary to make space.
*/
if(reader->bufferEnd == reader->readBufferSize) {
if(reader->bufferStart == 0) {
/*
* Entire read buffer is unread and contains no newline.
* Caller must have a large enough outBuffer to accumulate across fills,
* so we consume these bytes into outBuffer before refilling.
*/
errorOk();
}
memoryMove(
reader->readBuffer,
reader->readBuffer + reader->bufferStart,
unreadBytes
);
reader->bufferStart = 0;
reader->bufferEnd = unreadBytes;
}
errorChain(assetFileRead(
reader->file,
reader->readBuffer + reader->bufferEnd,
reader->readBufferSize - reader->bufferEnd
));
if(reader->file->lastRead == 0) {
reader->eof = true;
errorOk();
}
reader->bufferEnd += reader->file->lastRead;
errorOk();
}
errorret_t assetFileLineReaderNext(assetfilelinereader_t *reader) {
assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->file, "File cannot be NULL.");
assertNotNull(reader->readBuffer, "Read buffer cannot be NULL.");
assertNotNull(reader->outBuffer, "Out buffer cannot be NULL.");
reader->lineLength = 0;
for(;;) {
ssize_t newlineIndex = assetFileLineReaderFindNewline(reader);
if(newlineIndex >= 0) {
size_t chunkLength = (size_t)newlineIndex - reader->bufferStart;
errorret_t ret;
/* strip CR in CRLF */
if(
chunkLength > 0 &&
reader->readBuffer[(size_t)newlineIndex - 1] == '\r'
) {
chunkLength--;
}
errorChain(assetFileLineReaderAppend(
reader,
reader->readBuffer + reader->bufferStart,
chunkLength
));
reader->bufferStart = (size_t)newlineIndex + 1;
assetFileLineReaderTerminate(reader);
errorOk();
}
if(assetFileLineReaderUnreadBytes(reader) > 0) {
errorChain(assetFileLineReaderAppend(
reader,
assetFileLineReaderUnreadPtr(reader),
assetFileLineReaderUnreadBytes(reader)
));
reader->bufferStart = reader->bufferEnd;
}
if(reader->eof) {
if(reader->lineLength > 0) {
assetFileLineReaderTerminate(reader);
errorOk();
}
errorThrow("End of file reached.");
}
errorChain(assetFileLineReaderFill(reader));
}
}
-156
View File
@@ -1,156 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include <zip.h>
#define ASSET_FILE_NAME_MAX 48
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;
void *output;
zip_stat_t stat;
zip_int64_t size;
zip_int64_t position;
zip_int64_t lastRead;
zip_file_t *zipFile;
} assetfile_t;
/**
* Initializes the asset file structure in preparation for loading. This will
* stat the file but not open the handle.
*
* @param file The asset file structure to initialize.
* @param filename The name of the asset file to load.
* @param params Optional loader params.
* @param output Output pointer for the loader.
* @return Error indicating success or failure.
*/
errorret_t assetFileInit(
assetfile_t *file,
const char_t *filename,
void *params,
void *output
);
/**
* Opens the asset file for reading. After opening the loader is responsible
* for closing the file.
*
* @param file The asset file to open.
* @return An error code if the file could not be opened.
*/
errorret_t assetFileOpen(assetfile_t *file);
/**
* Rewind the file to the initial position.
*
* @param file The asset file to rewind.
*/
errorret_t assetFileRewind(assetfile_t *file);
/**
* Read bytes from the asset file. Assumes the file has already been opened
* prior to trying to read anything.
*
* @param file File to read from.
* @param buffer Buffer to read the file data into., or NULL to skip bytes.
* @param size Size of the buffer to read into.
*/
errorret_t assetFileRead(
assetfile_t *file,
void *buffer,
const size_t bufferSize
);
/**
* Closes the asset file and releases any resources associated with it.
*
* @param file The asset file to close.
* @return An error code if the file could not be closed properly.
*/
errorret_t assetFileClose(assetfile_t *file);
/**
* Disposes the asset file structure, closing any open handles and zeroing
* out the structure.
*
* @param file The asset file to dispose.
* @return An error code if the file could not be disposed properly.
*/
errorret_t assetFileDispose(assetfile_t *file);
/**
* Reads the entire contents of the asset file into a newly allocated buffer.
* The caller is responsible for freeing the buffer with memoryFree.
*
* @param file The asset file to read. Must be initialized but not open.
* @param outBuffer Receives a pointer to the allocated buffer.
* @param outSize Receives the number of bytes written to the buffer.
* @return An error code if the file could not be read.
*/
errorret_t assetFileReadEntire(
assetfile_t *file,
uint8_t **outBuffer,
size_t *outSize
);
typedef struct {
assetfile_t *file;
uint8_t *readBuffer;
size_t readBufferSize;
uint8_t *outBuffer;
size_t outBufferSize;
// A
size_t bufferStart;
size_t bufferEnd;
bool_t eof;//?
// Updated each reach:
size_t lineLength;
} assetfilelinereader_t;
/**
* Initializes a line reader for the given asset file. The line reader will read
* lines from the file into the provided line buffer, using the provided buffer
* for reading chunks of the file.
*
* @param file The asset file to read from. Must already be opened.
* @param readBuffer Buffer to use for reading chunks of the file.
* @param readBufferSize Size of the read buffer.
* @param outBuffer Buffer to read lines into. Lines will be null-terminated.
* @param outBufferSize Size of the output buffer.
* @return An initialized line reader structure.
*/
void assetFileLineReaderInit(
assetfilelinereader_t *reader,
assetfile_t *file,
uint8_t *readBuffer,
const size_t readBufferSize,
uint8_t *outBuffer,
const size_t outBufferSize
);
/**
* Reads the next line from the asset file into the line buffer. The line
* buffer is null-terminated and does not include the newline character.
*
* @param reader The line reader to read from.
* @return An error code if a failure occurs, or errorOk() if a line was read
* successfully. If the end of the file is reached, errorEndOfFile() is
* returned.
*/
errorret_t assetFileLineReaderNext(assetfilelinereader_t *reader);
-18
View File
@@ -1,18 +0,0 @@
# 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
assetentry.c
assetloader.c
)
# Subdirs
add_subdirectory(display)
add_subdirectory(locale)
add_subdirectory(json)
add_subdirectory(script)
-104
View File
@@ -1,104 +0,0 @@
/**
* 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.");
assertIsMainThread("Must be called from the main thread.");
memoryZero(entry, sizeof(assetentry_t));
stringCopy(entry->name, name, ASSET_FILE_NAME_MAX);
entry->type = type;
entry->state = ASSET_ENTRY_STATE_NOT_STARTED;
if(input) {
entry->inputData = *input;
entry->input = &entry->inputData;
} else {
memoryZero(&entry->inputData, sizeof(assetloaderinput_t));
entry->input = NULL;
}
refInit(&entry->refs, entry, NULL, NULL, NULL);
eventInit(
&entry->onLoaded,
entry->onLoadedCallbacks, entry->onLoadedUsers,
ASSET_ENTRY_EVENT_MAX
);
eventInit(
&entry->onUnloaded,
entry->onUnloadedCallbacks, entry->onUnloadedUsers,
ASSET_ENTRY_EVENT_MAX
);
eventInit(
&entry->onError,
entry->onErrorCallbacks, entry->onErrorUsers,
ASSET_ENTRY_EVENT_MAX
);
}
void assetEntryLock(assetentry_t *entry) {
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."
);
assertIsMainThread("Must be called from the main thread.");
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.");
assertIsMainThread("Must be called from the main thread.");
assertTrue(
entry->refs.count == 0,
"Asset entry still refed at dispose time."
);
eventInvoke(&entry->onUnloaded, entry);
errorChain(ASSET_LOADER_CALLBACKS[entry->type].dispose(entry));
memoryZero(entry, sizeof(assetentry_t));
errorOk();
}
-112
View File
@@ -1,112 +0,0 @@
/**
* 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 "event/event.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;
/** Maximum number of subscribers for each per-entry event. */
#define ASSET_ENTRY_EVENT_MAX 2
typedef struct assetentry_s assetentry_t;
struct assetentry_s {
char_t name[ASSET_FILE_NAME_MAX];
assetloadertype_t type;
assetloaderoutput_t data;
assetentrystate_t state;
ref_t refs;
assetloaderinput_t *input;
assetloaderinput_t inputData;
/**
* Fired once when loading completes successfully (params = assetentry_t *).
* Always invoked on the main thread.
*/
event_t onLoaded;
eventcallback_t onLoadedCallbacks[ASSET_ENTRY_EVENT_MAX];
void *onLoadedUsers[ASSET_ENTRY_EVENT_MAX];
/**
* Fired once when the entry is disposed/reaped (params = assetentry_t *).
* The asset data is still accessible when the callback runs.
* Always invoked on the main thread.
*/
event_t onUnloaded;
eventcallback_t onUnloadedCallbacks[ASSET_ENTRY_EVENT_MAX];
void *onUnloadedUsers[ASSET_ENTRY_EVENT_MAX];
/**
* Fired once when loading fails (params = assetentry_t *).
* Always invoked on the main thread.
*/
event_t onError;
eventcallback_t onErrorCallbacks[ASSET_ENTRY_EVENT_MAX];
void *onErrorUsers[ASSET_ENTRY_EVENT_MAX];
};
/**
* Initializes an asset entry with the given name and type. This does not load
* 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.
* Fires the onUnloaded event before releasing asset data.
*
* @param entry The asset entry to dispose.
* @return Any error that occurs during disposal.
*/
errorret_t assetEntryDispose(assetentry_t *entry);
-48
View File
@@ -1,48 +0,0 @@
/**
* 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
@@ -1,96 +0,0 @@
/**
* 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__); \
}
-26
View File
@@ -1,26 +0,0 @@
/**
* 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 {
threadmutex_t mutex;
assetloadertype_t type;
assetentry_t *entry;
assetloaderloading_t loading;
} assetloading_t;
typedef errorret_t (assetloadingcallback_t)(assetloading_t *loading);
typedef struct {
assetloadingcallback_t *loadSync;
} assetloadingcallbacks_t;
@@ -1,12 +0,0 @@
# 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
assetmeshloader.c
assettextureloader.c
assettilesetloader.c
)
@@ -1,180 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#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 assetMeshLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
if(loading->loading.mesh.state != ASSET_MESH_LOADING_STATE_READ_FILE) {
errorOk();
}
assetmeshoutput_t *out = &loading->entry->data.mesh;
assetfile_t *file = &loading->loading.mesh.file;
assetmeshinputaxis_t axis = loading->entry->inputData.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;
assetLoaderErrorChain(loading,
assetFileRead(file, &triangleCount, sizeof(uint32_t))
);
if(file->lastRead != sizeof(uint32_t)) {
assetLoaderErrorThrow(loading, "Failed to read tri count");
}
triangleCount = endianLittleToHost32(triangleCount);
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(errorIsNotOk(ret)) {
memoryFree(verts);
out->vertices = NULL;
assetLoaderErrorChain(loading, ret);
}
if(file->lastRead != sizeof(triData)) {
memoryFree(verts);
out->vertices = NULL;
assetLoaderErrorThrow(loading, "Failed to read triangle 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.a = 0xFF;
#endif
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]
);
}
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: {
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:
verts[i * 3 + j].pos[1] = -verts[i * 3 + j].pos[1];
break;
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: {
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:
break;
}
}
}
ret = assetFileClose(file);
if(errorIsNotOk(ret)) {
memoryFree(verts);
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 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 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();
}
@@ -1,58 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#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,
MESH_INPUT_AXIS_Z_UP,
MESH_INPUT_AXIS_X_UP,
MESH_INPUT_AXIS_Y_DOWN,
MESH_INPUT_AXIS_Z_DOWN,
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 {
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)
typedef struct {
vec3 normal;
float_t positions[3][3];
uint16_t attributeByteCount;
} assetmeshstltriangle_t;
#pragma pack(pop)
assertStructSize(assetmeshstltriangle_t, 50);
errorret_t assetMeshLoaderAsync(assetloading_t *loading);
errorret_t assetMeshLoaderSync(assetloading_t *loading);
errorret_t assetMeshDispose(assetentry_t *entry);
@@ -1,171 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assettextureloader.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,
.skip = assetTextureSkipper,
.eof = assetTextureEOF
};
int assetTextureReader(void *user, char *data, int size) {
assertNotNull(data, "Data buffer for stb_image callbacks cannot be NULL.");
assetfile_t *file = (assetfile_t*)user;
assertNotNull(file, "Asset file in stb_image callbacks cannot be NULL.");
errorret_t ret = assetFileRead(file, data, (size_t)size);
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
return -1;
}
return file->lastRead;
}
void assetTextureSkipper(void *user, int n) {
assetfile_t *file = (assetfile_t*)user;
assertNotNull(file, "Asset file in stb_image callbacks cannot be NULL.");
errorret_t ret = assetFileRead(file, NULL, (size_t)n);
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
}
}
int assetTextureEOF(void *user) {
assetfile_t *file = (assetfile_t*)user;
assertNotNull(file, "Asset file in stb_image callbacks cannot be NULL.");
return file->position >= file->size;
}
errorret_t assetTextureLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread.");
// 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(loading->entry->inputData.texture) {
case TEXTURE_FORMAT_RGBA:
channelsDesired = 4;
break;
default:
assetLoaderErrorThrow(loading, "Bad texture format.");
}
// Load image pixels.
loading->loading.texture.data = stbi_load_from_callbacks(
&ASSET_TEXTURE_STB_CALLBACKS,
file,
&loading->loading.texture.width,
&loading->loading.texture.height,
&loading->loading.texture.channels,
channelsDesired
);
// 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();
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(
loading->loading.texture.data,
loading->loading.texture.width,
loading->loading.texture.height,
loading->loading.texture.channels
);
}
loading->loading.texture.state = ASSET_TEXTURE_LOADING_STATE_CREATE_TEXTURE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
errorret_t assetTextureLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertIsMainThread("Must be called from the main thread.");
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;
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->inputData.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");
assertIsMainThread("Must be called from the main thread.");
return textureDispose(&entry->data.texture);
}
@@ -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 "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 {
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.
*
* @param user User data passed to the callback, should be an assetfile_t*.
* @param data Buffer to read the file data into.
* @param size Size of the buffer to read into.
* @return Number of bytes read, or -1 on error.
*/
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);
/**
* Synchronous loader for texture assets.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetTextureLoaderAsync(assetloading_t *loading);
/**
* Synchronous loader for texture assets.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetTextureLoaderSync(assetloading_t *loading);
/**
* Disposer for texture assets.
*
* @param entry Asset entry containing the texture to dispose.
* @return Error code indicating success or failure of the dispose operation.
*/
errorret_t assetTextureDispose(assetentry_t *entry);
@@ -1,124 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assettilesetloader.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 assetTilesetLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread.");
if(loading->loading.tileset.state != ASSET_TILESET_LOADING_STATE_READ_FILE) {
errorOk();
}
assertNull(loading->loading.tileset.data, "Data already defined?");
assetfile_t *file = &loading->loading.tileset.file;
assetLoaderErrorChain(loading,
assetFileInit(file, loading->entry->name, NULL, NULL)
);
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 assetTilesetLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_TILESET, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
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();
}
errorret_t assetTilesetDispose(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_TILESET, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
errorOk();
}
@@ -1,60 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/assetfile.h"
#include "display/texture/tileset.h"
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;
/**
* 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 loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
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);
-10
View File
@@ -1,10 +0,0 @@
# 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
assetjsonloader.c
)
@@ -1,95 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetjsonloader.h"
#include "util/memory.h"
#include "assert/assert.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
errorret_t assetJsonLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Async loader should not be on main thread.");
if(loading->loading.json.state != ASSET_JSON_LOADING_STATE_READ_FILE) {
errorOk();
}
assertNull(loading->loading.json.buffer, "Buffer already defined?");
assetfile_t *file = &loading->loading.json.file;
assetLoaderErrorChain(loading,
assetFileInit(file, loading->entry->name, NULL, NULL)
);
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));
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 assetJsonLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_JSON, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
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 assetJsonDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_JSON, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
yyjson_doc_free(entry->data.json);
entry->data.json = NULL;
errorOk();
}
@@ -1,37 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#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 {
assetfile_t file;
assetjsonloadingstate_t state;
uint8_t *buffer;
size_t size;
} assetjsonloaderloading_t;
typedef yyjson_doc * assetjsonoutput_t;
errorret_t assetJsonLoaderAsync(assetloading_t *loading);
errorret_t assetJsonLoaderSync(assetloading_t *loading);
errorret_t assetJsonDispose(assetentry_t *entry);
@@ -1,10 +0,0 @@
# 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
assetlocaleloader.c
)
@@ -1,860 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetlocaleloader.h"
#include "util/memory.h"
#include "util/math.h"
#include "util/string.h"
#include "assert/assert.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
errorret_t assetLocaleLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Async loader should not be on main thread.");
if(loading->loading.locale.state != ASSET_LOCALE_LOADER_STATE_LOAD_HEADER) {
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));
char_t buffer[1024];
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 assetLocaleLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_LOCALE, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
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.");
assertIsMainThread("Must be called from the main thread.");
assetlocalefile_t *localeFile = &entry->data.locale;
errorChain(assetFileClose(&localeFile->file));
return assetFileDispose(&localeFile->file);
}
// These functions probably need some cleaning;
errorret_t assetLocaleParseHeader(
assetlocalefile_t *localeFile,
char_t *headerBuffer,
const size_t headerBufferSize
) {
assertNotNull(localeFile, "Locale file cannot be NULL.");
assertNotNull(headerBuffer, "Header buffer cannot be NULL.");
assertTrue(headerBufferSize > 0, "Header buffer size must be > 0.");
// Find "Plural-Forms: " line and parse out plural form info
char_t *pluralFormsLine = strstr(headerBuffer, "Plural-Forms:");
if(!pluralFormsLine) {
errorOk();
}
pluralFormsLine += strlen("Plural-Forms:");
// Expect nplurals
char_t *npluralsStr = strstr(pluralFormsLine, "nplurals=");
if(!npluralsStr) {
errorThrow("Failed to find nplurals in Plural-Forms header.");
}
npluralsStr += strlen("nplurals=");
localeFile->pluralStateCount = (uint8_t)atoi(npluralsStr);
if(localeFile->pluralStateCount == 0) {
errorThrow("nplurals must be greater than 0.");
}
if(localeFile->pluralStateCount > ASSET_LOCALE_FILE_PLURAL_FORM_COUNT) {
errorThrow(
"nplurals exceeds maximum supported plural forms: %d > %d",
localeFile->pluralStateCount,
ASSET_LOCALE_FILE_PLURAL_FORM_COUNT
);
}
// Expect plural=
char_t *pluralStr = strstr(pluralFormsLine, "plural=");
if(!pluralStr) {
errorThrow("Failed to find plural in Plural-Forms header.");
}
pluralStr += strlen("plural=");
// Expect ( [expressions] )
char_t *openParen = strchr(pluralStr, '(');
char_t *closeParen = strrchr(pluralStr, ')');
if(!openParen || !closeParen || closeParen < openParen) {
errorThrow("Failed to find plural expression in Plural-Forms header.");
}
// Parse:
// n [op] value ? index : n [op] value ? index : ... : final_index
char_t *ptr = openParen + 1;
uint8_t pluralIndex = 0;
uint8_t definedCount = 0;
while(1) {
while(*ptr == ' ') ptr++;
// Allow grouped subexpressions like:
// (n<7 ? 2 : 3)
// or
// (((3)))
uint8_t parenDepth = 0;
while(*ptr == '(') {
parenDepth++;
ptr++;
while(*ptr == ' ') ptr++;
}
// Final fallback: just an integer
if(*ptr != 'n') {
char_t *endPtr = NULL;
int32_t finalIndex = (int32_t)strtol(ptr, &endPtr, 10);
if(endPtr == ptr) {
errorThrow("Expected final plural index.");
}
ptr = endPtr;
while(*ptr == ' ') ptr++;
while(parenDepth > 0) {
if(*ptr != ')') {
errorThrow("Expected ')' after final plural index.");
}
ptr++;
parenDepth--;
while(*ptr == ' ') ptr++;
}
if(*ptr != ')') {
errorThrow("Expected ')' at end of plural expression.");
}
if(finalIndex < 0 || finalIndex >= localeFile->pluralStateCount) {
errorThrow(
"Final plural expression index out of bounds: %d (nplurals: %d)",
finalIndex,
localeFile->pluralStateCount
);
}
localeFile->pluralDefaultIndex = (uint8_t)finalIndex;
definedCount++;
break;
}
if(pluralIndex >= localeFile->pluralStateCount - 1) {
errorThrow(
"Too many plural conditions. Expected %d clauses for nplurals=%d.",
localeFile->pluralStateCount - 1,
localeFile->pluralStateCount
);
}
ptr++; // skip 'n'
while(*ptr == ' ') ptr++;
// Determine operator
assetlocalepluraloperation_t op;
if(strncmp(ptr, "==", 2) == 0) {
op = ASSET_LOCALE_PLURAL_OP_EQUAL;
ptr += 2;
} else if(strncmp(ptr, "!=", 2) == 0) {
op = ASSET_LOCALE_PLURAL_OP_NOT_EQUAL;
ptr += 2;
} else if(strncmp(ptr, "<=", 2) == 0) {
op = ASSET_LOCALE_PLURAL_OP_LESS_EQUAL;
ptr += 2;
} else if(strncmp(ptr, ">=", 2) == 0) {
op = ASSET_LOCALE_PLURAL_OP_GREATER_EQUAL;
ptr += 2;
} else if(*ptr == '<') {
op = ASSET_LOCALE_PLURAL_OP_LESS;
ptr++;
} else if(*ptr == '>') {
op = ASSET_LOCALE_PLURAL_OP_GREATER;
ptr++;
} else {
errorThrow("Unsupported plural operator.");
}
while(*ptr == ' ') ptr++;
// Parse the comparitor value
char_t *endPtr = NULL;
int32_t value = (int32_t)strtol(ptr, &endPtr, 10);
if(endPtr == ptr) {
errorThrow("Expected value for plural expression.");
}
ptr = endPtr;
while(*ptr == ' ') ptr++;
// Parse ternary operator
if(*ptr != '?') {
errorThrow("Expected '?' after plural expression.");
}
ptr++;
while(*ptr == ' ') ptr++;
// Parse the indice
endPtr = NULL;
int32_t index = (int32_t)strtol(ptr, &endPtr, 10);
if(endPtr == ptr) {
errorThrow("Expected index for plural expression.");
}
ptr = endPtr;
if(index < 0 || index >= localeFile->pluralStateCount) {
errorThrow(
"Plural expression index out of bounds: %d (nplurals: %d)",
index,
localeFile->pluralStateCount
);
}
// Store plural expression.
localeFile->pluralIndices[pluralIndex] = (uint8_t)index;
localeFile->pluralOps[pluralIndex] = op;
localeFile->pluralValues[pluralIndex] = value;
pluralIndex++;
definedCount++;
while(*ptr == ' ') ptr++;
// Close any grouping parens that wrapped this conditional branch
while(parenDepth > 0) {
if(*ptr != ')') {
break;
}
ptr++;
parenDepth--;
while(*ptr == ' ') ptr++;
}
if(*ptr != ':') {
errorThrow("Expected ':' after plural expression.");
}
ptr++;
}
// Must define exactly nplurals outcomes:
// (nplurals - 1) conditional results + 1 final fallback result
if(
pluralIndex != localeFile->pluralStateCount - 1 ||
definedCount != localeFile->pluralStateCount
) {
errorThrow("Plural expression count does not match nplurals.");
}
errorOk();
}
uint8_t assetLocaleEvaluatePlural(
assetlocalefile_t *file,
const int32_t pluralCount
) {
assertNotNull(file, "Locale file cannot be NULL.");
assertTrue(pluralCount >= 0, "Plural count cannot be negative.");
for(uint8_t i = 0; i < file->pluralStateCount - 1; i++) {
int32_t value = file->pluralValues[i];
switch(file->pluralOps[i]) {
case ASSET_LOCALE_PLURAL_OP_EQUAL:
if(pluralCount == value) return file->pluralIndices[i];
break;
case ASSET_LOCALE_PLURAL_OP_NOT_EQUAL:
if(pluralCount != value) return file->pluralIndices[i];
break;
case ASSET_LOCALE_PLURAL_OP_LESS:
if(pluralCount < value) return file->pluralIndices[i];
break;
case ASSET_LOCALE_PLURAL_OP_LESS_EQUAL:
if(pluralCount <= value) return file->pluralIndices[i];
break;
case ASSET_LOCALE_PLURAL_OP_GREATER:
if(pluralCount > value) return file->pluralIndices[i];
break;
case ASSET_LOCALE_PLURAL_OP_GREATER_EQUAL:
if(pluralCount >= value) return file->pluralIndices[i];
break;
}
}
return file->pluralDefaultIndex;
}
errorret_t assetLocaleLineSkipBlanks(
assetfilelinereader_t *reader,
uint8_t *lineBuffer
) {
while(!reader->eof) {
// Skip blank lines
if(lineBuffer[0] == '\0') {
errorret_t r = assetFileLineReaderNext(reader);
if(errorIsNotOk(r)) {
errorCatch(r);
break;
}
continue;
}
// Skip comment lines
if(lineBuffer[0] == '#') {
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;
bool_t onlySpaces = true;
for(i = 0; i < lineLength; i++) {
if(lineBuffer[i] != ' ') {
onlySpaces = false;
break;
}
}
if(onlySpaces) {
errorret_t r = assetFileLineReaderNext(reader);
if(errorIsNotOk(r)) {
errorCatch(r);
break;
}
continue;
}
break;
}
errorOk();
}
errorret_t assetLocaleLineUnbuffer(
assetfilelinereader_t *reader,
uint8_t *lineBuffer,
uint8_t *stringBuffer,
const size_t stringBufferSize
) {
stringBuffer[0] = '\0';
// At the point this funciton is called, we are looking for an opening quote.
char_t *start = strchr((char_t *)lineBuffer, '"');
if(!start) {
errorThrow("Expected open (0) \"");
}
char *end = strchr(start + 1, '"');
if(!end) {
errorThrow("Expected close (0) \"");
}
*end = '\0';
if(strlen(start) >= stringBufferSize) {
errorThrow("String buffer overflow");
}
memoryCopy(stringBuffer, start + 1, strlen(start));
// Now start buffering lines out
while(!reader->eof) {
errorret_t r = assetFileLineReaderNext(reader);
if(errorIsNotOk(r)) {
errorCatch(r);
break;
}
// Skip blank lines
r = assetLocaleLineSkipBlanks(reader, lineBuffer);
if(errorIsNotOk(r)) {
errorCatch(r);
break;
}
// Skip starting spaces
char_t *ptr = (char_t *)lineBuffer;
while(*ptr == ' ') {
ptr++;
}
// Only consider lines starting with quote
if(*ptr != '"') {
break;
}
ptr++; // move past first quote
bool_t escaping = false;
char_t *dest = (char_t *)stringBuffer + strlen((char_t *)stringBuffer);
while(*ptr) {
if(escaping) {
// Handle escape sequences
switch(*ptr) {
case 'n': *dest++ = '\n'; break;
case 't': *dest++ = '\t'; break;
case '\\': *dest++ = '\\'; break;
case '"': *dest++ = '"'; break;
default:
errorThrow("Unknown escape sequence: \\%c", *ptr);
}
escaping = false;
} else if(*ptr == '\\') {
escaping = true;
} else if(*ptr == '"') {
// End of string
break;
} else {
// Regular character
*dest++ = *ptr;
}
if((size_t)(dest - (char_t *)stringBuffer) >= stringBufferSize) {
errorThrow("String buffer overflow");
}
ptr++;
}
*dest = '\0';
}
errorOk();
}
errorret_t assetLocaleGetString(
assetlocalefile_t *file,
const char_t *messageId,
const int32_t pluralCount,
char_t *stringBuffer,
const size_t stringBufferSize
) {
assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(messageId, "Message ID cannot be NULL.");
assertTrue(pluralCount >= 0, "Plural index cannot be negative.");
assertNotNull(stringBuffer, "String buffer cannot be NULL.");
assertTrue(stringBufferSize > 0, "String buffer size must be > 0");
assetfilelinereader_t reader;
bool_t msgidFound = false, msgidPluralFound = false, msgstrFound = false;
uint8_t msgidBuffer[256];
uint8_t msgidPluralBuffer[256];
uint8_t readBuffer[1024];
uint8_t lineBuffer[1024];
uint8_t pluralIndex = 0xFF;
msgidBuffer[0] = '\0';
msgidPluralBuffer[0] = '\0';
stringBuffer[0] = '\0';
// Rewind and start reading lines.
errorChain(assetFileRewind(&file->file));
assetFileLineReaderInit(
&reader,
&file->file,
readBuffer,
sizeof(readBuffer),
lineBuffer,
sizeof(lineBuffer)
);
// Skip blanks, comments, etc and start looking for msgid's
errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer));
while(!reader.eof) {
// Is this msgid?
if(memoryCompare(lineBuffer, "msgid", 5) != 0) {
errorChain(assetFileLineReaderNext(&reader));
msgidBuffer[0] = '\0';
continue;
}
// Unbuffer the msgid
assetLocaleLineUnbuffer(
&reader, lineBuffer, (uint8_t *)msgidBuffer, sizeof(msgidBuffer)
);
// Is this the needle?
if(memoryCompare(msgidBuffer, messageId, strlen(messageId)) != 0) {
continue;
}
msgidFound = true;
break;
}
if(!msgidFound) {
errorThrow("Failed to find message ID: %s", messageId);
}
// We are either going to see a msgstr or a msgid_plural
while(!reader.eof) {
errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer));
// Is msgid_plural?
if(
!msgidPluralFound &&
memoryCompare(lineBuffer, "msgid_plural", 12) == 0
) {
// Yes, start reading plural ID
assetLocaleLineUnbuffer(
&reader,
lineBuffer,
(uint8_t *)msgidPluralBuffer,
sizeof(msgidPluralBuffer)
);
msgidPluralFound = true;
// At this point we determine the plural index to use by running the
// plural formula
pluralIndex = assetLocaleEvaluatePlural(
file,
pluralCount
);
continue;
}
// Should be msgstr if not plural.
if(memoryCompare(lineBuffer, "msgstr", 6) != 0) {
errorThrow("Expected msgstr after msgid, found: %s", lineBuffer);
continue;
}
// If plural we need an index
if(msgidPluralFound) {
// Skip blank chars
char_t *ptr = (char_t *)lineBuffer + 6;
while(*ptr == ' ') {
ptr++;
}
if(*ptr != '[') {
errorThrow("Expected [ for plural form, found: %s", lineBuffer);
}
ptr++; // move past [
// Parse until ]
char *end = strchr(ptr, ']');
if(!end) {
errorThrow("Expected ] for plural form, found: %s", lineBuffer);
}
*end = '\0';
int32_t index = atoi(ptr);
if(index != pluralIndex) {
// Not the plural form we want, skip to the next useable line
while(!reader.eof) {
errorChain(assetFileLineReaderNext(&reader));
errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer));
if(
lineBuffer[0] == '\"' ||
lineBuffer[0] == '\0' ||
lineBuffer[0] == '#'
) continue;
break;
}
continue;
}
// Undo damage to line buffer from unbuffering.
*end = ']';
}
// Parse out msgstr
errorChain(assetLocaleLineUnbuffer(
&reader, lineBuffer, (uint8_t *)stringBuffer, stringBufferSize
));
msgstrFound = true;
break;
};
if(!msgstrFound) {
errorThrow("Failed to find msgstr for message ID: %s", messageId);
}
errorOk();
}
errorret_t assetLocaleGetStringWithVA(
assetlocalefile_t *file,
const char_t *messageId,
const int32_t pluralIndex,
char_t *buffer,
const size_t bufferSize,
...
) {
assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(messageId, "Message ID cannot be NULL.");
assertNotNull(buffer, "Buffer cannot be NULL.");
assertTrue(bufferSize > 0, "Buffer size must be > 0.");
assertTrue(pluralIndex >= 0, "Plural cannot be negative.");
char_t *tempBuffer;
tempBuffer = memoryAllocate(bufferSize);
errorret_t ret = assetLocaleGetString(
file,
messageId,
pluralIndex,
tempBuffer,
bufferSize
);
if(errorIsNotOk(ret)) {
memoryFree(tempBuffer);
return ret;
}
va_list args;
va_start(args, bufferSize);
int result = vsnprintf(buffer, bufferSize, tempBuffer, args);
va_end(args);
memoryFree(tempBuffer);
if(result < 0) {
errorThrow("Failed to format locale string for ID: %s", messageId);
}
return ret;
}
errorret_t assetLocaleGetStringWithArgs(
assetlocalefile_t *file,
const char_t *id,
const int32_t plural,
char_t *buffer,
const size_t bufferSize,
const assetlocalearg_t *args,
const size_t argCount
) {
assertNotNull(id, "Message ID cannot be NULL.");
assertNotNull(buffer, "Buffer cannot be NULL.");
assertTrue(bufferSize > 0, "Buffer size must be > 0.");
assertTrue(plural >= 0, "Plural cannot be negative.");
assertTrue(
argCount == 0 || args != NULL,
"Args cannot be NULL when argCount > 0."
);
char_t *format = memoryAllocate(bufferSize);
if(format == NULL) {
errorThrow("Failed to allocate format buffer.");
}
errorret_t ret = assetLocaleGetString(
file,
id,
plural,
format,
bufferSize
);
if(errorIsNotOk(ret)) {
memoryFree(format);
return ret;
}
size_t inIndex = 0;
size_t outIndex = 0;
size_t nextArg = 0;
buffer[0] = '\0';
while(format[inIndex] != '\0') {
if(format[inIndex] != '%') {
if(outIndex + 1 >= bufferSize) {
memoryFree(format);
errorThrow("Formatted locale string buffer overflow for ID: %s", id);
}
buffer[outIndex++] = format[inIndex++];
continue;
}
inIndex++;
/* Escaped percent */
if(format[inIndex] == '%') {
if(outIndex + 1 >= bufferSize) {
memoryFree(format);
errorThrow("Formatted locale string buffer overflow for ID: %s", id);
}
buffer[outIndex++] = '%';
inIndex++;
continue;
}
if(nextArg >= argCount) {
memoryFree(format);
errorThrow("Not enough locale arguments for ID: %s", id);
}
{
char_t specBuffer[32];
char_t valueBuffer[256];
size_t specLength = 0;
int written = 0;
char_t specifier;
specBuffer[specLength++] = '%';
/* Allow flags / width / precision */
while(format[inIndex] != '\0') {
char_t ch = format[inIndex];
if(
ch == '-' || ch == '+' || ch == ' ' || ch == '#' || ch == '0' ||
ch == '.' || (ch >= '0' && ch <= '9')
) {
if(specLength + 1 >= sizeof(specBuffer)) {
memoryFree(format);
errorThrow("Format specifier too long for ID: %s", id);
}
specBuffer[specLength++] = ch;
inIndex++;
continue;
}
break;
}
if(format[inIndex] == '\0') {
memoryFree(format);
errorThrow("Incomplete format specifier for ID: %s", id);
}
specifier = format[inIndex++];
if(specifier != 's' && specifier != 'd' && specifier != 'f') {
memoryFree(format);
errorThrow(
"Unsupported format specifier '%%%c' for ID: %s",
specifier,
id
);
}
specBuffer[specLength++] = specifier;
specBuffer[specLength] = '\0';
switch(specifier) {
case 's':
if(args[nextArg].type != ASSET_LOCALE_ARG_STRING) {
memoryFree(format);
errorThrow("Expected string locale argument for ID: %s", id);
}
written = snprintf(
valueBuffer,
sizeof(valueBuffer),
specBuffer,
args[nextArg].stringValue ? args[nextArg].stringValue : ""
);
break;
case 'd':
if(args[nextArg].type != ASSET_LOCALE_ARG_INT) {
memoryFree(format);
errorThrow("Expected int locale argument for ID: %s", id);
}
written = snprintf(
valueBuffer,
sizeof(valueBuffer),
specBuffer,
args[nextArg].intValue
);
break;
case 'f':
if(
args[nextArg].type != ASSET_LOCALE_ARG_FLOAT &&
args[nextArg].type != ASSET_LOCALE_ARG_INT
) {
memoryFree(format);
errorThrow("Expected float or int locale argument for ID: %s", id);
}
float_t floatValue = (
args[nextArg].type == ASSET_LOCALE_ARG_FLOAT ?
args[nextArg].floatValue :
(float_t)args[nextArg].intValue
);
written = snprintf(
valueBuffer,
sizeof(valueBuffer),
specBuffer,
floatValue
);
break;
}
nextArg++;
if(written < 0) {
memoryFree(format);
errorThrow("Failed to format locale string for ID: %s", id);
}
if(outIndex + (size_t)written >= bufferSize) {
memoryFree(format);
errorThrow("Formatted locale string buffer overflow for ID: %s", id);
}
memoryCopy(buffer + outIndex, valueBuffer, (size_t)written);
outIndex += (size_t)written;
}
}
buffer[outIndex] = '\0';
memoryFree(format);
errorOk();
}
@@ -1,281 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#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,
ASSET_LOCALE_PLURAL_OP_LESS,
ASSET_LOCALE_PLURAL_OP_LESS_EQUAL,
ASSET_LOCALE_PLURAL_OP_GREATER,
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;
int32_t intValue;
float_t floatValue;
};
} 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;
/** Convenience alias - the loaded output type of a locale asset entry. */
typedef assetlocalefile_t assetlocaleoutput_t;
/**
* 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 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,
const size_t headerBufferSize
);
/**
* 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,
uint8_t *lineBuffer
);
/**
* 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 filled 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,
uint8_t *lineBuffer,
uint8_t *stringBuffer,
const size_t stringBufferSize
);
/**
* 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,
const char_t *messageId,
const int32_t pluralCount,
char_t *stringBuffer,
const size_t stringBufferSize
);
/**
* 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,
const char_t *messageId,
const int32_t pluralCount,
char_t *buffer,
const size_t bufferSize,
...
);
/**
* 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,
const char_t *messageId,
const int32_t pluralCount,
char_t *buffer,
const size_t bufferSize,
const assetlocalearg_t *args,
const size_t argCount
);
@@ -1,9 +0,0 @@
# 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
)
@@ -1,137 +0,0 @@
/**
* 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 "script/module/require/modulerequire.h"
#include "util/memory.h"
#include "assert/assert.h"
#include <jerryscript.h>
errorret_t assetScriptLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Async loader should not be on main thread.");
if(loading->loading.script.state != ASSET_SCRIPT_LOADING_STATE_READ_FILE) {
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)
);
uint8_t *buffer = NULL;
size_t size = 0;
assetLoaderErrorChain(loading, assetFileReadEntire(file, &buffer, &size));
assetLoaderErrorChain(loading, assetFileDispose(file));
// Null-terminate for jerry_eval.
memoryResize((void **)&buffer, size, size + 1);
buffer[size] = '\0';
loading->loading.script.buffer = buffer;
loading->loading.script.size = size;
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.");
assertIsMainThread("Must be called from the main thread.");
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();
}
// Get read buffer
uint8_t *buffer = loading->loading.script.buffer;
assertNotNull(buffer, "Script buffer should have been loaded by now.");
// Get the current global script realm
jerry_value_t global = jerry_current_realm();
// Replace globalThis.module with a new `module = {}`
jerry_value_t oldModule = jerry_object_get_sz(global, "module");
jerry_value_t module = jerry_object();
jerry_object_set_sz(global, "module", module);
// Eval the script, we handle failure later down the code.
jerry_value_t result = jerry_eval(
buffer,
loading->loading.script.size,
JERRY_PARSE_NO_OPTS
);
// Free the read buffer
memoryFree(buffer);
loading->loading.script.buffer = NULL;
// Restore globalThis.module
jerry_object_set_sz(global, "module", oldModule);
jerry_value_free(oldModule);
jerry_value_free(global);
if(jerry_value_is_exception(result)) {
jerry_value_free(module);
loading->entry->data.script.exports = jerry_undefined();
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
// Get error string
char_t buf[256];
moduleBaseExceptionMessage(result, buf, sizeof(buf));
jerry_value_free(result);
assetLoaderErrorThrow(
loading,
"Script execution failed: %s: %s", loading->entry->name, buf
);
}
// Get module.exports
jerry_value_t exports = jerry_object_get_sz(module, "exports");
jerry_value_free(result);
jerry_value_free(module);
// Store the exports.
loading->entry->data.script.exports = exports;
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t assetScriptDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_SCRIPT, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
if(
entry->data.script.exports != 0 &&
!jerry_value_is_undefined(entry->data.script.exports)
) {
jerry_value_free(entry->data.script.exports);
entry->data.script.exports = 0;
}
errorOk();
}
@@ -1,60 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/assetfile.h"
#include "script/scriptmodule.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 scriptmodule_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;
/**
* Asynchronous loader for a script asset/module.
*
* @param loading The loading context.
* @returns An error code and state.
*/
errorret_t assetScriptLoaderAsync(assetloading_t *loading);
/**
* Synchronous loader for a script asset/module. This executes the script after
* it has been loaded by the async loader.
*
* @param loading The loading context.
* @returns An error code and state.
*/
errorret_t assetScriptLoaderSync(assetloading_t *loading);
/**
* Disposes of a loaded script asset/module.
*
* @param entry The asset entry to dispose.
* @returns An error code and state.
*/
errorret_t assetScriptDispose(assetentry_t *entry);
+1
View File
@@ -65,6 +65,7 @@ void consoleUpdate(void) {
errorret_t consoleDraw(void) { errorret_t consoleDraw(void) {
if(!CONSOLE.visible) errorOk(); if(!CONSOLE.visible) errorOk();
if(!FONT_DEFAULT.tileset) errorOk();
for(uint32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) { for(uint32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) {
errorChain(textDraw( errorChain(textDraw(
-9
View File
@@ -1,9 +0,0 @@
# 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
cutscene.c
)
-96
View File
@@ -1,96 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#include "cutscene.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "console/console.h"
#include "time/time.h"
cutscene_t CUTSCENE;
errorret_t cutsceneInit(void) {
memoryZero(&CUTSCENE, sizeof(cutscene_t));
errorOk();
}
errorret_t cutsceneUpdate(void) {
#ifdef DUSK_TIME_DYNAMIC
if(TIME.dynamicUpdate) {
errorOk();
}
#endif
if(!CUTSCENE.active) errorOk();
cutsceneevent_t *event = &CUTSCENE.events[CUTSCENE.eventCurrent];
if(event->onUpdate) errorChain(event->onUpdate());
errorOk();
}
errorret_t cutscenePlay(
const cutsceneevent_t *events,
const uint8_t eventCount
) {
assertNotNull(events, "Events cannot be null");
assertTrue(eventCount > 0, "Event count must be greater than zero");
assertTrue(
eventCount <= CUTSCENE_EVENT_COUNT_MAX,
"Event count exceeds CUTSCENE_EVENT_COUNT_MAX"
);
if(CUTSCENE.active) {
errorChain(cutsceneStop());
}
memoryCopy(CUTSCENE.events, events, sizeof(cutsceneevent_t) * eventCount);
CUTSCENE.eventCount = eventCount;
CUTSCENE.eventCurrent = 0;
CUTSCENE.active = true;
cutsceneevent_t *firstEvent = &CUTSCENE.events[0];
if(firstEvent->onStart) errorChain(firstEvent->onStart());
errorOk();
}
errorret_t cutsceneAdvance(void) {
if(!CUTSCENE.active) errorOk();
cutsceneevent_t *currentEvent = &CUTSCENE.events[CUTSCENE.eventCurrent];
if(currentEvent->onEnd) errorChain(currentEvent->onEnd());
CUTSCENE.eventCurrent++;
if(CUTSCENE.eventCurrent >= CUTSCENE.eventCount) {
if(CUTSCENE.onStop) errorChain(CUTSCENE.onStop());
CUTSCENE.active = false;
errorOk();
}
cutsceneevent_t *nextEvent = &CUTSCENE.events[CUTSCENE.eventCurrent];
if(nextEvent->onStart) errorChain(nextEvent->onStart());
consolePrint("Cutscene advance");
errorOk();
}
errorret_t cutsceneStop(void) {
if(!CUTSCENE.active) errorOk();
cutsceneevent_t *currentEvent = &CUTSCENE.events[CUTSCENE.eventCurrent];
if(currentEvent->onEnd) errorChain(currentEvent->onEnd());
if(CUTSCENE.onStop) errorChain(CUTSCENE.onStop());
CUTSCENE.active = false;
errorOk();
}
errorret_t cutsceneDispose(void) {
errorChain(cutsceneStop());
errorOk();
}
bool_t cutsceneIsActive(void) {
return CUTSCENE.active;
}
-85
View File
@@ -1,85 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#pragma once
#include "error/error.h"
#define CUTSCENE_EVENT_COUNT_MAX 16
typedef struct {
errorret_t (*onStart)(void);
errorret_t (*onEnd)(void);
errorret_t (*onUpdate)(void);
} cutsceneevent_t;
typedef struct {
cutsceneevent_t events[CUTSCENE_EVENT_COUNT_MAX];
uint8_t eventCount;
uint8_t eventCurrent;
errorret_t (*onStop)(void);
bool_t active;
} cutscene_t;
extern cutscene_t CUTSCENE;
/**
* Initializes the cutscene manager.
*
* @return Any error state that happened.
*/
errorret_t cutsceneInit(void);
/**
* Ticks the active cutscene event, calling its onUpdate callback.
* Does nothing when no cutscene is playing.
*
* @return Any error state that happened.
*/
errorret_t cutsceneUpdate(void);
/**
* Copies the given event array and begins playing from the first
* event. If a cutscene is already playing it is stopped first.
*
* @param events Array of events to copy.
* @param eventCount Number of events. Must be > 0 and
* <= CUTSCENE_EVENT_COUNT_MAX.
* @return Any error state that happened.
*/
errorret_t cutscenePlay(
const cutsceneevent_t *events,
const uint8_t eventCount
);
/**
* Ends the current event and starts the next one.
* Marks the cutscene as inactive after the last event ends.
* Does nothing when no cutscene is playing.
*
* @return Any error state that happened.
*/
errorret_t cutsceneAdvance(void);
/**
* Ends the current event and stops the cutscene immediately.
* Does nothing when no cutscene is playing.
*
* @return Any error state that happened.
*/
errorret_t cutsceneStop(void);
/**
* Disposes of the cutscene manager, stopping any active cutscene.
*
* @return Any error state that happened.
*/
errorret_t cutsceneDispose(void);
/**
* Returns whether a cutscene is currently playing.
*
* @return true if a cutscene is active.
*/
bool_t cutsceneIsActive(void);
+1
View File
@@ -7,6 +7,7 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
display.c display.c
renderpipeline.c
) )
# Subdirectories # Subdirectories
+39 -12
View File
@@ -1,13 +1,13 @@
/** /**
* Copyright (c) 2026 Dominic Masters * Copyright (c) 2026 Dominic Masters
* *
* This software is released under the MIT License. * This software is released under the MIT License.
* https://opensource.org/licenses/MIT * https://opensource.org/licenses/MIT
*/ */
#include "display/display.h" #include "display/display.h"
#include "display/framebuffer/framebuffer.h" #include "display/framebuffer/framebuffer.h"
#include "scene/scene.h" #include "display/renderpipeline.h"
#include "display/spritebatch/spritebatch.h" #include "display/spritebatch/spritebatch.h"
#include "display/mesh/quad.h" #include "display/mesh/quad.h"
#include "display/mesh/cube.h" #include "display/mesh/cube.h"
@@ -21,8 +21,10 @@
#include "assert/assert.h" #include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
#include "util/string.h" #include "util/string.h"
#include "asset/asset.h"
#include "display/shader/shaderlist.h" #include "display/shader/shaderlist.h"
#include "display/shader/shaderunlit.h"
#include "entity/component/display/entitycamera.h"
#include "entity/component.h"
#include "time/time.h" #include "time/time.h"
display_t DISPLAY = { 0 }; display_t DISPLAY = { 0 };
@@ -46,14 +48,12 @@ errorret_t displayInit(void) {
errorChain(planeInit()); errorChain(planeInit());
errorChain(capsuleInit()); errorChain(capsuleInit());
errorChain(triPrismInit()); errorChain(triPrismInit());
errorChain(frameBufferInitBackBuffer()); errorChain(frameBufferInitBackBuffer());
errorChain(spriteBatchInit()); errorChain(spriteBatchInit());
errorChain(textInit()); errorChain(textInit());
errorChain(screenInit()); errorChain(screenInit());
// Setup initial shader with default values
errorChain(shaderListInit()); errorChain(shaderListInit());
errorOk(); errorOk();
@@ -67,15 +67,43 @@ errorret_t displayUpdate(void) {
// Reset state // Reset state
spriteBatchClear(); spriteBatchClear();
errorChain(frameBufferBind(NULL)); errorChain(frameBufferBind(NULL));
// Bind screen and render scene // Bind screen and render
errorChain(screenBind()); errorChain(screenBind());
frameBufferClear( frameBufferClear(
FRAMEBUFFER_CLEAR_COLOR | FRAMEBUFFER_CLEAR_DEPTH, FRAMEBUFFER_CLEAR_COLOR | FRAMEBUFFER_CLEAR_DEPTH,
SCREEN.background SCREEN.background
); );
errorChain(sceneRender()); errorChain(componentRenderAll());
errorChain(renderPipeline(entityCameraGetCurrent()));
// UI Rendering
mat4 proj, view, ident;
glm_mat4_identity(ident);
errorChain(shaderBind(&SHADER_UNLIT));
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, ident));
glm_ortho(
0.0f, SCREEN.width,
SCREEN.height, 0.0f,
0.1f, 100.0f,
proj
);
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, proj));
glm_lookat(
(vec3){ 0.0f, 0.0f, 1.0f },
(vec3){ 0.0f, 0.0f, 0.0f },
(vec3){ 0.0f, 1.0f, 0.0f },
view
);
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, view));
errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_BLEND
}));
errorChain(uiRender());
// Finish up // Finish up
screenUnbind(); screenUnbind();
@@ -100,11 +128,10 @@ errorret_t displayDispose(void) {
errorChain(spriteBatchDispose()); errorChain(spriteBatchDispose());
screenDispose(); screenDispose();
errorChain(textDispose()); errorChain(textDispose());
#ifdef displayPlatformDispose #ifdef displayPlatformDispose
displayPlatformDispose(); displayPlatformDispose();
#endif #endif
// For now, we just return an OK error.
errorOk(); errorOk();
} }
@@ -5,7 +5,7 @@
* https://opensource.org/licenses/MIT * https://opensource.org/licenses/MIT
*/ */
#include "scenerenderpipeline.h" #include "renderpipeline.h"
#include "entity/entitymanager.h" #include "entity/entitymanager.h"
#include "entity/component/display/entityposition.h" #include "entity/component/display/entityposition.h"
#include "entity/component/display/entitycamera.h" #include "entity/component/display/entitycamera.h"
@@ -14,7 +14,7 @@
#include "display/displaystate.h" #include "display/displaystate.h"
#include "assert/assert.h" #include "assert/assert.h"
int8_t sceneRenderPipelineGetPriority(const entityrenderable_t *r) { int8_t renderPipelineGetPriority(const entityrenderable_t *r) {
assertNotNull(r, "Renderable cannot be null"); assertNotNull(r, "Renderable cannot be null");
if(r->priority != 0) return r->priority; if(r->priority != 0) return r->priority;
switch(r->type) { switch(r->type) {
@@ -29,15 +29,15 @@ int8_t sceneRenderPipelineGetPriority(const entityrenderable_t *r) {
} }
} }
int_t sceneRenderPipelineCompare(const void *a, const void *b) { int_t renderPipelineCompare(const void *a, const void *b) {
assertNotNull(a, "Entry a cannot be null"); assertNotNull(a, "Entry a cannot be null");
assertNotNull(b, "Entry b cannot be null"); assertNotNull(b, "Entry b cannot be null");
const scenerenderpipelineentry_t *ea = (const scenerenderpipelineentry_t *)a; const renderpipelineentry_t *ea = (const renderpipelineentry_t *)a;
const scenerenderpipelineentry_t *eb = (const scenerenderpipelineentry_t *)b; const renderpipelineentry_t *eb = (const renderpipelineentry_t *)b;
return (int_t)ea->effectivePriority - (int_t)eb->effectivePriority; return (int_t)ea->effectivePriority - (int_t)eb->effectivePriority;
} }
shader_t *sceneRenderPipelineGetShader(const entityrenderable_t *r) { shader_t *renderPipelineGetShader(const entityrenderable_t *r) {
assertNotNull(r, "Renderable cannot be null"); assertNotNull(r, "Renderable cannot be null");
switch(r->type) { switch(r->type) {
case ENTITY_RENDERABLE_TYPE_SPRITEBATCH: case ENTITY_RENDERABLE_TYPE_SPRITEBATCH:
@@ -50,7 +50,7 @@ shader_t *sceneRenderPipelineGetShader(const entityrenderable_t *r) {
} }
} }
errorret_t sceneRenderPipeline(const entityid_t cameraId) { errorret_t renderPipeline(const entityid_t cameraId) {
mat4 proj, view, model, ident; mat4 proj, view, model, ident;
glm_mat4_identity(ident); glm_mat4_identity(ident);
@@ -62,20 +62,20 @@ errorret_t sceneRenderPipeline(const entityid_t cameraId) {
if(cameraId == ENTITY_ID_INVALID || entCount == 0) errorOk(); if(cameraId == ENTITY_ID_INVALID || entCount == 0) errorOk();
scenerenderpipelineentry_t pipeline[ENTITY_COUNT_MAX]; renderpipelineentry_t pipeline[ENTITY_COUNT_MAX];
for(entityid_t i = 0; i < entCount; i++) { for(entityid_t i = 0; i < entCount; i++) {
entityrenderable_t *r = componentGetData( entityrenderable_t *r = componentGetData(
entities[i], components[i], COMPONENT_TYPE_RENDERABLE entities[i], components[i], COMPONENT_TYPE_RENDERABLE
); );
pipeline[i].entityId = entities[i]; pipeline[i].entityId = entities[i];
pipeline[i].componentId = components[i]; pipeline[i].componentId = components[i];
pipeline[i].effectivePriority = sceneRenderPipelineGetPriority(r); pipeline[i].effectivePriority = renderPipelineGetPriority(r);
} }
sort( sort(
pipeline, pipeline,
(size_t)entCount, (size_t)entCount,
sizeof(scenerenderpipelineentry_t), sizeof(renderpipelineentry_t),
sceneRenderPipelineCompare renderPipelineCompare
); );
componentid_t camPos = entityGetComponent(cameraId, COMPONENT_TYPE_POSITION); componentid_t camPos = entityGetComponent(cameraId, COMPONENT_TYPE_POSITION);
@@ -111,7 +111,7 @@ errorret_t sceneRenderPipeline(const entityid_t cameraId) {
entityrenderable_t *r = componentGetData( entityrenderable_t *r = componentGetData(
eid, cid, COMPONENT_TYPE_RENDERABLE eid, cid, COMPONENT_TYPE_RENDERABLE
); );
shader_t *s = sceneRenderPipelineGetShader(r); shader_t *s = renderPipelineGetShader(r);
componentid_t posComp = entityGetComponent(eid, COMPONENT_TYPE_POSITION); componentid_t posComp = entityGetComponent(eid, COMPONENT_TYPE_POSITION);
if(posComp == COMPONENT_ID_INVALID) { if(posComp == COMPONENT_ID_INVALID) {
@@ -15,27 +15,28 @@ typedef struct {
entityid_t entityId; entityid_t entityId;
componentid_t componentId; componentid_t componentId;
int8_t effectivePriority; int8_t effectivePriority;
} scenerenderpipelineentry_t; } renderpipelineentry_t;
/** /**
* Returns the effective render priority for a renderable. When the renderable's * Returns the effective render priority for a renderable. When the
* explicit priority is non-zero that value is returned directly; otherwise an * renderable's explicit priority is non-zero that value is returned directly;
* automatic value is derived from the renderable type and display state flags. * otherwise an automatic value is derived from the renderable type and display
* state flags.
* *
* @param r The renderable component data. * @param r The renderable component data.
* @return Effective priority: lower renders first, higher renders last. * @return Effective priority: lower renders first, higher renders last.
*/ */
int8_t sceneRenderPipelineGetPriority(const entityrenderable_t *r); int8_t renderPipelineGetPriority(const entityrenderable_t *r);
/** /**
* sortcompare_t comparator for scenerenderpipelineentry_t. Compares by * sortcompare_t comparator for renderpipelineentry_t. Compares by
* effectivePriority ascending so lower-priority entries sort first. * effectivePriority ascending so lower-priority entries sort first.
* *
* @param a Pointer to the first scenerenderpipelineentry_t. * @param a Pointer to the first renderpipelineentry_t.
* @param b Pointer to the second scenerenderpipelineentry_t. * @param b Pointer to the second renderpipelineentry_t.
* @return Negative, zero, or positive. * @return Negative, zero, or positive.
*/ */
int_t sceneRenderPipelineCompare(const void *a, const void *b); int_t renderPipelineCompare(const void *a, const void *b);
/** /**
* Returns the shader that will be used to render the given renderable. * Returns the shader that will be used to render the given renderable.
@@ -45,7 +46,7 @@ int_t sceneRenderPipelineCompare(const void *a, const void *b);
* @param r The renderable component data. * @param r The renderable component data.
* @return Pointer to the shader, never NULL. * @return Pointer to the shader, never NULL.
*/ */
shader_t *sceneRenderPipelineGetShader(const entityrenderable_t *r); shader_t *renderPipelineGetShader(const entityrenderable_t *r);
/** /**
* Renders all entities with renderable components in priority order. * Renders all entities with renderable components in priority order.
@@ -56,4 +57,4 @@ shader_t *sceneRenderPipelineGetShader(const entityrenderable_t *r);
* @param cameraId The entity ID of the active camera, or ENTITY_ID_INVALID. * @param cameraId The entity ID of the active camera, or ENTITY_ID_INVALID.
* @return Error state. * @return Error state.
*/ */
errorret_t sceneRenderPipeline(const entityid_t cameraId); errorret_t renderPipeline(const entityid_t cameraId);
+6 -17
View File
@@ -9,34 +9,18 @@
#include "assert/assert.h" #include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
#include "display/spritebatch/spritebatch.h" #include "display/spritebatch/spritebatch.h"
#include "asset/asset.h"
#include "asset/loader/display/assettextureloader.h"
#include "asset/loader/display/assettilesetloader.h"
#include "display/shader/shaderunlit.h" #include "display/shader/shaderunlit.h"
font_t FONT_DEFAULT; font_t FONT_DEFAULT;
errorret_t textInit(void) { errorret_t textInit(void) {
assetloaderinput_t input = { .texture = TEXTURE_FORMAT_RGBA }; memoryZero(&FONT_DEFAULT, sizeof(font_t));
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(); errorOk();
} }
errorret_t textDispose(void) { errorret_t textDispose(void) {
FONT_DEFAULT.texture = NULL; FONT_DEFAULT.texture = NULL;
FONT_DEFAULT.tileset = NULL; FONT_DEFAULT.tileset = NULL;
assetUnlock("ui/minogram.png");
assetUnlock("ui/minogram.dtf");
errorOk(); errorOk();
} }
@@ -44,6 +28,7 @@ spritebatchsprite_t textGetSprite(
const vec2 pos, const char_t c, const font_t *font const vec2 pos, const char_t c, const font_t *font
) { ) {
assertNotNull(font, "Font cannot be NULL"); assertNotNull(font, "Font cannot be NULL");
assertNotNull(font->tileset, "Font tileset not loaded.");
// Change char from ASCII to a tile index. // Change char from ASCII to a tile index.
int32_t tileIndex = (int32_t)(c) - TEXT_CHAR_START; int32_t tileIndex = (int32_t)(c) - TEXT_CHAR_START;
@@ -81,6 +66,8 @@ errorret_t textDraw(
font_t *font font_t *font
) { ) {
assertNotNull(text, "Text cannot be NULL"); assertNotNull(text, "Text cannot be NULL");
assertNotNull(font, "Font cannot be NULL");
if(!font->tileset) errorOk();
spritebatchsprite_t sprite; spritebatchsprite_t sprite;
shadermaterial_t material = { shadermaterial_t material = {
@@ -130,8 +117,10 @@ void textMeasure(
int32_t *outHeight int32_t *outHeight
) { ) {
assertNotNull(text, "Text cannot be NULL"); assertNotNull(text, "Text cannot be NULL");
assertNotNull(font, "Font cannot be NULL");
assertNotNull(outWidth, "Output width pointer cannot be NULL"); assertNotNull(outWidth, "Output width pointer cannot be NULL");
assertNotNull(outHeight, "Output height pointer cannot be NULL"); assertNotNull(outHeight, "Output height pointer cannot be NULL");
if(!font->tileset) { *outWidth = 0; *outHeight = 0; return; }
int32_t width = 0; int32_t width = 0;
int32_t height = font->tileset->tileHeight; int32_t height = font->tileset->tileHeight;
-1
View File
@@ -6,7 +6,6 @@
*/ */
#pragma once #pragma once
#include "asset/asset.h"
#include "display/text/font.h" #include "display/text/font.h"
#include "display/spritebatch/spritebatch.h" #include "display/spritebatch/spritebatch.h"
-17
View File
@@ -11,9 +11,6 @@
#include "input/input.h" #include "input/input.h"
#include "locale/localemanager.h" #include "locale/localemanager.h"
#include "display/display.h" #include "display/display.h"
#include "scene/scene.h"
#include "cutscene/cutscene.h"
#include "asset/asset.h"
#include "ui/ui.h" #include "ui/ui.h"
#include "ui/uitextbox.h" #include "ui/uitextbox.h"
#include "assert/assert.h" #include "assert/assert.h"
@@ -23,7 +20,6 @@
#include "network/network.h" #include "network/network.h"
#include "system/system.h" #include "system/system.h"
#include "console/console.h" #include "console/console.h"
#include "script/script.h"
#include "item/backpack.h" #include "item/backpack.h"
#include "save/save.h" #include "save/save.h"
@@ -42,22 +38,17 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
timeInit(); timeInit();
consoleInit(); consoleInit();
errorChain(inputInit()); errorChain(inputInit());
errorChain(assetInit());
// errorChain(saveInit()); // errorChain(saveInit());
errorChain(localeManagerInit()); errorChain(localeManagerInit());
errorChain(displayInit()); errorChain(displayInit());
errorChain(uiInit()); errorChain(uiInit());
errorChain(uiTextboxInit()); errorChain(uiTextboxInit());
errorChain(cutsceneInit());
entityManagerInit(); entityManagerInit();
backpackInit(); backpackInit();
physicsManagerInit(); physicsManagerInit();
errorChain(networkInit()); errorChain(networkInit());
errorChain(scriptInit());
errorChain(sceneInit());
consolePrint("Engine initialized"); consolePrint("Engine initialized");
errorChain(scriptExecFile("init.js"));
errorOk(); errorOk();
} }
@@ -73,10 +64,6 @@ errorret_t engineUpdate(void) {
errorChain(uiTextboxUpdate()); errorChain(uiTextboxUpdate());
physicsManagerUpdate(); physicsManagerUpdate();
errorChain(displayUpdate()); errorChain(displayUpdate());
errorChain(cutsceneUpdate());
errorChain(sceneUpdate());
errorChain(assetUpdate());
errorChain(scriptUpdate());
if(inputPressed(INPUT_ACTION_RAGEQUIT)) ENGINE.running = false; if(inputPressed(INPUT_ACTION_RAGEQUIT)) ENGINE.running = false;
errorOk(); errorOk();
@@ -88,9 +75,6 @@ void engineExit(void) {
errorret_t engineDispose(void) { errorret_t engineDispose(void) {
uiTextboxDispose(); uiTextboxDispose();
cutsceneDispose();
errorChain(sceneDispose());
errorChain(scriptDispose());
errorChain(networkDispose()); errorChain(networkDispose());
entityManagerDispose(); entityManagerDispose();
localeManagerDispose(); localeManagerDispose();
@@ -98,7 +82,6 @@ errorret_t engineDispose(void) {
consoleDispose(); consoleDispose();
errorChain(displayDispose()); errorChain(displayDispose());
// errorChain(saveDispose()); // errorChain(saveDispose());
errorChain(assetDispose());
errorOk(); errorOk();
} }
+2 -17
View File
@@ -1,6 +1,6 @@
/** /**
* Copyright (c) 2026 Dominic Masters * Copyright (c) 2026 Dominic Masters
* *
* This software is released under the MIT License. * This software is released under the MIT License.
* https://opensource.org/licenses/MIT * https://opensource.org/licenses/MIT
*/ */
@@ -19,25 +19,10 @@ errorret_t localeManagerInit() {
errorret_t localeManagerSetLocale(const localeinfo_t *locale) { errorret_t localeManagerSetLocale(const localeinfo_t *locale) {
assertNotNull(locale, "Locale cannot be NULL"); assertNotNull(locale, "Locale cannot be NULL");
if(LOCALE.entry != NULL) {
assetEntryUnlock(LOCALE.entry);
LOCALE.entry = NULL;
}
LOCALE.locale = locale; LOCALE.locale = locale;
LOCALE.entry = assetGetEntry(locale->file, ASSET_LOADER_TYPE_LOCALE, NULL);
assetEntryLock(LOCALE.entry);
errorChain(assetRequireLoaded(LOCALE.entry));
errorOk(); errorOk();
} }
void localeManagerDispose() { void localeManagerDispose() {
if(LOCALE.entry != NULL) {
assetEntryUnlock(LOCALE.entry);
LOCALE.entry = NULL;
}
LOCALE.locale = NULL; LOCALE.locale = NULL;
} }
+4 -50
View File
@@ -1,6 +1,6 @@
/** /**
* Copyright (c) 2026 Dominic Masters * Copyright (c) 2026 Dominic Masters
* *
* This software is released under the MIT License. * This software is released under the MIT License.
* https://opensource.org/licenses/MIT * https://opensource.org/licenses/MIT
*/ */
@@ -9,75 +9,29 @@
#include "error/error.h" #include "error/error.h"
#include "localemanager.h" #include "localemanager.h"
#include "locale/localeinfo.h" #include "locale/localeinfo.h"
#include "asset/asset.h"
typedef struct { typedef struct {
const localeinfo_t *locale; const localeinfo_t *locale;
assetentry_t *entry;
} localemanager_t; } localemanager_t;
extern localemanager_t LOCALE; extern localemanager_t LOCALE;
/** /**
* Initialize the locale system. * Initialize the locale system.
* *
* @return An error code if a failure occurs. * @return An error code if a failure occurs.
*/ */
errorret_t localeManagerInit(); errorret_t localeManagerInit();
/** /**
* Set the current locale. * Set the current locale.
* *
* @param locale The locale to set. * @param locale The locale to set.
* @return An error code if a failure occurs. * @return An error code if a failure occurs.
*/ */
errorret_t localeManagerSetLocale(const localeinfo_t *locale); errorret_t localeManagerSetLocale(const localeinfo_t *locale);
/**
* Get a localized string for the given message ID.
*
* @param id The message ID to retrieve.
* @param buffer Buffer to write the retrieved string to.
* @param bufferSize Size of the buffer.
* @param plural Plural index to retrieve.
* @param ... Additional arguments for formatting the string.
* @return An error code if a failure occurs.
*/
#define localeManagerGetText(id, buffer, bufferSize, plural, ...) \
assetLocaleGetStringWithVA( \
&LOCALE.entry->data.locale, \
id, \
plural, \
buffer, \
bufferSize, \
__VA_ARGS__ \
)
/**
* Get a localized string for the given message ID with a list of arguments.
*
* @param id The message ID to retrieve.
* @param buffer Buffer to write the retrieved string to.
* @param bufferSize Size of the buffer.
* @param plural Plural index to retrieve.
* @param args List of arguments for formatting the string.
* @param argCount Number of arguments in the list.
* @return An error code if a failure occurs.
*/
#define localeManagerGetTextArgs( \
id, buffer, bufferSize, plural, args, argCount \
) \
assetLocaleGetStringWithArgs( \
&LOCALE.entry->data.locale, \
id, \
plural, \
buffer, \
bufferSize, \
args, \
argCount \
)
/** /**
* Dispose of the locale system. * Dispose of the locale system.
*/ */
void localeManagerDispose(); void localeManagerDispose();
+3 -2
View File
@@ -7,11 +7,12 @@
#include "map.h" #include "map.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "asset/assetfile.h"
#include "util/memory.h" #include "util/memory.h"
#include "util/string.h" #include "util/string.h"
#include "console/console.h" #include "console/console.h"
#define MAP_PATH_MAX 128
map_t MAP; map_t MAP;
chunkindex_t mapChunkRelToIndex( chunkindex_t mapChunkRelToIndex(
@@ -39,7 +40,7 @@ errorret_t mapLoad(const char_t *handle) {
memoryZero(&MAP, sizeof(map_t)); memoryZero(&MAP, sizeof(map_t));
stringCopy(MAP.handle, handle, MAP_HANDLE_MAX); stringCopy(MAP.handle, handle, MAP_HANDLE_MAX);
char_t path[ASSET_FILE_NAME_MAX]; char_t path[MAP_PATH_MAX];
stringFormat(path, sizeof(path), "maps/%s/init.js", handle); stringFormat(path, sizeof(path), "maps/%s/init.js", handle);
consolePrint("Map loaded: %s", handle); consolePrint("Map loaded: %s", handle);
-23
View File
@@ -10,34 +10,11 @@
#include "entity/entitymanager.h" #include "entity/entitymanager.h"
#include "util/memory.h" #include "util/memory.h"
#include "util/string.h" #include "util/string.h"
#include "asset/asset.h"
#include "console/console.h" #include "console/console.h"
errorret_t mapChunkLoad(mapchunk_t *chunk) { errorret_t mapChunkLoad(mapchunk_t *chunk) {
chunk->entityCount = 0; chunk->entityCount = 0;
memoryZero(chunk->entities, sizeof(chunk->entities)); memoryZero(chunk->entities, sizeof(chunk->entities));
if(MAP.handle[0] == '\0') errorOk();
char_t path[ASSET_FILE_NAME_MAX];
stringFormat(
path, sizeof(path),
"maps/%s/chunks/%d_%d_%d.js",
MAP.handle,
(int)chunk->position.x,
(int)chunk->position.y,
(int)chunk->position.z
);
if(!assetFileExists(path)) errorOk();
consolePrint(
"Chunk loaded: %s [%d,%d,%d]",
path,
(int)chunk->position.x,
(int)chunk->position.y,
(int)chunk->position.z
);
errorOk(); errorOk();
} }
-15
View File
@@ -1,15 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(duskjs2c)
dusk_embed_js(${DUSK_LIBRARY_TARGET_NAME}
${CMAKE_CURRENT_SOURCE_DIR}/scene.js
)
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
scene.c
scenerenderpipeline.c
)
-85
View File
@@ -1,85 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#include "scene.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "time/time.h"
#include "display/screen/screen.h"
#include "entity/entitymanager.h"
#include "display/shader/shaderunlit.h"
#include "display/display.h"
#include "ui/ui.h"
#include "scene/scenerenderpipeline.h"
#include "entity/component.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
#include "script/module/modulebase.h"
#include "console/console.h"
#include "script/script.h"
#include "scene_js.h"
scene_t SCENE;
errorret_t sceneInit(void) {
memoryZero(&SCENE, sizeof(scene_t));
errorChain(scriptExecString(SCENE_JS));
errorOk();
}
errorret_t sceneUpdate(void) {
#ifdef DUSK_TIME_DYNAMIC
if(TIME.dynamicUpdate) {
errorChain(scriptExecString("Scene.dynamicUpdate();"));
} else {
errorChain(scriptExecString("Scene.update();"));
}
#else
errorChain(scriptExecString("Scene.update();"));
errorChain(scriptExecString("Scene.dynamicUpdate();"));
#endif
errorOk();
}
errorret_t sceneRender(void) {
mat4 proj, view, ident;
glm_mat4_identity(ident);
errorChain(componentRenderAll());
errorChain(sceneRenderPipeline(entityCameraGetCurrent()));
// UI Rendering
errorChain(shaderBind(&SHADER_UNLIT));
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, ident));
glm_ortho(
0.0f, SCREEN.width,
SCREEN.height, 0.0f,
0.1f, 100.0f,
proj
);
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, proj));
glm_lookat(
(vec3){ 0.0f, 0.0f, 1.0f },
(vec3){ 0.0f, 0.0f, 0.0f },
(vec3){ 0.0f, 1.0f, 0.0f },
view
);
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, view));
errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_BLEND
}));
errorChain(uiRender());
errorOk();
}
errorret_t sceneDispose(void) {
errorChain(scriptExecString("Scene.dispose();"));
errorOk();
}
-44
View File
@@ -1,44 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
typedef struct {
void *nothing;
} scene_t;
extern scene_t SCENE;
/**
* Initialises the scene manager.
*
* @return An error if the init failed, or errorOk() if it succeeded.
*/
errorret_t sceneInit(void);
/**
* Ticks the scene manager. Processes any pending scene transition, then
* calls scriptSceneUpdate on the active scene.
*
* @return An error if the update failed, or errorOk() if it succeeded.
*/
errorret_t sceneUpdate(void);
/**
* Renders the current scene (entities, render pipeline, UI).
*
* @return An error if the render failed, or errorOk() if it succeeded.
*/
errorret_t sceneRender(void);
/**
* Disposes the active scene immediately.
*
* @return An error if the dispose failed, or errorOk() if it succeeded.
*/
errorret_t sceneDispose(void);
-40
View File
@@ -1,40 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
var Scene = {
};
Scene.update = () => {
if(!Scene.current || !Scene.current.update) return;
Scene.current.update();
}
Scene.dynamicUpdate = () => {
if(!Scene.current || !Scene.current.dynamicUpdate) return;
Scene.current.dynamicUpdate();
}
Scene.set = (newScene) => {
// Current scene active?
if(Scene.current && Scene.current.dispose) {
Scene.current.dispose();
}
// Set new scene
Scene.current = newScene;
if(!newScene) return;
// Init
if(newScene.init) {
newScene.init();
}
};
Scene.dispose = () => {
if(Scene.current && Scene.current.dispose) {
Scene.current.dispose();
}
Scene.current = null;
}
-15
View File
@@ -1,15 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
script.c
scriptpromisepend.c
scriptproto.c
)
# Subdirectories
add_subdirectory(module)
-30
View File
@@ -1,30 +0,0 @@
# 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
modulelist.c
)
# Subdirs
add_subdirectory(animation)
add_subdirectory(asset)
add_subdirectory(console)
add_subdirectory(display)
add_subdirectory(engine)
add_subdirectory(entity)
add_subdirectory(event)
add_subdirectory(input)
add_subdirectory(item)
add_subdirectory(locale)
add_subdirectory(math)
add_subdirectory(overworld)
add_subdirectory(require)
add_subdirectory(save)
add_subdirectory(scene)
add_subdirectory(story)
add_subdirectory(system)
add_subdirectory(ui)
@@ -1,10 +0,0 @@
# 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
moduleanimation.c
moduleeasing.c
)
@@ -1,88 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduleanimation.h"
#include "animation/animation.h"
typedef struct {
keyframe_t keyframes[MODULE_ANIMATION_KEYFRAME_MAX];
animation_t anim;
} moduleanimationdata_t;
scriptproto_t MODULE_ANIMATION_PROTO;
moduleBaseFunction(moduleAnimationCtor) {
if(argc < 1 || !jerry_value_is_array(args[0])) {
return moduleBaseThrow(
"Animation: expected array of keyframe objects"
);
}
uint32_t len = jerry_array_length(args[0]);
if(len == 0) {
return moduleBaseThrow(
"Animation: keyframe array must not be empty"
);
}
const uint16_t count = len > (uint32_t)MODULE_ANIMATION_KEYFRAME_MAX
? (uint16_t)MODULE_ANIMATION_KEYFRAME_MAX
: (uint16_t)len;
moduleanimationdata_t *d = (moduleanimationdata_t *)memoryAllocate(
sizeof(moduleanimationdata_t)
);
for(uint16_t i = 0; i < count; i++) {
jerry_value_t elem = jerry_object_get_index(args[0], (uint32_t)i);
jerry_value_t jtm = moduleBaseGetProp(elem, "time");
jerry_value_t jvl = moduleBaseGetProp(elem, "value");
jerry_value_t jea = moduleBaseGetProp(elem, "easing");
d->keyframes[i].time = moduleBaseValueFloat(jtm);
d->keyframes[i].value = moduleBaseValueFloat(jvl);
d->keyframes[i].easing = jerry_value_is_number(jea)
? (easingtype_t)moduleBaseValueInt(jea)
: EASING_LINEAR;
jerry_value_free(jea);
jerry_value_free(jvl);
jerry_value_free(jtm);
jerry_value_free(elem);
}
animationInit(&d->anim, d->keyframes, count);
jerry_object_set_native_ptr(
callInfo->this_value, &MODULE_ANIMATION_PROTO.info, d
);
return jerry_undefined();
}
moduleBaseFunction(moduleAnimationGetValue) {
moduleBaseRequireArgs(1);
moduleBaseRequireNumber(0);
moduleanimationdata_t *d = (moduleanimationdata_t *)scriptProtoGetValue(
&MODULE_ANIMATION_PROTO, callInfo->this_value
);
if(!d) return jerry_undefined();
return jerry_number(
(double)animationGetValue(&d->anim, moduleBaseArgFloat(0))
);
}
void moduleAnimationInit(void) {
scriptProtoInit(
&MODULE_ANIMATION_PROTO, "Animation",
sizeof(moduleanimationdata_t), moduleAnimationCtor
);
scriptProtoDefineFunc(
&MODULE_ANIMATION_PROTO, "getValue", moduleAnimationGetValue
);
}
void moduleAnimationDispose(void) {
scriptProtoDispose(&MODULE_ANIMATION_PROTO);
}
@@ -1,42 +0,0 @@
/**
* 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"
/** Maximum keyframes per Animation instance. */
#define MODULE_ANIMATION_KEYFRAME_MAX 64
extern scriptproto_t MODULE_ANIMATION_PROTO;
/**
* new Animation(keyframes) - creates an Animation from a JS array of
* `{time, value, easing?}` descriptor objects.
*
* @param args[0] Array of keyframe descriptors.
*/
moduleBaseFunction(moduleAnimationCtor);
/**
* animation.getValue(time) - interpolates the animation at `time`.
*
* @param args[0] Time in seconds (number).
* @return Interpolated float value.
*/
moduleBaseFunction(moduleAnimationGetValue);
/**
* Initializes the Animation module and registers the Animation constructor.
*/
void moduleAnimationInit(void);
/**
* Disposes the Animation module.
*/
void moduleAnimationDispose(void);
@@ -1,51 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduleeasing.h"
#include "animation/easing.h"
scriptproto_t MODULE_EASING_PROTO;
moduleBaseFunction(moduleEasingApply) {
moduleBaseRequireArgs(2);
moduleBaseRequireNumber(0);
moduleBaseRequireNumber(1);
const int32_t type = moduleBaseArgInt(0);
if(type < 0 || type >= (int32_t)EASING_COUNT) {
return moduleBaseThrow("Easing.apply: invalid easing type");
}
const float_t t = moduleBaseArgFloat(1);
return jerry_number((double)easingApply((easingtype_t)type, t));
}
void moduleEasingInit(void) {
scriptProtoInit(&MODULE_EASING_PROTO, "Easing", 0, NULL);
scriptProtoDefineStaticFunc(
&MODULE_EASING_PROTO, "apply", moduleEasingApply
);
moduleBaseSetInt("EASING_LINEAR", EASING_LINEAR);
moduleBaseSetInt("EASING_IN_SINE", EASING_IN_SINE);
moduleBaseSetInt("EASING_OUT_SINE", EASING_OUT_SINE);
moduleBaseSetInt("EASING_IN_OUT_SINE", EASING_IN_OUT_SINE);
moduleBaseSetInt("EASING_IN_QUAD", EASING_IN_QUAD);
moduleBaseSetInt("EASING_OUT_QUAD", EASING_OUT_QUAD);
moduleBaseSetInt("EASING_IN_OUT_QUAD", EASING_IN_OUT_QUAD);
moduleBaseSetInt("EASING_IN_CUBIC", EASING_IN_CUBIC);
moduleBaseSetInt("EASING_OUT_CUBIC", EASING_OUT_CUBIC);
moduleBaseSetInt("EASING_IN_OUT_CUBIC", EASING_IN_OUT_CUBIC);
moduleBaseSetInt("EASING_IN_QUART", EASING_IN_QUART);
moduleBaseSetInt("EASING_OUT_QUART", EASING_OUT_QUART);
moduleBaseSetInt("EASING_IN_OUT_QUART", EASING_IN_OUT_QUART);
moduleBaseSetInt("EASING_IN_BACK", EASING_IN_BACK);
moduleBaseSetInt("EASING_OUT_BACK", EASING_OUT_BACK);
moduleBaseSetInt("EASING_IN_OUT_BACK", EASING_IN_OUT_BACK);
}
void moduleEasingDispose(void) {
scriptProtoDispose(&MODULE_EASING_PROTO);
}
@@ -1,32 +0,0 @@
/**
* 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"
extern scriptproto_t MODULE_EASING_PROTO;
/**
* Easing.apply(type, t) - applies easing function `type` to normalized
* time `t` and returns the eased value.
*
* @param args[0] Easing type constant (EASING_*).
* @param args[1] Normalized input time in [0, 1].
* @return Eased value as a number.
*/
moduleBaseFunction(moduleEasingApply);
/**
* Initializes the Easing module and injects all EASING_* constants.
*/
void moduleEasingInit(void);
/**
* Disposes the Easing module.
*/
void moduleEasingDispose(void);
@@ -1,11 +0,0 @@
# 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
moduleassetentry.c
moduleassetbatch.c
moduleasset.c
)
-132
View File
@@ -1,132 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduleasset.h"
scriptproto_t MODULE_ASSET_PROTO;
moduleBaseFunction(moduleAssetExists) {
moduleBaseRequireArgs(1);
moduleBaseRequireString(0);
char_t buf[256];
moduleBaseToString(args[0], buf, sizeof(buf));
return jerry_boolean(assetFileExists(buf));
}
moduleBaseFunction(moduleAssetLock) {
moduleBaseRequireArgs(2);
moduleBaseRequireString(0);
moduleBaseRequireNumber(1);
char_t buf[256];
moduleBaseToString(args[0], buf, sizeof(buf));
assetloadertype_t type = (assetloadertype_t)moduleBaseArgInt(1);
assetloaderinput_t input;
assetloaderinput_t *inputPtr = NULL;
if(argc >= 3 && jerry_value_is_number(args[2])) {
int32_t inputVal = moduleBaseArgInt(2);
switch(type) {
case ASSET_LOADER_TYPE_TEXTURE:
input.texture = (textureformat_t)inputVal;
inputPtr = &input;
break;
case ASSET_LOADER_TYPE_MESH:
input.mesh = (assetmeshinputaxis_t)inputVal;
inputPtr = &input;
break;
default:
break;
}
}
assetentry_t *entry = assetLock(buf, type, inputPtr);
if(!entry) return moduleBaseThrow("Asset.lock: failed to lock asset");
jsassetentry_t e = { .entry = entry };
return scriptProtoCreateValue(&MODULE_ASSET_ENTRY_PROTO, &e);
}
moduleBaseFunction(moduleAssetRequireLoaded) {
moduleBaseRequireArgs(1);
jsassetentry_t *e = (jsassetentry_t *)scriptProtoGetValue(
&MODULE_ASSET_ENTRY_PROTO, args[0]
);
if(!e || !e->entry) {
return moduleBaseThrow("Asset.requireLoaded: expected AssetEntry");
}
errorret_t err = assetRequireLoaded(e->entry);
if(errorIsNotOk(err)) return moduleBaseThrowError(err);
return jerry_value_copy(args[0]);
}
moduleBaseFunction(moduleAssetUnlock) {
moduleBaseRequireArgs(1);
moduleBaseRequireString(0);
char_t buf[256];
moduleBaseToString(args[0], buf, sizeof(buf));
assetUnlock(buf);
return jerry_undefined();
}
void moduleAssetInit(void) {
moduleAssetEntryInit();
moduleAssetBatchInit();
scriptProtoInit(&MODULE_ASSET_PROTO, "Asset", sizeof(uint8_t), NULL);
scriptProtoDefineStaticFunc(
&MODULE_ASSET_PROTO, "exists", moduleAssetExists
);
scriptProtoDefineStaticFunc(
&MODULE_ASSET_PROTO, "lock", moduleAssetLock
);
scriptProtoDefineStaticFunc(
&MODULE_ASSET_PROTO, "unlock", moduleAssetUnlock
);
scriptProtoDefineStaticFunc(
&MODULE_ASSET_PROTO, "requireLoaded", moduleAssetRequireLoaded
);
jerry_value_t global = MODULE_ASSET_PROTO.prototype;
struct { const char_t *name; int val; } types[] = {
{ "TYPE_MESH", ASSET_LOADER_TYPE_MESH },
{ "TYPE_TEXTURE", ASSET_LOADER_TYPE_TEXTURE },
{ "TYPE_TILESET", ASSET_LOADER_TYPE_TILESET },
{ "TYPE_LOCALE", ASSET_LOADER_TYPE_LOCALE },
{ "TYPE_JSON", ASSET_LOADER_TYPE_JSON },
{ "TYPE_SCRIPT", ASSET_LOADER_TYPE_SCRIPT },
};
for(int i = 0; i < 6; i++) {
jerry_value_t k = jerry_string_sz(types[i].name);
jerry_value_t v = jerry_number((double)types[i].val);
jerry_object_set(global, k, v);
jerry_value_free(v);
jerry_value_free(k);
}
struct { const char_t *name; int val; } axes[] = {
{ "MESH_AXIS_Y_UP", MESH_INPUT_AXIS_Y_UP },
{ "MESH_AXIS_Z_UP", MESH_INPUT_AXIS_Z_UP },
{ "MESH_AXIS_X_UP", MESH_INPUT_AXIS_X_UP },
{ "MESH_AXIS_Y_DOWN", MESH_INPUT_AXIS_Y_DOWN },
{ "MESH_AXIS_Z_DOWN", MESH_INPUT_AXIS_Z_DOWN },
{ "MESH_AXIS_X_DOWN", MESH_INPUT_AXIS_X_DOWN },
};
for(int i = 0; i < 6; i++) {
jerry_value_t k = jerry_string_sz(axes[i].name);
jerry_value_t v = jerry_number((double)axes[i].val);
jerry_object_set(global, k, v);
jerry_value_free(v);
jerry_value_free(k);
}
}
void moduleAssetDispose(void) {
scriptProtoDispose(&MODULE_ASSET_PROTO);
moduleAssetBatchDispose();
moduleAssetEntryDispose();
}
@@ -1,61 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/module/modulebase.h"
#include "script/scriptproto.h"
#include "script/module/display/moduletexture.h"
#include "script/module/asset/moduleassetentry.h"
#include "script/module/asset/moduleassetbatch.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
extern scriptproto_t MODULE_ASSET_PROTO;
/**
* Asset.exists(path) - returns true if the path exists in the asset archive.
* @param args[0] Archive-relative path string.
*/
moduleBaseFunction(moduleAssetExists);
/**
* Asset.lock(path, type, input?) - locks an asset entry and returns an
* AssetEntry. The entry begins loading in the background.
*
* @param args[0] Path string.
* @param args[1] Loader type constant (Asset.TYPE_*).
* @param args[2] Optional loader input (Texture.FORMAT_* or Asset.MESH_AXIS_*).
*/
moduleBaseFunction(moduleAssetLock);
/**
* Asset.requireLoaded(entry) - blocks until the entry is fully loaded.
* @param args[0] An AssetEntry object.
* @return The same AssetEntry for chaining.
* @throws If the load fails.
*/
moduleBaseFunction(moduleAssetRequireLoaded);
/**
* Asset.unlock(path) - releases the asset lock for the given path.
* Prefer entry.unlock() on the AssetEntry object directly.
* @param args[0] Path string originally passed to Asset.lock().
*/
moduleBaseFunction(moduleAssetUnlock);
/**
* Initializes the Asset module. Also calls moduleAssetEntryInit() and
* moduleAssetBatchInit(), then registers the global Asset object with
* exists/lock/unlock/requireLoaded and all TYPE_* / MESH_AXIS_* constants.
*/
void moduleAssetInit(void);
/**
* Disposes the Asset module and calls moduleAssetBatchDispose() and
* moduleAssetEntryDispose().
*/
void moduleAssetDispose(void);
@@ -1,388 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduleassetbatch.h"
#include "util/string.h"
#define ASSET_BATCH_PEND_MAX 8
scriptproto_t MODULE_ASSET_BATCH_PROTO;
static scriptpromisepend_t ASSET_BATCH_PEND[ASSET_BATCH_PEND_MAX];
static uint32_t ASSET_BATCH_PEND_COUNT = 0;
void assetBatchOnLoadedFire(void *params, void *user) {
assetbatch_t *batch = (assetbatch_t *)user;
jerry_value_t undef = jerry_undefined();
uint32_t n = scriptPromisePendResolve(
ASSET_BATCH_PEND, &ASSET_BATCH_PEND_COUNT, batch, undef
);
jerry_value_free(undef);
if(!n) return;
eventUnsubscribe(&batch->onLoaded, assetBatchOnLoadedFire);
eventUnsubscribe(&batch->onError, assetBatchOnErrorFire);
}
void assetBatchOnErrorFire(void *params, void *user) {
assetbatch_t *batch = (assetbatch_t *)user;
jerry_value_t err = jerry_string_sz("Asset batch failed to load");
uint32_t n = scriptPromisePendReject(
ASSET_BATCH_PEND, &ASSET_BATCH_PEND_COUNT, batch, err
);
jerry_value_free(err);
if(!n) return;
eventUnsubscribe(&batch->onLoaded, assetBatchOnLoadedFire);
eventUnsubscribe(&batch->onError, assetBatchOnErrorFire);
}
void moduleAssetBatchFree(void *ptr, jerry_object_native_info_t *info) {
jsassetbatch_t *b = (jsassetbatch_t *)ptr;
if(b && b->batch) {
assetBatchDispose(b->batch);
memoryFree(b->batch);
}
memoryFree(ptr);
}
jsassetbatch_t *moduleAssetBatchSelf(const jerry_call_info_t *callInfo) {
return (jsassetbatch_t *)scriptProtoGetValue(
&MODULE_ASSET_BATCH_PROTO, callInfo->this_value
);
}
moduleBaseFunction(moduleAssetBatchCtor) {
if(argc < 1 || !jerry_value_is_array(args[0])) {
return moduleBaseThrow("AssetBatch: expected an array of descriptors");
}
uint32_t count = jerry_array_length(args[0]);
if(count == 0 || count > (uint32_t)ASSET_BATCH_COUNT_MAX) {
return moduleBaseThrow("AssetBatch: descriptor count out of range");
}
assetbatchdesc_t descs[ASSET_BATCH_COUNT_MAX];
char_t paths[ASSET_BATCH_COUNT_MAX][ASSET_FILE_NAME_MAX];
for(uint32_t i = 0; i < count; i++) {
jerry_value_t desc = jerry_object_get_index(args[0], i);
if(!jerry_value_is_object(desc)) {
jerry_value_free(desc);
return moduleBaseThrow(
"AssetBatch: each descriptor must be an object"
);
}
jerry_value_t pathProp = moduleBaseGetProp(desc, "path");
if(!jerry_value_is_string(pathProp)) {
jerry_value_free(pathProp);
jerry_value_free(desc);
return moduleBaseThrow(
"AssetBatch: descriptor.path must be a string"
);
}
jerry_size_t pathLen = jerry_string_to_buffer(
pathProp, JERRY_ENCODING_UTF8,
(jerry_char_t *)paths[i], ASSET_FILE_NAME_MAX - 1
);
paths[i][pathLen] = '\0';
jerry_value_free(pathProp);
descs[i].path = paths[i];
jerry_value_t typeProp = moduleBaseGetProp(desc, "type");
if(!jerry_value_is_number(typeProp)) {
jerry_value_free(typeProp);
jerry_value_free(desc);
return moduleBaseThrow(
"AssetBatch: descriptor.type must be a number"
);
}
descs[i].type = (assetloadertype_t)moduleBaseValueInt(typeProp);
jerry_value_free(typeProp);
memoryZero(&descs[i].input, sizeof(assetloaderinput_t));
jerry_value_t inputProp = moduleBaseGetProp(desc, "format");
if(jerry_value_is_undefined(inputProp)) {
jerry_value_free(inputProp);
inputProp = moduleBaseGetProp(desc, "input");
}
if(jerry_value_is_undefined(inputProp)) {
jerry_value_free(inputProp);
inputProp = moduleBaseGetProp(desc, "axis");
}
if(jerry_value_is_number(inputProp)) {
int32_t v = moduleBaseValueInt(inputProp);
switch(descs[i].type) {
case ASSET_LOADER_TYPE_TEXTURE:
descs[i].input.texture = (textureformat_t)v;
break;
case ASSET_LOADER_TYPE_MESH:
descs[i].input.mesh = (assetmeshinputaxis_t)v;
break;
default:
break;
}
}
jerry_value_free(inputProp);
jerry_value_free(desc);
}
assetbatch_t *batch = (assetbatch_t *)memoryAllocate(sizeof(assetbatch_t));
assetBatchInit(batch, (uint16_t)count, descs);
jsassetbatch_t init = { .batch = batch };
return scriptProtoCreateValue(&MODULE_ASSET_BATCH_PROTO, &init);
}
moduleBaseFunction(moduleAssetBatchGetCount) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_number(0.0);
return jerry_number((double)b->batch->count);
}
moduleBaseFunction(moduleAssetBatchGetIsLoaded) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_boolean(false);
return jerry_boolean(assetBatchIsLoaded(b->batch));
}
moduleBaseFunction(moduleAssetBatchGetHasError) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_boolean(false);
return jerry_boolean(assetBatchHasError(b->batch));
}
moduleBaseFunction(moduleAssetBatchRequireLoaded) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) {
return moduleBaseThrow(
"AssetBatch.requireLoaded: batch already disposed"
);
}
errorret_t err = assetBatchRequireLoaded(b->batch);
if(errorIsNotOk(err)) return moduleBaseThrowError(err);
return jerry_value_copy(callInfo->this_value);
}
moduleBaseFunction(moduleAssetBatchLoaded) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) {
return moduleBaseThrow("AssetBatch.loaded: batch disposed");
}
jerry_value_t promise = jerry_promise();
if(assetBatchIsLoaded(b->batch)) {
jerry_value_t undef = jerry_undefined();
jerry_value_t r = jerry_promise_resolve(promise, undef);
jerry_value_free(undef);
jerry_value_free(r);
return promise;
}
if(assetBatchHasError(b->batch)) {
jerry_value_t err = jerry_string_sz("Asset batch failed to load");
jerry_value_t r = jerry_promise_reject(promise, err);
jerry_value_free(err);
jerry_value_free(r);
return promise;
}
if(ASSET_BATCH_PEND_COUNT >= ASSET_BATCH_PEND_MAX) {
jerry_value_free(promise);
return moduleBaseThrow("AssetBatch.loaded: too many pending");
}
if(!scriptPromisePendHas(
ASSET_BATCH_PEND, ASSET_BATCH_PEND_COUNT, b->batch
)) {
eventSubscribe(
&b->batch->onLoaded, assetBatchOnLoadedFire, b->batch
);
eventSubscribe(
&b->batch->onError, assetBatchOnErrorFire, b->batch
);
}
scriptPromisePendAdd(
ASSET_BATCH_PEND, &ASSET_BATCH_PEND_COUNT,
ASSET_BATCH_PEND_MAX, b->batch, promise
);
return promise;
}
moduleBaseFunction(moduleAssetBatchLock) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_undefined();
assetBatchLock(b->batch);
return jerry_value_copy(callInfo->this_value);
}
moduleBaseFunction(moduleAssetBatchUnlock) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_undefined();
assetBatchUnlock(b->batch);
assetBatchDispose(b->batch);
memoryFree(b->batch);
b->batch = NULL;
return jerry_undefined();
}
moduleBaseFunction(moduleAssetBatchEntry) {
moduleBaseRequireArgs(1);
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_undefined();
uint32_t idx = (uint32_t)moduleBaseArgInt(0);
if(idx >= (uint32_t)b->batch->count) return jerry_undefined();
assetentry_t *entry = b->batch->entries[idx];
if(!entry) return jerry_undefined();
assetEntryLock(entry);
jsassetentry_t e = { .entry = entry };
return scriptProtoCreateValue(&MODULE_ASSET_ENTRY_PROTO, &e);
}
moduleBaseFunction(moduleAssetBatchGetAssetByPath) {
moduleBaseRequireArgs(1);
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_undefined();
if(!jerry_value_is_string(args[0])) return jerry_undefined();
char_t path[ASSET_FILE_NAME_MAX];
jerry_size_t pathLen = jerry_string_to_buffer(
args[0], JERRY_ENCODING_UTF8,
(jerry_char_t *)path, ASSET_FILE_NAME_MAX - 1
);
path[pathLen] = '\0';
for(uint16_t i = 0; i < b->batch->count; i++) {
assetentry_t *entry = b->batch->entries[i];
if(!entry) continue;
if(!stringEquals(entry->name, path)) continue;
assetEntryLock(entry);
jsassetentry_t e = { .entry = entry };
return scriptProtoCreateValue(&MODULE_ASSET_ENTRY_PROTO, &e);
}
return jerry_undefined();
}
moduleBaseFunction(moduleAssetBatchGetOnLoaded) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_undefined();
return moduleEventGetOrCreate(
callInfo, &b->batch->onLoaded, "_onLoaded"
);
}
moduleBaseFunction(moduleAssetBatchGetOnEntryLoaded) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_undefined();
return moduleEventGetOrCreate(
callInfo, &b->batch->onEntryLoaded, "_onEntryLoaded"
);
}
moduleBaseFunction(moduleAssetBatchGetOnError) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_undefined();
return moduleEventGetOrCreate(
callInfo, &b->batch->onError, "_onError"
);
}
moduleBaseFunction(moduleAssetBatchGetOnEntryError) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_undefined();
return moduleEventGetOrCreate(
callInfo, &b->batch->onEntryError, "_onEntryError"
);
}
moduleBaseFunction(moduleAssetBatchToString) {
jsassetbatch_t *b = moduleAssetBatchSelf(callInfo);
if(!b || !b->batch) return jerry_string_sz("AssetBatch:invalid");
char_t buf[32];
snprintf(buf, sizeof(buf), "AssetBatch(%u)", (unsigned)b->batch->count);
return jerry_string_sz(buf);
}
void moduleAssetBatchInit(void) {
ASSET_BATCH_PEND_COUNT = 0;
scriptProtoInit(
&MODULE_ASSET_BATCH_PROTO, "AssetBatch",
sizeof(jsassetbatch_t), moduleAssetBatchCtor
);
MODULE_ASSET_BATCH_PROTO.info.free_cb = moduleAssetBatchFree;
scriptProtoDefineProp(
&MODULE_ASSET_BATCH_PROTO, "count",
moduleAssetBatchGetCount, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_BATCH_PROTO, "isLoaded",
moduleAssetBatchGetIsLoaded, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_BATCH_PROTO, "hasError",
moduleAssetBatchGetHasError, NULL
);
scriptProtoDefineFunc(
&MODULE_ASSET_BATCH_PROTO, "requireLoaded",
moduleAssetBatchRequireLoaded
);
scriptProtoDefineFunc(
&MODULE_ASSET_BATCH_PROTO, "loaded", moduleAssetBatchLoaded
);
scriptProtoDefineFunc(
&MODULE_ASSET_BATCH_PROTO, "lock", moduleAssetBatchLock
);
scriptProtoDefineFunc(
&MODULE_ASSET_BATCH_PROTO, "unlock", moduleAssetBatchUnlock
);
scriptProtoDefineFunc(
&MODULE_ASSET_BATCH_PROTO, "entry", moduleAssetBatchEntry
);
scriptProtoDefineFunc(
&MODULE_ASSET_BATCH_PROTO, "getAssetByPath",
moduleAssetBatchGetAssetByPath
);
scriptProtoDefineProp(
&MODULE_ASSET_BATCH_PROTO, "onLoaded",
moduleAssetBatchGetOnLoaded, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_BATCH_PROTO, "onEntryLoaded",
moduleAssetBatchGetOnEntryLoaded, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_BATCH_PROTO, "onError",
moduleAssetBatchGetOnError, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_BATCH_PROTO, "onEntryError",
moduleAssetBatchGetOnEntryError, NULL
);
scriptProtoDefineToString(
&MODULE_ASSET_BATCH_PROTO, moduleAssetBatchToString
);
}
void moduleAssetBatchDispose(void) {
for(uint32_t i = 0; i < ASSET_BATCH_PEND_COUNT; i++) {
bool_t seen = false;
for(uint32_t j = 0; j < i; j++) {
if(ASSET_BATCH_PEND[j].key == ASSET_BATCH_PEND[i].key) {
seen = true; break;
}
}
if(!seen) {
assetbatch_t *b = (assetbatch_t *)ASSET_BATCH_PEND[i].key;
eventUnsubscribe(&b->onLoaded, assetBatchOnLoadedFire);
eventUnsubscribe(&b->onError, assetBatchOnErrorFire);
}
}
scriptPromisePendFreeAll(ASSET_BATCH_PEND, &ASSET_BATCH_PEND_COUNT);
scriptProtoDispose(&MODULE_ASSET_BATCH_PROTO);
}
@@ -1,142 +0,0 @@
/**
* 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/scriptpromisepend.h"
#include "script/module/asset/moduleassetentry.h"
#include "asset/assetbatch.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
#include "util/memory.h"
extern scriptproto_t MODULE_ASSET_BATCH_PROTO;
/** C struct wrapped by every AssetBatch JS instance. */
typedef struct {
assetbatch_t *batch;
} jsassetbatch_t;
/**
* Resolves all pending loaded() promises for this batch, then
* unsubscribes from both onLoaded and onError.
*
* @param params Unused.
* @param user The assetbatch_t pointer that finished loading.
*/
void assetBatchOnLoadedFire(void *params, void *user);
/**
* Rejects all pending loaded() promises for this batch, then
* unsubscribes from both onLoaded and onError.
*
* @param params Unused.
* @param user The assetbatch_t pointer that errored.
*/
void assetBatchOnErrorFire(void *params, void *user);
/**
* GC free callback - disposes and frees the batch when the JS object is
* garbage collected.
*
* @param ptr Native jsassetbatch_t pointer.
* @param info Native info (unused).
*/
void moduleAssetBatchFree(void *ptr, jerry_object_native_info_t *info);
/**
* Returns the jsassetbatch_t pointer from the current this_value.
*
* @param callInfo The call info.
* @return Pointer to the jsassetbatch_t, or NULL if invalid.
*/
jsassetbatch_t *moduleAssetBatchSelf(const jerry_call_info_t *callInfo);
/**
* AssetBatch(descriptors[]) constructor. Parses an array of
* {path, type, format?/input?/axis?} descriptor objects, locks all assets,
* and returns an AssetBatch JS object. Works with and without `new`.
*
* @param args[0] Array of AssetBatchDescriptor objects.
*/
moduleBaseFunction(moduleAssetBatchCtor);
/** @return Number of entries in the batch. */
moduleBaseFunction(moduleAssetBatchGetCount);
/** @return True when every entry has reached LOADED. */
moduleBaseFunction(moduleAssetBatchGetIsLoaded);
/** @return True if any entry is in an ERROR state. */
moduleBaseFunction(moduleAssetBatchGetHasError);
/**
* requireLoaded() - blocks until every entry is loaded.
* @return this for chaining.
* @throws If any entry fails to load.
*/
moduleBaseFunction(moduleAssetBatchRequireLoaded);
/**
* loaded() - returns a Promise that resolves when all entries load, or
* rejects if any entry errors. Resolves immediately if already loaded.
*/
moduleBaseFunction(moduleAssetBatchLoaded);
/**
* lock() - acquires one additional lock on every entry.
* @return this for chaining.
*/
moduleBaseFunction(moduleAssetBatchLock);
/**
* unlock() - releases all locks and disposes the batch. The object is
* invalid after this call.
*/
moduleBaseFunction(moduleAssetBatchUnlock);
/**
* entry(index) - returns the AssetEntry at index, adding an independent
* lock.
* @param args[0] Index (number).
* @return AssetEntry, or undefined if out of range.
*/
moduleBaseFunction(moduleAssetBatchEntry);
/**
* getAssetByPath(path) - finds the first entry whose name matches path
* and returns it as a locked AssetEntry, or undefined if not found.
* The returned entry must be unlocked separately when no longer needed.
* @param args[0] Path string to match against entry names.
* @return AssetEntry, or undefined if no entry matches.
*/
moduleBaseFunction(moduleAssetBatchGetAssetByPath);
/** @return The onLoaded Event (fires once when all entries load). */
moduleBaseFunction(moduleAssetBatchGetOnLoaded);
/** @return The onEntryLoaded Event (fires per entry). */
moduleBaseFunction(moduleAssetBatchGetOnEntryLoaded);
/** @return The onError Event (fires once when any entry errors). */
moduleBaseFunction(moduleAssetBatchGetOnError);
/** @return The onEntryError Event (fires per errored entry). */
moduleBaseFunction(moduleAssetBatchGetOnEntryError);
/** @return "AssetBatch(n)" string. */
moduleBaseFunction(moduleAssetBatchToString);
/**
* Initializes the AssetBatch module and registers the global AssetBatch
* class.
*/
void moduleAssetBatchInit(void);
/** Disposes the AssetBatch module. */
void moduleAssetBatchDispose(void);
@@ -1,269 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduleassetentry.h"
#define ASSET_ENTRY_PEND_MAX 16
scriptproto_t MODULE_ASSET_ENTRY_PROTO;
static scriptpromisepend_t ASSET_ENTRY_PEND[ASSET_ENTRY_PEND_MAX];
static uint32_t ASSET_ENTRY_PEND_COUNT = 0;
void assetEntryOnLoadedFire(void *params, void *user) {
assetentry_t *entry = (assetentry_t *)user;
jerry_value_t undef = jerry_undefined();
uint32_t n = scriptPromisePendResolve(
ASSET_ENTRY_PEND, &ASSET_ENTRY_PEND_COUNT, entry, undef
);
jerry_value_free(undef);
if(!n) return;
eventUnsubscribe(&entry->onLoaded, assetEntryOnLoadedFire);
eventUnsubscribe(&entry->onError, assetEntryOnErrorFire);
}
void assetEntryOnErrorFire(void *params, void *user) {
assetentry_t *entry = (assetentry_t *)user;
jerry_value_t err = jerry_string_sz("Asset failed to load");
uint32_t n = scriptPromisePendReject(
ASSET_ENTRY_PEND, &ASSET_ENTRY_PEND_COUNT, entry, err
);
jerry_value_free(err);
if(!n) return;
eventUnsubscribe(&entry->onLoaded, assetEntryOnLoadedFire);
eventUnsubscribe(&entry->onError, assetEntryOnErrorFire);
}
void moduleAssetEntryFree(void *ptr, jerry_object_native_info_t *info) {
jsassetentry_t *e = (jsassetentry_t *)ptr;
if(e && e->entry) {
assetUnlockEntry(e->entry);
e->entry = NULL;
}
memoryFree(ptr);
}
jsassetentry_t *moduleAssetEntrySelf(const jerry_call_info_t *callInfo) {
return (jsassetentry_t *)scriptProtoGetValue(
&MODULE_ASSET_ENTRY_PROTO, callInfo->this_value
);
}
moduleBaseFunction(moduleAssetEntryCtor) {
return moduleBaseThrow("AssetEntry cannot be instantiated with new");
}
moduleBaseFunction(moduleAssetEntryGetName) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) return jerry_undefined();
return jerry_string_sz(e->entry->name);
}
moduleBaseFunction(moduleAssetEntryGetState) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) return jerry_undefined();
return jerry_number((double)e->entry->state);
}
moduleBaseFunction(moduleAssetEntryGetType) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) return jerry_undefined();
return jerry_number((double)e->entry->type);
}
moduleBaseFunction(moduleAssetEntryGetIsLoaded) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) return jerry_boolean(false);
return jerry_boolean(e->entry->state == ASSET_ENTRY_STATE_LOADED);
}
moduleBaseFunction(moduleAssetEntryGetTexture) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) return jerry_undefined();
if(e->entry->type != ASSET_LOADER_TYPE_TEXTURE) return jerry_undefined();
if(e->entry->state != ASSET_ENTRY_STATE_LOADED) return jerry_undefined();
assetEntryLock(e->entry);
jstexture_t tex = { .entry = e->entry };
return scriptProtoCreateValue(&MODULE_TEXTURE_PROTO, &tex);
}
moduleBaseFunction(moduleAssetEntryRequireLoaded) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) {
return moduleBaseThrow("AssetEntry.requireLoaded: invalid entry");
}
errorret_t err = assetRequireLoaded(e->entry);
if(errorIsNotOk(err)) return moduleBaseThrowError(err);
return jerry_value_copy(callInfo->this_value);
}
moduleBaseFunction(moduleAssetEntryUnlock) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) return jerry_undefined();
assetUnlockEntry(e->entry);
e->entry = NULL;
return jerry_undefined();
}
moduleBaseFunction(moduleAssetEntryGetOnLoaded) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) return jerry_undefined();
return moduleEventGetOrCreate(
callInfo, &e->entry->onLoaded, "_onLoaded"
);
}
moduleBaseFunction(moduleAssetEntryGetOnUnloaded) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) return jerry_undefined();
return moduleEventGetOrCreate(
callInfo, &e->entry->onUnloaded, "_onUnloaded"
);
}
moduleBaseFunction(moduleAssetEntryGetOnError) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) return jerry_undefined();
return moduleEventGetOrCreate(
callInfo, &e->entry->onError, "_onError"
);
}
moduleBaseFunction(moduleAssetEntryLoaded) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) {
return moduleBaseThrow("AssetEntry.loaded: invalid entry");
}
jerry_value_t promise = jerry_promise();
if(e->entry->state == ASSET_ENTRY_STATE_LOADED) {
jerry_value_t undef = jerry_undefined();
jerry_value_t r = jerry_promise_resolve(promise, undef);
jerry_value_free(undef);
jerry_value_free(r);
return promise;
}
if(e->entry->state == ASSET_ENTRY_STATE_ERROR) {
jerry_value_t err = jerry_string_sz("Asset failed to load");
jerry_value_t r = jerry_promise_reject(promise, err);
jerry_value_free(err);
jerry_value_free(r);
return promise;
}
if(ASSET_ENTRY_PEND_COUNT >= ASSET_ENTRY_PEND_MAX) {
jerry_value_free(promise);
return moduleBaseThrow("AssetEntry.loaded: too many pending");
}
if(!scriptPromisePendHas(
ASSET_ENTRY_PEND, ASSET_ENTRY_PEND_COUNT, e->entry
)) {
eventSubscribe(
&e->entry->onLoaded, assetEntryOnLoadedFire, e->entry
);
eventSubscribe(
&e->entry->onError, assetEntryOnErrorFire, e->entry
);
}
scriptPromisePendAdd(
ASSET_ENTRY_PEND, &ASSET_ENTRY_PEND_COUNT,
ASSET_ENTRY_PEND_MAX, e->entry, promise
);
return promise;
}
moduleBaseFunction(moduleAssetEntryToString) {
jsassetentry_t *e = moduleAssetEntrySelf(callInfo);
if(!e || !e->entry) return jerry_string_sz("AssetEntry:invalid");
char_t buf[64];
snprintf(buf, sizeof(buf), "AssetEntry(%s)", e->entry->name);
return jerry_string_sz(buf);
}
void moduleAssetEntryInit(void) {
ASSET_ENTRY_PEND_COUNT = 0;
scriptProtoInit(
&MODULE_ASSET_ENTRY_PROTO, "AssetEntry",
sizeof(jsassetentry_t), moduleAssetEntryCtor
);
MODULE_ASSET_ENTRY_PROTO.info.free_cb = moduleAssetEntryFree;
scriptProtoDefineProp(
&MODULE_ASSET_ENTRY_PROTO, "name", moduleAssetEntryGetName, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_ENTRY_PROTO, "state", moduleAssetEntryGetState, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_ENTRY_PROTO, "type", moduleAssetEntryGetType, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_ENTRY_PROTO, "isLoaded",
moduleAssetEntryGetIsLoaded, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_ENTRY_PROTO, "texture",
moduleAssetEntryGetTexture, NULL
);
scriptProtoDefineFunc(
&MODULE_ASSET_ENTRY_PROTO, "requireLoaded",
moduleAssetEntryRequireLoaded
);
scriptProtoDefineFunc(
&MODULE_ASSET_ENTRY_PROTO, "unlock", moduleAssetEntryUnlock
);
scriptProtoDefineProp(
&MODULE_ASSET_ENTRY_PROTO, "onLoaded",
moduleAssetEntryGetOnLoaded, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_ENTRY_PROTO, "onUnloaded",
moduleAssetEntryGetOnUnloaded, NULL
);
scriptProtoDefineProp(
&MODULE_ASSET_ENTRY_PROTO, "onError",
moduleAssetEntryGetOnError, NULL
);
scriptProtoDefineFunc(
&MODULE_ASSET_ENTRY_PROTO, "loaded", moduleAssetEntryLoaded
);
scriptProtoDefineToString(
&MODULE_ASSET_ENTRY_PROTO, moduleAssetEntryToString
);
jerry_value_t ctor = MODULE_ASSET_ENTRY_PROTO.constructor;
struct { const char_t *name; int val; } states[] = {
{ "NOT_STARTED", ASSET_ENTRY_STATE_NOT_STARTED },
{ "PENDING", ASSET_ENTRY_STATE_PENDING_ASYNC },
{ "LOADING", ASSET_ENTRY_STATE_LOADING_ASYNC },
{ "LOADED", ASSET_ENTRY_STATE_LOADED },
{ "ERROR", ASSET_ENTRY_STATE_ERROR },
};
for(int i = 0; i < 5; i++) {
jerry_value_t k = jerry_string_sz(states[i].name);
jerry_value_t v = jerry_number((double)states[i].val);
jerry_object_set(ctor, k, v);
jerry_value_free(v);
jerry_value_free(k);
}
}
void moduleAssetEntryDispose(void) {
for(uint32_t i = 0; i < ASSET_ENTRY_PEND_COUNT; i++) {
bool_t seen = false;
for(uint32_t j = 0; j < i; j++) {
if(ASSET_ENTRY_PEND[j].key == ASSET_ENTRY_PEND[i].key) {
seen = true; break;
}
}
if(!seen) {
assetentry_t *e = (assetentry_t *)ASSET_ENTRY_PEND[i].key;
eventUnsubscribe(&e->onLoaded, assetEntryOnLoadedFire);
eventUnsubscribe(&e->onError, assetEntryOnErrorFire);
}
}
scriptPromisePendFreeAll(ASSET_ENTRY_PEND, &ASSET_ENTRY_PEND_COUNT);
scriptProtoDispose(&MODULE_ASSET_ENTRY_PROTO);
}
@@ -1,115 +0,0 @@
/**
* 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/scriptpromisepend.h"
#include "script/module/display/moduletexture.h"
#include "script/module/event/moduleevent.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
#include "asset/loader/assetentry.h"
#include "util/memory.h"
extern scriptproto_t MODULE_ASSET_ENTRY_PROTO;
/** C struct wrapped by every AssetEntry JS instance. */
typedef struct {
assetentry_t *entry;
} jsassetentry_t;
/**
* Resolves all pending loaded() promises for this entry, then
* unsubscribes from both onLoaded and onError.
*
* @param params Unused.
* @param user The assetentry_t pointer that finished loading.
*/
void assetEntryOnLoadedFire(void *params, void *user);
/**
* Rejects all pending loaded() promises for this entry, then
* unsubscribes from both onLoaded and onError.
*
* @param params Unused.
* @param user The assetentry_t pointer that errored.
*/
void assetEntryOnErrorFire(void *params, void *user);
/**
* GC free callback - releases the asset lock when the AssetEntry JS object
* is garbage collected.
*
* @param ptr Native jsassetentry_t pointer.
* @param info Native info (unused).
*/
void moduleAssetEntryFree(void *ptr, jerry_object_native_info_t *info);
/**
* Returns the jsassetentry_t pointer from the current this_value.
*
* @param callInfo The call info.
* @return Pointer to the jsassetentry_t, or NULL if invalid.
*/
jsassetentry_t *moduleAssetEntrySelf(const jerry_call_info_t *callInfo);
/** AssetEntry() constructor - always throws; not directly instantiable. */
moduleBaseFunction(moduleAssetEntryCtor);
/** @return Archive-relative path used as the cache key. */
moduleBaseFunction(moduleAssetEntryGetName);
/** @return Current loading state as a number (AssetEntry.* constants). */
moduleBaseFunction(moduleAssetEntryGetState);
/** @return Loader type constant. */
moduleBaseFunction(moduleAssetEntryGetType);
/** @return True when the entry has fully loaded. */
moduleBaseFunction(moduleAssetEntryGetIsLoaded);
/**
* Returns a Texture wrapping this entry's texture data. The Texture holds
* its own asset lock independently of this AssetEntry.
* @return A Texture JS object, or undefined if not a loaded texture.
*/
moduleBaseFunction(moduleAssetEntryGetTexture);
/**
* requireLoaded() - blocks until the entry is LOADED or ERROR.
* @return this for chaining.
* @throws If the load fails.
*/
moduleBaseFunction(moduleAssetEntryRequireLoaded);
/** unlock() - releases the asset lock immediately. */
moduleBaseFunction(moduleAssetEntryUnlock);
/** @return The onLoaded Event for this entry. */
moduleBaseFunction(moduleAssetEntryGetOnLoaded);
/** @return The onUnloaded Event for this entry. */
moduleBaseFunction(moduleAssetEntryGetOnUnloaded);
/** @return The onError Event for this entry. */
moduleBaseFunction(moduleAssetEntryGetOnError);
/**
* loaded() - returns a Promise that resolves when the entry loads, or
* rejects on error. Resolves immediately if already loaded.
*/
moduleBaseFunction(moduleAssetEntryLoaded);
/** @return "AssetEntry(name)" string. */
moduleBaseFunction(moduleAssetEntryToString);
/** Initializes the AssetEntry module. */
void moduleAssetEntryInit(void);
/** Disposes the AssetEntry module. */
void moduleAssetEntryDispose(void);
@@ -1,202 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduleeventproxy.h"
#include "util/memory.h"
scriptproto_t MODULE_EVENT_PROXY_PROTO;
void moduleEventProxyTrampoline0(void *params, void *user) {
jerry_value_t fn = (jerry_value_t)(uintptr_t)user;
jerry_value_t ret = jerry_call(fn, jerry_undefined(), NULL, 0);
jerry_value_free(ret);
}
void moduleEventProxyTrampoline1(void *params, void *user) {
jerry_value_t fn = (jerry_value_t)(uintptr_t)user;
jerry_value_t ret = jerry_call(fn, jerry_undefined(), NULL, 0);
jerry_value_free(ret);
}
void moduleEventProxyTrampoline2(void *params, void *user) {
jerry_value_t fn = (jerry_value_t)(uintptr_t)user;
jerry_value_t ret = jerry_call(fn, jerry_undefined(), NULL, 0);
jerry_value_free(ret);
}
void moduleEventProxyTrampoline3(void *params, void *user) {
jerry_value_t fn = (jerry_value_t)(uintptr_t)user;
jerry_value_t ret = jerry_call(fn, jerry_undefined(), NULL, 0);
jerry_value_free(ret);
}
eventcallback_t MODULE_EVENT_PROXY_TRAMPOLINES[MODULE_EVENT_PROXY_MAX_SLOTS] = {
moduleEventProxyTrampoline0,
moduleEventProxyTrampoline1,
moduleEventProxyTrampoline2,
moduleEventProxyTrampoline3,
};
void moduleEventProxyFree(void *ptr, jerry_object_native_info_t *info) {
jseventproxy_t *ep = (jseventproxy_t *)ptr;
if(ep) {
for(uint32_t i = 0; i < MODULE_EVENT_PROXY_MAX_SLOTS; i++) {
if(jerry_value_is_function(ep->fns[i])) {
if(ep->event) {
eventUnsubscribe(ep->event, MODULE_EVENT_PROXY_TRAMPOLINES[i]);
}
jerry_value_free(ep->fns[i]);
}
}
}
memoryFree(ptr);
}
jseventproxy_t *moduleEventProxySelf(const jerry_call_info_t *callInfo) {
return (jseventproxy_t *)scriptProtoGetValue(
&MODULE_EVENT_PROXY_PROTO, callInfo->this_value
);
}
static jerry_value_t moduleEventProxyGetSlot(
const jerry_call_info_t *callInfo,
const uint32_t slot
) {
jseventproxy_t *ep = moduleEventProxySelf(callInfo);
if(!ep || !ep->event || slot >= ep->event->size) return jerry_null();
return jerry_value_is_function(ep->fns[slot])
? jerry_value_copy(ep->fns[slot])
: jerry_null();
}
static jerry_value_t moduleEventProxySetSlot(
const jerry_call_info_t *callInfo,
const jerry_value_t args[],
const jerry_length_t argc,
const uint32_t slot
) {
jseventproxy_t *ep = moduleEventProxySelf(callInfo);
if(!ep || !ep->event || slot >= ep->event->size) return jerry_undefined();
if(jerry_value_is_function(ep->fns[slot])) {
eventUnsubscribe(ep->event, MODULE_EVENT_PROXY_TRAMPOLINES[slot]);
jerry_value_free(ep->fns[slot]);
ep->fns[slot] = jerry_undefined();
}
jerry_value_t val = (argc > 0) ? args[0] : jerry_undefined();
if(jerry_value_is_function(val)) {
ep->fns[slot] = jerry_value_copy(val);
eventSubscribe(
ep->event,
MODULE_EVENT_PROXY_TRAMPOLINES[slot],
(void *)(uintptr_t)ep->fns[slot]
);
}
return jerry_undefined();
}
moduleBaseFunction(moduleEventProxyGet0) {
return moduleEventProxyGetSlot(callInfo, 0);
}
moduleBaseFunction(moduleEventProxySet0) {
return moduleEventProxySetSlot(callInfo, args, argc, 0);
}
moduleBaseFunction(moduleEventProxyGet1) {
return moduleEventProxyGetSlot(callInfo, 1);
}
moduleBaseFunction(moduleEventProxySet1) {
return moduleEventProxySetSlot(callInfo, args, argc, 1);
}
moduleBaseFunction(moduleEventProxyGet2) {
return moduleEventProxyGetSlot(callInfo, 2);
}
moduleBaseFunction(moduleEventProxySet2) {
return moduleEventProxySetSlot(callInfo, args, argc, 2);
}
moduleBaseFunction(moduleEventProxyGet3) {
return moduleEventProxyGetSlot(callInfo, 3);
}
moduleBaseFunction(moduleEventProxySet3) {
return moduleEventProxySetSlot(callInfo, args, argc, 3);
}
moduleBaseFunction(moduleEventProxyGetLength) {
jseventproxy_t *ep = moduleEventProxySelf(callInfo);
if(!ep || !ep->event) return jerry_number(0.0);
return jerry_number((double)ep->event->size);
}
moduleBaseFunction(moduleEventProxyToString) {
return jerry_string_sz("EventProxy");
}
jerry_value_t moduleEventProxyGetOrCreate(
const jerry_call_info_t *callInfo,
event_t *event,
const char_t *pinKey
) {
jerry_value_t keyStr = jerry_string_sz(pinKey);
jerry_value_t existing = jerry_object_get(callInfo->this_value, keyStr);
if(!jerry_value_is_undefined(existing)) {
jerry_value_free(keyStr);
return existing;
}
jerry_value_free(existing);
jseventproxy_t ep;
ep.event = event;
for(uint32_t i = 0; i < MODULE_EVENT_PROXY_MAX_SLOTS; i++) {
ep.fns[i] = jerry_undefined();
}
jerry_value_t proxy = scriptProtoCreateValue(
&MODULE_EVENT_PROXY_PROTO, &ep
);
jerry_object_set(callInfo->this_value, keyStr, proxy);
jerry_value_free(keyStr);
return proxy;
}
void moduleEventProxyInit(void) {
scriptProtoInit(
&MODULE_EVENT_PROXY_PROTO, NULL,
sizeof(jseventproxy_t), NULL
);
MODULE_EVENT_PROXY_PROTO.info.free_cb = moduleEventProxyFree;
scriptProtoDefineProp(
&MODULE_EVENT_PROXY_PROTO, "0",
moduleEventProxyGet0, moduleEventProxySet0
);
scriptProtoDefineProp(
&MODULE_EVENT_PROXY_PROTO, "1",
moduleEventProxyGet1, moduleEventProxySet1
);
scriptProtoDefineProp(
&MODULE_EVENT_PROXY_PROTO, "2",
moduleEventProxyGet2, moduleEventProxySet2
);
scriptProtoDefineProp(
&MODULE_EVENT_PROXY_PROTO, "3",
moduleEventProxyGet3, moduleEventProxySet3
);
scriptProtoDefineProp(
&MODULE_EVENT_PROXY_PROTO, "length",
moduleEventProxyGetLength, NULL
);
scriptProtoDefineToString(
&MODULE_EVENT_PROXY_PROTO, moduleEventProxyToString
);
}
void moduleEventProxyDispose(void) {
scriptProtoDispose(&MODULE_EVENT_PROXY_PROTO);
}
@@ -1,10 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
/* Merged into script/module/event/moduleevent.h */
#pragma once
#include "script/module/event/moduleevent.h"
@@ -1,9 +0,0 @@
# 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
moduleconsole.c
)
@@ -1,66 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduleconsole.h"
#include "util/string.h"
#include <string.h>
scriptproto_t MODULE_CONSOLE_PROTO;
moduleBaseFunction(moduleConsolePrint) {
char_t buf[512];
char_t msg[4096];
size_t msgLen = 0;
for(jerry_length_t i = 0; i < argc; ++i) {
jerry_value_t strVal = jerry_value_to_string(args[i]);
moduleBaseToString(strVal, buf, sizeof(buf));
jerry_value_free(strVal);
size_t partLen = strlen(buf);
if(msgLen + partLen + 1 < sizeof(msg)) {
stringCopy(msg + msgLen, buf, sizeof(msg) - msgLen);
msgLen += partLen;
}
if(i + 1 < argc && msgLen + 1 < sizeof(msg)) {
msg[msgLen++] = '\t';
msg[msgLen] = '\0';
}
}
consolePrint("%s", msg);
return jerry_undefined();
}
moduleBaseFunction(moduleConsoleGetVisible) {
return jerry_boolean(CONSOLE.visible);
}
moduleBaseFunction(moduleConsoleSetVisible) {
moduleBaseRequireArgs(1);
CONSOLE.visible = moduleBaseArgBool(0);
return jerry_undefined();
}
void moduleConsoleInit(void) {
scriptProtoInit(
&MODULE_CONSOLE_PROTO, "Console",
sizeof(uint8_t), NULL
);
scriptProtoDefineStaticFunc(
&MODULE_CONSOLE_PROTO, "print", moduleConsolePrint
);
scriptProtoDefineStaticProp(
&MODULE_CONSOLE_PROTO, "visible",
moduleConsoleGetVisible, moduleConsoleSetVisible
);
}
void moduleConsoleDispose(void) {
scriptProtoDispose(&MODULE_CONSOLE_PROTO);
}
@@ -1,36 +0,0 @@
/**
* 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"
extern scriptproto_t MODULE_CONSOLE_PROTO;
/**
* Console.print(...args) - concatenates all args tab-separated and prints
* to the engine console.
*/
moduleBaseFunction(moduleConsolePrint);
/** @return Whether the console overlay is currently visible. */
moduleBaseFunction(moduleConsoleGetVisible);
/** Sets console visibility. @param args[0] Boolean visible state. */
moduleBaseFunction(moduleConsoleSetVisible);
/**
* Initializes the Console module and registers the global Console object with
* print() and the visible property.
*/
void moduleConsoleInit(void);
/**
* Disposes the Console module.
*/
void moduleConsoleDispose(void);
@@ -1,11 +0,0 @@
# 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
modulecolor.c
modulescreen.c
moduletexture.c
)
@@ -1,149 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "modulecolor.h"
scriptproto_t MODULE_COLOR_PROTO;
color_t *moduleColorFrom(const jerry_value_t val) {
return (color_t *)scriptProtoGetValue(&MODULE_COLOR_PROTO, val);
}
jerry_value_t moduleColorPush(const color_t c) {
return scriptProtoCreateValue(&MODULE_COLOR_PROTO, &c);
}
moduleBaseFunction(moduleColorConstructor) {
color_t *ptr = (color_t *)memoryAllocate(sizeof(color_t));
ptr->r = (uint8_t)moduleBaseOptInt(0, 0);
ptr->g = (uint8_t)moduleBaseOptInt(1, 0);
ptr->b = (uint8_t)moduleBaseOptInt(2, 0);
ptr->a = (uint8_t)moduleBaseOptInt(3, 255);
jerry_object_set_native_ptr(
callInfo->this_value, &MODULE_COLOR_PROTO.info, ptr
);
return jerry_undefined();
}
moduleBaseFunction(moduleColorGetR) {
color_t *c = moduleColorFrom(callInfo->this_value);
if(!c) return jerry_undefined();
return jerry_number((double)c->r);
}
moduleBaseFunction(moduleColorSetR) {
moduleBaseRequireArgs(1);
color_t *c = moduleColorFrom(callInfo->this_value);
if(!c) return jerry_undefined();
c->r = (uint8_t)moduleBaseArgInt(0);
return jerry_undefined();
}
moduleBaseFunction(moduleColorGetG) {
color_t *c = moduleColorFrom(callInfo->this_value);
if(!c) return jerry_undefined();
return jerry_number((double)c->g);
}
moduleBaseFunction(moduleColorSetG) {
moduleBaseRequireArgs(1);
color_t *c = moduleColorFrom(callInfo->this_value);
if(!c) return jerry_undefined();
c->g = (uint8_t)moduleBaseArgInt(0);
return jerry_undefined();
}
moduleBaseFunction(moduleColorGetB) {
color_t *c = moduleColorFrom(callInfo->this_value);
if(!c) return jerry_undefined();
return jerry_number((double)c->b);
}
moduleBaseFunction(moduleColorSetB) {
moduleBaseRequireArgs(1);
color_t *c = moduleColorFrom(callInfo->this_value);
if(!c) return jerry_undefined();
c->b = (uint8_t)moduleBaseArgInt(0);
return jerry_undefined();
}
moduleBaseFunction(moduleColorGetA) {
color_t *c = moduleColorFrom(callInfo->this_value);
if(!c) return jerry_undefined();
return jerry_number((double)c->a);
}
moduleBaseFunction(moduleColorSetA) {
moduleBaseRequireArgs(1);
color_t *c = moduleColorFrom(callInfo->this_value);
if(!c) return jerry_undefined();
c->a = (uint8_t)moduleBaseArgInt(0);
return jerry_undefined();
}
moduleBaseFunction(moduleColorToString) {
color_t *c = moduleColorFrom(callInfo->this_value);
if(!c) return jerry_string_sz("Color:invalid");
char_t buf[32];
snprintf(buf, sizeof(buf), "Color(%u,%u,%u,%u)",
(unsigned)c->r, (unsigned)c->g,
(unsigned)c->b, (unsigned)c->a
);
return jerry_string_sz(buf);
}
void moduleColorInit(void) {
scriptProtoInit(
&MODULE_COLOR_PROTO, "Color",
sizeof(color_t), moduleColorConstructor
);
scriptProtoDefineProp(
&MODULE_COLOR_PROTO, "r", moduleColorGetR, moduleColorSetR
);
scriptProtoDefineProp(
&MODULE_COLOR_PROTO, "g", moduleColorGetG, moduleColorSetG
);
scriptProtoDefineProp(
&MODULE_COLOR_PROTO, "b", moduleColorGetB, moduleColorSetB
);
scriptProtoDefineProp(
&MODULE_COLOR_PROTO, "a", moduleColorGetA, moduleColorSetA
);
scriptProtoDefineToString(&MODULE_COLOR_PROTO, moduleColorToString);
struct { const char_t *name; color_t val; } constants[] = {
{ "WHITE", COLOR_WHITE },
{ "BLACK", COLOR_BLACK },
{ "RED", COLOR_RED },
{ "GREEN", COLOR_GREEN },
{ "BLUE", COLOR_BLUE },
{ "YELLOW", COLOR_YELLOW },
{ "CYAN", COLOR_CYAN },
{ "MAGENTA", COLOR_MAGENTA },
{ "TRANSPARENT", COLOR_TRANSPARENT },
{ "GRAY", COLOR_GRAY },
{ "LIGHT_GRAY", COLOR_LIGHT_GRAY },
{ "DARK_GRAY", COLOR_DARK_GRAY },
{ "ORANGE", COLOR_ORANGE },
{ "PURPLE", COLOR_PURPLE },
{ "PINK", COLOR_PINK },
{ "TEAL", COLOR_TEAL },
{ "CORNFLOWER_BLUE", COLOR_CORNFLOWER_BLUE },
};
jerry_value_t ctor = MODULE_COLOR_PROTO.constructor;
for(int i = 0; i < (int)(sizeof(constants)/sizeof(constants[0])); i++) {
jerry_value_t k = jerry_string_sz(constants[i].name);
jerry_value_t v = moduleColorPush(constants[i].val);
jerry_object_set(ctor, k, v);
jerry_value_free(v);
jerry_value_free(k);
}
}
void moduleColorDispose(void) {
scriptProtoDispose(&MODULE_COLOR_PROTO);
}
@@ -1,67 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/module/modulebase.h"
#include "script/scriptproto.h"
#include "util/memory.h"
#include "display/color.h"
extern scriptproto_t MODULE_COLOR_PROTO;
/**
* Returns the native color_t pointer from a Color JS value.
*
* @param val The JS value to extract from.
* @return Pointer to the color data, or NULL if not a Color.
*/
color_t *moduleColorFrom(const jerry_value_t val);
/**
* Creates a Color JS object wrapping a copy of a C color_t.
*
* @param c The source color.
* @return A new Color JS object.
*/
jerry_value_t moduleColorPush(const color_t c);
/** Color(r?, g?, b?, a?) constructor. */
moduleBaseFunction(moduleColorConstructor);
/** @return The red channel as a number. */
moduleBaseFunction(moduleColorGetR);
/** Sets the red channel. @param args[0] New r value (0-255). */
moduleBaseFunction(moduleColorSetR);
/** @return The green channel as a number. */
moduleBaseFunction(moduleColorGetG);
/** Sets the green channel. @param args[0] New g value (0-255). */
moduleBaseFunction(moduleColorSetG);
/** @return The blue channel as a number. */
moduleBaseFunction(moduleColorGetB);
/** Sets the blue channel. @param args[0] New b value (0-255). */
moduleBaseFunction(moduleColorSetB);
/** @return The alpha channel as a number. */
moduleBaseFunction(moduleColorGetA);
/** Sets the alpha channel. @param args[0] New a value (0-255). */
moduleBaseFunction(moduleColorSetA);
/** @return "Color(r,g,b,a)" string. */
moduleBaseFunction(moduleColorToString);
/**
* Initializes the Color module and registers the global Color class with named
* color constants (Color.WHITE, Color.RED, etc.).
*/
void moduleColorInit(void);
/**
* Disposes the Color module.
*/
void moduleColorDispose(void);
@@ -1,39 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "modulescreen.h"
scriptproto_t MODULE_SCREEN_PROTO;
moduleBaseFunction(moduleScreenGetWidth) {
return jerry_number((double)SCREEN.width);
}
moduleBaseFunction(moduleScreenGetHeight) {
return jerry_number((double)SCREEN.height);
}
moduleBaseFunction(moduleScreenGetAspect) {
return jerry_number((double)SCREEN.aspect);
}
void moduleScreenInit(void) {
scriptProtoInit(&MODULE_SCREEN_PROTO, "Screen", sizeof(uint8_t), NULL);
scriptProtoDefineStaticProp(
&MODULE_SCREEN_PROTO, "width", moduleScreenGetWidth, NULL
);
scriptProtoDefineStaticProp(
&MODULE_SCREEN_PROTO, "height", moduleScreenGetHeight, NULL
);
scriptProtoDefineStaticProp(
&MODULE_SCREEN_PROTO, "aspect", moduleScreenGetAspect, NULL
);
}
void moduleScreenDispose(void) {
scriptProtoDispose(&MODULE_SCREEN_PROTO);
}
@@ -1,33 +0,0 @@
/**
* 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"
extern scriptproto_t MODULE_SCREEN_PROTO;
/** @return Current screen width in pixels. */
moduleBaseFunction(moduleScreenGetWidth);
/** @return Current screen height in pixels. */
moduleBaseFunction(moduleScreenGetHeight);
/** @return Current screen aspect ratio (width / height). */
moduleBaseFunction(moduleScreenGetAspect);
/**
* Initializes the Screen module and registers read-only width/height/aspect
* properties on the global Screen object.
*/
void moduleScreenInit(void);
/**
* Disposes the Screen module.
*/
void moduleScreenDispose(void);
@@ -1,85 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduletexture.h"
scriptproto_t MODULE_TEXTURE_PROTO;
void moduleTextureFree(void *ptr, jerry_object_native_info_t *info) {
jstexture_t *tex = (jstexture_t *)ptr;
if(tex && tex->entry) {
assetUnlockEntry(tex->entry);
tex->entry = NULL;
}
memoryFree(ptr);
}
jstexture_t *moduleTextureSelf(const jerry_call_info_t *callInfo) {
return (jstexture_t *)scriptProtoGetValue(
&MODULE_TEXTURE_PROTO, callInfo->this_value
);
}
moduleBaseFunction(moduleTextureCtor) {
return moduleBaseThrow("Texture cannot be instantiated with new");
}
moduleBaseFunction(moduleTextureGetWidth) {
jstexture_t *t = moduleTextureSelf(callInfo);
if(!t || !t->entry) return jerry_undefined();
return jerry_number((double)t->entry->data.texture.width);
}
moduleBaseFunction(moduleTextureGetHeight) {
jstexture_t *t = moduleTextureSelf(callInfo);
if(!t || !t->entry) return jerry_undefined();
return jerry_number((double)t->entry->data.texture.height);
}
moduleBaseFunction(moduleTextureToString) {
jstexture_t *t = moduleTextureSelf(callInfo);
if(!t || !t->entry) return jerry_string_sz("Texture:invalid");
char_t buf[64];
snprintf(buf, sizeof(buf), "Texture(%dx%d)",
t->entry->data.texture.width,
t->entry->data.texture.height
);
return jerry_string_sz(buf);
}
void moduleTextureInit(void) {
scriptProtoInit(
&MODULE_TEXTURE_PROTO, "Texture",
sizeof(jstexture_t), moduleTextureCtor
);
MODULE_TEXTURE_PROTO.info.free_cb = moduleTextureFree;
scriptProtoDefineProp(
&MODULE_TEXTURE_PROTO, "width", moduleTextureGetWidth, NULL
);
scriptProtoDefineProp(
&MODULE_TEXTURE_PROTO, "height", moduleTextureGetHeight, NULL
);
scriptProtoDefineToString(&MODULE_TEXTURE_PROTO, moduleTextureToString);
jerry_value_t ctor = MODULE_TEXTURE_PROTO.constructor;
struct { const char_t *name; int val; } formats[] = {
{ "FORMAT_RGBA", TEXTURE_FORMAT_RGBA },
{ "FORMAT_PALETTE", TEXTURE_FORMAT_PALETTE },
};
for(int i = 0; i < 2; i++) {
jerry_value_t k = jerry_string_sz(formats[i].name);
jerry_value_t v = jerry_number((double)formats[i].val);
jerry_object_set(ctor, k, v);
jerry_value_free(v);
jerry_value_free(k);
}
}
void moduleTextureDispose(void) {
scriptProtoDispose(&MODULE_TEXTURE_PROTO);
}
@@ -1,61 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/module/modulebase.h"
#include "script/scriptproto.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
#include "display/texture/texture.h"
#include "util/memory.h"
extern scriptproto_t MODULE_TEXTURE_PROTO;
/** C struct wrapped by every Texture JS instance. */
typedef struct {
assetentry_t *entry;
} jstexture_t;
/**
* GC free callback - unlocks the asset entry when the Texture JS object is
* garbage collected.
*
* @param ptr Native jstexture_t pointer.
* @param info Native info (unused).
*/
void moduleTextureFree(void *ptr, jerry_object_native_info_t *info);
/**
* Returns the jstexture_t pointer from the current this_value.
*
* @param callInfo The call info.
* @return Pointer to the jstexture_t, or NULL if invalid.
*/
jstexture_t *moduleTextureSelf(const jerry_call_info_t *callInfo);
/** Texture() constructor - always throws; not directly instantiable. */
moduleBaseFunction(moduleTextureCtor);
/** @return Texture width in pixels, or undefined if not loaded. */
moduleBaseFunction(moduleTextureGetWidth);
/** @return Texture height in pixels, or undefined if not loaded. */
moduleBaseFunction(moduleTextureGetHeight);
/** @return "Texture(WxH)" string. */
moduleBaseFunction(moduleTextureToString);
/**
* Initializes the Texture module and registers the global Texture class with
* FORMAT_RGBA and FORMAT_PALETTE constants.
*/
void moduleTextureInit(void);
/**
* Disposes the Texture module.
*/
void moduleTextureDispose(void);
@@ -1,11 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
moduleengine.c
moduleframe.c
moduletimeout.c
)
@@ -1,34 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduleengine.h"
scriptproto_t MODULE_ENGINE_PROTO;
moduleBaseFunction(moduleEngineGetRunning) {
return jerry_boolean(ENGINE.running);
}
moduleBaseFunction(moduleEngineExit) {
ENGINE.running = false;
return jerry_undefined();
}
void moduleEngineInit(void) {
scriptProtoInit(&MODULE_ENGINE_PROTO, "Engine", sizeof(uint8_t), NULL);
scriptProtoDefineStaticProp(
&MODULE_ENGINE_PROTO, "running",
moduleEngineGetRunning, NULL
);
scriptProtoDefineStaticFunc(
&MODULE_ENGINE_PROTO, "exit", moduleEngineExit
);
}
void moduleEngineDispose(void) {
scriptProtoDispose(&MODULE_ENGINE_PROTO);
}
@@ -1,30 +0,0 @@
/**
* 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"
extern scriptproto_t MODULE_ENGINE_PROTO;
/** @return True if the engine main loop is still running. */
moduleBaseFunction(moduleEngineGetRunning);
/** Signals the engine to stop on the next frame. */
moduleBaseFunction(moduleEngineExit);
/**
* Initializes the Engine module and registers the global Engine object with the
* running property and exit() method.
*/
void moduleEngineInit(void);
/**
* Disposes the Engine module.
*/
void moduleEngineDispose(void);
@@ -1,45 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduleframe.h"
jerry_value_t MODULE_FRAME_PENDING[MODULE_FRAME_PENDING_MAX];
uint32_t MODULE_FRAME_PENDING_COUNT = 0;
moduleBaseFunction(moduleFrameFrame) {
if(MODULE_FRAME_PENDING_COUNT >= MODULE_FRAME_PENDING_MAX) {
return moduleBaseThrow("Too many pending frame() calls");
}
jerry_value_t promise = jerry_promise();
MODULE_FRAME_PENDING[MODULE_FRAME_PENDING_COUNT++] =
jerry_value_copy(promise);
return promise;
}
void moduleFrameFlush(void) {
uint32_t count = MODULE_FRAME_PENDING_COUNT;
MODULE_FRAME_PENDING_COUNT = 0;
for(uint32_t i = 0; i < count; i++) {
jerry_value_t ret = jerry_promise_resolve(
MODULE_FRAME_PENDING[i], jerry_undefined()
);
jerry_value_free(ret);
jerry_value_free(MODULE_FRAME_PENDING[i]);
}
}
void moduleFrameInit(void) {
MODULE_FRAME_PENDING_COUNT = 0;
moduleBaseDefineGlobalMethod("frame", moduleFrameFrame);
}
void moduleFrameDispose(void) {
for(uint32_t i = 0; i < MODULE_FRAME_PENDING_COUNT; i++) {
jerry_value_free(MODULE_FRAME_PENDING[i]);
}
MODULE_FRAME_PENDING_COUNT = 0;
}
@@ -1,37 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/module/modulebase.h"
/** Maximum number of concurrent frame() awaits. */
#define MODULE_FRAME_PENDING_MAX 64
extern jerry_value_t MODULE_FRAME_PENDING[MODULE_FRAME_PENDING_MAX];
extern uint32_t MODULE_FRAME_PENDING_COUNT;
/**
* frame() - returns a Promise that resolves at the start of the next frame.
* Used as `await frame()` inside an async script loop.
*/
moduleBaseFunction(moduleFrameFrame);
/**
* Resolves all pending frame() promises. Must be called once per frame before
* jerry_run_jobs() so that awaiting scripts resume in the same tick.
*/
void moduleFrameFlush(void);
/**
* Initializes the frame module and registers the global frame() function.
*/
void moduleFrameInit(void);
/**
* Disposes the frame module, releasing any unresolved pending promises.
*/
void moduleFrameDispose(void);
@@ -1,61 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduletimeout.h"
moduletimeoutentry_t MODULE_TIMEOUT_PENDING[MODULE_TIMEOUT_PENDING_MAX];
uint32_t MODULE_TIMEOUT_PENDING_COUNT = 0;
moduleBaseFunction(moduleTimeoutTimeout) {
moduleBaseRequireArgs(1);
moduleBaseRequireNumber(0);
if(MODULE_TIMEOUT_PENDING_COUNT >= MODULE_TIMEOUT_PENDING_MAX) {
return moduleBaseThrow("Too many pending timeout() calls");
}
float_t ms = moduleBaseArgFloat(0);
jerry_value_t promise = jerry_promise();
MODULE_TIMEOUT_PENDING[MODULE_TIMEOUT_PENDING_COUNT].promise =
jerry_value_copy(promise);
MODULE_TIMEOUT_PENDING[MODULE_TIMEOUT_PENDING_COUNT].targetTime =
TIME.time + ms / 1000.0f;
MODULE_TIMEOUT_PENDING_COUNT++;
return promise;
}
void moduleTimeoutFlush(void) {
uint32_t i = 0;
while(i < MODULE_TIMEOUT_PENDING_COUNT) {
if(TIME.time >= MODULE_TIMEOUT_PENDING[i].targetTime) {
jerry_value_t ret = jerry_promise_resolve(
MODULE_TIMEOUT_PENDING[i].promise, jerry_undefined()
);
jerry_value_free(ret);
jerry_value_free(MODULE_TIMEOUT_PENDING[i].promise);
MODULE_TIMEOUT_PENDING_COUNT--;
if(i < MODULE_TIMEOUT_PENDING_COUNT) {
MODULE_TIMEOUT_PENDING[i] =
MODULE_TIMEOUT_PENDING[MODULE_TIMEOUT_PENDING_COUNT];
}
} else {
i++;
}
}
}
void moduleTimeoutInit(void) {
MODULE_TIMEOUT_PENDING_COUNT = 0;
moduleBaseDefineGlobalMethod("timeout", moduleTimeoutTimeout);
}
void moduleTimeoutDispose(void) {
for(uint32_t i = 0; i < MODULE_TIMEOUT_PENDING_COUNT; i++) {
jerry_value_free(MODULE_TIMEOUT_PENDING[i].promise);
}
MODULE_TIMEOUT_PENDING_COUNT = 0;
}
@@ -1,46 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/module/modulebase.h"
#include "time/time.h"
/** Maximum number of concurrent timeout() awaits. */
#define MODULE_TIMEOUT_PENDING_MAX 64
/** Entry tracking one pending timeout() promise. */
typedef struct {
jerry_value_t promise;
float_t targetTime;
} moduletimeoutentry_t;
extern moduletimeoutentry_t MODULE_TIMEOUT_PENDING[MODULE_TIMEOUT_PENDING_MAX];
extern uint32_t MODULE_TIMEOUT_PENDING_COUNT;
/**
* timeout(ms) - returns a Promise that resolves after at least `ms`
* milliseconds have elapsed. Used as `await timeout(500)`.
*
* @param args[0] Delay in milliseconds (number).
*/
moduleBaseFunction(moduleTimeoutTimeout);
/**
* Resolves any pending timeout() promises whose target time has passed.
* Must be called once per frame before jerry_run_jobs().
*/
void moduleTimeoutFlush(void);
/**
* Initializes the timeout module and registers the global timeout() function.
*/
void moduleTimeoutInit(void);
/**
* Disposes the timeout module, releasing any unresolved pending promises.
*/
void moduleTimeoutDispose(void);
@@ -1,13 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
modulecomponent.c
moduleentity.c
)
# Subdirs
add_subdirectory(component)
@@ -1,14 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
modulecomponentlist.c
)
# Subdirs
add_subdirectory(display)
add_subdirectory(physics)
add_subdirectory(trigger)
@@ -1,11 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
modulecamera.c
moduleposition.c
modulerenderable.c
)
@@ -1,169 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "modulecamera.h"
scriptproto_t MODULE_CAMERA_PROTO;
jscomponent_t *moduleCameraSelf(const jerry_call_info_t *callInfo) {
return (jscomponent_t *)scriptProtoGetValue(
&MODULE_CAMERA_PROTO, callInfo->this_value
);
}
entitycamera_t *moduleCameraData(const jscomponent_t *c) {
return (entitycamera_t *)componentGetData(
c->entityId, c->componentId, COMPONENT_TYPE_CAMERA
);
}
moduleBaseFunction(moduleCameraCtor) {
return moduleBaseThrow("Camera cannot be instantiated with new");
}
moduleBaseFunction(moduleCameraGetEntity) {
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_undefined();
return jerry_number((double)c->entityId);
}
moduleBaseFunction(moduleCameraGetId) {
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_undefined();
return jerry_number((double)c->componentId);
}
moduleBaseFunction(moduleCameraGetFov) {
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_undefined();
entitycamera_t *cam = moduleCameraData(c);
if(!cam) return jerry_undefined();
return jerry_number((double)cam->perspective.fov);
}
moduleBaseFunction(moduleCameraSetFov) {
moduleBaseRequireArgs(1);
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_undefined();
entitycamera_t *cam = moduleCameraData(c);
if(!cam) return jerry_undefined();
cam->perspective.fov = moduleBaseArgFloat(0);
return jerry_undefined();
}
moduleBaseFunction(moduleCameraGetNearClip) {
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_undefined();
entitycamera_t *cam = moduleCameraData(c);
if(!cam) return jerry_undefined();
return jerry_number((double)cam->nearClip);
}
moduleBaseFunction(moduleCameraSetNearClip) {
moduleBaseRequireArgs(1);
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_undefined();
entitycamera_t *cam = moduleCameraData(c);
if(!cam) return jerry_undefined();
cam->nearClip = moduleBaseArgFloat(0);
return jerry_undefined();
}
moduleBaseFunction(moduleCameraGetFarClip) {
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_undefined();
entitycamera_t *cam = moduleCameraData(c);
if(!cam) return jerry_undefined();
return jerry_number((double)cam->farClip);
}
moduleBaseFunction(moduleCameraSetFarClip) {
moduleBaseRequireArgs(1);
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_undefined();
entitycamera_t *cam = moduleCameraData(c);
if(!cam) return jerry_undefined();
cam->farClip = moduleBaseArgFloat(0);
return jerry_undefined();
}
moduleBaseFunction(moduleCameraGetProjType) {
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_undefined();
entitycamera_t *cam = moduleCameraData(c);
if(!cam) return jerry_undefined();
return jerry_number((double)cam->projType);
}
moduleBaseFunction(moduleCameraSetProjType) {
moduleBaseRequireArgs(1);
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_undefined();
entitycamera_t *cam = moduleCameraData(c);
if(!cam) return jerry_undefined();
cam->projType = (entitycameraprojectiontype_t)moduleBaseArgInt(0);
return jerry_undefined();
}
moduleBaseFunction(moduleCameraToString) {
jscomponent_t *c = moduleCameraSelf(callInfo);
if(!c) return jerry_string_sz("Camera:invalid");
char_t buf[32];
snprintf(buf, sizeof(buf), "Camera(%u)", (unsigned)c->componentId);
return jerry_string_sz(buf);
}
void moduleCameraInit(void) {
scriptProtoInit(
&MODULE_CAMERA_PROTO, "Camera",
sizeof(jscomponent_t), moduleCameraCtor
);
scriptProtoDefineProp(
&MODULE_CAMERA_PROTO, "entity", moduleCameraGetEntity, NULL
);
scriptProtoDefineProp(
&MODULE_CAMERA_PROTO, "id", moduleCameraGetId, NULL
);
scriptProtoDefineProp(
&MODULE_CAMERA_PROTO, "fov", moduleCameraGetFov, moduleCameraSetFov
);
scriptProtoDefineProp(
&MODULE_CAMERA_PROTO, "nearClip",
moduleCameraGetNearClip, moduleCameraSetNearClip
);
scriptProtoDefineProp(
&MODULE_CAMERA_PROTO, "farClip",
moduleCameraGetFarClip, moduleCameraSetFarClip
);
scriptProtoDefineProp(
&MODULE_CAMERA_PROTO, "projType",
moduleCameraGetProjType, moduleCameraSetProjType
);
scriptProtoDefineToString(&MODULE_CAMERA_PROTO, moduleCameraToString);
jerry_value_t ctor = MODULE_CAMERA_PROTO.constructor;
struct { const char_t *name; int val; } projtypes[] = {
{ "PERSPECTIVE", ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE },
{
"PERSPECTIVE_FLIPPED",
ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED
},
{ "ORTHOGRAPHIC", ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC },
};
for(int i = 0; i < 3; i++) {
jerry_value_t k = jerry_string_sz(projtypes[i].name);
jerry_value_t v = jerry_number((double)projtypes[i].val);
jerry_object_set(ctor, k, v);
jerry_value_free(v);
jerry_value_free(k);
}
}
void moduleCameraDispose(void) {
scriptProtoDispose(&MODULE_CAMERA_PROTO);
}
@@ -1,62 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/module/modulebase.h"
#include "script/scriptproto.h"
#include "script/module/entity/modulecomponent.h"
#include "entity/component/display/entitycamera.h"
extern scriptproto_t MODULE_CAMERA_PROTO;
/** Camera() constructor - always throws; not directly instantiable. */
moduleBaseFunction(moduleCameraCtor);
/** @return Entity ID that owns this camera component. */
moduleBaseFunction(moduleCameraGetEntity);
/** @return This component's ID. */
moduleBaseFunction(moduleCameraGetId);
/** @return Field of view in degrees. */
moduleBaseFunction(moduleCameraGetFov);
/** Sets field of view. @param args[0] FOV in degrees. */
moduleBaseFunction(moduleCameraSetFov);
/** @return Near clip plane distance. */
moduleBaseFunction(moduleCameraGetNearClip);
/** Sets near clip plane. @param args[0] Near clip distance. */
moduleBaseFunction(moduleCameraSetNearClip);
/** @return Far clip plane distance. */
moduleBaseFunction(moduleCameraGetFarClip);
/** Sets far clip plane. @param args[0] Far clip distance. */
moduleBaseFunction(moduleCameraSetFarClip);
/** @return Projection type constant (Camera.PERSPECTIVE etc.). */
moduleBaseFunction(moduleCameraGetProjType);
/** Sets projection type. @param args[0] entitycameraprojectiontype_t value. */
moduleBaseFunction(moduleCameraSetProjType);
/** @return "Camera(id)" string. */
moduleBaseFunction(moduleCameraToString);
/**
* Initializes the Camera module and registers the global Camera class with
* fov/nearClip/farClip/projType properties and PERSPECTIVE/ORTHOGRAPHIC
* constants.
*/
void moduleCameraInit(void);
/**
* Disposes the Camera module.
*/
void moduleCameraDispose(void);
@@ -1,244 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduleposition.h"
scriptproto_t MODULE_POSITION_PROTO;
jscomponent_t *modulePositionSelf(const jerry_call_info_t *callInfo) {
return (jscomponent_t *)scriptProtoGetValue(
&MODULE_POSITION_PROTO, callInfo->this_value
);
}
moduleBaseFunction(modulePositionCtor) {
return moduleBaseThrow("Position cannot be instantiated with new");
}
moduleBaseFunction(modulePositionGetEntity) {
jscomponent_t *c = modulePositionSelf(callInfo);
if(!c) return jerry_undefined();
return jerry_number((double)c->entityId);
}
moduleBaseFunction(modulePositionGetId) {
jscomponent_t *c = modulePositionSelf(callInfo);
if(!c) return jerry_undefined();
return jerry_number((double)c->componentId);
}
moduleBaseFunction(modulePositionGetLocalPos) {
jscomponent_t *c = modulePositionSelf(callInfo);
if(!c) return jerry_undefined();
vec3 v;
entityPositionGetLocalPosition(c->entityId, c->componentId, v);
return moduleVec3Push(v);
}
moduleBaseFunction(modulePositionSetLocalPos) {
moduleBaseRequireArgs(1);
jscomponent_t *c = modulePositionSelf(callInfo);
if(!c) return jerry_undefined();
float_t *v = moduleVec3From(args[0]);
if(!v) return moduleBaseThrow("Position.localPosition: expected Vec3");
entityPositionSetLocalPosition(c->entityId, c->componentId, v);
return jerry_undefined();
}
moduleBaseFunction(modulePositionGetWorldPos) {
jscomponent_t *c = modulePositionSelf(callInfo);
if(!c) return jerry_undefined();
vec3 v;
entityPositionGetWorldPosition(c->entityId, c->componentId, v);
return moduleVec3Push(v);
}
moduleBaseFunction(modulePositionSetWorldPos) {
moduleBaseRequireArgs(1);
jscomponent_t *c = modulePositionSelf(callInfo);
if(!c) return jerry_undefined();
float_t *v = moduleVec3From(args[0]);
if(!v) return moduleBaseThrow("Position.worldPosition: expected Vec3");
entityPositionSetWorldPosition(c->entityId, c->componentId, v);
return jerry_undefined();
}
moduleBaseFunction(modulePositionGetLocalRot) {
jscomponent_t *c = modulePositionSelf(callInfo);
if(!c) return jerry_undefined();
vec3 v;
entityPositionGetLocalRotation(c->entityId, c->componentId, v);
return moduleVec3Push(v);
}
moduleBaseFunction(modulePositionSetLocalRot) {
moduleBaseRequireArgs(1);
jscomponent_t *c = modulePositionSelf(callInfo);
if(!c) return jerry_undefined();
float_t *v = moduleVec3From(args[0]);
if(!v) return moduleBaseThrow("Position.localRotation: expected Vec3");
entityPositionSetLocalRotation(c->entityId, c->componentId, v);
return jerry_undefined();
}
moduleBaseFunction(modulePositionGetWorldRot) {
jscomponent_t *c = modulePositionSelf(callInfo);
if(!c) return jerry_undefined();
vec3 v;
entityPositionGetWorldRotation(c->entityId, c->componentId, v);
return moduleVec3Push(v);
}
moduleBaseFunction(modulePositionSetWorldRot) {
moduleBaseRequireArgs(1);
jscomponent_t *c = modulePositionSelf(callInfo);
if(!c) return jerry_undefined();
float_t *v = moduleVec3From(args[0]);
if(!v) return moduleBaseThrow("Position.worldRotation: expected Vec3");
entityPositionSetWorldRotation(c->entityId, c->componentId, v);
return jerry_undefined();
}
moduleBaseFunction(modulePositionGetLocalScale) {
jscomponent_t *c = modulePositionSelf(callInfo);
if(!c) return jerry_undefined();
vec3 v;
entityPositionGetLocalScale(c->entityId, c->componentId, v);
return moduleVec3Push(v);
}
moduleBaseFunction(modulePositionSetLocalScale) {
moduleBaseRequireArgs(1);
jscomponent_t *c = modulePositionSelf(callInfo);
if(!c) return jerry_undefined();
float_t *v = moduleVec3From(args[0]);
if(!v) return moduleBaseThrow("Position.localScale: expected Vec3");
entityPositionSetLocalScale(c->entityId, c->componentId, v);
return jerry_undefined();
}
moduleBaseFunction(modulePositionGetWorldScale) {
jscomponent_t *c = modulePositionSelf(callInfo);
if(!c) return jerry_undefined();
vec3 v;
entityPositionGetWorldScale(c->entityId, c->componentId, v);
return moduleVec3Push(v);
}
moduleBaseFunction(modulePositionSetWorldScale) {
moduleBaseRequireArgs(1);
jscomponent_t *c = modulePositionSelf(callInfo);
if(!c) return jerry_undefined();
float_t *v = moduleVec3From(args[0]);
if(!v) return moduleBaseThrow("Position.worldScale: expected Vec3");
entityPositionSetWorldScale(c->entityId, c->componentId, v);
return jerry_undefined();
}
moduleBaseFunction(modulePositionLookAt) {
jscomponent_t *c = modulePositionSelf(callInfo);
if(!c) return jerry_undefined();
moduleBaseRequireArgs(1);
float_t *target = moduleVec3From(args[0]);
if(!target) return moduleBaseThrow("Position.lookAt: expected Vec3 target");
vec3 eye;
entityPositionGetLocalPosition(c->entityId, c->componentId, eye);
vec3 up = { 0.0f, 1.0f, 0.0f };
if(argc >= 2) {
float_t *upArg = moduleVec3From(args[1]);
if(upArg) glm_vec3_copy(upArg, up);
}
entityPositionLookAt(c->entityId, c->componentId, eye, target, up);
return jerry_undefined();
}
moduleBaseFunction(modulePositionSetParent) {
jscomponent_t *c = modulePositionSelf(callInfo);
if(!c) return jerry_undefined();
if(argc == 0 ||
jerry_value_is_null(args[0]) ||
jerry_value_is_undefined(args[0])) {
entityPositionSetParent(
c->entityId, c->componentId,
ENTITY_ID_INVALID, COMPONENT_ID_INVALID
);
return jerry_undefined();
}
jscomponent_t *parent = (jscomponent_t *)scriptProtoGetValue(
&MODULE_POSITION_PROTO, args[0]
);
if(!parent) {
return moduleBaseThrow(
"Position.setParent: expected Position or null"
);
}
entityPositionSetParent(
c->entityId, c->componentId,
parent->entityId, parent->componentId
);
return jerry_undefined();
}
moduleBaseFunction(modulePositionToString) {
jscomponent_t *c = modulePositionSelf(callInfo);
if(!c) return jerry_string_sz("Position:invalid");
char_t buf[32];
snprintf(buf, sizeof(buf), "Position(%u)", (unsigned)c->componentId);
return jerry_string_sz(buf);
}
void modulePositionInit(void) {
scriptProtoInit(
&MODULE_POSITION_PROTO, "Position",
sizeof(jscomponent_t), modulePositionCtor
);
scriptProtoDefineProp(
&MODULE_POSITION_PROTO, "entity", modulePositionGetEntity, NULL
);
scriptProtoDefineProp(
&MODULE_POSITION_PROTO, "id", modulePositionGetId, NULL
);
scriptProtoDefineProp(
&MODULE_POSITION_PROTO, "localPosition",
modulePositionGetLocalPos, modulePositionSetLocalPos
);
scriptProtoDefineProp(
&MODULE_POSITION_PROTO, "worldPosition",
modulePositionGetWorldPos, modulePositionSetWorldPos
);
scriptProtoDefineProp(
&MODULE_POSITION_PROTO, "localRotation",
modulePositionGetLocalRot, modulePositionSetLocalRot
);
scriptProtoDefineProp(
&MODULE_POSITION_PROTO, "worldRotation",
modulePositionGetWorldRot, modulePositionSetWorldRot
);
scriptProtoDefineProp(
&MODULE_POSITION_PROTO, "localScale",
modulePositionGetLocalScale, modulePositionSetLocalScale
);
scriptProtoDefineProp(
&MODULE_POSITION_PROTO, "worldScale",
modulePositionGetWorldScale, modulePositionSetWorldScale
);
scriptProtoDefineFunc(
&MODULE_POSITION_PROTO, "lookAt", modulePositionLookAt
);
scriptProtoDefineFunc(
&MODULE_POSITION_PROTO, "setParent", modulePositionSetParent
);
scriptProtoDefineToString(&MODULE_POSITION_PROTO, modulePositionToString);
}
void modulePositionDispose(void) {
scriptProtoDispose(&MODULE_POSITION_PROTO);
}
@@ -1,89 +0,0 @@
/**
* 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/math/modulevec3.h"
#include "script/module/entity/modulecomponent.h"
#include "entity/component/display/entityposition.h"
extern scriptproto_t MODULE_POSITION_PROTO;
/** Position() constructor - always throws; not directly instantiable. */
moduleBaseFunction(modulePositionCtor);
/** @return Entity ID that owns this position component. */
moduleBaseFunction(modulePositionGetEntity);
/** @return This component's ID. */
moduleBaseFunction(modulePositionGetId);
/** @return Local position as a Vec3. */
moduleBaseFunction(modulePositionGetLocalPos);
/** Sets local position. @param args[0] Vec3. */
moduleBaseFunction(modulePositionSetLocalPos);
/** @return World position as a Vec3. */
moduleBaseFunction(modulePositionGetWorldPos);
/** Sets world position. @param args[0] Vec3. */
moduleBaseFunction(modulePositionSetWorldPos);
/** @return Local rotation as a Vec3 (Euler angles). */
moduleBaseFunction(modulePositionGetLocalRot);
/** Sets local rotation. @param args[0] Vec3. */
moduleBaseFunction(modulePositionSetLocalRot);
/** @return World rotation as a Vec3 (Euler angles). */
moduleBaseFunction(modulePositionGetWorldRot);
/** Sets world rotation. @param args[0] Vec3. */
moduleBaseFunction(modulePositionSetWorldRot);
/** @return Local scale as a Vec3. */
moduleBaseFunction(modulePositionGetLocalScale);
/** Sets local scale. @param args[0] Vec3. */
moduleBaseFunction(modulePositionSetLocalScale);
/** @return World scale as a Vec3. */
moduleBaseFunction(modulePositionGetWorldScale);
/** Sets world scale. @param args[0] Vec3. */
moduleBaseFunction(modulePositionSetWorldScale);
/**
* lookAt(target, up?) - orients the component toward a target point.
* @param args[0] Vec3 target position.
* @param args[1] Optional Vec3 up vector (defaults to world up).
*/
moduleBaseFunction(modulePositionLookAt);
/**
* setParent(position?) - parents this component to another Position, or
* unparents when called with null/undefined.
* @param args[0] Position component or null/undefined.
*/
moduleBaseFunction(modulePositionSetParent);
/** @return "Position(id)" string. */
moduleBaseFunction(modulePositionToString);
/**
* Initializes the Position module and registers the global Position class with
* localPosition/worldPosition/localRotation/worldRotation/localScale/worldScale
* properties and lookAt/setParent methods.
*/
void modulePositionInit(void);
/**
* Disposes the Position module.
*/
void modulePositionDispose(void);
@@ -1,278 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "modulerenderable.h"
scriptproto_t MODULE_RENDERABLE_PROTO;
jscomponent_t *moduleRenderableSelf(const jerry_call_info_t *callInfo) {
return (jscomponent_t *)scriptProtoGetValue(
&MODULE_RENDERABLE_PROTO, callInfo->this_value
);
}
entityrenderable_t *moduleRenderableData(const jscomponent_t *c) {
return (entityrenderable_t *)componentGetData(
c->entityId, c->componentId, COMPONENT_TYPE_RENDERABLE
);
}
float_t moduleRenderableArrayFloat(
const jerry_value_t arr,
const uint32_t idx,
const float_t def
) {
if(idx >= jerry_array_length(arr)) return def;
jerry_value_t v = jerry_object_get_index(arr, idx);
float_t f = jerry_value_is_number(v)
? (float_t)jerry_value_as_number(v) : def;
jerry_value_free(v);
return f;
}
moduleBaseFunction(moduleRenderableCtor) {
return moduleBaseThrow("Renderable cannot be instantiated with new");
}
moduleBaseFunction(moduleRenderableGetEntity) {
jscomponent_t *c = moduleRenderableSelf(callInfo);
if(!c) return jerry_undefined();
return jerry_number((double)c->entityId);
}
moduleBaseFunction(moduleRenderableGetId) {
jscomponent_t *c = moduleRenderableSelf(callInfo);
if(!c) return jerry_undefined();
return jerry_number((double)c->componentId);
}
moduleBaseFunction(moduleRenderableGetType) {
jscomponent_t *c = moduleRenderableSelf(callInfo);
if(!c) return jerry_undefined();
entityrenderable_t *r = moduleRenderableData(c);
if(!r) return jerry_undefined();
return jerry_number((double)r->type);
}
moduleBaseFunction(moduleRenderableSetType) {
moduleBaseRequireArgs(1);
jscomponent_t *c = moduleRenderableSelf(callInfo);
if(!c) return jerry_undefined();
entityRenderableSetType(
c->entityId, c->componentId,
(entityrenderabletype_t)moduleBaseArgInt(0)
);
return jerry_undefined();
}
moduleBaseFunction(moduleRenderableGetPriority) {
jscomponent_t *c = moduleRenderableSelf(callInfo);
if(!c) return jerry_undefined();
entityrenderable_t *r = moduleRenderableData(c);
if(!r) return jerry_undefined();
return jerry_number((double)r->priority);
}
moduleBaseFunction(moduleRenderableSetPriority) {
moduleBaseRequireArgs(1);
jscomponent_t *c = moduleRenderableSelf(callInfo);
if(!c) return jerry_undefined();
entityRenderableSetPriority(
c->entityId, c->componentId,
(int8_t)moduleBaseArgInt(0)
);
return jerry_undefined();
}
moduleBaseFunction(moduleRenderableGetColor) {
jscomponent_t *c = moduleRenderableSelf(callInfo);
if(!c) return jerry_undefined();
entityrenderable_t *r = moduleRenderableData(c);
if(!r) return jerry_undefined();
return moduleColorPush(r->data.material.material.unlit.color);
}
moduleBaseFunction(moduleRenderableSetColor) {
moduleBaseRequireArgs(1);
jscomponent_t *c = moduleRenderableSelf(callInfo);
if(!c) return jerry_undefined();
entityrenderable_t *r = moduleRenderableData(c);
if(!r) return jerry_undefined();
color_t *col = moduleColorFrom(args[0]);
if(!col) return moduleBaseThrow("Renderable.color: expected Color");
r->data.material.material.unlit.color = *col;
return jerry_undefined();
}
moduleBaseFunction(moduleRenderableGetTexture) {
jerry_value_t key = jerry_string_sz("_tex");
jerry_value_t val = jerry_object_get(callInfo->this_value, key);
jerry_value_free(key);
return val;
}
moduleBaseFunction(moduleRenderableSetTexture) {
moduleBaseRequireArgs(1);
jscomponent_t *c = moduleRenderableSelf(callInfo);
if(!c) return jerry_undefined();
entityrenderable_t *r = moduleRenderableData(c);
if(!r) return jerry_undefined();
jstexture_t *tex = (jstexture_t *)scriptProtoGetValue(
&MODULE_TEXTURE_PROTO, args[0]
);
if(!tex || !tex->entry) {
return moduleBaseThrow("Renderable.texture: expected Texture");
}
r->type = ENTITY_RENDERABLE_TYPE_SPRITEBATCH;
r->data.spritebatch.texture = &tex->entry->data.texture;
jerry_value_t pinKey = jerry_string_sz("_tex");
jerry_object_set(callInfo->this_value, pinKey, args[0]);
jerry_value_free(pinKey);
return jerry_undefined();
}
moduleBaseFunction(moduleRenderableGetSprites) {
jscomponent_t *c = moduleRenderableSelf(callInfo);
if(!c) return jerry_undefined();
entityrenderable_t *r = moduleRenderableData(c);
if(!r) return jerry_undefined();
const entityrenderablespritebatch_t *sb = &r->data.spritebatch;
jerry_value_t arr = jerry_array((uint32_t)sb->spriteCount);
for(uint32_t i = 0; i < (uint32_t)sb->spriteCount; i++) {
const spritebatchsprite_t *s = &sb->sprites[i];
float_t vals[10] = {
s->min[0], s->min[1], s->min[2],
s->max[0], s->max[1], s->max[2],
s->uvMin[0], s->uvMin[1],
s->uvMax[0], s->uvMax[1],
};
jerry_value_t sprite = jerry_array(10);
for(uint32_t j = 0; j < 10; j++) {
jerry_value_t num = jerry_number((double)vals[j]);
jerry_object_set_index(sprite, j, num);
jerry_value_free(num);
}
jerry_object_set_index(arr, i, sprite);
jerry_value_free(sprite);
}
return arr;
}
moduleBaseFunction(moduleRenderableSetSprites) {
moduleBaseRequireArgs(1);
jscomponent_t *c = moduleRenderableSelf(callInfo);
if(!c) return jerry_undefined();
entityrenderable_t *r = moduleRenderableData(c);
if(!r) return jerry_undefined();
if(!jerry_value_is_array(args[0])) {
return moduleBaseThrow("Renderable.sprites: expected Array");
}
entityrenderablespritebatch_t *sb = &r->data.spritebatch;
uint32_t count = jerry_array_length(args[0]);
if(count > ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX) {
return moduleBaseThrow("Renderable.sprites: exceeds sprite capacity");
}
sb->spriteCount = 0;
for(uint32_t i = 0; i < count; i++) {
jerry_value_t elem = jerry_object_get_index(args[0], i);
if(!jerry_value_is_array(elem)) {
jerry_value_free(elem);
return moduleBaseThrow(
"Renderable.sprites: each element must be an Array"
);
}
spritebatchsprite_t s;
if(jerry_array_length(elem) >= 10) {
s.min[0] = moduleRenderableArrayFloat(elem, 0, 0.0f);
s.min[1] = moduleRenderableArrayFloat(elem, 1, 0.0f);
s.min[2] = moduleRenderableArrayFloat(elem, 2, 0.0f);
s.max[0] = moduleRenderableArrayFloat(elem, 3, 0.0f);
s.max[1] = moduleRenderableArrayFloat(elem, 4, 0.0f);
s.max[2] = moduleRenderableArrayFloat(elem, 5, 0.0f);
s.uvMin[0] = moduleRenderableArrayFloat(elem, 6, 0.0f);
s.uvMin[1] = moduleRenderableArrayFloat(elem, 7, 0.0f);
s.uvMax[0] = moduleRenderableArrayFloat(elem, 8, 1.0f);
s.uvMax[1] = moduleRenderableArrayFloat(elem, 9, 1.0f);
} else {
s.min[0] = moduleRenderableArrayFloat(elem, 0, 0.0f);
s.min[1] = moduleRenderableArrayFloat(elem, 1, 0.0f);
s.min[2] = 0.0f;
s.max[0] = moduleRenderableArrayFloat(elem, 2, 0.0f);
s.max[1] = moduleRenderableArrayFloat(elem, 3, 0.0f);
s.max[2] = 0.0f;
s.uvMin[0] = moduleRenderableArrayFloat(elem, 4, 0.0f);
s.uvMin[1] = moduleRenderableArrayFloat(elem, 5, 0.0f);
s.uvMax[0] = moduleRenderableArrayFloat(elem, 6, 1.0f);
s.uvMax[1] = moduleRenderableArrayFloat(elem, 7, 1.0f);
}
jerry_value_free(elem);
sb->sprites[sb->spriteCount++] = s;
}
return jerry_undefined();
}
moduleBaseFunction(moduleRenderableToString) {
jscomponent_t *c = moduleRenderableSelf(callInfo);
if(!c) return jerry_string_sz("Renderable:invalid");
char_t buf[32];
snprintf(buf, sizeof(buf), "Renderable(%u)", (unsigned)c->componentId);
return jerry_string_sz(buf);
}
void moduleRenderableInit(void) {
scriptProtoInit(
&MODULE_RENDERABLE_PROTO, "Renderable",
sizeof(jscomponent_t), moduleRenderableCtor
);
scriptProtoDefineProp(
&MODULE_RENDERABLE_PROTO, "entity", moduleRenderableGetEntity, NULL
);
scriptProtoDefineProp(
&MODULE_RENDERABLE_PROTO, "id", moduleRenderableGetId, NULL
);
scriptProtoDefineProp(
&MODULE_RENDERABLE_PROTO, "type",
moduleRenderableGetType, moduleRenderableSetType
);
scriptProtoDefineProp(
&MODULE_RENDERABLE_PROTO, "priority",
moduleRenderableGetPriority, moduleRenderableSetPriority
);
scriptProtoDefineProp(
&MODULE_RENDERABLE_PROTO, "color",
moduleRenderableGetColor, moduleRenderableSetColor
);
scriptProtoDefineProp(
&MODULE_RENDERABLE_PROTO, "texture",
moduleRenderableGetTexture, moduleRenderableSetTexture
);
scriptProtoDefineProp(
&MODULE_RENDERABLE_PROTO, "sprites",
moduleRenderableGetSprites, moduleRenderableSetSprites
);
scriptProtoDefineToString(&MODULE_RENDERABLE_PROTO, moduleRenderableToString);
jerry_value_t ctor = MODULE_RENDERABLE_PROTO.constructor;
struct { const char_t *name; int val; } types[] = {
{ "SHADER_MATERIAL", ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL },
{ "SPRITEBATCH", ENTITY_RENDERABLE_TYPE_SPRITEBATCH },
{ "CUSTOM", ENTITY_RENDERABLE_TYPE_CUSTOM },
};
for(int i = 0; i < 3; i++) {
jerry_value_t k = jerry_string_sz(types[i].name);
jerry_value_t v = jerry_number((double)types[i].val);
jerry_object_set(ctor, k, v);
jerry_value_free(v);
jerry_value_free(k);
}
}
void moduleRenderableDispose(void) {
scriptProtoDispose(&MODULE_RENDERABLE_PROTO);
}
@@ -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 "script/module/modulebase.h"
#include "script/module/display/modulecolor.h"
#include "script/module/display/moduletexture.h"
#include "script/module/entity/modulecomponent.h"
#include "script/scriptproto.h"
#include "entity/component/display/entityrenderable.h"
extern scriptproto_t MODULE_RENDERABLE_PROTO;
/** Renderable() constructor - always throws; not directly instantiable. */
moduleBaseFunction(moduleRenderableCtor);
/** @return Entity ID that owns this renderable component. */
moduleBaseFunction(moduleRenderableGetEntity);
/** @return This component's ID. */
moduleBaseFunction(moduleRenderableGetId);
/** @return Render type constant (Renderable.SPRITEBATCH etc.). */
moduleBaseFunction(moduleRenderableGetType);
/** Sets render type. @param args[0] entityrenderabletype_t value. */
moduleBaseFunction(moduleRenderableSetType);
/** @return Render priority (signed integer). */
moduleBaseFunction(moduleRenderableGetPriority);
/** Sets render priority. @param args[0] Priority integer. */
moduleBaseFunction(moduleRenderableSetPriority);
/** @return Material color as a Color object. */
moduleBaseFunction(moduleRenderableGetColor);
/** Sets material color. @param args[0] Color object. */
moduleBaseFunction(moduleRenderableSetColor);
/** @return The pinned Texture JS instance, or undefined if none set. */
moduleBaseFunction(moduleRenderableGetTexture);
/**
* Sets the texture for SPRITEBATCH rendering. Also switches the render type to
* SPRITEBATCH and pins the JS Texture object to prevent GC.
* @param args[0] Texture JS object.
*/
moduleBaseFunction(moduleRenderableSetTexture);
/**
* @return JS array of sprite sub-arrays, each with 10 numbers:
* [x1,y1,z1, x2,y2,z2, u1,v1, u2,v2].
*/
moduleBaseFunction(moduleRenderableGetSprites);
/**
* Sets sprite data. Accepts an array of sub-arrays - 10 elements (3D) or
* 8 elements (2D, z defaults to 0).
* @param args[0] Array of sprite sub-arrays.
*/
moduleBaseFunction(moduleRenderableSetSprites);
/** @return "Renderable(id)" string. */
moduleBaseFunction(moduleRenderableToString);
/**
* Initializes the Renderable module and registers the global Renderable class
* with type/priority/color/texture/sprites properties and SPRITEBATCH/
* SHADER_MATERIAL/CUSTOM constants.
*/
void moduleRenderableInit(void);
/**
* Disposes the Renderable module.
*/
void moduleRenderableDispose(void);
@@ -1,44 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "modulecomponentlist.h"
jerry_value_t moduleComponentListCreateInstance(
const componenttype_t type,
const jscomponent_t *comp
) {
switch(type) {
case COMPONENT_TYPE_CAMERA:
return scriptProtoCreateValue(&MODULE_CAMERA_PROTO, comp);
case COMPONENT_TYPE_PHYSICS:
return scriptProtoCreateValue(&MODULE_PHYSICS_PROTO, comp);
case COMPONENT_TYPE_POSITION:
return scriptProtoCreateValue(&MODULE_POSITION_PROTO, comp);
case COMPONENT_TYPE_RENDERABLE:
return scriptProtoCreateValue(&MODULE_RENDERABLE_PROTO, comp);
case COMPONENT_TYPE_TRIGGER:
return scriptProtoCreateValue(&MODULE_TRIGGER_PROTO, comp);
default:
return scriptProtoCreateValue(&MODULE_COMPONENT_PROTO, comp);
}
}
void moduleComponentListInit(void) {
moduleCameraInit();
modulePhysicsInit();
modulePositionInit();
moduleRenderableInit();
moduleTriggerInit();
}
void moduleComponentListDispose(void) {
moduleTriggerDispose();
moduleRenderableDispose();
modulePositionDispose();
modulePhysicsDispose();
moduleCameraDispose();
}
@@ -1,38 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/module/entity/modulecomponent.h"
#include "display/modulecamera.h"
#include "display/moduleposition.h"
#include "display/modulerenderable.h"
#include "physics/modulephysics.h"
#include "trigger/moduletrigger.h"
/**
* Returns a typed JS instance for a newly-added component. Falls back to the
* generic Component proto for types that have no specific module yet.
*
* @param type Component type constant.
* @param comp Initialized jscomponent_t with entityId and componentId.
* @return A jerry_value_t JS object of the appropriate subtype.
*/
jerry_value_t moduleComponentListCreateInstance(
const componenttype_t type,
const jscomponent_t *comp
);
/**
* Initializes all component sub-modules (camera, physics, position, renderable,
* trigger).
*/
void moduleComponentListInit(void);
/**
* Disposes all component sub-modules in reverse init order.
*/
void moduleComponentListDispose(void);
@@ -1,9 +0,0 @@
# 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
modulephysics.c
)
@@ -1,222 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "modulephysics.h"
#include "util/memory.h"
scriptproto_t MODULE_PHYSICS_PROTO;
jscomponent_t *modulePhysicsSelf(const jerry_call_info_t *callInfo) {
return (jscomponent_t *)scriptProtoGetValue(
&MODULE_PHYSICS_PROTO, callInfo->this_value
);
}
moduleBaseFunction(modulePhysicsCtor) {
return moduleBaseThrow("Physics cannot be instantiated with new");
}
moduleBaseFunction(modulePhysicsGetEntity) {
jscomponent_t *c = modulePhysicsSelf(callInfo);
if(!c) return jerry_undefined();
return jerry_number((double)c->entityId);
}
moduleBaseFunction(modulePhysicsGetId) {
jscomponent_t *c = modulePhysicsSelf(callInfo);
if(!c) return jerry_undefined();
return jerry_number((double)c->componentId);
}
moduleBaseFunction(modulePhysicsGetBodyType) {
jscomponent_t *c = modulePhysicsSelf(callInfo);
if(!c) return jerry_undefined();
return jerry_number(
(double)entityPhysicsGetBodyType(c->entityId, c->componentId)
);
}
moduleBaseFunction(modulePhysicsSetBodyType) {
moduleBaseRequireArgs(1);
jscomponent_t *c = modulePhysicsSelf(callInfo);
if(!c) return jerry_undefined();
entityPhysicsSetBodyType(
c->entityId, c->componentId,
(physicsbodytype_t)moduleBaseArgInt(0)
);
return jerry_undefined();
}
moduleBaseFunction(modulePhysicsGetShape) {
jscomponent_t *c = modulePhysicsSelf(callInfo);
if(!c) return jerry_undefined();
return jerry_number(
(double)entityPhysicsGetShape(
c->entityId, c->componentId
).type
);
}
moduleBaseFunction(modulePhysicsSetShape) {
moduleBaseRequireArgs(1);
jscomponent_t *c = modulePhysicsSelf(callInfo);
if(!c) return jerry_undefined();
physicsshape_t shape;
memoryZero(&shape, sizeof(physicsshape_t));
shape.type = (physicshapetype_t)moduleBaseArgInt(0);
switch(shape.type) {
case PHYSICS_SHAPE_CUBE:
shape.data.cube.halfExtents[0] = 0.5f;
shape.data.cube.halfExtents[1] = 0.5f;
shape.data.cube.halfExtents[2] = 0.5f;
break;
case PHYSICS_SHAPE_SPHERE:
shape.data.sphere.radius = 0.5f;
break;
case PHYSICS_SHAPE_CAPSULE:
shape.data.capsule.radius = 0.5f;
shape.data.capsule.halfHeight = 0.5f;
break;
case PHYSICS_SHAPE_PLANE:
shape.data.plane.normal[1] = 1.0f;
shape.data.plane.distance = 0.0f;
break;
default:
break;
}
entityPhysicsSetShape(c->entityId, c->componentId, shape);
return jerry_undefined();
}
moduleBaseFunction(modulePhysicsGetVelocity) {
jscomponent_t *c = modulePhysicsSelf(callInfo);
if(!c) return jerry_undefined();
vec3 v;
entityPhysicsGetVelocity(c->entityId, c->componentId, v);
return moduleVec3Push(v);
}
moduleBaseFunction(modulePhysicsSetVelocity) {
moduleBaseRequireArgs(1);
jscomponent_t *c = modulePhysicsSelf(callInfo);
if(!c) return jerry_undefined();
float_t *v = moduleVec3From(args[0]);
if(!v) return moduleBaseThrow("Physics.velocity: expected Vec3");
entityPhysicsSetVelocity(c->entityId, c->componentId, v);
return jerry_undefined();
}
moduleBaseFunction(modulePhysicsGetGravityScale) {
jscomponent_t *c = modulePhysicsSelf(callInfo);
if(!c) return jerry_undefined();
entityphysics_t *p = entityPhysicsGet(c->entityId, c->componentId);
if(!p) return jerry_undefined();
return jerry_number((double)p->gravityScale);
}
moduleBaseFunction(modulePhysicsSetGravityScale) {
moduleBaseRequireArgs(1);
jscomponent_t *c = modulePhysicsSelf(callInfo);
if(!c) return jerry_undefined();
entityphysics_t *p = entityPhysicsGet(c->entityId, c->componentId);
if(!p) return jerry_undefined();
p->gravityScale = moduleBaseArgFloat(0);
return jerry_undefined();
}
moduleBaseFunction(modulePhysicsGetOnGround) {
jscomponent_t *c = modulePhysicsSelf(callInfo);
if(!c) return jerry_undefined();
return jerry_boolean(entityPhysicsIsOnGround(c->entityId, c->componentId));
}
moduleBaseFunction(modulePhysicsApplyImpulse) {
moduleBaseRequireArgs(1);
jscomponent_t *c = modulePhysicsSelf(callInfo);
if(!c) return jerry_undefined();
float_t *v = moduleVec3From(args[0]);
if(!v) return moduleBaseThrow("Physics.applyImpulse: expected Vec3");
entityPhysicsApplyImpulse(c->entityId, c->componentId, v);
return jerry_undefined();
}
moduleBaseFunction(modulePhysicsToString) {
jscomponent_t *c = modulePhysicsSelf(callInfo);
if(!c) return jerry_string_sz("Physics:invalid");
char_t buf[32];
snprintf(buf, sizeof(buf), "Physics(%u)", (unsigned)c->componentId);
return jerry_string_sz(buf);
}
void modulePhysicsInit(void) {
scriptProtoInit(
&MODULE_PHYSICS_PROTO, "Physics",
sizeof(jscomponent_t), modulePhysicsCtor
);
scriptProtoDefineProp(
&MODULE_PHYSICS_PROTO, "entity", modulePhysicsGetEntity, NULL
);
scriptProtoDefineProp(
&MODULE_PHYSICS_PROTO, "id", modulePhysicsGetId, NULL
);
scriptProtoDefineProp(
&MODULE_PHYSICS_PROTO, "bodyType",
modulePhysicsGetBodyType, modulePhysicsSetBodyType
);
scriptProtoDefineProp(
&MODULE_PHYSICS_PROTO, "shape",
modulePhysicsGetShape, modulePhysicsSetShape
);
scriptProtoDefineProp(
&MODULE_PHYSICS_PROTO, "velocity",
modulePhysicsGetVelocity, modulePhysicsSetVelocity
);
scriptProtoDefineProp(
&MODULE_PHYSICS_PROTO, "gravityScale",
modulePhysicsGetGravityScale, modulePhysicsSetGravityScale
);
scriptProtoDefineProp(
&MODULE_PHYSICS_PROTO, "onGround", modulePhysicsGetOnGround, NULL
);
scriptProtoDefineFunc(
&MODULE_PHYSICS_PROTO, "applyImpulse", modulePhysicsApplyImpulse
);
scriptProtoDefineToString(&MODULE_PHYSICS_PROTO, modulePhysicsToString);
jerry_value_t ctor = MODULE_PHYSICS_PROTO.constructor;
struct { const char_t *name; int val; } bodyTypes[] = {
{ "STATIC", PHYSICS_BODY_STATIC },
{ "DYNAMIC", PHYSICS_BODY_DYNAMIC },
{ "KINEMATIC", PHYSICS_BODY_KINEMATIC },
};
for(int i = 0; i < 3; i++) {
jerry_value_t k = jerry_string_sz(bodyTypes[i].name);
jerry_value_t v = jerry_number((double)bodyTypes[i].val);
jerry_object_set(ctor, k, v);
jerry_value_free(v);
jerry_value_free(k);
}
struct { const char_t *name; int val; } shapes[] = {
{ "SHAPE_CUBE", PHYSICS_SHAPE_CUBE },
{ "SHAPE_SPHERE", PHYSICS_SHAPE_SPHERE },
{ "SHAPE_CAPSULE", PHYSICS_SHAPE_CAPSULE },
{ "SHAPE_PLANE", PHYSICS_SHAPE_PLANE },
};
for(int i = 0; i < 4; i++) {
jerry_value_t k = jerry_string_sz(shapes[i].name);
jerry_value_t v = jerry_number((double)shapes[i].val);
jerry_object_set(ctor, k, v);
jerry_value_free(v);
jerry_value_free(k);
}
}
void modulePhysicsDispose(void) {
scriptProtoDispose(&MODULE_PHYSICS_PROTO);
}
@@ -1,72 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/module/modulebase.h"
#include "script/scriptproto.h"
#include "script/module/math/modulevec3.h"
#include "script/module/entity/modulecomponent.h"
#include "entity/component/physics/entityphysics.h"
extern scriptproto_t MODULE_PHYSICS_PROTO;
/** Physics() constructor - always throws; not directly instantiable. */
moduleBaseFunction(modulePhysicsCtor);
/** @return Entity ID that owns this physics component. */
moduleBaseFunction(modulePhysicsGetEntity);
/** @return This component's ID. */
moduleBaseFunction(modulePhysicsGetId);
/** @return Body type constant (Physics.STATIC, .DYNAMIC, .KINEMATIC). */
moduleBaseFunction(modulePhysicsGetBodyType);
/** Sets body type. @param args[0] physicsbodytype_t value. */
moduleBaseFunction(modulePhysicsSetBodyType);
/** @return Collision shape type constant (Physics.SHAPE_*). */
moduleBaseFunction(modulePhysicsGetShape);
/** Sets collision shape type. @param args[0] physicshapetype_t value. */
moduleBaseFunction(modulePhysicsSetShape);
/** @return Linear velocity as a Vec3. */
moduleBaseFunction(modulePhysicsGetVelocity);
/** Sets linear velocity. @param args[0] Vec3. */
moduleBaseFunction(modulePhysicsSetVelocity);
/** @return Gravity scale multiplier (float). */
moduleBaseFunction(modulePhysicsGetGravityScale);
/** Sets gravity scale. @param args[0] Float multiplier. */
moduleBaseFunction(modulePhysicsSetGravityScale);
/** @return True when the body is resting on a surface. */
moduleBaseFunction(modulePhysicsGetOnGround);
/**
* applyImpulse(impulse) - applies an instantaneous force to the body.
* @param args[0] Vec3 impulse vector.
*/
moduleBaseFunction(modulePhysicsApplyImpulse);
/** @return "Physics(id)" string. */
moduleBaseFunction(modulePhysicsToString);
/**
* Initializes the Physics module and registers the global Physics class with
* bodyType/shape/velocity/gravityScale/onGround properties, applyImpulse
* method, and STATIC/DYNAMIC/KINEMATIC and SHAPE_* constants.
*/
void modulePhysicsInit(void);
/**
* Disposes the Physics module.
*/
void modulePhysicsDispose(void);

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