Script stuff

This commit is contained in:
2026-07-31 09:40:53 -05:00
parent d49194fc4d
commit 07f98c119a
45 changed files with 2094 additions and 201 deletions
+72
View File
@@ -0,0 +1,72 @@
// Radius must stay well outside the floor's footprint (a 20x20 plane has a
// corner-to-center distance of 10*sqrt(2) =~ 14.1) -- orbiting inside that
// puts parts of the floor's own geometry near/behind the camera's view
// direction, which the PSP's legacy GU pipeline can't clip properly and
// drops the whole triangle instead of clipping it.
var CAMERA_ORBIT_RADIUS = 18.0;
var CAMERA_ORBIT_HEIGHT = 10.0;
var CAMERA_ORBIT_SPEED = 0.5;
var cameraOrbitAngle = 0.0;
var cameraPosition = null;
// Orbits the camera around the world origin at a fixed radius/height/
// speed, always looking back at the origin. Called once up front (so the
// very first rendered frame is already positioned correctly) and then
// once per frame via update() below.
function updateCameraOrbit() {
cameraOrbitAngle += Time.delta * CAMERA_ORBIT_SPEED;
var eyeX = Math.cos(cameraOrbitAngle) * CAMERA_ORBIT_RADIUS;
var eyeY = CAMERA_ORBIT_HEIGHT;
var eyeZ = Math.sin(cameraOrbitAngle) * CAMERA_ORBIT_RADIUS;
cameraPosition.lookAt(eyeX, eyeY, eyeZ, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
}
// Called once per engine frame (see engineUpdate() -> scriptManagerCall
// Global("update")).
function update() {
if(cameraPosition) updateCameraOrbit();
}
(function setup() {
var scene = new Scene();
scene.setActive();
// Camera, orbiting the origin (see updateCameraOrbit above).
var camera = new Entity();
cameraPosition = camera.add(POSITION);
camera.add(CAMERA);
updateCameraOrbit();
// Static ground plane. Physics ignores the entity's position component --
// the shape's own normal/distance fully define the plane in world space.
var plane = new Entity();
var planePosition = plane.add(POSITION);
planePosition.setLocalPosition(-10.0, 0.0, -10.0);
planePosition.setLocalScale(20.0, 1.0, 20.0);
var planePhysics = plane.add(PHYSICS);
planePhysics.setBodyType(PHYSICS_BODY_STATIC);
planePhysics.setShape(PHYSICS_SHAPE_PLANE, 0.0, 1.0, 0.0, 0.0);
var planeRenderable = plane.add(RENDERABLE);
planeRenderable.setMesh(0, MESH_PLANE);
planeRenderable.setColor(128, 128, 128, 255);
// Player: dynamic capsule body, moved relative to the camera by
// PLAYER's own update callback (see entityplayer.c).
var player = new Entity();
var playerPosition = player.add(POSITION);
playerPosition.setLocalPosition(0.0, 2.0, 0.0);
var playerPhysics = player.add(PHYSICS);
playerPhysics.setShape(PHYSICS_SHAPE_CAPSULE, 0.5, 0.5);
var playerRenderable = player.add(RENDERABLE);
playerRenderable.setMesh(0, MESH_CAPSULE);
playerRenderable.setColor(0, 0, 255, 255);
player.add(PLAYER);
})();
+1
View File
@@ -74,6 +74,7 @@ errorret_t engineUpdate(void) {
consoleUpdate(); consoleUpdate();
errorChain(gameUpdate()); errorChain(gameUpdate());
errorChain(scriptManagerCallGlobal("update"));
errorChain(sceneUpdate()); errorChain(sceneUpdate());
errorChain(assetUpdate()); errorChain(assetUpdate());
errorChain(uiUpdate()); errorChain(uiUpdate());
@@ -80,6 +80,32 @@ void entityRenderableSetColor(
r->data.material.material.unlit.color = color; r->data.material.material.unlit.color = color;
} }
void entityRenderableSetMesh(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const uint8_t slot,
mesh_t *mesh
) {
assertNotNull(mesh, "Mesh cannot be null");
assertTrue(slot < ENTITY_RENDERABLE_MESHES_MAX, "Mesh slot out of bounds");
entityrenderable_t *r = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
);
assertTrue(
r->type == ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL,
"Renderable must be ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL to set a mesh"
);
r->data.material.meshes[slot] = mesh;
r->data.material.meshOffsets[slot] = 0;
r->data.material.meshCounts[slot] = -1;
if(slot >= r->data.material.meshCount) {
r->data.material.meshCount = slot + 1;
}
}
void entityRenderableSetDraw( void entityRenderableSetDraw(
entitymanager_t *mgr, entitymanager_t *mgr,
const entityid_t entityId, const entityid_t entityId,
@@ -142,6 +142,26 @@ void entityRenderableSetColor(
const color_t color const color_t color
); );
/**
* Sets one of the material renderable's meshes, drawn in full (no offset/
* count override). Only meaningful when the renderable is (or defaults
* to) ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL -- asserts otherwise.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity to configure.
* @param componentId The renderable component.
* @param slot Index into the material's meshes array (0 to
* ENTITY_RENDERABLE_MESHES_MAX - 1).
* @param mesh The mesh to draw in that slot.
*/
void entityRenderableSetMesh(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const uint8_t slot,
mesh_t *mesh
);
/** /**
* Sets the draw callback, switching the type to * Sets the draw callback, switching the type to
* ENTITY_RENDERABLE_TYPE_CUSTOM. * ENTITY_RENDERABLE_TYPE_CUSTOM.
+3
View File
@@ -10,3 +10,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
add_subdirectory(entity) add_subdirectory(entity)
add_subdirectory(scene) add_subdirectory(scene)
add_subdirectory(require)
add_subdirectory(display)
add_subdirectory(time)
@@ -0,0 +1,9 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
modulemesh.c
)
@@ -0,0 +1,27 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "modulemesh.h"
#include "script/module/modulebase.h"
#include "display/mesh/cube.h"
#include "display/mesh/plane.h"
#include "display/mesh/sphere.h"
#include "display/mesh/capsule.h"
#include "display/mesh/quad.h"
#include "display/mesh/triprism.h"
void moduleMeshInit(void) {
moduleBaseSetWrappedPointer("MESH_CUBE", &CUBE_MESH_SIMPLE);
moduleBaseSetWrappedPointer("MESH_PLANE", &PLANE_MESH_SIMPLE);
moduleBaseSetWrappedPointer("MESH_SPHERE", &SPHERE_MESH_SIMPLE);
moduleBaseSetWrappedPointer("MESH_CAPSULE", &CAPSULE_MESH_SIMPLE);
moduleBaseSetWrappedPointer("MESH_TRIPRISM", &TRIPRISM_MESH_SIMPLE);
moduleBaseSetWrappedPointer("MESH_QUAD", &QUAD_MESH_SIMPLE);
}
void moduleMeshDispose(void) {
}
@@ -0,0 +1,20 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
/**
* Registers the built-in primitive meshes (MESH_CUBE, MESH_PLANE,
* MESH_SPHERE, MESH_CAPSULE, MESH_TRIPRISM, MESH_QUAD) as global,
* engine-owned pointer values usable with Renderable.setMesh().
*/
void moduleMeshInit(void);
/**
* Disposes the mesh module's script resources.
*/
void moduleMeshDispose(void);
@@ -6,4 +6,8 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
modulecomponent.c modulecomponent.c
modulecomponentlist.c
) )
add_subdirectory(display)
add_subdirectory(physics)
@@ -0,0 +1,10 @@
# 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
modulepositioncomponent.c
modulerenderablecomponent.c
)
@@ -0,0 +1,150 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "modulepositioncomponent.h"
#include "script/module/modulebase.h"
#include "entity/component/display/entityposition.h"
#include "entity/component.h"
#include "util/string.h"
scriptproto_t MODULE_POSITION_PROTO;
moduleBaseFunction(modulePositionGetType) {
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPositionGet);
return jerry_number(h->type);
}
moduleBaseFunction(modulePositionGetEntityId) {
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPositionGet);
return jerry_number(h->entityId);
}
moduleBaseFunction(modulePositionDisposeMethod) {
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPositionGet);
componentDispose(h->mgr, h->entityId, h->componentId);
return jerry_undefined();
}
moduleBaseFunction(modulePositionToString) {
modulecomponenthandle_t *h = moduleEntityPositionGet(callInfo);
if(!h) return jerry_string_sz("Position(?)");
char_t buf[32];
stringFormat(buf, sizeof(buf), "Position(entityId=%d)", h->entityId);
return jerry_string_sz(buf);
}
moduleBaseFunction(modulePositionSetLocalPosition) {
moduleBaseRequireArgs(3);
moduleBaseRequireNumber(0); moduleBaseRequireNumber(1);
moduleBaseRequireNumber(2);
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPositionGet);
entityPositionSetLocalPosition(h->mgr, h->entityId, h->componentId, (vec3){
moduleBaseArgFloat(0), moduleBaseArgFloat(1), moduleBaseArgFloat(2)
});
return jerry_undefined();
}
moduleBaseFunction(modulePositionGetLocalPosition) {
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPositionGet);
vec3 pos;
entityPositionGetLocalPosition(h->mgr, h->entityId, h->componentId, pos);
return moduleBaseVec3ToObject(pos);
}
moduleBaseFunction(modulePositionSetLocalScale) {
moduleBaseRequireArgs(3);
moduleBaseRequireNumber(0); moduleBaseRequireNumber(1);
moduleBaseRequireNumber(2);
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPositionGet);
entityPositionSetLocalScale(h->mgr, h->entityId, h->componentId, (vec3){
moduleBaseArgFloat(0), moduleBaseArgFloat(1), moduleBaseArgFloat(2)
});
return jerry_undefined();
}
moduleBaseFunction(modulePositionGetLocalScale) {
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPositionGet);
vec3 scale;
entityPositionGetLocalScale(h->mgr, h->entityId, h->componentId, scale);
return moduleBaseVec3ToObject(scale);
}
moduleBaseFunction(modulePositionLookAt) {
moduleBaseRequireArgs(9);
for(jerry_length_t i = 0; i < 9; i++) moduleBaseRequireNumber(i);
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPositionGet);
entityPositionLookAt(
h->mgr, h->entityId, h->componentId,
(vec3){
moduleBaseArgFloat(0), moduleBaseArgFloat(1), moduleBaseArgFloat(2)
},
(vec3){
moduleBaseArgFloat(3), moduleBaseArgFloat(4), moduleBaseArgFloat(5)
},
(vec3){
moduleBaseArgFloat(6), moduleBaseArgFloat(7), moduleBaseArgFloat(8)
}
);
return jerry_undefined();
}
void moduleEntityPositionInit(void) {
scriptProtoInit(
&MODULE_POSITION_PROTO, "Position", sizeof(modulecomponenthandle_t), NULL
);
scriptProtoDefineToString(&MODULE_POSITION_PROTO, modulePositionToString);
scriptProtoDefineProp(
&MODULE_POSITION_PROTO, "type", modulePositionGetType, NULL
);
scriptProtoDefineProp(
&MODULE_POSITION_PROTO, "entityId", modulePositionGetEntityId, NULL
);
scriptProtoDefineFunc(
&MODULE_POSITION_PROTO, "dispose", modulePositionDisposeMethod
);
scriptProtoDefineFunc(
&MODULE_POSITION_PROTO, "setLocalPosition", modulePositionSetLocalPosition
);
scriptProtoDefineFunc(
&MODULE_POSITION_PROTO, "getLocalPosition", modulePositionGetLocalPosition
);
scriptProtoDefineFunc(
&MODULE_POSITION_PROTO, "setLocalScale", modulePositionSetLocalScale
);
scriptProtoDefineFunc(
&MODULE_POSITION_PROTO, "getLocalScale", modulePositionGetLocalScale
);
scriptProtoDefineFunc(
&MODULE_POSITION_PROTO, "lookAt", modulePositionLookAt
);
}
void moduleEntityPositionDispose(void) {
}
jerry_value_t moduleEntityPositionCreate(
const modulecomponenthandle_t *handle
) {
return scriptProtoCreateValue(&MODULE_POSITION_PROTO, handle);
}
modulecomponenthandle_t *moduleEntityPositionGet(
const jerry_call_info_t *callInfo
) {
return (modulecomponenthandle_t *)scriptProtoGetValue(
&MODULE_POSITION_PROTO, callInfo->this_value
);
}
@@ -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/scriptproto.h"
#include "script/module/entity/component/modulecomponent.h"
#include <jerryscript.h>
extern scriptproto_t MODULE_POSITION_PROTO;
/**
* Registers the Position class: a typed wrapper over COMPONENT_TYPE_
* POSITION. Has no constructor -- instances are only ever created via
* moduleEntityPositionCreate(), e.g. from Entity.add()/getComponent().
*/
void moduleEntityPositionInit(void);
/**
* Disposes the Position class's script resources.
*/
void moduleEntityPositionDispose(void);
/**
* Wraps a component handle as a new JS Position instance.
*
* @param handle The handle to copy into the new instance.
* @return The new JS object.
*/
jerry_value_t moduleEntityPositionCreate(
const modulecomponenthandle_t *handle
);
/**
* Internal. Gets the native handle wrapped by a Position instance's
* `this` value.
*
* @param callInfo The JS call info, whose this_value is the instance.
* @return The wrapped handle, or NULL if this_value isn't a Position.
*/
modulecomponenthandle_t *moduleEntityPositionGet(
const jerry_call_info_t *callInfo
);
@@ -0,0 +1,123 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "modulerenderablecomponent.h"
#include "script/module/modulebase.h"
#include "entity/component/display/entityrenderable.h"
#include "entity/component.h"
#include "display/color.h"
#include "util/string.h"
scriptproto_t MODULE_RENDERABLE_PROTO;
moduleBaseFunction(moduleRenderableGetType) {
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityRenderableGet);
return jerry_number(h->type);
}
moduleBaseFunction(moduleRenderableGetEntityId) {
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityRenderableGet);
return jerry_number(h->entityId);
}
moduleBaseFunction(moduleRenderableDisposeMethod) {
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityRenderableGet);
componentDispose(h->mgr, h->entityId, h->componentId);
return jerry_undefined();
}
moduleBaseFunction(moduleRenderableToString) {
modulecomponenthandle_t *h = moduleEntityRenderableGet(callInfo);
if(!h) return jerry_string_sz("Renderable(?)");
char_t buf[32];
stringFormat(buf, sizeof(buf), "Renderable(entityId=%d)", h->entityId);
return jerry_string_sz(buf);
}
moduleBaseFunction(moduleRenderableSetColor) {
moduleBaseRequireArgs(4);
moduleBaseRequireNumber(0); moduleBaseRequireNumber(1);
moduleBaseRequireNumber(2); moduleBaseRequireNumber(3);
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityRenderableGet);
entityRenderableSetColor(h->mgr, h->entityId, h->componentId, color(
(uint8_t)moduleBaseArgInt(0), (uint8_t)moduleBaseArgInt(1),
(uint8_t)moduleBaseArgInt(2), (uint8_t)moduleBaseArgInt(3)
));
return jerry_undefined();
}
moduleBaseFunction(moduleRenderableSetMesh) {
moduleBaseRequireArgs(2); moduleBaseRequireNumber(0);
moduleBaseRequireObject(1);
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityRenderableGet);
mesh_t *mesh = (mesh_t *)moduleBaseUnwrapPointer(args[1]);
if(!mesh) return moduleBaseThrow("Renderable.setMesh: invalid mesh");
entityRenderableSetMesh(
h->mgr, h->entityId, h->componentId, (uint8_t)moduleBaseArgInt(0), mesh
);
return jerry_undefined();
}
moduleBaseFunction(moduleRenderableSetPriority) {
moduleBaseRequireArgs(1); moduleBaseRequireNumber(0);
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityRenderableGet);
entityRenderableSetPriority(
h->mgr, h->entityId, h->componentId, (int8_t)moduleBaseArgInt(0)
);
return jerry_undefined();
}
void moduleEntityRenderableInit(void) {
scriptProtoInit(
&MODULE_RENDERABLE_PROTO,
"Renderable", sizeof(modulecomponenthandle_t), NULL
);
scriptProtoDefineToString(&MODULE_RENDERABLE_PROTO, moduleRenderableToString);
scriptProtoDefineProp(
&MODULE_RENDERABLE_PROTO, "type", moduleRenderableGetType, NULL
);
scriptProtoDefineProp(
&MODULE_RENDERABLE_PROTO, "entityId", moduleRenderableGetEntityId, NULL
);
scriptProtoDefineFunc(
&MODULE_RENDERABLE_PROTO, "dispose", moduleRenderableDisposeMethod
);
scriptProtoDefineFunc(
&MODULE_RENDERABLE_PROTO, "setColor", moduleRenderableSetColor
);
scriptProtoDefineFunc(
&MODULE_RENDERABLE_PROTO, "setMesh", moduleRenderableSetMesh
);
scriptProtoDefineFunc(
&MODULE_RENDERABLE_PROTO, "setPriority", moduleRenderableSetPriority
);
}
void moduleEntityRenderableDispose(void) {
}
jerry_value_t moduleEntityRenderableCreate(
const modulecomponenthandle_t *handle
) {
return scriptProtoCreateValue(&MODULE_RENDERABLE_PROTO, handle);
}
modulecomponenthandle_t *moduleEntityRenderableGet(
const jerry_call_info_t *callInfo
) {
return (modulecomponenthandle_t *)scriptProtoGetValue(
&MODULE_RENDERABLE_PROTO, callInfo->this_value
);
}
@@ -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/scriptproto.h"
#include "script/module/entity/component/modulecomponent.h"
#include <jerryscript.h>
extern scriptproto_t MODULE_RENDERABLE_PROTO;
/**
* Registers the Renderable class: a typed wrapper over COMPONENT_TYPE_
* RENDERABLE. Has no constructor -- instances are only ever created via
* moduleEntityRenderableCreate(), e.g. from Entity.add()/getComponent().
*/
void moduleEntityRenderableInit(void);
/**
* Disposes the Renderable class's script resources.
*/
void moduleEntityRenderableDispose(void);
/**
* Wraps a component handle as a new JS Renderable instance.
*
* @param handle The handle to copy into the new instance.
* @return The new JS object.
*/
jerry_value_t moduleEntityRenderableCreate(
const modulecomponenthandle_t *handle
);
/**
* Internal. Gets the native handle wrapped by a Renderable instance's
* `this` value.
*
* @param callInfo The JS call info, whose this_value is the instance.
* @return The wrapped handle, or NULL if this_value isn't a Renderable.
*/
modulecomponenthandle_t *moduleEntityRenderableGet(
const jerry_call_info_t *callInfo
);
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "modulecomponentlist.h"
#include "script/module/entity/component/display/modulepositioncomponent.h"
#include "script/module/entity/component/physics/modulephysicscomponent.h"
#include "script/module/entity/component/display/modulerenderablecomponent.h"
void moduleComponentListInit(void) {
moduleEntityPositionInit();
moduleEntityPhysicsInit();
moduleEntityRenderableInit();
}
void moduleComponentListDispose(void) {
moduleEntityRenderableDispose();
moduleEntityPhysicsDispose();
moduleEntityPositionDispose();
}
jerry_value_t moduleComponentListCreate(
const modulecomponenthandle_t *handle
) {
switch(handle->type) {
case COMPONENT_TYPE_POSITION:
return moduleEntityPositionCreate(handle);
case COMPONENT_TYPE_PHYSICS:
return moduleEntityPhysicsCreate(handle);
case COMPONENT_TYPE_RENDERABLE:
return moduleEntityRenderableCreate(handle);
default:
return moduleComponentCreate(handle);
}
}
@@ -0,0 +1,34 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/module/entity/component/modulecomponent.h"
#include <jerryscript.h>
/**
* Registers every typed per-component-type JS wrapper module (Position,
* Physics, Renderable, ...). Called once by moduleListInit().
*/
void moduleComponentListInit(void);
/**
* Disposes every typed component wrapper module. Called once by
* moduleListDispose().
*/
void moduleComponentListDispose(void);
/**
* Wraps a component handle using the most specific JS class available
* for its type (e.g. Position for COMPONENT_TYPE_POSITION), falling back
* to the generic Component wrapper for types with no typed wrapper yet.
*
* @param handle The handle to wrap.
* @return The new JS object.
*/
jerry_value_t moduleComponentListCreate(
const modulecomponenthandle_t *handle
);
@@ -0,0 +1,9 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
modulephysicscomponent.c
)
@@ -0,0 +1,211 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "modulephysicscomponent.h"
#include "script/module/modulebase.h"
#include "entity/component/physics/entityphysics.h"
#include "entity/component.h"
#include "util/string.h"
#include "util/memory.h"
scriptproto_t MODULE_PHYSICS_PROTO;
moduleBaseFunction(modulePhysicsGetType) {
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPhysicsGet);
return jerry_number(h->type);
}
moduleBaseFunction(modulePhysicsGetEntityId) {
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPhysicsGet);
return jerry_number(h->entityId);
}
moduleBaseFunction(modulePhysicsDisposeMethod) {
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPhysicsGet);
componentDispose(h->mgr, h->entityId, h->componentId);
return jerry_undefined();
}
moduleBaseFunction(modulePhysicsToString) {
modulecomponenthandle_t *h = moduleEntityPhysicsGet(callInfo);
if(!h) return jerry_string_sz("Physics(?)");
char_t buf[32];
stringFormat(buf, sizeof(buf), "Physics(entityId=%d)", h->entityId);
return jerry_string_sz(buf);
}
moduleBaseFunction(modulePhysicsSetBodyType) {
moduleBaseRequireArgs(1); moduleBaseRequireNumber(0);
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPhysicsGet);
entityPhysicsSetBodyType(
h->mgr, h->entityId, h->componentId,
(physicsbodytype_t)moduleBaseArgInt(0)
);
return jerry_undefined();
}
moduleBaseFunction(modulePhysicsGetBodyType) {
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPhysicsGet);
return jerry_number(
entityPhysicsGetBodyType(h->mgr, h->entityId, h->componentId)
);
}
moduleBaseFunction(modulePhysicsSetShape) {
moduleBaseRequireArgs(1); moduleBaseRequireNumber(0);
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPhysicsGet);
physicsshape_t shape;
memoryZero(&shape, sizeof(shape));
shape.type = (physicshapetype_t)moduleBaseArgInt(0);
switch(shape.type) {
case PHYSICS_SHAPE_CUBE:
moduleBaseRequireArgs(4);
moduleBaseRequireNumber(1); moduleBaseRequireNumber(2);
moduleBaseRequireNumber(3);
shape.data.cube.halfExtents[0] = moduleBaseArgFloat(1);
shape.data.cube.halfExtents[1] = moduleBaseArgFloat(2);
shape.data.cube.halfExtents[2] = moduleBaseArgFloat(3);
break;
case PHYSICS_SHAPE_SPHERE:
moduleBaseRequireArgs(2); moduleBaseRequireNumber(1);
shape.data.sphere.radius = moduleBaseArgFloat(1);
break;
case PHYSICS_SHAPE_CAPSULE:
moduleBaseRequireArgs(3);
moduleBaseRequireNumber(1); moduleBaseRequireNumber(2);
shape.data.capsule.radius = moduleBaseArgFloat(1);
shape.data.capsule.halfHeight = moduleBaseArgFloat(2);
break;
case PHYSICS_SHAPE_PLANE:
moduleBaseRequireArgs(5);
moduleBaseRequireNumber(1); moduleBaseRequireNumber(2);
moduleBaseRequireNumber(3); moduleBaseRequireNumber(4);
shape.data.plane.normal[0] = moduleBaseArgFloat(1);
shape.data.plane.normal[1] = moduleBaseArgFloat(2);
shape.data.plane.normal[2] = moduleBaseArgFloat(3);
shape.data.plane.distance = moduleBaseArgFloat(4);
break;
default:
return moduleBaseThrow("Physics.setShape: unsupported shape type");
}
entityPhysicsSetShape(h->mgr, h->entityId, h->componentId, shape);
return jerry_undefined();
}
moduleBaseFunction(modulePhysicsSetVelocity) {
moduleBaseRequireArgs(3);
moduleBaseRequireNumber(0); moduleBaseRequireNumber(1);
moduleBaseRequireNumber(2);
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPhysicsGet);
entityPhysicsSetVelocity(h->mgr, h->entityId, h->componentId, (vec3){
moduleBaseArgFloat(0), moduleBaseArgFloat(1), moduleBaseArgFloat(2)
});
return jerry_undefined();
}
moduleBaseFunction(modulePhysicsGetVelocity) {
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPhysicsGet);
vec3 velocity;
entityPhysicsGetVelocity(h->mgr, h->entityId, h->componentId, velocity);
return moduleBaseVec3ToObject(velocity);
}
moduleBaseFunction(modulePhysicsApplyImpulse) {
moduleBaseRequireArgs(3);
moduleBaseRequireNumber(0); moduleBaseRequireNumber(1);
moduleBaseRequireNumber(2);
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPhysicsGet);
entityPhysicsApplyImpulse(h->mgr, h->entityId, h->componentId, (vec3){
moduleBaseArgFloat(0), moduleBaseArgFloat(1), moduleBaseArgFloat(2)
});
return jerry_undefined();
}
moduleBaseFunction(modulePhysicsIsOnGround) {
moduleBaseGetOrReturn(modulecomponenthandle_t, h, moduleEntityPhysicsGet);
return jerry_boolean(
entityPhysicsIsOnGround(h->mgr, h->entityId, h->componentId)
);
}
void moduleEntityPhysicsInit(void) {
scriptProtoInit(
&MODULE_PHYSICS_PROTO, "Physics", sizeof(modulecomponenthandle_t), NULL
);
scriptProtoDefineToString(&MODULE_PHYSICS_PROTO, modulePhysicsToString);
scriptProtoDefineProp(
&MODULE_PHYSICS_PROTO, "type", modulePhysicsGetType, NULL
);
scriptProtoDefineProp(
&MODULE_PHYSICS_PROTO, "entityId", modulePhysicsGetEntityId, NULL
);
scriptProtoDefineFunc(
&MODULE_PHYSICS_PROTO, "dispose", modulePhysicsDisposeMethod
);
scriptProtoDefineFunc(
&MODULE_PHYSICS_PROTO, "setBodyType", modulePhysicsSetBodyType
);
scriptProtoDefineFunc(
&MODULE_PHYSICS_PROTO, "getBodyType", modulePhysicsGetBodyType
);
scriptProtoDefineFunc(
&MODULE_PHYSICS_PROTO, "setShape", modulePhysicsSetShape
);
scriptProtoDefineFunc(
&MODULE_PHYSICS_PROTO, "setVelocity", modulePhysicsSetVelocity
);
scriptProtoDefineFunc(
&MODULE_PHYSICS_PROTO, "getVelocity", modulePhysicsGetVelocity
);
scriptProtoDefineFunc(
&MODULE_PHYSICS_PROTO, "applyImpulse", modulePhysicsApplyImpulse
);
scriptProtoDefineFunc(
&MODULE_PHYSICS_PROTO, "isOnGround", modulePhysicsIsOnGround
);
moduleBaseSetInt("PHYSICS_BODY_STATIC", PHYSICS_BODY_STATIC);
moduleBaseSetInt("PHYSICS_BODY_DYNAMIC", PHYSICS_BODY_DYNAMIC);
moduleBaseSetInt("PHYSICS_BODY_KINEMATIC", PHYSICS_BODY_KINEMATIC);
moduleBaseSetInt("PHYSICS_SHAPE_CUBE", PHYSICS_SHAPE_CUBE);
moduleBaseSetInt("PHYSICS_SHAPE_SPHERE", PHYSICS_SHAPE_SPHERE);
moduleBaseSetInt("PHYSICS_SHAPE_CAPSULE", PHYSICS_SHAPE_CAPSULE);
moduleBaseSetInt("PHYSICS_SHAPE_PLANE", PHYSICS_SHAPE_PLANE);
}
void moduleEntityPhysicsDispose(void) {
}
jerry_value_t moduleEntityPhysicsCreate(
const modulecomponenthandle_t *handle
) {
return scriptProtoCreateValue(&MODULE_PHYSICS_PROTO, handle);
}
modulecomponenthandle_t *moduleEntityPhysicsGet(
const jerry_call_info_t *callInfo
) {
return (modulecomponenthandle_t *)scriptProtoGetValue(
&MODULE_PHYSICS_PROTO, callInfo->this_value
);
}
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/scriptproto.h"
#include "script/module/entity/component/modulecomponent.h"
#include <jerryscript.h>
extern scriptproto_t MODULE_PHYSICS_PROTO;
/**
* Registers the Physics class: a typed wrapper over COMPONENT_TYPE_
* PHYSICS. Also registers PHYSICS_BODY_* and PHYSICS_SHAPE_* as global
* integer constants for use with setBodyType()/setShape(). Has no
* constructor -- instances are only ever created via
* moduleEntityPhysicsCreate(), e.g. from Entity.add()/getComponent().
*/
void moduleEntityPhysicsInit(void);
/**
* Disposes the Physics class's script resources.
*/
void moduleEntityPhysicsDispose(void);
/**
* Wraps a component handle as a new JS Physics instance.
*
* @param handle The handle to copy into the new instance.
* @return The new JS object.
*/
jerry_value_t moduleEntityPhysicsCreate(
const modulecomponenthandle_t *handle
);
/**
* Internal. Gets the native handle wrapped by a Physics instance's
* `this` value.
*
* @param callInfo The JS call info, whose this_value is the instance.
* @return The wrapped handle, or NULL if this_value isn't a Physics.
*/
modulecomponenthandle_t *moduleEntityPhysicsGet(
const jerry_call_info_t *callInfo
);
+3 -2
View File
@@ -8,6 +8,7 @@
#include "moduleentity.h" #include "moduleentity.h"
#include "script/module/modulebase.h" #include "script/module/modulebase.h"
#include "script/module/entity/component/modulecomponent.h" #include "script/module/entity/component/modulecomponent.h"
#include "script/module/entity/component/modulecomponentlist.h"
#include "scene/scene.h" #include "scene/scene.h"
#include "entity/entitymanager.h" #include "entity/entitymanager.h"
#include "entity/entity.h" #include "entity/entity.h"
@@ -67,7 +68,7 @@ moduleBaseFunction(moduleEntityAddComponent) {
modulecomponenthandle_t h = { modulecomponenthandle_t h = {
.mgr = inst->mgr, .entityId = inst->id, .componentId = id, .type = type .mgr = inst->mgr, .entityId = inst->id, .componentId = id, .type = type
}; };
return moduleComponentCreate(&h); return moduleComponentListCreate(&h);
} }
moduleBaseFunction(moduleEntityGetComponentMethod) { moduleBaseFunction(moduleEntityGetComponentMethod) {
@@ -85,7 +86,7 @@ moduleBaseFunction(moduleEntityGetComponentMethod) {
modulecomponenthandle_t h = { modulecomponenthandle_t h = {
.mgr = inst->mgr, .entityId = inst->id, .componentId = id, .type = type .mgr = inst->mgr, .entityId = inst->id, .componentId = id, .type = type
}; };
return moduleComponentCreate(&h); return moduleComponentListCreate(&h);
} }
moduleBaseFunction(moduleEntityDisposeMethod) { moduleBaseFunction(moduleEntityDisposeMethod) {
+27
View File
@@ -394,3 +394,30 @@ static inline float_t moduleBaseValueFloat(jerry_value_t val) {
static inline int32_t moduleBaseValueInt(jerry_value_t val) { static inline int32_t moduleBaseValueInt(jerry_value_t val) {
return (int32_t)jerry_value_as_number(val); return (int32_t)jerry_value_as_number(val);
} }
/**
* Wraps a vec3 as a plain JS {x, y, z} object.
*/
static inline jerry_value_t moduleBaseVec3ToObject(const vec3 v) {
jerry_value_t obj = jerry_object();
jerry_value_t xKey = jerry_string_sz("x");
jerry_value_t xVal = jerry_number(v[0]);
jerry_object_set(obj, xKey, xVal);
jerry_value_free(xVal);
jerry_value_free(xKey);
jerry_value_t yKey = jerry_string_sz("y");
jerry_value_t yVal = jerry_number(v[1]);
jerry_object_set(obj, yKey, yVal);
jerry_value_free(yVal);
jerry_value_free(yKey);
jerry_value_t zKey = jerry_string_sz("z");
jerry_value_t zVal = jerry_number(v[2]);
jerry_object_set(obj, zKey, zVal);
jerry_value_free(zVal);
jerry_value_free(zKey);
return obj;
}
+12
View File
@@ -7,13 +7,21 @@
#include "modulelist.h" #include "modulelist.h"
#include "script/module/moduleplatform.h" #include "script/module/moduleplatform.h"
#include "script/module/require/modulerequire.h"
#include "script/module/display/modulemesh.h"
#include "script/module/time/moduletime.h"
#include "script/module/entity/component/modulecomponent.h" #include "script/module/entity/component/modulecomponent.h"
#include "script/module/entity/component/modulecomponentlist.h"
#include "script/module/entity/moduleentity.h" #include "script/module/entity/moduleentity.h"
#include "script/module/scene/modulescene.h" #include "script/module/scene/modulescene.h"
void moduleListInit(void) { void moduleListInit(void) {
modulePlatform(); modulePlatform();
moduleRequireInit();
moduleMeshInit();
moduleTimeInit();
moduleComponentInit(); moduleComponentInit();
moduleComponentListInit();
moduleEntityInit(); moduleEntityInit();
moduleSceneInit(); moduleSceneInit();
} }
@@ -21,5 +29,9 @@ void moduleListInit(void) {
void moduleListDispose(void) { void moduleListDispose(void) {
moduleSceneDispose(); moduleSceneDispose();
moduleEntityDispose(); moduleEntityDispose();
moduleComponentListDispose();
moduleComponentDispose(); moduleComponentDispose();
moduleTimeDispose();
moduleMeshDispose();
moduleRequireDispose();
} }
+3 -2
View File
@@ -8,8 +8,9 @@
#pragma once #pragma once
/** /**
* Registers every script module (Component, Entity, Scene, the platform * Registers every script module (Component + its typed per-type
* globals). Called once by scriptManagerInit(). * wrappers, Entity, Scene, require(), Mesh, Time, the platform globals).
* Called once by scriptManagerInit().
*/ */
void moduleListInit(void); void moduleListInit(void);
@@ -0,0 +1,9 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
modulerequire.c
)
@@ -0,0 +1,271 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "modulerequire.h"
#include "script/module/modulebase.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#define MODULE_REQUIRE_WRAP_PREFIX "(function(module){\n"
#define MODULE_REQUIRE_WRAP_SUFFIX "\n})"
modulerequire_t MODULE_REQUIRE;
moduleBaseFunction(moduleRequireFn) {
moduleBaseRequireArgs(1); moduleBaseRequireString(0);
char_t requested[ASSET_FILE_NAME_MAX];
moduleBaseToString(args[0], requested, sizeof(requested));
char_t resolved[ASSET_FILE_NAME_MAX];
moduleRequireResolvePath(requested, resolved, sizeof(resolved));
jerry_value_t cachedModule;
if(moduleRequireCacheGet(resolved, &cachedModule)) {
return moduleBaseGetProp(cachedModule, "exports");
}
assetfile_t file;
errorret_t err = assetFileInit(&file, resolved, NULL, NULL);
if(errorIsNotOk(err)) return moduleBaseThrowError(err);
uint8_t *buffer = NULL;
size_t size = 0;
err = assetFileReadEntire(&file, &buffer, &size);
if(errorIsNotOk(err)) return moduleBaseThrowError(err);
err = assetFileDispose(&file);
if(errorIsNotOk(err)) {
memoryFree(buffer);
return moduleBaseThrowError(err);
}
// Wrap the source in a function so `module` is a real parameter, not a
// global - each required file's scope stays isolated from the others.
const size_t prefixLen = sizeof(MODULE_REQUIRE_WRAP_PREFIX) - 1;
const size_t suffixLen = sizeof(MODULE_REQUIRE_WRAP_SUFFIX) - 1;
const size_t wrappedLen = prefixLen + size + suffixLen;
char_t *wrapped = (char_t *)memoryAllocate(wrappedLen + 1);
memoryCopy(wrapped, MODULE_REQUIRE_WRAP_PREFIX, prefixLen);
memoryCopy(wrapped + prefixLen, buffer, size);
memoryCopy(wrapped + prefixLen + size, MODULE_REQUIRE_WRAP_SUFFIX, suffixLen);
wrapped[wrappedLen] = '\0';
memoryFree(buffer);
jerry_value_t fnValue = jerry_eval(
(const jerry_char_t *)wrapped, wrappedLen, JERRY_PARSE_NO_OPTS
);
memoryFree(wrapped);
if(jerry_value_is_exception(fnValue)) return fnValue;
// Cache before calling so a circular require() sees this module object
// (with whatever `module.exports` has been set so far) instead of
// recursing forever.
jerry_value_t moduleObj = jerry_object();
moduleRequireCacheSet(resolved, moduleObj);
char_t dir[ASSET_FILE_NAME_MAX];
moduleRequireDirname(resolved, dir, sizeof(dir));
moduleRequireDirPush(dir);
jerry_value_t callArgs[1] = { moduleObj };
jerry_value_t callResult = jerry_call(fnValue, jerry_undefined(), callArgs, 1);
jerry_value_free(fnValue);
moduleRequireDirPop();
if(jerry_value_is_exception(callResult)) {
moduleRequireCacheDelete(resolved);
jerry_value_free(moduleObj);
return callResult;
}
jerry_value_free(callResult);
return moduleBaseGetProp(moduleObj, "exports");
}
void moduleRequireInit(void) {
memoryZero(&MODULE_REQUIRE, sizeof(modulerequire_t));
moduleBaseDefineGlobalMethod("require", moduleRequireFn);
}
void moduleRequireDispose(void) {
for(uint8_t i = 0; i < MODULE_REQUIRE.cacheCount; i++) {
jerry_value_free(MODULE_REQUIRE.cache[i].moduleObj);
}
MODULE_REQUIRE.cacheCount = 0;
MODULE_REQUIRE.dirStackCount = 0;
}
void moduleRequireResolvePath(
const char_t *requested,
char_t *outResolved,
const size_t outResolvedSize
) {
assertNotNull(requested, "Requested path cannot be NULL");
assertNotNull(outResolved, "Output buffer cannot be NULL");
bool_t isRelative = (
(requested[0] == '.' && requested[1] == '/') ||
(requested[0] == '.' && requested[1] == '.' && requested[2] == '/')
);
char_t combined[MODULE_REQUIRE_PATH_MAX];
if(isRelative) {
stringFormat(
combined, sizeof(combined) - 1, "%s%s",
moduleRequireDirStackTop(), requested
);
} else {
stringCopy(combined, requested, sizeof(combined) - 1);
}
moduleRequireNormalize(combined, outResolved, outResolvedSize);
}
void moduleRequireNormalize(
const char_t *path,
char_t *outNormalized,
const size_t outSize
) {
assertNotNull(path, "Path cannot be NULL");
assertNotNull(outNormalized, "Output buffer cannot be NULL");
char_t scratch[MODULE_REQUIRE_PATH_MAX];
stringCopy(scratch, path, sizeof(scratch) - 1);
char_t *segments[MODULE_REQUIRE_SEGMENT_MAX];
uint8_t segmentCount = 0;
char_t *cursor = scratch;
bool_t more = true;
while(more) {
char_t *segment = cursor;
while(*cursor != '/' && *cursor != '\0') cursor++;
more = (*cursor == '/');
if(more) { *cursor = '\0'; cursor++; }
if(segment[0] == '\0' || stringEquals(segment, ".")) {
// Skip empty (e.g. leading/double slash) and "." segments.
} else if(stringEquals(segment, "..")) {
if(segmentCount > 0) segmentCount--;
} else {
assertTrue(
segmentCount < MODULE_REQUIRE_SEGMENT_MAX,
"require() path has too many segments"
);
segments[segmentCount++] = segment;
}
}
size_t written = 0;
outNormalized[0] = '\0';
for(uint8_t i = 0; i < segmentCount; i++) {
if(i > 0) {
assertTrue(written + 1 < outSize, "require() path too long");
outNormalized[written++] = '/';
}
size_t segLen = 0;
while(segments[i][segLen] != '\0') segLen++;
assertTrue(written + segLen < outSize, "require() path too long");
memoryCopy(outNormalized + written, segments[i], segLen);
written += segLen;
}
outNormalized[written] = '\0';
}
void moduleRequireDirname(
const char_t *path,
char_t *outDir,
const size_t outDirSize
) {
assertNotNull(path, "Path cannot be NULL");
assertNotNull(outDir, "Output buffer cannot be NULL");
char_t *lastSlash = stringFindLastChar(path, '/');
if(lastSlash == NULL) {
outDir[0] = '\0';
return;
}
size_t len = (size_t)(lastSlash - path) + 1;
assertTrue(len < outDirSize, "Directory path too long");
memoryCopy(outDir, path, len);
outDir[len] = '\0';
}
bool_t moduleRequireCacheGet(
const char_t *resolvedPath,
jerry_value_t *outModuleObj
) {
assertNotNull(resolvedPath, "Resolved path cannot be NULL");
assertNotNull(outModuleObj, "Output module object cannot be NULL");
for(uint8_t i = 0; i < MODULE_REQUIRE.cacheCount; i++) {
if(stringEquals(MODULE_REQUIRE.cache[i].path, resolvedPath)) {
*outModuleObj = MODULE_REQUIRE.cache[i].moduleObj;
return true;
}
}
return false;
}
void moduleRequireCacheSet(
const char_t *resolvedPath,
const jerry_value_t moduleObj
) {
assertNotNull(resolvedPath, "Resolved path cannot be NULL");
assertTrue(
MODULE_REQUIRE.cacheCount < MODULE_REQUIRE_CACHE_MAX,
"require() module cache is full"
);
modulerequirecacheentry_t *entry =
&MODULE_REQUIRE.cache[MODULE_REQUIRE.cacheCount++];
stringCopy(entry->path, resolvedPath, ASSET_FILE_NAME_MAX - 1);
entry->moduleObj = moduleObj;
}
void moduleRequireCacheDelete(const char_t *resolvedPath) {
assertNotNull(resolvedPath, "Resolved path cannot be NULL");
for(uint8_t i = 0; i < MODULE_REQUIRE.cacheCount; i++) {
if(!stringEquals(MODULE_REQUIRE.cache[i].path, resolvedPath)) continue;
MODULE_REQUIRE.cacheCount--;
MODULE_REQUIRE.cache[i] = MODULE_REQUIRE.cache[MODULE_REQUIRE.cacheCount];
return;
}
}
void moduleRequireDirPush(const char_t *dir) {
assertNotNull(dir, "Directory cannot be NULL");
assertTrue(
MODULE_REQUIRE.dirStackCount < MODULE_REQUIRE_STACK_MAX,
"require() nesting too deep"
);
stringCopy(
MODULE_REQUIRE.dirStack[MODULE_REQUIRE.dirStackCount++],
dir, ASSET_FILE_NAME_MAX - 1
);
}
void moduleRequireDirPop(void) {
assertTrue(
MODULE_REQUIRE.dirStackCount > 0, "require() dir stack underflow"
);
MODULE_REQUIRE.dirStackCount--;
}
const char_t * moduleRequireDirStackTop(void) {
if(MODULE_REQUIRE.dirStackCount == 0) return "";
return MODULE_REQUIRE.dirStack[MODULE_REQUIRE.dirStackCount - 1];
}
@@ -0,0 +1,150 @@
/**
* 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/assetfile.h"
#include <jerryscript.h>
/** Max number of distinct files require() will keep resolved/cached. */
#define MODULE_REQUIRE_CACHE_MAX 32
/** Max require() nesting depth (a file requiring a file requiring...). */
#define MODULE_REQUIRE_STACK_MAX 16
/** Max path segments handled while resolving a single require() call. */
#define MODULE_REQUIRE_SEGMENT_MAX 16
/** Scratch buffer size used while joining a base dir with a request. */
#define MODULE_REQUIRE_PATH_MAX (ASSET_FILE_NAME_MAX * 2)
typedef struct {
char_t path[ASSET_FILE_NAME_MAX];
jerry_value_t moduleObj;
} modulerequirecacheentry_t;
typedef struct {
modulerequirecacheentry_t cache[MODULE_REQUIRE_CACHE_MAX];
uint8_t cacheCount;
/** Directories of files currently mid-require(), innermost last. */
char_t dirStack[MODULE_REQUIRE_STACK_MAX][ASSET_FILE_NAME_MAX];
uint8_t dirStackCount;
} modulerequire_t;
extern modulerequire_t MODULE_REQUIRE;
/**
* Registers the global require() function.
*/
void moduleRequireInit(void);
/**
* Frees every cached module object. Called once by moduleListDispose().
*/
void moduleRequireDispose(void);
/**
* Resolves a require() argument to an asset-root-relative path. Paths
* starting with "./" or "../" are resolved against the directory of the
* file currently being required (or the asset root if require() is not
* currently nested); any other path is treated as already asset-root-
* relative. "." and ".." segments are collapsed either way.
*
* @param requested The raw string passed to require().
* @param outResolved Output buffer for the resolved path.
* @param outResolvedSize Capacity of outResolved, including the null
* terminator.
*/
void moduleRequireResolvePath(
const char_t *requested,
char_t *outResolved,
const size_t outResolvedSize
);
/**
* Collapses "." and ".." segments out of a slash-separated path.
*
* @param path The path to normalize.
* @param outNormalized Output buffer for the normalized path.
* @param outSize Capacity of outNormalized, including the null
* terminator.
*/
void moduleRequireNormalize(
const char_t *path,
char_t *outNormalized,
const size_t outSize
);
/**
* Extracts the directory portion (including trailing slash) of a
* resolved path, or an empty string if the path has no directory.
*
* @param path The resolved path.
* @param outDir Output buffer for the directory.
* @param outDirSize Capacity of outDir, including the null terminator.
*/
void moduleRequireDirname(
const char_t *path,
char_t *outDir,
const size_t outDirSize
);
/**
* Looks up a previously require()'d module by its resolved path.
*
* @param resolvedPath The resolved path to look up.
* @param outModuleObj Receives the cached module object if found.
* @return true if a cache entry was found.
*/
bool_t moduleRequireCacheGet(
const char_t *resolvedPath,
jerry_value_t *outModuleObj
);
/**
* Registers a module object under a resolved path. Takes ownership of
* the passed value - it is only freed by moduleRequireDispose() or
* moduleRequireCacheDelete().
*
* @param resolvedPath The resolved path to cache under.
* @param moduleObj The module object to cache.
*/
void moduleRequireCacheSet(
const char_t *resolvedPath,
const jerry_value_t moduleObj
);
/**
* Evicts and frees a cache entry. Used to roll back a failed load so a
* later require() of the same path can retry instead of being stuck
* with a broken cache entry.
*
* @param resolvedPath The resolved path to evict.
*/
void moduleRequireCacheDelete(const char_t *resolvedPath);
/**
* Pushes a directory onto the require() nesting stack, becoming the new
* base for resolving relative requires until popped.
*
* @param dir The directory (including trailing slash, or empty) to push.
*/
void moduleRequireDirPush(const char_t *dir);
/**
* Pops the require() nesting stack.
*/
void moduleRequireDirPop(void);
/**
* Gets the current top of the require() nesting stack.
*
* @return The current base directory, or an empty string if the stack is
* empty (i.e. require() is not currently nested).
*/
const char_t * moduleRequireDirStackTop(void);
@@ -0,0 +1,9 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
moduletime.c
)
+29
View File
@@ -0,0 +1,29 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "moduletime.h"
#include "script/module/modulebase.h"
#include "time/time.h"
moduleBaseFunction(moduleTimeGetDelta) {
return jerry_number(TIME.delta);
}
moduleBaseFunction(moduleTimeGetTime) {
return jerry_number(TIME.time);
}
void moduleTimeInit(void) {
jerry_value_t timeObj = jerry_object();
moduleBaseDefineProperty(timeObj, "delta", moduleTimeGetDelta, NULL);
moduleBaseDefineProperty(timeObj, "time", moduleTimeGetTime, NULL);
moduleBaseSetValue("Time", timeObj);
jerry_value_free(timeObj);
}
void moduleTimeDispose(void) {
}
+20
View File
@@ -0,0 +1,20 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
/**
* Registers the global `Time` object, exposing `Time.delta` and
* `Time.time` as live getters over the engine's TIME state (see
* time/time.h). Values always reflect the most recent timeUpdate().
*/
void moduleTimeInit(void);
/**
* Disposes the time module's script resources.
*/
void moduleTimeDispose(void);
+2 -4
View File
@@ -6,12 +6,10 @@
*/ */
#include "game/game.h" #include "game/game.h"
#include "scene/scene.h" #include "script/scriptmanager.h"
#include "scene/overworldscene.h"
errorret_t gameInit(void) { errorret_t gameInit(void) {
sceneid_t testSceneId = overworldSceneCreate(); errorChain(scriptManagerExecFile("scripts/overworldscene.js", NULL));
sceneSetActive(testSceneId);
errorOk(); errorOk();
} }
-5
View File
@@ -3,9 +3,4 @@
# 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
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
overworldscene.c
)
add_subdirectory(prefab) add_subdirectory(prefab)
-138
View File
@@ -1,138 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "overworldscene.h"
#include "scene/scene.h"
#include "entity/entitymanager.h"
#include "entity/component/display/entityposition.h"
#include "entity/component/display/entitycamera.h"
#include "entity/component/display/entityrenderable.h"
#include "entity/component/physics/entityphysics.h"
#include "display/mesh/plane.h"
#include "display/mesh/capsule.h"
#include "display/color.h"
#include "time/time.h"
// Radius must stay well outside the floor's footprint (a 20x20 plane has a
// corner-to-center distance of 10*sqrt(2) =~ 14.1) -- orbiting inside that
// puts parts of the floor's own geometry near/behind the camera's view
// direction, which the PSP's legacy GU pipeline can't clip properly and
// drops the whole triangle instead of clipping it.
#define OVERWORLD_SCENE_CAMERA_ORBIT_RADIUS 18.0f
#define OVERWORLD_SCENE_CAMERA_ORBIT_HEIGHT 10.0f
#define OVERWORLD_SCENE_CAMERA_ORBIT_SPEED 0.5f
static overworldcameraorbit_t OVERWORLD_SCENE_CAMERA_ORBIT;
sceneid_t overworldSceneCreate(void) {
sceneid_t sceneId = sceneCreate();
entitymanager_t *mgr = sceneGetEntities(sceneId);
// Camera, orbiting the origin (see overworldSceneCameraOrbitUpdate).
entityid_t camEntity = entityManagerAdd(mgr);
componentid_t camPosition = entityAddComponent(
mgr, camEntity, COMPONENT_TYPE_POSITION
);
entityAddComponent(mgr, camEntity, COMPONENT_TYPE_CAMERA);
OVERWORLD_SCENE_CAMERA_ORBIT = (overworldcameraorbit_t){
.angle = 0.0f,
.radius = OVERWORLD_SCENE_CAMERA_ORBIT_RADIUS,
.height = OVERWORLD_SCENE_CAMERA_ORBIT_HEIGHT,
.speed = OVERWORLD_SCENE_CAMERA_ORBIT_SPEED
};
entityUpdateAdd(
mgr, camEntity, overworldSceneCameraOrbitUpdate, camPosition,
&OVERWORLD_SCENE_CAMERA_ORBIT
);
// Position it correctly for the very first rendered frame, rather than
// waiting for the first fixed tick to run.
overworldSceneCameraOrbitUpdate(
mgr, camEntity, camPosition, &OVERWORLD_SCENE_CAMERA_ORBIT
);
// Static ground plane. Physics ignores the entity's position component --
// the shape's own normal/distance fully define the plane in world space.
entityid_t planeEntity = entityManagerAdd(mgr);
componentid_t planePosition = entityAddComponent(
mgr, planeEntity, COMPONENT_TYPE_POSITION
);
entityPositionSetLocalPosition(
mgr, planeEntity, planePosition, (vec3){ -10.0f, 0.0f, -10.0f }
);
entityPositionSetLocalScale(
mgr, planeEntity, planePosition, (vec3){ 20.0f, 1.0f, 20.0f }
);
componentid_t planePhysics = entityAddComponent(
mgr, planeEntity, COMPONENT_TYPE_PHYSICS
);
entityPhysicsSetBodyType(
mgr, planeEntity, planePhysics, PHYSICS_BODY_STATIC
);
entityPhysicsSetShape(mgr, planeEntity, planePhysics, (physicsshape_t){
.type = PHYSICS_SHAPE_PLANE,
.data.plane = { .normal = { 0.0f, 1.0f, 0.0f }, .distance = 0.0f }
});
componentid_t planeRenderable = entityAddComponent(
mgr, planeEntity, COMPONENT_TYPE_RENDERABLE
);
entityrenderable_t *planeR = componentGetData(
mgr, planeEntity, planeRenderable, COMPONENT_TYPE_RENDERABLE
);
planeR->data.material.meshes[0] = &PLANE_MESH_SIMPLE;
entityRenderableSetColor(mgr, planeEntity, planeRenderable, COLOR_GRAY);
// Player: dynamic capsule body, moved relative to the camera by
// COMPONENT_TYPE_PLAYER's own update callback (see entityplayer.c).
entityid_t playerEntity = entityManagerAdd(mgr);
componentid_t playerPosition = entityAddComponent(
mgr, playerEntity, COMPONENT_TYPE_POSITION
);
entityPositionSetLocalPosition(
mgr, playerEntity, playerPosition, (vec3){ 0.0f, 2.0f, 0.0f }
);
componentid_t playerPhysics = entityAddComponent(
mgr, playerEntity, COMPONENT_TYPE_PHYSICS
);
entityPhysicsSetShape(mgr, playerEntity, playerPhysics, (physicsshape_t){
.type = PHYSICS_SHAPE_CAPSULE,
.data.capsule = { .radius = 0.5f, .halfHeight = 0.5f }
});
componentid_t playerRenderable = entityAddComponent(
mgr, playerEntity, COMPONENT_TYPE_RENDERABLE
);
entityrenderable_t *playerR = componentGetData(
mgr, playerEntity, playerRenderable, COMPONENT_TYPE_RENDERABLE
);
playerR->data.material.meshes[0] = &CAPSULE_MESH_SIMPLE;
entityRenderableSetColor(mgr, playerEntity, playerRenderable, COLOR_BLUE);
entityAddComponent(mgr, playerEntity, COMPONENT_TYPE_PLAYER);
return sceneId;
}
void overworldSceneCameraOrbitUpdate(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
void *user
) {
overworldcameraorbit_t *orbit = (overworldcameraorbit_t *)user;
orbit->angle += TIME.delta * orbit->speed;
vec3 eye = {
cosf(orbit->angle) * orbit->radius,
orbit->height,
sinf(orbit->angle) * orbit->radius
};
entityPositionLookAt(
mgr, entityId, componentId,
eye,
(vec3){ 0.0f, 0.0f, 0.0f },
(vec3){ 0.0f, 1.0f, 0.0f }
);
}
-45
View File
@@ -1,45 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "scene/scenebase.h"
#include "entity/entitybase.h"
typedef struct {
float_t angle;
float_t radius;
float_t height;
float_t speed;
} overworldcameraorbit_t;
/**
* Creates the overworld scene: a static ground plane, a player entity
* (dynamic capsule body, moved relative to the camera by
* COMPONENT_TYPE_PLAYER's own update callback), and a camera orbiting the
* origin (see overworldSceneCameraOrbitUpdate). Does not make the scene
* active -- call sceneSetActive with the returned ID.
*
* @return The ID of the newly created scene.
*/
sceneid_t overworldSceneCreate(void);
/**
* Per-tick update for the test camera: orbits it around the world origin
* at a fixed radius/height/speed, always looking back at the origin.
* Registered automatically by overworldSceneCreate.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The camera entity ID.
* @param componentId The camera's position component ID.
* @param user Pointer to this camera's overworldcameraorbit_t state.
*/
void overworldSceneCameraOrbitUpdate(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
void *user
);
+1
View File
@@ -14,5 +14,6 @@ add_subdirectory(entity)
add_subdirectory(physics) add_subdirectory(physics)
add_subdirectory(scene) add_subdirectory(scene)
# add_subdirectory(item) # add_subdirectory(item)
add_subdirectory(script)
add_subdirectory(time) add_subdirectory(time)
add_subdirectory(util) add_subdirectory(util)
+13
View File
@@ -0,0 +1,13 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(dusktest)
dusktest(test_modulerequire.c)
dusktest(test_overworldscene.c)
target_compile_definitions(test_overworldscene PRIVATE
DUSK_ASSETS_DIR="${DUSK_ASSETS_DIR}"
)
+228
View File
@@ -0,0 +1,228 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "asset/asset.h"
#include "script/scriptmanager.h"
#include "script/module/modulebase.h"
#include "util/memory.h"
#include <zip.h>
#include <string.h>
#include <stdlib.h>
// ============================================================
// In-memory ZIP fixture + script manager lifecycle
// ============================================================
static zip_t *g_zip = NULL;
static int require_setup(void **state) {
zip_error_t err;
zip_error_init(&err);
zip_source_t *write_src = zip_source_buffer_create(NULL, 0, 1, &err);
if(!write_src) return -1;
zip_t *za = zip_open_from_source(write_src, ZIP_TRUNCATE, &err);
if(!za) { zip_source_free(write_src); return -1; }
#define ADD_FILE(name, content) do { \
zip_source_t *s = zip_source_buffer(za, content, strlen(content), 0); \
if(zip_file_add(za, name, s, ZIP_FL_OVERWRITE) < 0) { \
zip_close(za); return -1; \
} \
} while(0)
ADD_FILE(
"lib.js",
"module.exports = { test: function() { return 'hello world'; } };"
);
ADD_FILE(
"counted.js",
"GLOBAL_COUNTER = "
"(typeof GLOBAL_COUNTER === 'undefined' ? 0 : GLOBAL_COUNTER) + 1;\n"
"module.exports = { count: GLOBAL_COUNTER };"
);
ADD_FILE(
"dir/parent.js",
"var child = require('./child.js');\n"
"module.exports = { childValue: child.value };"
);
ADD_FILE("dir/child.js", "module.exports = { value: 42 };");
ADD_FILE(
"circular-a.js",
"var b = require('./circular-b.js');\n"
"module.exports = "
"{ name: 'a', bName: b.name, bSawUndefinedA: b.aWasUndefined };"
);
ADD_FILE(
"circular-b.js",
"var a = require('./circular-a.js');\n"
"module.exports = "
"{ name: 'b', aWasUndefined: (typeof a === 'undefined') };"
);
#undef ADD_FILE
zip_source_keep(write_src);
if(zip_close(za) != 0) { zip_source_free(write_src); return -1; }
zip_stat_t zs;
memset(&zs, 0, sizeof(zs));
if(zip_source_stat(write_src, &zs) != 0 || !(zs.valid & ZIP_STAT_SIZE)) {
zip_source_free(write_src); return -1;
}
void *zipbuf = malloc((size_t)zs.size);
if(!zipbuf) { zip_source_free(write_src); return -1; }
if(zip_source_open(write_src) != 0) {
free(zipbuf); zip_source_free(write_src); return -1;
}
zip_source_read(write_src, zipbuf, (zip_uint64_t)zs.size);
zip_source_close(write_src);
zip_source_free(write_src);
zip_error_init(&err);
zip_source_t *read_src = zip_source_buffer_create(
zipbuf, (zip_uint64_t)zs.size, 1, &err
);
if(!read_src) { free(zipbuf); return -1; }
g_zip = zip_open_from_source(read_src, 0, &err);
if(!g_zip) { zip_source_free(read_src); return -1; }
ASSET.zip = g_zip;
errorret_t ret = scriptManagerInit();
if(errorIsNotOk(ret)) { errorCatch(ret); return -1; }
return 0;
}
static int require_teardown(void **state) {
errorret_t ret = scriptManagerDispose();
if(errorIsNotOk(ret)) errorCatch(ret);
if(g_zip) { zip_close(g_zip); g_zip = NULL; }
ASSET.zip = NULL;
return 0;
}
// ============================================================
// Tests
// ============================================================
static void test_require_basic(void **state) {
jerry_value_t result;
errorret_t ret = scriptManagerExec(
"var something = require('./lib.js'); something.test();", &result
);
assert_true(errorIsOk(ret));
assert_true(jerry_value_is_string(result));
char_t buf[64];
moduleBaseToString(result, buf, sizeof(buf));
assert_string_equal(buf, "hello world");
jerry_value_free(result);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_require_caches_and_executes_once(void **state) {
jerry_value_t result;
errorret_t ret = scriptManagerExec(
"require('./counted.js'); require('./counted.js');"
"require('./counted.js').count;",
&result
);
assert_true(errorIsOk(ret));
assert_true(jerry_value_is_number(result));
assert_int_equal(moduleBaseValueInt(result), 1);
jerry_value_free(result);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_require_relative_path_resolves_to_requiring_files_dir(
void **state
) {
jerry_value_t result;
errorret_t ret = scriptManagerExec(
"require('dir/parent.js').childValue;", &result
);
assert_true(errorIsOk(ret));
assert_true(jerry_value_is_number(result));
assert_int_equal(moduleBaseValueInt(result), 42);
jerry_value_free(result);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_require_circular_does_not_hang(void **state) {
jerry_value_t result;
errorret_t ret = scriptManagerExec("require('./circular-a.js');", &result);
assert_true(errorIsOk(ret));
assert_true(jerry_value_is_object(result));
jerry_value_t nameVal = moduleBaseGetProp(result, "name");
char_t nameBuf[16];
moduleBaseToString(nameVal, nameBuf, sizeof(nameBuf));
assert_string_equal(nameBuf, "a");
jerry_value_free(nameVal);
jerry_value_t bNameVal = moduleBaseGetProp(result, "bName");
char_t bNameBuf[16];
moduleBaseToString(bNameVal, bNameBuf, sizeof(bNameBuf));
assert_string_equal(bNameBuf, "b");
jerry_value_free(bNameVal);
// circular-b.js's require('./circular-a.js') ran while circular-a.js was
// still mid-execution (before it set module.exports), so it must have
// seen the in-progress module's exports as still undefined.
jerry_value_t sawVal = moduleBaseGetProp(result, "bSawUndefinedA");
assert_true(jerry_value_is_true(sawVal));
jerry_value_free(sawVal);
jerry_value_free(result);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_require_missing_file_throws(void **state) {
errorret_t ret = scriptManagerExec("require('./does-not-exist.js');", NULL);
assert_true(errorIsNotOk(ret));
errorCatch(ret);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
// ============================================================
// main
// ============================================================
int main(void) {
assertInit();
const struct CMUnitTest tests[] = {
cmocka_unit_test_setup_teardown(
test_require_basic, require_setup, require_teardown
),
cmocka_unit_test_setup_teardown(
test_require_caches_and_executes_once, require_setup, require_teardown
),
cmocka_unit_test_setup_teardown(
test_require_relative_path_resolves_to_requiring_files_dir,
require_setup, require_teardown
),
cmocka_unit_test_setup_teardown(
test_require_circular_does_not_hang, require_setup, require_teardown
),
cmocka_unit_test_setup_teardown(
test_require_missing_file_throws, require_setup, require_teardown
),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+243
View File
@@ -0,0 +1,243 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "util/memory.h"
#include "time/time.h"
#include "scene/scene.h"
#include "entity/entitymanager.h"
#include "entity/component.h"
#include "entity/component/display/entityposition.h"
#include "entity/component/physics/entityphysics.h"
#include "entity/component/display/entityrenderable.h"
#include "display/mesh/plane.h"
#include "display/mesh/capsule.h"
#include "script/scriptmanager.h"
#include <math.h>
#include <stdio.h>
#ifndef DUSK_ASSETS_DIR
#error "DUSK_ASSETS_DIR must be defined"
#endif
// Reads the real, shipped overworldscene.js from disk (not a copy embedded
// in this test) so this test actually verifies what ships.
static char_t *readScriptSource(void) {
char_t path[512];
snprintf(
path, sizeof(path), "%s/scripts/overworldscene.js", DUSK_ASSETS_DIR
);
FILE *f = fopen(path, "rb");
assert_non_null(f);
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
char_t *buf = (char_t *)memoryAllocate((size_t)size + 1);
size_t read = fread(buf, 1, (size_t)size, f);
fclose(f);
buf[read] = '\0';
return buf;
}
static int overworld_setup(void **state) {
sceneInit();
errorret_t ret = scriptManagerInit();
if(errorIsNotOk(ret)) { errorCatch(ret); return -1; }
return 0;
}
static int overworld_teardown(void **state) {
errorret_t ret = scriptManagerDispose();
if(errorIsNotOk(ret)) errorCatch(ret);
sceneDispose();
// JerryScript defers freeing native-wrapped handles (Entity/Scene/
// Position/Physics/Renderable instances) until GC/jerry_cleanup() runs,
// so the leak check can only be meaningful after scriptManagerDispose()
// has actually run above -- not inside the test body.
assert_int_equal(memoryGetAllocatedCount(), 0);
return 0;
}
static void test_overworldscene_builds_expected_entities(void **state) {
char_t *src = readScriptSource();
errorret_t ret = scriptManagerExec(src, NULL);
memoryFree(src);
assert_true(errorIsOk(ret));
sceneid_t sceneId = sceneGetActive();
assert_true(sceneId != SCENE_ID_INVALID);
entitymanager_t *mgr = sceneGetEntities(sceneId);
// Entity 0: camera. Position + Camera components, initial orbit
// position placed with angle=0 (Time.delta defaults to 0 with no
// timeInit()/timeUpdate() in this test), i.e. eye=(radius, height, 0).
entityid_t camEntity = 0;
componentid_t camPos = entityGetComponent(
mgr, camEntity, COMPONENT_TYPE_POSITION
);
assert_true(camPos != COMPONENT_ID_INVALID);
assert_true(
entityGetComponent(mgr, camEntity, COMPONENT_TYPE_CAMERA) !=
COMPONENT_ID_INVALID
);
vec3 camPosition;
entityPositionGetLocalPosition(mgr, camEntity, camPos, camPosition);
assert_float_equal(camPosition[0], 18.0f, 0.0001f);
assert_float_equal(camPosition[1], 10.0f, 0.0001f);
assert_float_equal(camPosition[2], 0.0f, 0.0001f);
// Entity 1: static ground plane.
entityid_t planeEntity = 1;
componentid_t planePos = entityGetComponent(
mgr, planeEntity, COMPONENT_TYPE_POSITION
);
assert_true(planePos != COMPONENT_ID_INVALID);
vec3 planePosition, planeScale;
entityPositionGetLocalPosition(mgr, planeEntity, planePos, planePosition);
entityPositionGetLocalScale(mgr, planeEntity, planePos, planeScale);
assert_float_equal(planePosition[0], -10.0f, 0.0001f);
assert_float_equal(planePosition[1], 0.0f, 0.0001f);
assert_float_equal(planePosition[2], -10.0f, 0.0001f);
assert_float_equal(planeScale[0], 20.0f, 0.0001f);
assert_float_equal(planeScale[1], 1.0f, 0.0001f);
assert_float_equal(planeScale[2], 20.0f, 0.0001f);
componentid_t planePhysics = entityGetComponent(
mgr, planeEntity, COMPONENT_TYPE_PHYSICS
);
assert_true(planePhysics != COMPONENT_ID_INVALID);
assert_int_equal(
entityPhysicsGetBodyType(mgr, planeEntity, planePhysics),
PHYSICS_BODY_STATIC
);
physicsshape_t planeShape = entityPhysicsGetShape(
mgr, planeEntity, planePhysics
);
assert_int_equal(planeShape.type, PHYSICS_SHAPE_PLANE);
assert_float_equal(planeShape.data.plane.normal[0], 0.0f, 0.0001f);
assert_float_equal(planeShape.data.plane.normal[1], 1.0f, 0.0001f);
assert_float_equal(planeShape.data.plane.normal[2], 0.0f, 0.0001f);
assert_float_equal(planeShape.data.plane.distance, 0.0f, 0.0001f);
componentid_t planeRenderable = entityGetComponent(
mgr, planeEntity, COMPONENT_TYPE_RENDERABLE
);
assert_true(planeRenderable != COMPONENT_ID_INVALID);
entityrenderable_t *planeR = componentGetData(
mgr, planeEntity, planeRenderable, COMPONENT_TYPE_RENDERABLE
);
assert_ptr_equal(planeR->data.material.meshes[0], &PLANE_MESH_SIMPLE);
assert_int_equal(planeR->data.material.material.unlit.color.r, 128);
assert_int_equal(planeR->data.material.material.unlit.color.g, 128);
assert_int_equal(planeR->data.material.material.unlit.color.b, 128);
assert_int_equal(planeR->data.material.material.unlit.color.a, 255);
// Entity 2: player. Dynamic capsule body (default body type), PLAYER
// component present.
entityid_t playerEntity = 2;
componentid_t playerPos = entityGetComponent(
mgr, playerEntity, COMPONENT_TYPE_POSITION
);
assert_true(playerPos != COMPONENT_ID_INVALID);
vec3 playerPosition;
entityPositionGetLocalPosition(mgr, playerEntity, playerPos, playerPosition);
assert_float_equal(playerPosition[0], 0.0f, 0.0001f);
assert_float_equal(playerPosition[1], 2.0f, 0.0001f);
assert_float_equal(playerPosition[2], 0.0f, 0.0001f);
componentid_t playerPhysics = entityGetComponent(
mgr, playerEntity, COMPONENT_TYPE_PHYSICS
);
assert_true(playerPhysics != COMPONENT_ID_INVALID);
assert_int_equal(
entityPhysicsGetBodyType(mgr, playerEntity, playerPhysics),
PHYSICS_BODY_DYNAMIC
);
physicsshape_t playerShape = entityPhysicsGetShape(
mgr, playerEntity, playerPhysics
);
assert_int_equal(playerShape.type, PHYSICS_SHAPE_CAPSULE);
assert_float_equal(playerShape.data.capsule.radius, 0.5f, 0.0001f);
assert_float_equal(playerShape.data.capsule.halfHeight, 0.5f, 0.0001f);
componentid_t playerRenderable = entityGetComponent(
mgr, playerEntity, COMPONENT_TYPE_RENDERABLE
);
assert_true(playerRenderable != COMPONENT_ID_INVALID);
entityrenderable_t *playerR = componentGetData(
mgr, playerEntity, playerRenderable, COMPONENT_TYPE_RENDERABLE
);
assert_ptr_equal(playerR->data.material.meshes[0], &CAPSULE_MESH_SIMPLE);
assert_int_equal(playerR->data.material.material.unlit.color.r, 0);
assert_int_equal(playerR->data.material.material.unlit.color.g, 0);
assert_int_equal(playerR->data.material.material.unlit.color.b, 255);
assert_int_equal(playerR->data.material.material.unlit.color.a, 255);
assert_true(
entityGetComponent(mgr, playerEntity, COMPONENT_TYPE_PLAYER) !=
COMPONENT_ID_INVALID
);
}
static void test_overworldscene_update_orbits_camera(void **state) {
char_t *src = readScriptSource();
errorret_t ret = scriptManagerExec(src, NULL);
memoryFree(src);
assert_true(errorIsOk(ret));
sceneid_t sceneId = sceneGetActive();
entitymanager_t *mgr = sceneGetEntities(sceneId);
entityid_t camEntity = 0;
componentid_t camPos = entityGetComponent(
mgr, camEntity, COMPONENT_TYPE_POSITION
);
// Drive one frame with a known delta and confirm the camera orbited by
// exactly angle = delta * speed (0.1 * 0.5 = 0.05 rad).
TIME.delta = 0.1f;
ret = scriptManagerCallGlobal("update");
assert_true(errorIsOk(ret));
vec3 camPosition;
entityPositionGetLocalPosition(mgr, camEntity, camPos, camPosition);
const float_t expectedAngle = 0.1f * 0.5f;
assert_float_equal(
camPosition[0], cosf(expectedAngle) * 18.0f, 0.0001f
);
assert_float_equal(camPosition[1], 10.0f, 0.0001f);
assert_float_equal(
camPosition[2], sinf(expectedAngle) * 18.0f, 0.0001f
);
TIME.delta = 0.0f;
}
int main(void) {
assertInit();
const struct CMUnitTest tests[] = {
cmocka_unit_test_setup_teardown(
test_overworldscene_builds_expected_entities,
overworld_setup, overworld_teardown
),
cmocka_unit_test_setup_teardown(
test_overworldscene_update_orbits_camera,
overworld_setup, overworld_teardown
),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
/**
* Built-in primitive meshes, usable with Renderable.setMesh(). Opaque
* engine-owned handles -- do not inspect or mutate.
*/
declare const MESH_CUBE: object;
declare const MESH_PLANE: object;
declare const MESH_SPHERE: object;
declare const MESH_CAPSULE: object;
declare const MESH_TRIPRISM: object;
declare const MESH_QUAD: object;
+7 -5
View File
@@ -4,11 +4,13 @@
// https://opensource.org/licenses/MIT // https://opensource.org/licenses/MIT
/** /**
* A component attached to an Entity. Generic across every component * A component attached to an Entity: the operations every component
* type for now -- there are no typed per-type properties yet (e.g. no * supports regardless of type. Entity.add()/Entity.getComponent() return
* `.position` on a POSITION component), just the operations every * a typed subclass for component types that have one -- see position.d.ts
* component supports regardless of type. Only ever obtained via * (POSITION), physics.d.ts (PHYSICS), and renderable.d.ts (RENDERABLE).
* Entity.add()/Entity.getComponent(), never constructed directly. * Every other component type (CAMERA, TRIGGER, ANIMATION, PLAYER,
* INTERACTABLE) still only returns this generic Component. Never
* constructed directly.
*/ */
declare class Component { declare class Component {
/** The componenttype_t this component was added as, e.g. POSITION. */ /** The componenttype_t this component was added as, e.g. POSITION. */
+54
View File
@@ -0,0 +1,54 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
/// <reference path="component.d.ts" />
/** Physics body type constants, usable with Physics.setBodyType(). */
declare const PHYSICS_BODY_STATIC: number;
declare const PHYSICS_BODY_DYNAMIC: number;
declare const PHYSICS_BODY_KINEMATIC: number;
/**
* Physics shape type constants, usable with Physics.setShape(). Argument
* shape per type:
* - PHYSICS_SHAPE_CUBE: (type, halfExtentX, halfExtentY, halfExtentZ)
* - PHYSICS_SHAPE_SPHERE: (type, radius)
* - PHYSICS_SHAPE_CAPSULE: (type, radius, halfHeight)
* - PHYSICS_SHAPE_PLANE: (type, normalX, normalY, normalZ, distance)
*/
declare const PHYSICS_SHAPE_CUBE: number;
declare const PHYSICS_SHAPE_SPHERE: number;
declare const PHYSICS_SHAPE_CAPSULE: number;
declare const PHYSICS_SHAPE_PLANE: number;
/**
* Typed wrapper for a PHYSICS component. Returned by Entity.add(PHYSICS)
* / Entity.getComponent(PHYSICS) instead of the generic Component.
*/
declare class Physics extends Component {
/** Sets the body type (see PHYSICS_BODY_* constants). */
setBodyType(type: number): void;
/** Gets the body type. */
getBodyType(): number;
/**
* Sets the collision shape. See PHYSICS_SHAPE_* constants for the
* expected trailing arguments per shape type.
*/
setShape(type: number, ...args: number[]): void;
/** Sets the body's velocity (not an impulse -- affected by mass/drag). */
setVelocity(x: number, y: number, z: number): void;
/** Gets the body's velocity. */
getVelocity(): { x: number; y: number; z: number };
/** Applies an immediate velocity change, unaffected by mass/drag. */
applyImpulse(x: number, y: number, z: number): void;
/** True if the body rested on a surface during the last physics step. */
isOnGround(): boolean;
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
/// <reference path="component.d.ts" />
/**
* Typed wrapper for a POSITION component. Returned by Entity.add(POSITION)
* / Entity.getComponent(POSITION) instead of the generic Component.
*/
declare class Position extends Component {
/** Sets the local position (not world position -- see entityposition.h). */
setLocalPosition(x: number, y: number, z: number): void;
/** Gets the cached local position. */
getLocalPosition(): { x: number; y: number; z: number };
/** Sets the local scale. */
setLocalScale(x: number, y: number, z: number): void;
/** Gets the cached local scale. */
getLocalScale(): { x: number; y: number; z: number };
/**
* Positions and orients the entity at (ex, ey, ez), facing
* (tx, ty, tz), with (ux, uy, uz) as up.
*/
lookAt(
ex: number, ey: number, ez: number,
tx: number, ty: number, tz: number,
ux: number, uy: number, uz: number
): void;
}
+26
View File
@@ -0,0 +1,26 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
/// <reference path="component.d.ts" />
/**
* Typed wrapper for a RENDERABLE component. Returned by
* Entity.add(RENDERABLE) / Entity.getComponent(RENDERABLE) instead of
* the generic Component. Only meaningful for the default shader-material
* renderable type.
*/
declare class Renderable extends Component {
/** Sets the unlit material color (each channel 0-255). */
setColor(r: number, g: number, b: number, a: number): void;
/**
* Sets one of the material's mesh slots. mesh must be one of the
* built-in MESH_* constants (see mesh.d.ts).
*/
setMesh(slot: number, mesh: object): void;
/** Sets the render priority. 0 = auto (derived from type/flags). */
setPriority(priority: number): void;
}
+6
View File
@@ -3,6 +3,12 @@
// 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
/// <reference path="require.d.ts" />
/// <reference path="entity/entity.d.ts" /> /// <reference path="entity/entity.d.ts" />
/// <reference path="entity/component/component.d.ts" /> /// <reference path="entity/component/component.d.ts" />
/// <reference path="entity/component/position.d.ts" />
/// <reference path="entity/component/physics.d.ts" />
/// <reference path="entity/component/renderable.d.ts" />
/// <reference path="scene/scene.d.ts" /> /// <reference path="scene/scene.d.ts" />
/// <reference path="display/mesh.d.ts" />
/// <reference path="time/time.d.ts" />
+20
View File
@@ -0,0 +1,20 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
/**
* Loads and executes another script file (relative to the asset root,
* or to the requiring file's own directory if path starts with "./" or
* "../"), returning its `module.exports`. Each resolved path is only
* executed once - subsequent require() calls for the same file return
* the cached exports.
*/
declare function require(path: string): any;
/**
* Only defined inside a file loaded via require() - not present in a
* top-level script executed directly. Set `module.exports` to whatever
* value require() of this file should return.
*/
declare const module: { exports: any };
+15
View File
@@ -0,0 +1,15 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
/**
* Live view over the engine's fixed-timestep clock (see time/time.h).
* `delta`/`time` always reflect the most recent timeUpdate() call.
*/
declare const Time: {
/** Fixed simulation timestep, in seconds (always DUSK_TIME_STEP). */
readonly delta: number;
/** Total elapsed fixed-simulation time, in seconds. */
readonly time: number;
};