diff --git a/src/dusk/asset/asset.c b/src/dusk/asset/asset.c
index b3f747cb..aa55e948 100644
--- a/src/dusk/asset/asset.c
+++ b/src/dusk/asset/asset.c
@@ -96,6 +96,34 @@ errorret_t assetRequireLoaded(assetentry_t *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) {
// Determine how many available loading slots we have.
assetloading_t *availableLoading[ASSET_LOADING_COUNT_MAX];
@@ -216,6 +244,33 @@ errorret_t assetUpdate(void) {
continue;
}
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
+
+
+ // Reap entries that have no external locks (refs.count == 1 means only the
+ // system hold remains). Only safe to reap LOADED and NOT_STARTED states —
+ // mid-load entries are left for the next cycle.
+ entry = ASSET.entries;
+ do {
+ if(entry->state != ASSET_ENTRY_STATE_LOADED) {
+ entry++;
+ continue;
+ }
+
+ if(entry->type == ASSET_LOADER_TYPE_NULL) {
+ entry++;
+ continue;
+ }
+
+ if(entry->refs.count > 0) {
+ entry++;
+ continue;
+ }
+
+ consolePrint("Reaping asset %s", entry->name);
+ errorChain(assetEntryDispose(entry));
+ entry++;
+ } while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
+
errorOk();
}
diff --git a/src/dusk/asset/asset.h b/src/dusk/asset/asset.h
index 13162d03..e98697be 100644
--- a/src/dusk/asset/asset.h
+++ b/src/dusk/asset/asset.h
@@ -56,11 +56,11 @@ errorret_t assetInit(void);
bool_t assetFileExists(const char_t *filename);
/**
- * Gets, or creates, a new asset entry. You will need to lock the asset soon
- * after creating or else it will be freed up on the next update cycle.
- *
+ * 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,
@@ -68,6 +68,38 @@ assetentry_t * assetGetEntry(
assetloaderinput_t *input
);
+/**
+ * Gets, creates, and locks an asset entry. The asset will begin loading on
+ * the next assetUpdate. Call assetUnlock when done to allow the entry to be
+ * reclaimed.
+ *
+ * @param name Filename of the asset.
+ * @param type Type of the asset.
+ * @param input Loader-specific parameters.
+ * @return The locked asset entry.
+ */
+assetentry_t * assetLock(
+ const char_t *name,
+ const assetloadertype_t type,
+ assetloaderinput_t *input
+);
+
+/**
+ * Releases a lock on an asset entry by name. When all locks are released the
+ * entry will be reclaimed at the start of the next assetUpdate.
+ *
+ * @param name Filename of the asset to unlock.
+ */
+void assetUnlock(const char_t *name);
+
+/**
+ * Releases a lock on an asset entry by pointer. When all locks are released
+ * the entry will be reclaimed at the start of the next assetUpdate.
+ *
+ * @param entry The asset entry to unlock.
+ */
+void assetUnlockEntry(assetentry_t *entry);
+
/**
* Requires an asset entry to be loaded. This will block until the asset entry
* is fully loaded.
diff --git a/src/dusk/display/text/text.c b/src/dusk/display/text/text.c
index e71310a7..fe8cc2ba 100644
--- a/src/dusk/display/text/text.c
+++ b/src/dusk/display/text/text.c
@@ -20,35 +20,25 @@ errorret_t textInit(void) {
assetloaderinput_t input = {
.texture = TEXTURE_FORMAT_RGBA
};
- assetentry_t *entryTexture = assetGetEntry(
+ assetentry_t *entryTexture = assetLock(
"ui/minogram.png", ASSET_LOADER_TYPE_TEXTURE, &input
);
- assetentry_t *entryTileset = assetGetEntry(
+ assetentry_t *entryTileset = assetLock(
"ui/minogram.dtf", ASSET_LOADER_TYPE_TILESET, NULL
- // "ui/minogram.dtx", ASSET_LOADER_TYPE_TILESET, NULL
);
errorChain(assetRequireLoaded(entryTexture));
errorChain(assetRequireLoaded(entryTileset));
+
FONT_DEFAULT.texture = &entryTexture->data.texture;
FONT_DEFAULT.tileset = &entryTileset->data.tileset;
-
- // assetentry_t *entryTileset = assetGetEntry(
- // "ui/minogram.dtf", ASSET_LOADER_TYPE_TILESET
- // );
-
- // assetbatch_t batch;
- // assetBatchInit(&batch, NULL, NULL, NULL);
- // assetBatchTexture(&batch, "ui/minogram.png", TEXTURE_FORMAT_RGBA, &FONT_DEFAULT.texture);
- // assetBatchTileset(&batch, "ui/minogram.dtf", &FONT_DEFAULT.tileset);
- // errorChain(assetBatchLoad(&batch));
errorOk();
}
errorret_t textDispose(void) {
- // assetCacheRelease(&ASSET.cache, "ui/minogram.png");
- // assetCacheRelease(&ASSET.cache, "ui/minogram.dtf");
FONT_DEFAULT.texture = NULL;
FONT_DEFAULT.tileset = NULL;
+ assetUnlock("ui/minogram.png");
+ assetUnlock("ui/minogram.dtf");
errorOk();
}
diff --git a/src/dusk/locale/localemanager.c b/src/dusk/locale/localemanager.c
index 948b7f41..046e6080 100644
--- a/src/dusk/locale/localemanager.c
+++ b/src/dusk/locale/localemanager.c
@@ -20,14 +20,24 @@ errorret_t localeManagerInit() {
errorret_t localeManagerSetLocale(const localeinfo_t *locale) {
assertNotNull(locale, "Locale cannot be NULL");
+ if(LOCALE.entry != NULL) {
+ assetEntryUnlock(LOCALE.entry);
+ LOCALE.entry = NULL;
+ }
+
LOCALE.locale = locale;
LOCALE.entry = assetGetEntry(locale->file, ASSET_LOADER_TYPE_LOCALE, NULL);
+ assetEntryLock(LOCALE.entry);
errorChain(assetRequireLoaded(LOCALE.entry));
errorOk();
}
void localeManagerDispose() {
- LOCALE.entry = NULL;
+ if(LOCALE.entry != NULL) {
+ assetEntryUnlock(LOCALE.entry);
+ LOCALE.entry = NULL;
+ }
+
LOCALE.locale = NULL;
}
\ No newline at end of file
diff --git a/src/dusk/script/module/display/modulescreen.h b/src/dusk/script/module/display/modulescreen.h
new file mode 100644
index 00000000..c2bc3204
--- /dev/null
+++ b/src/dusk/script/module/display/modulescreen.h
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+#pragma once
+#include "script/module/modulebase.h"
+#include "script/scriptproto.h"
+#include "display/screen/screen.h"
+
+static scriptproto_t MODULE_SCREEN_PROTO;
+
+moduleBaseFunction(moduleScreenGetWidth) {
+ return jerry_number((double)SCREEN.width);
+}
+
+moduleBaseFunction(moduleScreenGetHeight) {
+ return jerry_number((double)SCREEN.height);
+}
+
+moduleBaseFunction(moduleScreenGetAspect) {
+ return jerry_number((double)SCREEN.aspect);
+}
+
+static void moduleScreenInit(void) {
+ scriptProtoInit(&MODULE_SCREEN_PROTO, "Screen", sizeof(uint8_t), NULL);
+
+ scriptProtoDefineStaticProp(
+ &MODULE_SCREEN_PROTO, "width",
+ moduleScreenGetWidth, NULL
+ );
+ scriptProtoDefineStaticProp(
+ &MODULE_SCREEN_PROTO, "height",
+ moduleScreenGetHeight, NULL
+ );
+ scriptProtoDefineStaticProp(
+ &MODULE_SCREEN_PROTO, "aspect",
+ moduleScreenGetAspect, NULL
+ );
+}
+
+static void moduleScreenDispose(void) {
+ scriptProtoDispose(&MODULE_SCREEN_PROTO);
+}
diff --git a/src/dusk/script/module/entity/component/modulecomponentlist.h b/src/dusk/script/module/entity/component/modulecomponentlist.h
new file mode 100644
index 00000000..f50c4def
--- /dev/null
+++ b/src/dusk/script/module/entity/component/modulecomponentlist.h
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+#pragma once
+
+/* Include component modules here as they are added. */
+
+static void moduleComponentListInit(void) {
+}
+
+static void moduleComponentListDispose(void) {
+}
diff --git a/src/dusk/script/module/entity/modulecomponent.h b/src/dusk/script/module/entity/modulecomponent.h
new file mode 100644
index 00000000..dc036b00
--- /dev/null
+++ b/src/dusk/script/module/entity/modulecomponent.h
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+#pragma once
+#include "script/module/modulebase.h"
+#include "script/scriptproto.h"
+#include "entity/component.h"
+
+/** C struct wrapped by every Component JS instance. */
+typedef struct {
+ entityid_t entityId;
+ componentid_t componentId;
+} jscomponent_t;
+
+static scriptproto_t MODULE_COMPONENT_PROTO;
+
+moduleBaseFunction(moduleComponentCtor) {
+ (void)callInfo; (void)args; (void)argc;
+ return moduleBaseThrow("Component cannot be instantiated with new");
+}
+
+moduleBaseFunction(moduleComponentGetEntity) {
+ jscomponent_t *comp = scriptProtoGetValue(
+ &MODULE_COMPONENT_PROTO, callInfo->this_value
+ );
+ if(!comp) return jerry_undefined();
+ return jerry_number((double)comp->entityId);
+}
+
+moduleBaseFunction(moduleComponentGetId) {
+ jscomponent_t *comp = scriptProtoGetValue(
+ &MODULE_COMPONENT_PROTO, callInfo->this_value
+ );
+ if(!comp) return jerry_undefined();
+ return jerry_number((double)comp->componentId);
+}
+
+moduleBaseFunction(moduleComponentToString) {
+ jscomponent_t *comp = scriptProtoGetValue(
+ &MODULE_COMPONENT_PROTO, callInfo->this_value
+ );
+ if(!comp) return jerry_string_sz("Component:invalid");
+ jerry_value_t num = jerry_number((double)comp->componentId);
+ jerry_value_t str = jerry_value_to_string(num);
+ jerry_value_free(num);
+ return str;
+}
+
+static void moduleComponentInit(void) {
+ scriptProtoInit(
+ &MODULE_COMPONENT_PROTO, "Component",
+ sizeof(jscomponent_t), moduleComponentCtor
+ );
+
+ /* Instance properties */
+ scriptProtoDefineProp(
+ &MODULE_COMPONENT_PROTO, "entity",
+ moduleComponentGetEntity, NULL
+ );
+ scriptProtoDefineProp(
+ &MODULE_COMPONENT_PROTO, "id",
+ moduleComponentGetId, NULL
+ );
+ scriptProtoDefineToString(&MODULE_COMPONENT_PROTO, moduleComponentToString);
+
+ /* Component.POSITION, Component.CAMERA, etc. from componentlist.h */
+ jerry_value_t ctor = MODULE_COMPONENT_PROTO.constructor;
+#define X(enumName, type, field, init, dispose, render) \
+ do { \
+ jerry_value_t _key = jerry_string_sz(#enumName); \
+ jerry_value_t _val = jerry_number((double)COMPONENT_TYPE_##enumName); \
+ jerry_object_set(ctor, _key, _val); \
+ jerry_value_free(_val); \
+ jerry_value_free(_key); \
+ } while(0);
+#include "entity/componentlist.h"
+#undef X
+
+ /* Component.INVALID */
+ jerry_value_t _key = jerry_string_sz("INVALID");
+ jerry_value_t _val = jerry_number((double)COMPONENT_ID_INVALID);
+ jerry_object_set(ctor, _key, _val);
+ jerry_value_free(_val);
+ jerry_value_free(_key);
+}
+
+static void moduleComponentDispose(void) {
+ scriptProtoDispose(&MODULE_COMPONENT_PROTO);
+}
diff --git a/src/dusk/script/module/entity/moduleentity.h b/src/dusk/script/module/entity/moduleentity.h
new file mode 100644
index 00000000..460bdbed
--- /dev/null
+++ b/src/dusk/script/module/entity/moduleentity.h
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+#pragma once
+#include "script/module/modulebase.h"
+#include "script/scriptproto.h"
+#include "script/module/entity/modulecomponent.h"
+#include "entity/entitymanager.h"
+
+/** C struct wrapped by every Entity JS instance. */
+typedef struct {
+ entityid_t id;
+} jsentity_t;
+
+static scriptproto_t MODULE_ENTITY_PROTO;
+
+moduleBaseFunction(moduleEntityCtor) {
+ (void)callInfo; (void)args; (void)argc;
+ return moduleBaseThrow("Entity cannot be instantiated with new");
+}
+
+moduleBaseFunction(moduleEntityCreate) {
+ entityid_t id = entityManagerAdd();
+ if(id == ENTITY_ID_INVALID) {
+ return moduleBaseThrow("Entity.create: no entity slots available");
+ }
+ jsentity_t ent = { .id = id };
+ return scriptProtoCreateValue(&MODULE_ENTITY_PROTO, &ent);
+}
+
+moduleBaseFunction(moduleEntityDisposeEntity) {
+ moduleBaseRequireArgs(1);
+ jsentity_t *ent = scriptProtoGetValue(&MODULE_ENTITY_PROTO, args[0]);
+ if(!ent) return moduleBaseThrow("Entity.dispose: expected Entity object");
+ entityDispose(ent->id);
+ return jerry_undefined();
+}
+
+moduleBaseFunction(moduleEntityAdd) {
+ jsentity_t *ent = scriptProtoGetValue(
+ &MODULE_ENTITY_PROTO, callInfo->this_value
+ );
+ if(!ent) return moduleBaseThrow("Entity.add: invalid this");
+ moduleBaseRequireArgs(1);
+ moduleBaseRequireNumber(0);
+
+ const componenttype_t type = (componenttype_t)moduleBaseArgInt(0);
+ if(type <= COMPONENT_TYPE_NULL || type >= COMPONENT_TYPE_COUNT) {
+ return moduleBaseThrow("Entity.add: invalid component type");
+ }
+
+ componentid_t cid = entityAddComponent(ent->id, type);
+ if(cid == COMPONENT_ID_INVALID) {
+ return moduleBaseThrow("Entity.add: failed to add component");
+ }
+
+ jscomponent_t comp = { .entityId = ent->id, .componentId = cid };
+ return scriptProtoCreateValue(&MODULE_COMPONENT_PROTO, &comp);
+}
+
+moduleBaseFunction(moduleEntityToString) {
+ jsentity_t *ent = scriptProtoGetValue(
+ &MODULE_ENTITY_PROTO, callInfo->this_value
+ );
+ if(!ent) return jerry_string_sz("Entity:invalid");
+ jerry_value_t num = jerry_number((double)ent->id);
+ jerry_value_t str = jerry_value_to_string(num);
+ jerry_value_free(num);
+ return str;
+}
+
+static void moduleEntityInit(void) {
+ scriptProtoInit(
+ &MODULE_ENTITY_PROTO, "Entity",
+ sizeof(jsentity_t), moduleEntityCtor
+ );
+
+ /* Static methods */
+ scriptProtoDefineStaticFunc(
+ &MODULE_ENTITY_PROTO, "create", moduleEntityCreate
+ );
+ scriptProtoDefineStaticFunc(
+ &MODULE_ENTITY_PROTO, "dispose", moduleEntityDisposeEntity
+ );
+
+ /* Entity.INVALID */
+ jerry_value_t ctor = MODULE_ENTITY_PROTO.constructor;
+ jerry_value_t _key = jerry_string_sz("INVALID");
+ jerry_value_t _val = jerry_number((double)ENTITY_ID_INVALID);
+ jerry_object_set(ctor, _key, _val);
+ jerry_value_free(_val);
+ jerry_value_free(_key);
+
+ /* Instance methods */
+ scriptProtoDefineFunc(&MODULE_ENTITY_PROTO, "add", moduleEntityAdd);
+ scriptProtoDefineToString(&MODULE_ENTITY_PROTO, moduleEntityToString);
+}
+
+static void moduleEntityDispose(void) {
+ scriptProtoDispose(&MODULE_ENTITY_PROTO);
+}
diff --git a/src/dusk/script/module/input/moduleinput.h b/src/dusk/script/module/input/moduleinput.h
index bc1e9080..5a6c6e7e 100644
--- a/src/dusk/script/module/input/moduleinput.h
+++ b/src/dusk/script/module/input/moduleinput.h
@@ -13,7 +13,12 @@
static scriptproto_t MODULE_INPUT_PROTO;
-/** Validates an inputaction_t argument and returns a type error if bad. */
+/**
+ * Validates an inputaction_t argument and returns a type error if bad.
+ *
+ * @param i Argument index.
+ * @param ctx Context string for error messages.
+ */
#define moduleInputRequireAction(i, ctx) do { \
if(!jerry_value_is_number(args[(i)])) { \
return moduleBaseThrow(ctx ": action must be a number"); \
diff --git a/src/dusk/script/module/math/modulevec3.h b/src/dusk/script/module/math/modulevec3.h
new file mode 100644
index 00000000..e2695706
--- /dev/null
+++ b/src/dusk/script/module/math/modulevec3.h
@@ -0,0 +1,127 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+#pragma once
+#include "script/module/modulebase.h"
+#include "script/scriptproto.h"
+#include "util/memory.h"
+#include "cglm/cglm.h"
+
+static scriptproto_t MODULE_VEC3_PROTO;
+
+/**
+ * Returns the native float[3] pointer from the Vec3 instance that owns
+ * the current call (this_value). Returns NULL if not a Vec3.
+ */
+static inline float_t *moduleVec3Get(const jerry_call_info_t *callInfo) {
+ return (float_t *)scriptProtoGetValue(
+ &MODULE_VEC3_PROTO, callInfo->this_value
+ );
+}
+
+/**
+ * Returns the native float[3] pointer from any jerry value.
+ * Returns NULL if the value is not a Vec3 instance.
+ */
+static inline float_t *moduleVec3From(const jerry_value_t val) {
+ return (float_t *)scriptProtoGetValue(&MODULE_VEC3_PROTO, val);
+}
+
+/**
+ * Creates a Vec3 JS object from a C vec3 array.
+ *
+ * @param v Source vec3 to copy.
+ * @return A new Vec3 JS instance owning a copy of the data.
+ */
+static inline jerry_value_t moduleVec3Push(const vec3 v) {
+ return scriptProtoCreateValue(&MODULE_VEC3_PROTO, v);
+}
+
+moduleBaseFunction(moduleVec3Constructor) {
+ float_t *ptr = (float_t *)memoryAllocate(sizeof(vec3));
+ ptr[0] = moduleBaseOptFloat(0, 0.0f);
+ ptr[1] = moduleBaseOptFloat(1, 0.0f);
+ ptr[2] = moduleBaseOptFloat(2, 0.0f);
+ jerry_object_set_native_ptr(
+ callInfo->this_value, &MODULE_VEC3_PROTO.info, ptr
+ );
+ return jerry_undefined();
+}
+
+moduleBaseFunction(moduleVec3GetX) {
+ float_t *v = moduleVec3Get(callInfo);
+ if(!v) return jerry_undefined();
+ return jerry_number((double)v[0]);
+}
+
+moduleBaseFunction(moduleVec3SetX) {
+ moduleBaseRequireArgs(1);
+ float_t *v = moduleVec3Get(callInfo);
+ if(!v) return jerry_undefined();
+ v[0] = moduleBaseArgFloat(0);
+ return jerry_undefined();
+}
+
+moduleBaseFunction(moduleVec3GetY) {
+ float_t *v = moduleVec3Get(callInfo);
+ if(!v) return jerry_undefined();
+ return jerry_number((double)v[1]);
+}
+
+moduleBaseFunction(moduleVec3SetY) {
+ moduleBaseRequireArgs(1);
+ float_t *v = moduleVec3Get(callInfo);
+ if(!v) return jerry_undefined();
+ v[1] = moduleBaseArgFloat(0);
+ return jerry_undefined();
+}
+
+moduleBaseFunction(moduleVec3GetZ) {
+ float_t *v = moduleVec3Get(callInfo);
+ if(!v) return jerry_undefined();
+ return jerry_number((double)v[2]);
+}
+
+moduleBaseFunction(moduleVec3SetZ) {
+ moduleBaseRequireArgs(1);
+ float_t *v = moduleVec3Get(callInfo);
+ if(!v) return jerry_undefined();
+ v[2] = moduleBaseArgFloat(0);
+ return jerry_undefined();
+}
+
+moduleBaseFunction(moduleVec3ToString) {
+ float_t *v = moduleVec3Get(callInfo);
+ if(!v) return jerry_string_sz("Vec3:invalid");
+ char_t buf[64];
+ snprintf(buf, sizeof(buf), "Vec3(%g, %g, %g)",
+ (double)v[0], (double)v[1], (double)v[2]
+ );
+ return jerry_string_sz(buf);
+}
+
+static void moduleVec3Init(void) {
+ scriptProtoInit(
+ &MODULE_VEC3_PROTO, "Vec3",
+ sizeof(vec3), moduleVec3Constructor
+ );
+
+ scriptProtoDefineProp(
+ &MODULE_VEC3_PROTO, "x", moduleVec3GetX, moduleVec3SetX
+ );
+ scriptProtoDefineProp(
+ &MODULE_VEC3_PROTO, "y", moduleVec3GetY, moduleVec3SetY
+ );
+ scriptProtoDefineProp(
+ &MODULE_VEC3_PROTO, "z", moduleVec3GetZ, moduleVec3SetZ
+ );
+ scriptProtoDefineToString(&MODULE_VEC3_PROTO, moduleVec3ToString);
+}
+
+static void moduleVec3Dispose(void) {
+ scriptProtoDispose(&MODULE_VEC3_PROTO);
+}
diff --git a/src/dusk/script/module/modulelist.h b/src/dusk/script/module/modulelist.h
index 4376276e..f0000402 100644
--- a/src/dusk/script/module/modulelist.h
+++ b/src/dusk/script/module/modulelist.h
@@ -7,20 +7,38 @@
#pragma once
#include "script/module/console/moduleconsole.h"
+#include "script/module/display/modulescreen.h"
#include "script/module/engine/moduleengine.h"
+#include "script/module/entity/component/modulecomponentlist.h"
+#include "script/module/entity/modulecomponent.h"
+#include "script/module/entity/moduleentity.h"
#include "script/module/input/moduleinput.h"
+#include "script/module/math/modulevec3.h"
+#include "script/module/scene/modulescene.h"
#include "script/module/system/modulesystem.h"
static void moduleListInit(void) {
moduleConsoleInit();
+ moduleScreenInit();
moduleEngineInit();
+ moduleVec3Init();
+ moduleComponentInit();
+ moduleEntityInit();
+ moduleComponentListInit();
moduleInputInit();
+ moduleSceneInit();
moduleSystemInit();
}
static void moduleListDispose(void) {
moduleSystemDispose();
+ moduleSceneDispose();
moduleInputDispose();
+ moduleComponentListDispose();
+ moduleEntityDispose();
+ moduleComponentDispose();
+ moduleVec3Dispose();
moduleEngineDispose();
+ moduleScreenDispose();
moduleConsoleDispose();
}
diff --git a/src/dusk/script/module/scene/modulescene.h b/src/dusk/script/module/scene/modulescene.h
new file mode 100644
index 00000000..4ea309cd
--- /dev/null
+++ b/src/dusk/script/module/scene/modulescene.h
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+#pragma once
+#include "script/module/modulebase.h"
+#include "script/scriptproto.h"
+#include "scene/scene.h"
+
+static scriptproto_t MODULE_SCENE_PROTO;
+
+static void moduleSceneInit(void) {
+ scriptProtoInit(&MODULE_SCENE_PROTO, "Scene", sizeof(uint8_t), NULL);
+}
+
+static void moduleSceneDispose(void) {
+ scriptProtoDispose(&MODULE_SCENE_PROTO);
+}
diff --git a/src/dusk/script/script.c b/src/dusk/script/script.c
index aa5799de..a08ba1c4 100644
--- a/src/dusk/script/script.c
+++ b/src/dusk/script/script.c
@@ -59,9 +59,11 @@ errorret_t scriptExecFile(const char_t *path) {
assertNotNull(path, "Path cannot be NULL");
assertTrue(SCRIPT.initialized, "Script system not initialized");
- assetentry_t *entry = assetGetEntry(path, ASSET_LOADER_TYPE_SCRIPT, NULL);
+ assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_SCRIPT, NULL);
assertNotNull(entry, "Failed to get asset entry for script");
errorChain(assetRequireLoaded(entry));
+ assetUnlockEntry(entry);
+
errorOk();
}
diff --git a/src/dusk/util/ref.c b/src/dusk/util/ref.c
index 4ea507e0..faf84a9c 100644
--- a/src/dusk/util/ref.c
+++ b/src/dusk/util/ref.c
@@ -18,7 +18,7 @@ void refInit(
) {
assertNotNull(ref, "Ref cannot be NULL.");
memoryZero(ref, sizeof(ref_t));
- ref->count = 1;
+ ref->count = 0;
ref->data = data;
ref->onLock = onLock;
ref->onUnlock = onUnlock;
@@ -27,14 +27,14 @@ void refInit(
void refLock(ref_t *ref) {
assertNotNull(ref, "Ref cannot be NULL.");
- assertTrue(ref->count > 0, "Cannot lock a ref with zero count.");
+ assertTrue(ref->count >= 0, "Cannot lock a ref with negative count.");
ref->count++;
if(ref->onLock != NULL) ref->onLock(ref);
}
bool_t refUnlock(ref_t *ref) {
assertNotNull(ref, "Ref cannot be NULL.");
- assertTrue(ref->count > 0, "Cannot unlock a ref with zero count.");
+ assertTrue(ref->count >= 0, "Cannot unlock a ref with negative count.");
ref->count--;
diff --git a/types/console.d.ts b/types/console.d.ts
new file mode 100644
index 00000000..c55e4f2f
--- /dev/null
+++ b/types/console.d.ts
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+/**
+ * Interface for the in-game developer console.
+ */
+interface ConsoleNamespace {
+ /**
+ * Prints one or more values to the in-game console, separated by tabs.
+ * Each argument is coerced to a string before printing.
+ *
+ * @param args - Values to print.
+ *
+ * @example
+ * Console.print("x =", player.x, "y =", player.y);
+ */
+ print(...args: unknown[]): void;
+
+ /**
+ * Whether the in-game console overlay is currently visible.
+ * Set to `true` to show the console, `false` to hide it.
+ *
+ * @example
+ * Console.visible = true;
+ */
+ visible: boolean;
+}
+
+/** In-game developer console. */
+declare var Console: ConsoleNamespace;
diff --git a/types/engine.d.ts b/types/engine.d.ts
new file mode 100644
index 00000000..55391d02
--- /dev/null
+++ b/types/engine.d.ts
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+/**
+ * Controls over the engine main loop.
+ */
+interface EngineNamespace {
+ /**
+ * Whether the engine main loop is still running (read-only).
+ * Becomes `false` after `Engine.exit()` is called.
+ *
+ * @example
+ * while (Engine.running) { ... }
+ */
+ readonly running: boolean;
+
+ /**
+ * Requests an orderly shutdown of the engine.
+ * Sets the internal running flag to `false`; the main loop exits at the end
+ * of the current tick.
+ *
+ * @example
+ * Engine.exit();
+ */
+ exit(): void;
+}
+
+/** Engine lifecycle controls. */
+declare var Engine: EngineNamespace;
diff --git a/types/index.d.ts b/types/index.d.ts
new file mode 100644
index 00000000..6da3d3ba
--- /dev/null
+++ b/types/index.d.ts
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ *
+ * Root type declarations for the Dusk engine built-in JavaScript modules.
+ * These globals are injected by the native JerryScript runtime — they are not
+ * ES modules and cannot be imported.
+ *
+ * Reference this file from a jsconfig.json or tsconfig.json to get
+ * IntelliSense across all game scripts:
+ *
+ * { "compilerOptions": { "typeRoots": ["./types"] } }
+ */
+
+///
+///
+///
+///
+///
diff --git a/types/input.d.ts b/types/input.d.ts
new file mode 100644
index 00000000..07c645bd
--- /dev/null
+++ b/types/input.d.ts
@@ -0,0 +1,115 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+/**
+ * Opaque type alias for input action identifiers.
+ * Use the `INPUT_ACTION_*` constants rather than raw numbers.
+ */
+type InputAction = number;
+
+/**
+ * Polling-based input queries and button rebinding.
+ */
+interface InputNamespace {
+ /**
+ * Returns `true` while the given action is held down this frame.
+ *
+ * @param action - An `INPUT_ACTION_*` constant.
+ *
+ * @example
+ * if (Input.isDown(INPUT_ACTION_UP)) { player.moveUp(); }
+ */
+ isDown(action: InputAction): boolean;
+
+ /**
+ * Returns `true` if the given action was held down in the previous frame.
+ *
+ * @param action - An `INPUT_ACTION_*` constant.
+ */
+ wasDown(action: InputAction): boolean;
+
+ /**
+ * Returns `true` on the first frame the action transitions from up → down.
+ *
+ * @param action - An `INPUT_ACTION_*` constant.
+ *
+ * @example
+ * if (Input.pressed(INPUT_ACTION_ACCEPT)) { confirmSelection(); }
+ */
+ pressed(action: InputAction): boolean;
+
+ /**
+ * Returns `true` on the first frame the action transitions from down → up.
+ *
+ * @param action - An `INPUT_ACTION_*` constant.
+ */
+ released(action: InputAction): boolean;
+
+ /**
+ * Returns the continuous (analog) value of an action in the range `0.0–1.0`.
+ * Digital buttons return either `0` or `1`.
+ *
+ * @param action - An `INPUT_ACTION_*` constant.
+ *
+ * @example
+ * const speed = Input.getValue(INPUT_ACTION_UP) * MAX_SPEED;
+ */
+ getValue(action: InputAction): number;
+
+ /**
+ * Returns a signed axis value in the range `-1.0–1.0`, derived from two
+ * opposing actions: `getValue(pos) - getValue(neg)`.
+ *
+ * @param neg - Action mapped to the negative direction.
+ * @param pos - Action mapped to the positive direction.
+ *
+ * @example
+ * const moveX = Input.axis(INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT);
+ */
+ axis(neg: InputAction, pos: InputAction): number;
+
+ /**
+ * Rebinds a physical button (by name) to a logical input action at runtime.
+ * Throws if `buttonName` is unknown or `action` is out of range.
+ *
+ * @param buttonName - Platform-specific button identifier string (e.g. `"A"`, `"START"`).
+ * @param action - Target `INPUT_ACTION_*` constant.
+ *
+ * @example
+ * Input.bind("A", INPUT_ACTION_ACCEPT);
+ */
+ bind(buttonName: string, action: InputAction): void;
+}
+
+/** Polling-based input system. */
+declare var Input: InputNamespace;
+
+// ---------------------------------------------------------------------------
+// Input action constants.
+// Injected as plain global variables by the engine at startup.
+// ---------------------------------------------------------------------------
+
+/** Move / navigate upward. */
+declare var INPUT_ACTION_UP: InputAction;
+/** Move / navigate downward. */
+declare var INPUT_ACTION_DOWN: InputAction;
+/** Move / navigate left. */
+declare var INPUT_ACTION_LEFT: InputAction;
+/** Move / navigate right. */
+declare var INPUT_ACTION_RIGHT: InputAction;
+/** Confirm / accept the current selection. */
+declare var INPUT_ACTION_ACCEPT: InputAction;
+/** Cancel / go back. */
+declare var INPUT_ACTION_CANCEL: InputAction;
+/** Emergency quit (e.g. hold-to-exit on embedded platforms). */
+declare var INPUT_ACTION_RAGEQUIT: InputAction;
+/** Toggle the developer console overlay. */
+declare var INPUT_ACTION_CONSOLE: InputAction;
+/** Pointer / cursor horizontal position (analog). */
+declare var INPUT_ACTION_POINTERX: InputAction;
+/** Pointer / cursor vertical position (analog). */
+declare var INPUT_ACTION_POINTERY: InputAction;
diff --git a/types/screen.d.ts b/types/screen.d.ts
new file mode 100644
index 00000000..1b303ffa
--- /dev/null
+++ b/types/screen.d.ts
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+/**
+ * Read-only information about the current display surface.
+ */
+interface ScreenNamespace {
+ /** Current render-target width in pixels (read-only). */
+ readonly width: number;
+
+ /** Current render-target height in pixels (read-only). */
+ readonly height: number;
+
+ /**
+ * Aspect ratio of the current render target: `width / height` (read-only).
+ *
+ * @example
+ * if (Screen.aspect > 1) { /* landscape *\/ }
+ */
+ readonly aspect: number;
+}
+
+/** Current display / render-target dimensions. */
+declare var Screen: ScreenNamespace;
diff --git a/types/system.d.ts b/types/system.d.ts
new file mode 100644
index 00000000..beceddf4
--- /dev/null
+++ b/types/system.d.ts
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2026 Dominic Masters
+ *
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+/**
+ * Runtime platform detection and system-level information.
+ */
+interface SystemNamespace {
+ /**
+ * Numeric identifier for the platform the engine is running on (read-only).
+ * Compare against the `System.PLATFORM_*` constants.
+ *
+ * @example
+ * if (System.platform === System.PLATFORM_PSP) { useLowResAssets(); }
+ */
+ readonly platform: number;
+
+ /** Linux desktop. */
+ readonly PLATFORM_LINUX: number;
+
+ /** Knulli handheld (Linux-based). */
+ readonly PLATFORM_KNULLI: number;
+
+ /** Sony PlayStation Portable. */
+ readonly PLATFORM_PSP: number;
+
+ /** Nintendo GameCube. */
+ readonly PLATFORM_GAMECUBE: number;
+
+ /** Nintendo Wii. */
+ readonly PLATFORM_WII: number;
+}
+
+/** Platform detection and system-level information. */
+declare var System: SystemNamespace;