diff --git a/assets/engine.js b/assets/engine.js new file mode 100644 index 00000000..174718c7 --- /dev/null +++ b/assets/engine.js @@ -0,0 +1,46 @@ +var Actions; +var camera, cameraPosition; +var cube, cubePosition, cubeRenderable, cubeMesh; + +// init() is called via scriptManagerCallGlobal(), which pumps the asset +// system + job queue until any promise it returns settles - so it's safe +// to await include() here even though this runs before the main loop. +async function init() { + Actions = await include("input.js"); + + camera = new Entity(); + cameraPosition = camera.add(POSITION); + camera.add(CAMERA); + cameraPosition.position = new Vec3(3, 3, -6); + cameraPosition.lookAt(new Vec3(0, 0, 0)); + + cube = new Entity(); + cubePosition = cube.add(POSITION); + cubeRenderable = cube.add(RENDERABLE); + cubeMesh = Mesh.createCube(); + cubeRenderable.mesh = cubeMesh; + cubeRenderable.color = Color.red(); +} + +// Runs every frame (including dynamic/interpolation frames) - use for +// smooth, purely presentational animation. +function update() { + cubePosition.rotation.y += TIME.delta * 1.5; + cubePosition.rotation.x += TIME.delta * 0.7; +} + +// Runs once per fixed timestep only - use for gameplay logic that should +// be deterministic and independent of display refresh rate. +function fixedUpdate() { + var move = 3.0 * TIME.delta; + if(Input.isDown(Actions.LEFT)) cubePosition.position.x -= move; + if(Input.isDown(Actions.RIGHT)) cubePosition.position.x += move; + if(Input.isDown(Actions.UP)) cubePosition.position.z += move; + if(Input.isDown(Actions.DOWN)) cubePosition.position.z -= move; + if(Input.pressed(Actions.ACCEPT)) cubePosition.position = new Vec3(0, 0, 0); +} + +function deinit() { + cube.dispose(); + camera.dispose(); +} diff --git a/assets/input.js b/assets/input.js new file mode 100644 index 00000000..865baf4a --- /dev/null +++ b/assets/input.js @@ -0,0 +1,27 @@ +// Binds physical buttons to abstract actions, then exports the action +// constants so other scripts don't need to know raw INPUT_ACTION_* names. +Input.bind("w", INPUT_ACTION_UP); +Input.bind("s", INPUT_ACTION_DOWN); +Input.bind("a", INPUT_ACTION_LEFT); +Input.bind("d", INPUT_ACTION_RIGHT); +Input.bind("space", INPUT_ACTION_ACCEPT); +Input.bind("escape", INPUT_ACTION_RAGEQUIT); + +if(typeof INPUT_GAMEPAD !== "undefined") { + Input.bind("gamepad_up", INPUT_ACTION_UP); + Input.bind("gamepad_down", INPUT_ACTION_DOWN); + Input.bind("gamepad_left", INPUT_ACTION_LEFT); + Input.bind("gamepad_right", INPUT_ACTION_RIGHT); + Input.bind("gamepad_a", INPUT_ACTION_ACCEPT); + Input.bind("gamepad_start", INPUT_ACTION_RAGEQUIT); +} + +module = { + UP: INPUT_ACTION_UP, + DOWN: INPUT_ACTION_DOWN, + LEFT: INPUT_ACTION_LEFT, + RIGHT: INPUT_ACTION_RIGHT, + ACCEPT: INPUT_ACTION_ACCEPT, + CANCEL: INPUT_ACTION_CANCEL, + RAGEQUIT: INPUT_ACTION_RAGEQUIT +}; diff --git a/assets/locale/en_US.po b/assets/locale/en_US.po new file mode 100644 index 00000000..776972d5 --- /dev/null +++ b/assets/locale/en_US.po @@ -0,0 +1,2 @@ +msgid "test.string" +msgstr "This is a test string" \ No newline at end of file diff --git a/cmake/modules/Findjerryscript.cmake b/cmake/modules/Findjerryscript.cmake new file mode 100644 index 00000000..b0722855 --- /dev/null +++ b/cmake/modules/Findjerryscript.cmake @@ -0,0 +1,96 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Turn things off we don't need +set(JERRY_CMDLINE OFF CACHE BOOL "" FORCE) +set(JERRY_EXT ON CACHE BOOL "" FORCE) +set(JERRY_DEBUGGER OFF CACHE BOOL "" FORCE) +set(JERRY_BUILTIN_DATE OFF CACHE BOOL "" FORCE) +set(ENABLE_LTO OFF CACHE BOOL "" FORCE) + +# Fetch Jerry +include(FetchContent) +FetchContent_Declare( + jerryscript + GIT_REPOSITORY https://git.wish.moe/YourWishes/jerryscript + GIT_TAG float32-fix +) +FetchContent_MakeAvailable(jerryscript) + +# Mark found +set(jerryscript_FOUND ON) + +# Define targets +if(TARGET jerryscript-core) + set(JERRY_CORE_TARGET jerryscript-core) +elseif(TARGET jerry-core) + set(JERRY_CORE_TARGET jerry-core) +endif() + +if(TARGET jerryscript-ext) + set(JERRY_EXT_TARGET jerryscript-ext) +elseif(TARGET jerry-ext) + set(JERRY_EXT_TARGET jerry-ext) +endif() + +if(TARGET jerryscript-port-default) + set(JERRY_PORT_TARGET jerryscript-port-default) +elseif(TARGET jerry-port-default) + set(JERRY_PORT_TARGET jerry-port-default) +elseif(TARGET jerryscript-port) + set(JERRY_PORT_TARGET jerryscript-port) +elseif(TARGET jerry-port) + set(JERRY_PORT_TARGET jerry-port) +endif() + +if(NOT JERRY_CORE_TARGET) + message(FATAL_ERROR "JerryScript core target not found") +endif() + +if(NOT JERRY_EXT_TARGET) + message(FATAL_ERROR "JerryScript ext target not found") +endif() + +if(NOT JERRY_PORT_TARGET) + message(FATAL_ERROR "JerryScript port target not found") +endif() + +foreach(tgt IN ITEMS + ${JERRY_CORE_TARGET} + ${JERRY_EXT_TARGET} + ${JERRY_PORT_TARGET} +) + if(TARGET ${tgt}) + set_property(TARGET ${tgt} PROPERTY INTERPROCEDURAL_OPTIMIZATION OFF) + target_compile_definitions(${JERRY_CORE_TARGET} PRIVATE + JERRY_NUMBER_TYPE_FLOAT64=0 + JERRY_BUILTIN_DATE=0 + ) + endif() +endforeach() + +# Export include dirs through the targets +target_include_directories(${JERRY_CORE_TARGET} INTERFACE + ${jerryscript_SOURCE_DIR}/jerry-core/include +) + +target_include_directories(${JERRY_EXT_TARGET} INTERFACE + ${jerryscript_SOURCE_DIR}/jerry-ext/include +) + +target_include_directories(${JERRY_PORT_TARGET} INTERFACE + ${jerryscript_SOURCE_DIR}/jerry-port/default/include +) + +# Suppress JerryScript-only warning +if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(${JERRY_CORE_TARGET} PRIVATE + -Wno-error + ) +endif() + +add_library(jerryscript::core ALIAS ${JERRY_CORE_TARGET}) +add_library(jerryscript::ext ALIAS ${JERRY_EXT_TARGET}) +add_library(jerryscript::port ALIAS ${JERRY_PORT_TARGET}) diff --git a/src/dusk/CMakeLists.txt b/src/dusk/CMakeLists.txt index e802c9fc..5b7df4b4 100644 --- a/src/dusk/CMakeLists.txt +++ b/src/dusk/CMakeLists.txt @@ -32,6 +32,15 @@ if(NOT yyjson_FOUND) endif() endif() +if(NOT jerryscript_FOUND) + find_package(jerryscript REQUIRED) + target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC + jerryscript::core + jerryscript::ext + jerryscript::port + ) +endif() + if(DUSK_BACKTRACE) target_link_options(${DUSK_LIBRARY_TARGET_NAME} PUBLIC -rdynamic) target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC @@ -57,12 +66,15 @@ add_subdirectory(assert) add_subdirectory(asset) add_subdirectory(console) add_subdirectory(display) +add_subdirectory(entity) add_subdirectory(log) add_subdirectory(engine) add_subdirectory(error) add_subdirectory(input) add_subdirectory(locale) +add_subdirectory(physics) add_subdirectory(scene) +add_subdirectory(script) add_subdirectory(system) add_subdirectory(time) add_subdirectory(ui) diff --git a/src/dusk/asset/asset.h b/src/dusk/asset/asset.h index ed09a854..16215d9e 100644 --- a/src/dusk/asset/asset.h +++ b/src/dusk/asset/asset.h @@ -23,7 +23,7 @@ #define ASSET_FILE_NAME "dusk.dsk" #define ASSET_HEADER_SIZE 3 -#define ASSET_LOADING_COUNT_MAX 10 +#define ASSET_LOADING_COUNT_MAX 16 #define ASSET_ENTRY_COUNT_MAX 64 typedef struct asset_s { diff --git a/src/dusk/asset/loader/CMakeLists.txt b/src/dusk/asset/loader/CMakeLists.txt index 44bb11b1..16d2cc18 100644 --- a/src/dusk/asset/loader/CMakeLists.txt +++ b/src/dusk/asset/loader/CMakeLists.txt @@ -15,4 +15,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} add_subdirectory(display) add_subdirectory(locale) add_subdirectory(json) -add_subdirectory(dmf) \ No newline at end of file +add_subdirectory(dmf) +add_subdirectory(script) \ No newline at end of file diff --git a/src/dusk/asset/loader/assetloader.c b/src/dusk/asset/loader/assetloader.c index 6ce9494b..71696cf4 100644 --- a/src/dusk/asset/loader/assetloader.c +++ b/src/dusk/asset/loader/assetloader.c @@ -45,4 +45,9 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = { .loadAsync = assetJsonLoaderAsync, .dispose = assetJsonDispose }, + + [ASSET_LOADER_TYPE_SCRIPT] = { + .loadSync = assetScriptLoaderSync, + .dispose = assetScriptDispose + }, }; diff --git a/src/dusk/asset/loader/assetloader.h b/src/dusk/asset/loader/assetloader.h index 5edae5ff..a58319b3 100644 --- a/src/dusk/asset/loader/assetloader.h +++ b/src/dusk/asset/loader/assetloader.h @@ -12,6 +12,7 @@ #include "asset/loader/display/assettilesetloader.h" #include "asset/loader/locale/assetlocaleloader.h" #include "asset/loader/json/assetjsonloader.h" +#include "asset/loader/script/assetscriptloader.h" typedef enum { ASSET_LOADER_TYPE_NULL, @@ -22,6 +23,7 @@ typedef enum { ASSET_LOADER_TYPE_TILESET, ASSET_LOADER_TYPE_LOCALE, ASSET_LOADER_TYPE_JSON, + ASSET_LOADER_TYPE_SCRIPT, ASSET_LOADER_TYPE_COUNT } assetloadertype_t; @@ -42,6 +44,7 @@ typedef union { assettilesetoutput_t tileset; assetlocaleoutput_t locale; assetjsonoutput_t json; + assetscriptoutput_t script; } assetloaderoutput_t; typedef union { diff --git a/src/dusk/asset/loader/script/CMakeLists.txt b/src/dusk/asset/loader/script/CMakeLists.txt new file mode 100644 index 00000000..76d9bd92 --- /dev/null +++ b/src/dusk/asset/loader/script/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + assetscriptloader.c +) diff --git a/src/dusk/asset/loader/script/assetscriptloader.c b/src/dusk/asset/loader/script/assetscriptloader.c new file mode 100644 index 00000000..a868ec14 --- /dev/null +++ b/src/dusk/asset/loader/script/assetscriptloader.c @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "assetscriptloader.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "asset/loader/assetloading.h" +#include "asset/loader/assetentry.h" + +static void assetScriptSettleWithError( + assetscriptoutput_t *output, + const char_t *message +) { + jerry_value_t errVal = jerry_string_sz(message); + jerry_value_t rejectResult = jerry_promise_reject(output->promise, errVal); + jerry_value_free(rejectResult); + jerry_value_free(errVal); +} + +errorret_t assetScriptLoaderSync(assetloading_t *loading) { + assertNotNull(loading, "Loading cannot be NULL"); + assertTrue(loading->type == ASSET_LOADER_TYPE_SCRIPT, "Invalid type."); + assertIsMainThread("Must be called from the main thread."); + + assetentry_t *entry = loading->entry; + assetscriptoutput_t *output = &entry->data.script; + assertTrue( + output->promise != 0, + "Script entry has no promise - was it requested via include()?" + ); + + assetfile_t file; + uint8_t *buffer = NULL; + size_t size = 0; + + errorret_t err = assetFileInit(&file, entry->name, NULL, NULL); + if(errorIsOk(err)) err = assetFileReadEntire(&file, &buffer, &size); + if(errorIsOk(err)) err = assetFileDispose(&file); + + if(errorIsNotOk(err)) { + assetScriptSettleWithError(output, err.state->message); + entry->state = ASSET_ENTRY_STATE_ERROR; + errorChain(err); + } + + char_t *src = (char_t *)memoryAllocate(size + 1); + memoryCopy(src, buffer, size); + src[size] = '\0'; + memoryFree(buffer); + + // Scripts export their public API by assigning to the global `module`. + // Swap it out around the eval so this doesn't clobber a caller's own + // in-flight include() of a different file. + jerry_value_t global = jerry_current_realm(); + jerry_value_t moduleKey = jerry_string_sz("module"); + jerry_value_t prevModule = jerry_object_get(global, moduleKey); + jerry_value_t undef = jerry_undefined(); + jerry_object_set(global, moduleKey, undef); + jerry_value_free(undef); + + jerry_value_t evalResult = jerry_eval( + (const jerry_char_t *)src, size, JERRY_PARSE_NO_OPTS + ); + memoryFree(src); + + if(jerry_value_is_exception(evalResult)) { + jerry_value_t errVal = jerry_exception_value(evalResult, false); + jerry_value_t rejectResult = jerry_promise_reject(output->promise, errVal); + jerry_value_free(rejectResult); + jerry_value_free(errVal); + jerry_value_free(evalResult); + + jerry_value_t moduleVal = jerry_object_get(global, moduleKey); + jerry_value_free(moduleVal); + jerry_object_set(global, moduleKey, prevModule); + jerry_value_free(prevModule); + jerry_value_free(moduleKey); + jerry_value_free(global); + + entry->state = ASSET_ENTRY_STATE_ERROR; + errorThrow("Script error in '%s'", entry->name); + } + jerry_value_free(evalResult); + + jerry_value_t moduleVal = jerry_object_get(global, moduleKey); + jerry_object_set(global, moduleKey, prevModule); + jerry_value_free(prevModule); + jerry_value_free(moduleKey); + jerry_value_free(global); + + jerry_value_t resolveResult = jerry_promise_resolve(output->promise, moduleVal); + jerry_value_free(resolveResult); + jerry_value_free(moduleVal); + + entry->state = ASSET_ENTRY_STATE_LOADED; + errorOk(); +} + +errorret_t assetScriptDispose(assetentry_t *entry) { + assertNotNull(entry, "Asset entry cannot be NULL"); + assertTrue(entry->type == ASSET_LOADER_TYPE_SCRIPT, "Invalid type."); + assertIsMainThread("Must be called from the main thread."); + + assetscriptoutput_t *output = &entry->data.script; + if(output->promise != 0) { + jerry_value_free(output->promise); + output->promise = 0; + } + + errorOk(); +} diff --git a/src/dusk/asset/loader/script/assetscriptloader.h b/src/dusk/asset/loader/script/assetscriptloader.h new file mode 100644 index 00000000..80c22225 --- /dev/null +++ b/src/dusk/asset/loader/script/assetscriptloader.h @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "asset/assetfile.h" +#include + +typedef struct assetloading_s assetloading_t; +typedef struct assetentry_s assetentry_t; + +/** + * Output data for a script asset entry. The promise is created once, the + * first time a script requests this file (see moduleIncludeInclude), and is + * owned by the entry for its lifetime - every subsequent request for the + * same file is handed a copy of this same promise instead of starting a + * second load. It is resolved (with the script's exported `module` value) + * or rejected (with the JS exception) exactly once, the moment the entry + * transitions to ASSET_ENTRY_STATE_LOADED or ASSET_ENTRY_STATE_ERROR. + */ +typedef struct { + jerry_value_t promise; +} assetscriptoutput_t; + +/** + * Loads and evaluates a script asset synchronously (reads the whole file + * from the archive, then runs it) and resolves/rejects the entry's promise + * with the outcome. Scripts are small and read instantly, so there is no + * async (background-thread) phase - this is the only loader function. + * + * @param loading The asset loading slot. + * @return An error if reading failed, or errorOk() (a script exception is + * reported via promise rejection, not a returned error). + */ +errorret_t assetScriptLoaderSync(assetloading_t *loading); + +/** + * Releases the promise reference held by a script asset entry. + * + * @param entry The asset entry to dispose. + */ +errorret_t assetScriptDispose(assetentry_t *entry); diff --git a/src/dusk/display/display.c b/src/dusk/display/display.c index 4e514306..614a9e9a 100644 --- a/src/dusk/display/display.c +++ b/src/dusk/display/display.c @@ -8,6 +8,7 @@ #include "display/display.h" #include "display/framebuffer/framebuffer.h" #include "scene/scene.h" +#include "entity/entityrender.h" #include "display/spritebatch/spritebatch.h" #include "display/mesh/quad.h" #include "display/mesh/cube.h" @@ -80,6 +81,7 @@ errorret_t displayUpdate(void) { ); errorChain(sceneRender()); + errorChain(entityRenderAll()); // Finish up screenUnbind(); diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index d89f0357..72f2e295 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -15,10 +15,23 @@ #include "asset/asset.h" #include "ui/ui.h" #include "assert/assert.h" +#include "entity/entitymanager.h" +#include "physics/physicsmanager.h" +#include "script/scriptmanager.h" #include "network/network.h" #include "system/system.h" #include "console/console.h" +double jerry_port_current_time(void) { + dusktimeepoch_t epoch = timeGetEpoch(); + return epoch.time * 1000.0; +} + +int32_t jerry_port_local_tza(double unix_ms) { + (void) unix_ms; + return 0; +} + engine_t ENGINE; errorret_t engineInit(const int32_t argc, const char_t **argv) { @@ -36,11 +49,17 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { errorChain(inputInit()); errorChain(assetInit()); errorChain(localeManagerInit()); + errorChain(scriptManagerInit()); errorChain(displayInit()); errorChain(uiInit()); + entityManagerInit(); + physicsManagerInit(); errorChain(networkInit()); errorChain(sceneInit()); + errorChain(scriptManagerExecFile("engine.js", NULL)); + errorChain(scriptManagerCallGlobal("init")); + consolePrint("Engine initialized"); #ifdef DUSK_ASSERTIONS_FAKED @@ -58,7 +77,22 @@ errorret_t engineUpdate(void) { timeUpdate(); inputUpdate(); consoleUpdate(); + physicsManagerUpdate(); + + // Fixed-step logic: runs once per DUSK_TIME_STEP tick. On platforms + // without dynamic timing every frame is a fixed step; on platforms with + // it, dynamic (interpolation) frames are skipped. + #ifdef DUSK_TIME_DYNAMIC + if(!TIME.dynamicUpdate) { + #endif + entityManagerFixedUpdate(); + errorChain(scriptManagerCallGlobal("fixedUpdate")); + #ifdef DUSK_TIME_DYNAMIC + } + #endif + errorChain(sceneUpdate()); + errorChain(scriptManagerCallGlobal("update")); errorChain(assetUpdate()); errorChain(uiUpdate()); @@ -73,13 +107,21 @@ void engineExit(void) { } errorret_t engineDispose(void) { + errorChain(scriptManagerCallGlobal("deinit")); errorChain(sceneDispose()); errorChain(networkDispose()); + entityManagerDispose(); localeManagerDispose(); errorChain(uiDispose()); consoleDispose(); errorChain(displayDispose()); + + // Must run before scriptManagerDispose(): asset entries (e.g. loaded + // scripts) hold jerry_value_t references (promises) that need to be + // released via their loader's dispose callback before jerry_cleanup() + // runs, which fatally asserts if anything is still held. errorChain(assetDispose()); + errorChain(scriptManagerDispose()); errorOk(); } diff --git a/src/dusk/entity/CMakeLists.txt b/src/dusk/entity/CMakeLists.txt new file mode 100644 index 00000000..439e4cf3 --- /dev/null +++ b/src/dusk/entity/CMakeLists.txt @@ -0,0 +1,16 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Sources +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + entity.c + entitymanager.c + component.c + entityrender.c +) + +# Subdirs +add_subdirectory(component) \ No newline at end of file diff --git a/src/dusk/entity/component.c b/src/dusk/entity/component.c new file mode 100644 index 00000000..935e5d56 --- /dev/null +++ b/src/dusk/entity/component.c @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "entitymanager.h" +#include "assert/assert.h" +#include "util/memory.h" + +componentdefinition_t COMPONENT_DEFINITIONS[] = { + [COMPONENT_TYPE_NULL] = { 0 }, + + #define X(enm, type, field, iMethod, dMethod, fMethod) \ + [COMPONENT_TYPE_##enm] = { \ + .enumName = #enm, \ + .name = #field, \ + .init = iMethod, \ + .dispose = dMethod, \ + .fixedUpdate = fMethod \ + }, + + #include "componentlist.h" + #undef X + + [COMPONENT_TYPE_COUNT] = { 0 } +}; + +void componentInit( + const entityid_t entityId, + const componentid_t componentId, + const componenttype_t type +) { + assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB"); + assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB"); + assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB"); + assertTrue(type != COMPONENT_TYPE_NULL, "Cannot initialize null component"); + + componentindex_t index = componentGetIndex(entityId, componentId); + component_t *cmp = &ENTITY_MANAGER.components[index]; + memoryZero(cmp, sizeof(component_t)); + + cmp->type = type; + if(COMPONENT_DEFINITIONS[type].init) { + COMPONENT_DEFINITIONS[type].init(entityId, componentId); + } +} + +void * componentGetData( + const entityid_t entityId, + const componentid_t componentId, + const componenttype_t type +) { + assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB"); + assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB"); + assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB"); + assertTrue(type != COMPONENT_TYPE_NULL, "Cannot get data of null component"); + + componentindex_t index = componentGetIndex(entityId, componentId); + component_t *cmp = &ENTITY_MANAGER.components[index]; + assertTrue(cmp->type == type, "Component type mismatch"); + + return &cmp->data; +} + +componentindex_t componentGetIndex( + const entityid_t entityId, + const componentid_t componentId +) { + assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB"); + assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB"); + return (entityId * ENTITY_COMPONENT_COUNT_MAX) + componentId; +} + +entityid_t componentGetEntitiesWithComponent( + const componenttype_t type, + entityid_t outEntities[ENTITY_COUNT_MAX], + componentid_t outComponents[ENTITY_COUNT_MAX] +) { + assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB"); + assertTrue(type != COMPONENT_TYPE_NULL, "Cannot check NULL type"); + assertNotNull(outEntities, "Output entities array cannot be null"); + assertNotNull(outComponents, "Output components array cannot be null"); + + entityid_t written = 0; + for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) { + componentid_t used = ENTITY_MANAGER.entitiesWithComponent[ + type * ENTITY_COUNT_MAX + i + ]; + if(used == COMPONENT_ID_INVALID) continue; + assertTrue( + ENTITY_MANAGER.components[componentGetIndex(i, used)].type == type, + "Component type mismatch in entitiesWithComponent lookup" + ); + assertTrue( + (ENTITY_MANAGER.entities[i].state & ENTITY_STATE_ACTIVE) != 0, + "Inactive entity in entitiesWithComponent lookup" + ); + assertTrue( + used < ENTITY_COMPONENT_COUNT_MAX, + "Component ID OOB in entitiesWithComponent lookup" + ); + assertTrue( + componentGetIndex(i,used) < ENTITY_COUNT_MAX*ENTITY_COMPONENT_COUNT_MAX, + "Component index OOB in entitiesWithComponent lookup" + ); + assertTrue( + ENTITY_MANAGER.components[componentGetIndex(i,used)].type == type, + "Component type mismatch in entitiesWithComponent lookup" + ); + outComponents[written] = used; + outEntities[written++] = i; + } + return written; +} + +void componentDispose( + const entityid_t entityId, + const componentid_t componentId +) { + assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB"); + assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB"); + + componentindex_t index = componentGetIndex(entityId, componentId); + component_t *cmp = &ENTITY_MANAGER.components[index]; + if(cmp->type == COMPONENT_TYPE_NULL) return; + + if(COMPONENT_DEFINITIONS[cmp->type].dispose) { + COMPONENT_DEFINITIONS[cmp->type].dispose(entityId, componentId); + } + + cmp->type = COMPONENT_TYPE_NULL; +} + +void componentFixedUpdateAll(void) { + for(entityid_t e = 0; e < ENTITY_COUNT_MAX; e++) { + if((ENTITY_MANAGER.entities[e].state & ENTITY_STATE_ACTIVE) == 0) continue; + + for(componentid_t c = 0; c < ENTITY_COMPONENT_COUNT_MAX; c++) { + componentindex_t index = componentGetIndex(e, c); + componenttype_t type = ENTITY_MANAGER.components[index].type; + if(type == COMPONENT_TYPE_NULL) continue; + if(COMPONENT_DEFINITIONS[type].fixedUpdate) { + COMPONENT_DEFINITIONS[type].fixedUpdate(e, c); + } + } + } +} \ No newline at end of file diff --git a/src/dusk/entity/component.h b/src/dusk/entity/component.h new file mode 100644 index 00000000..32f03f62 --- /dev/null +++ b/src/dusk/entity/component.h @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "entitybase.h" + +#define X(enumName, type, field, init, dispose, fixedUpdate) \ + // do nothing +#include "componentlist.h" +#undef X + +typedef union { + #define X(enumName, type, field, init, dispose, fixedUpdate) type field; + #include "componentlist.h" + #undef X +} componentdata_t; + +typedef struct { + const char_t *enumName; + const char_t *name; + void (*init)(const entityid_t, const componentid_t); + void (*dispose)(const entityid_t, const componentid_t); + void (*fixedUpdate)(const entityid_t, const componentid_t); +} componentdefinition_t; + +typedef enum { + COMPONENT_TYPE_NULL, + + #define X(enumName, type, field, init, dispose, fixedUpdate) \ + COMPONENT_TYPE_##enumName, + #include "componentlist.h" + #undef X + + COMPONENT_TYPE_COUNT +} componenttype_t; + +typedef struct { + componenttype_t type; + componentdata_t data; +} component_t; + +extern componentdefinition_t COMPONENT_DEFINITIONS[]; + +/** + * Initializes a component of the given type for the entity with component ID. + * + * @param entityId The entity ID. + * @param componentId The component ID. + * @param type The type of the component to initialize. + */ +void componentInit( + const entityid_t entityId, + const componentid_t componentId, + const componenttype_t type +); + +/** + * Gets the pointer to the data of a component for the entity with component ID. + * + * @param entityId The entity ID. + * @param componentId The component ID. + * @param type The type of the component to get, only used for assertion. + * @return A pointer to the component data. + */ +void * componentGetData( + const entityid_t entityId, + const componentid_t componentId, + const componenttype_t type +); + +/** + * Gets the index of a component for the entity with component ID. + * + * @param entityId The entity ID. + * @param componentId The component ID. + * @return The index of the component in the component array. + */ +componentindex_t componentGetIndex( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Gets the entity IDs of all entities with a component of the given type. + * + * @param type The type of the component to get entities for. + * @param outEntities An array to write the entity IDs to, must be at least + * ENTITY_COUNT_MAX in size. + * @param outComponents An array to write the component IDs to. + * @return The number of entity IDs written to outEntities. + */ +entityid_t componentGetEntitiesWithComponent( + const componenttype_t type, + entityid_t outEntities[ENTITY_COUNT_MAX], + componentid_t outComponents[ENTITY_COUNT_MAX] +); + +/** + * Disposes of a component for the entity with component ID. + * + * @param entityId The entity ID. + * @param componentId The component ID. + */ +void componentDispose( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Calls fixedUpdate on every active component that defines one. Intended to + * be called once per fixed timestep - see entityManagerFixedUpdate(). + */ +void componentFixedUpdateAll(void); \ No newline at end of file diff --git a/src/dusk/entity/component/CMakeLists.txt b/src/dusk/entity/component/CMakeLists.txt new file mode 100644 index 00000000..4558fc62 --- /dev/null +++ b/src/dusk/entity/component/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +add_subdirectory(display) +add_subdirectory(physics) +add_subdirectory(script) +add_subdirectory(trigger) \ No newline at end of file diff --git a/src/dusk/entity/component/display/CMakeLists.txt b/src/dusk/entity/component/display/CMakeLists.txt new file mode 100644 index 00000000..eeca751f --- /dev/null +++ b/src/dusk/entity/component/display/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Sources +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + entityposition.c + entitycamera.c + entityrenderable.c +) \ No newline at end of file diff --git a/src/dusk/entity/component/display/entitycamera.c b/src/dusk/entity/component/display/entitycamera.c new file mode 100644 index 00000000..50479512 --- /dev/null +++ b/src/dusk/entity/component/display/entitycamera.c @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "entity/entitymanager.h" +#include "entity/entity.h" +#include "entity/component/display/entityposition.h" +#include "display/framebuffer/framebuffer.h" +#include "display/screen/screen.h" + +void entityCameraInit(const entityid_t ent, const componentid_t comp) { + entitycamera_t *cam = (entitycamera_t *)componentGetData( + ent, comp, COMPONENT_TYPE_CAMERA + ); + cam->nearClip = 0.1f; + cam->farClip = 100.0f; + cam->projType = ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE; + cam->perspective.fov = glm_rad(45.0f); +} + +void entityCameraGetProjection( + const entityid_t ent, + const componentid_t comp, + mat4 out +) { + entitycamera_t *cam = (entitycamera_t *)componentGetData( + ent, comp, COMPONENT_TYPE_CAMERA + ); + + if( + cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE || + cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED + ) { + glm_mat4_identity(out); + glm_perspective( + cam->perspective.fov, + SCREEN.aspect, + cam->nearClip, + cam->farClip, + out + ); + + if(cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED) { + out[1][1] *= -1.0f; + } + } else if(cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) { + glm_mat4_identity(out); + glm_ortho( + cam->orthographic.left, + cam->orthographic.right, + cam->orthographic.top, + cam->orthographic.bottom, + cam->nearClip, + cam->farClip, + out + ); + } +} + +entityid_t entityCameraGetCurrent(void) { + entityid_t camEnts[ENTITY_COUNT_MAX]; + componentid_t camComps[ENTITY_COUNT_MAX]; + entityid_t count = componentGetEntitiesWithComponent( + COMPONENT_TYPE_CAMERA, camEnts, camComps + ); + if(count == 0) return ENTITY_COUNT_MAX; + return camEnts[0]; +} + +void entityCameraGetForward(const entityid_t entityId, vec2 out) { + componentid_t posComp = entityGetComponent(entityId, COMPONENT_TYPE_POSITION); + entityposition_t *pos = entityPositionGet(entityId, posComp); + // View matrix column layout: M[col][row], + // forward = {-M[0][2], -M[1][2], -M[2][2]} + float_t fx = -pos->worldTransform[0][2]; + float_t fz = -pos->worldTransform[2][2]; + float_t len = sqrtf(fx * fx + fz * fz); + if(len > 1e-6f) { fx /= len; fz /= len; } + out[0] = fx; + out[1] = fz; +} + +void entityCameraGetRight(const entityid_t entityId, vec2 out) { + componentid_t posComp = entityGetComponent(entityId, COMPONENT_TYPE_POSITION); + entityposition_t *pos = entityPositionGet(entityId, posComp); + // View matrix column layout: right = {M[0][0], M[1][0], M[2][0]} + float_t rx = pos->worldTransform[0][0]; + float_t rz = pos->worldTransform[2][0]; + float_t len = sqrtf(rx * rx + rz * rz); + if(len > 1e-6f) { rx /= len; rz /= len; } + out[0] = rx; + out[1] = rz; +} \ No newline at end of file diff --git a/src/dusk/entity/component/display/entitycamera.h b/src/dusk/entity/component/display/entitycamera.h new file mode 100644 index 00000000..be8c510d --- /dev/null +++ b/src/dusk/entity/component/display/entitycamera.h @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "entity/entitybase.h" + +typedef enum { + ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE, + ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED, + ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC +} entitycameraprojectiontype_t; + +typedef struct { + union { + struct { + float_t fov; + } perspective; + + struct { + float_t left; + float_t right; + float_t top; + float_t bottom; + } orthographic; + }; + + float_t nearClip; + float_t farClip; + entitycameraprojectiontype_t projType; +} entitycamera_t; + +/** + * Initializes an entity camera component. + * + * @param ent The entity ID. + * @param comp The component ID. + */ +void entityCameraInit(const entityid_t ent, const componentid_t comp); + +/** + * Renders out the projection matrix for the given camera. + * + * @param ent The entity ID. + * @param comp The component ID. + * @param out The output projection matrix. + */ +void entityCameraGetProjection( + const entityid_t ent, + const componentid_t comp, + mat4 out +); + +/** + * Returns the entity ID of the first active camera, or ENTITY_COUNT_MAX if + * none are active. + */ +entityid_t entityCameraGetCurrent(void); + +/** + * Gets the camera's horizontal forward direction (XZ plane) from its position + * component. Automatically finds the position component on the entity. + * + * @param entityId The camera entity ID. + * @param out Output vec2: {forwardX, forwardZ} normalized. + */ +void entityCameraGetForward(const entityid_t entityId, vec2 out); + +/** + * Gets the camera's horizontal right direction (XZ plane) from its position + * component. Automatically finds the position component on the entity. + * + * @param entityId The camera entity ID. + * @param out Output vec2: {rightX, rightZ} normalized. + */ +void entityCameraGetRight(const entityid_t entityId, vec2 out); \ No newline at end of file diff --git a/src/dusk/entity/component/display/entityposition.c b/src/dusk/entity/component/display/entityposition.c new file mode 100644 index 00000000..3c1c0c30 --- /dev/null +++ b/src/dusk/entity/component/display/entityposition.c @@ -0,0 +1,316 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "entity/entitymanager.h" + +// Lazily recompute worldTransform from the parent chain. +static void entityPositionUpdateWorld(entityposition_t *pos) { + if(!pos->dirty) return; + + if(pos->parentEntityId == ENTITY_ID_INVALID) { + glm_mat4_copy(pos->localTransform, pos->worldTransform); + } else { + entityposition_t *parent = componentGetData( + pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION + ); + entityPositionUpdateWorld(parent); + glm_mat4_mul(parent->worldTransform, pos->localTransform, pos->worldTransform); + } + + pos->dirty = false; +} + +void entityPositionMarkDirty(entityposition_t *pos) { + pos->dirty = true; + for(uint8_t i = 0; i < pos->childCount; i++) { + entityposition_t *child = componentGetData( + pos->childEntityIds[i], pos->childComponentIds[i], COMPONENT_TYPE_POSITION + ); + entityPositionMarkDirty(child); + } +} + +void entityPositionInit( + const entityid_t entityId, + const componentid_t componentId +) { + entityposition_t *pos = componentGetData( + entityId, componentId, COMPONENT_TYPE_POSITION + ); + + glm_vec3_zero(pos->position); + glm_vec3_zero(pos->rotation); + glm_vec3_one(pos->scale); + glm_mat4_identity(pos->localTransform); + glm_mat4_identity(pos->worldTransform); + pos->dirty = false; + pos->parentEntityId = ENTITY_ID_INVALID; + pos->parentComponentId = COMPONENT_ID_INVALID; + pos->childCount = 0; +} + +void entityPositionLookAt( + const entityid_t entityId, + const componentid_t componentId, + vec3 target, + vec3 up, + vec3 eye +) { + entityposition_t *pos = componentGetData( + entityId, componentId, COMPONENT_TYPE_POSITION + ); + glm_lookat(eye, target, up, pos->localTransform); + entityPositionDecompose(pos); + entityPositionMarkDirty(pos); +} + +void entityPositionGetTransform( + const entityid_t entityId, + const componentid_t componentId, + mat4 dest +) { + entityposition_t *pos = componentGetData( + entityId, componentId, COMPONENT_TYPE_POSITION + ); + entityPositionUpdateWorld(pos); + glm_mat4_copy(pos->worldTransform, dest); +} + +void entityPositionGetLocalTransform( + const entityid_t entityId, + const componentid_t componentId, + mat4 dest +) { + entityposition_t *pos = componentGetData( + entityId, componentId, COMPONENT_TYPE_POSITION + ); + glm_mat4_copy(pos->localTransform, dest); +} + +void entityPositionGetPosition( + const entityid_t entityId, + const componentid_t componentId, + vec3 dest +) { + entityposition_t *pos = componentGetData( + entityId, componentId, COMPONENT_TYPE_POSITION + ); + glm_vec3_copy(pos->position, dest); +} + +void entityPositionSetPosition( + const entityid_t entityId, + const componentid_t componentId, + vec3 position +) { + entityposition_t *pos = componentGetData( + entityId, componentId, COMPONENT_TYPE_POSITION + ); + glm_vec3_copy(position, pos->position); + entityPositionRebuild(pos); +} + +void entityPositionGetRotation( + const entityid_t entityId, + const componentid_t componentId, + vec3 dest +) { + entityposition_t *pos = componentGetData( + entityId, componentId, COMPONENT_TYPE_POSITION + ); + glm_vec3_copy(pos->rotation, dest); +} + +void entityPositionSetRotation( + const entityid_t entityId, + const componentid_t componentId, + vec3 rotation +) { + entityposition_t *pos = componentGetData( + entityId, componentId, COMPONENT_TYPE_POSITION + ); + glm_vec3_copy(rotation, pos->rotation); + entityPositionRebuild(pos); +} + +void entityPositionGetScale( + const entityid_t entityId, + const componentid_t componentId, + vec3 dest +) { + entityposition_t *pos = componentGetData( + entityId, componentId, COMPONENT_TYPE_POSITION + ); + glm_vec3_copy(pos->scale, dest); +} + +void entityPositionSetScale( + const entityid_t entityId, + const componentid_t componentId, + vec3 scale +) { + entityposition_t *pos = componentGetData( + entityId, componentId, COMPONENT_TYPE_POSITION + ); + glm_vec3_copy(scale, pos->scale); + entityPositionRebuild(pos); +} + +void entityPositionSetParent( + const entityid_t entityId, + const componentid_t componentId, + const entityid_t parentEntityId, + const componentid_t parentComponentId +) { + entityposition_t *pos = componentGetData( + entityId, componentId, COMPONENT_TYPE_POSITION + ); + + // Remove from old parent's child list. + if(pos->parentEntityId != ENTITY_ID_INVALID) { + entityposition_t *oldParent = componentGetData( + pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION + ); + for(uint8_t i = 0; i < oldParent->childCount; i++) { + if( + oldParent->childEntityIds[i] == entityId && + oldParent->childComponentIds[i] == componentId + ) { + oldParent->childCount--; + for(uint8_t j = i; j < oldParent->childCount; j++) { + oldParent->childEntityIds[j] = oldParent->childEntityIds[j + 1]; + oldParent->childComponentIds[j] = oldParent->childComponentIds[j + 1]; + } + break; + } + } + } + + pos->parentEntityId = parentEntityId; + pos->parentComponentId = parentComponentId; + + // Register with new parent. + if(parentEntityId != ENTITY_ID_INVALID) { + entityposition_t *parent = componentGetData( + parentEntityId, parentComponentId, COMPONENT_TYPE_POSITION + ); + if(parent->childCount < ENTITY_POSITION_CHILDREN_MAX) { + parent->childEntityIds[parent->childCount] = entityId; + parent->childComponentIds[parent->childCount] = componentId; + parent->childCount++; + } + } + + entityPositionMarkDirty(pos); +} + +entityposition_t *entityPositionGet( + const entityid_t entityId, + const componentid_t componentId +) { + return componentGetData( + entityId, componentId, COMPONENT_TYPE_POSITION + ); +} + +void entityPositionRebuild(entityposition_t *pos) { + glm_mat4_identity(pos->localTransform); + glm_translate(pos->localTransform, pos->position); + glm_rotate_x(pos->localTransform, pos->rotation[0], pos->localTransform); + glm_rotate_y(pos->localTransform, pos->rotation[1], pos->localTransform); + glm_rotate_z(pos->localTransform, pos->rotation[2], pos->localTransform); + glm_scale(pos->localTransform, pos->scale); + entityPositionMarkDirty(pos); +} + +void entityPositionDisposeDeep( + const entityid_t entityId, + const componentid_t componentId +) { + entityposition_t *pos = entityPositionGet(entityId, componentId); + + // Detach from parent so the parent's child list stays consistent. + if(pos->parentEntityId != ENTITY_ID_INVALID) { + entityPositionSetParent(entityId, componentId, ENTITY_ID_INVALID, COMPONENT_ID_INVALID); + } + + // Copy the child list before disposing self (entityDispose invalidates pos). + uint8_t childCount = pos->childCount; + entityid_t childEntityIds[ENTITY_POSITION_CHILDREN_MAX]; + componentid_t childComponentIds[ENTITY_POSITION_CHILDREN_MAX]; + for(uint8_t i = 0; i < childCount; i++) { + childEntityIds[i] = pos->childEntityIds[i]; + childComponentIds[i] = pos->childComponentIds[i]; + // Sever the child's parent link so it won't try to modify our disposed data. + entityposition_t *child = entityPositionGet(childEntityIds[i], childComponentIds[i]); + child->parentEntityId = ENTITY_ID_INVALID; + child->parentComponentId = COMPONENT_ID_INVALID; + } + + entityDispose(entityId); + + for(uint8_t i = 0; i < childCount; i++) { + entityPositionDisposeDeep(childEntityIds[i], childComponentIds[i]); + } +} + +void entityPositionDecompose(entityposition_t *pos) { + // Translation: column 3 + pos->position[0] = pos->localTransform[3][0]; + pos->position[1] = pos->localTransform[3][1]; + pos->position[2] = pos->localTransform[3][2]; + + // Scale: length of each basis column (xyz only) + pos->scale[0] = sqrtf( + pos->localTransform[0][0] * pos->localTransform[0][0] + + pos->localTransform[0][1] * pos->localTransform[0][1] + + pos->localTransform[0][2] * pos->localTransform[0][2] + ); + pos->scale[1] = sqrtf( + pos->localTransform[1][0] * pos->localTransform[1][0] + + pos->localTransform[1][1] * pos->localTransform[1][1] + + pos->localTransform[1][2] * pos->localTransform[1][2] + ); + pos->scale[2] = sqrtf( + pos->localTransform[2][0] * pos->localTransform[2][0] + + pos->localTransform[2][1] * pos->localTransform[2][1] + + pos->localTransform[2][2] * pos->localTransform[2][2] + ); + + // Normalize columns to isolate the rotation matrix. + float invS0 = pos->scale[0] > 0.0f ? 1.0f / pos->scale[0] : 0.0f; + float invS1 = pos->scale[1] > 0.0f ? 1.0f / pos->scale[1] : 0.0f; + float invS2 = pos->scale[2] > 0.0f ? 1.0f / pos->scale[2] : 0.0f; + + mat4 r; + glm_mat4_identity(r); + r[0][0] = pos->localTransform[0][0] * invS0; + r[0][1] = pos->localTransform[0][1] * invS0; + r[0][2] = pos->localTransform[0][2] * invS0; + r[1][0] = pos->localTransform[1][0] * invS1; + r[1][1] = pos->localTransform[1][1] * invS1; + r[1][2] = pos->localTransform[1][2] * invS1; + r[2][0] = pos->localTransform[2][0] * invS2; + r[2][1] = pos->localTransform[2][1] * invS2; + r[2][2] = pos->localTransform[2][2] * invS2; + + // Extract XYZ euler angles (R = Rx * Ry * Rz, column-major) + float sinBeta = glm_clamp(r[2][0], -1.0f, 1.0f); + pos->rotation[1] = asinf(sinBeta); + float cosBeta = cosf(pos->rotation[1]); + + if(fabsf(cosBeta) > 1e-6f) { + pos->rotation[0] = atan2f(-r[2][1], r[2][2]); + pos->rotation[2] = atan2f(-r[1][0], r[0][0]); + } else { + // Gimbal lock: pin Z to 0, recover X. + pos->rotation[2] = 0.0f; + pos->rotation[0] = (sinBeta > 0.0f) + ? atan2f(r[0][1], r[1][1]) + : -atan2f(r[0][1], r[1][1]); + } +} diff --git a/src/dusk/entity/component/display/entityposition.h b/src/dusk/entity/component/display/entityposition.h new file mode 100644 index 00000000..1513bfbb --- /dev/null +++ b/src/dusk/entity/component/display/entityposition.h @@ -0,0 +1,184 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "entity/entitybase.h" + +#define ENTITY_POSITION_CHILDREN_MAX 8 + +typedef struct { + mat4 localTransform; + mat4 worldTransform; + vec3 position; + vec3 rotation; + vec3 scale; + bool dirty; + entityid_t parentEntityId; + componentid_t parentComponentId; + uint8_t childCount; + entityid_t childEntityIds[ENTITY_POSITION_CHILDREN_MAX]; + componentid_t childComponentIds[ENTITY_POSITION_CHILDREN_MAX]; +} entityposition_t; + +/** + * Initialize the entity position component. + */ +void entityPositionInit( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Transforms the entity's local transform to look at a target point. + * + * @param entityId The entity ID. + * @param componentId The component ID. + * @param target The target point to look at. + * @param up The up vector. + * @param eye The eye/camera position. + */ +void entityPositionLookAt( + const entityid_t entityId, + const componentid_t componentId, + vec3 target, + vec3 up, + vec3 eye +); + +/** + * Gets the world-space transform matrix, recomputing it lazily if dirty. + * + * @param entityId The entity ID. + * @param componentId The component ID. + * @param dest Destination matrix. + */ +void entityPositionGetTransform( + const entityid_t entityId, + const componentid_t componentId, + mat4 dest +); + +/** + * Gets the local transform matrix (does not include parent transforms). + * + * @param entityId The entity ID. + * @param componentId The component ID. + * @param dest Destination matrix. + */ +void entityPositionGetLocalTransform( + const entityid_t entityId, + const componentid_t componentId, + mat4 dest +); + +/** + * Gets the cached local position. + */ +void entityPositionGetPosition( + const entityid_t entityId, + const componentid_t componentId, + vec3 dest +); + +/** + * Sets the local position and marks the world transform dirty. + */ +void entityPositionSetPosition( + const entityid_t entityId, + const componentid_t componentId, + vec3 position +); + +/** + * Gets the cached local euler rotation (XYZ, radians). + */ +void entityPositionGetRotation( + const entityid_t entityId, + const componentid_t componentId, + vec3 dest +); + +/** + * Sets the local euler rotation (XYZ, radians) and marks the world transform dirty. + */ +void entityPositionSetRotation( + const entityid_t entityId, + const componentid_t componentId, + vec3 rotation +); + +/** + * Gets the cached local scale. + */ +void entityPositionGetScale( + const entityid_t entityId, + const componentid_t componentId, + vec3 dest +); + +/** + * Sets the local scale and marks the world transform dirty. + */ +void entityPositionSetScale( + const entityid_t entityId, + const componentid_t componentId, + vec3 scale +); + +/** + * Sets the parent of this entity's position component. + * Pass ENTITY_ID_INVALID / COMPONENT_ID_INVALID to detach from any parent. + * + * @param entityId The child entity ID. + * @param componentId The child component ID. + * @param parentEntityId The parent entity ID. + * @param parentComponentId The parent component ID. + */ +void entityPositionSetParent( + const entityid_t entityId, + const componentid_t componentId, + const entityid_t parentEntityId, + const componentid_t parentComponentId +); + +/** + * Returns a direct pointer to the entity position component data. + * After modifying localTransform directly, call entityPositionMarkDirty(). + */ +entityposition_t *entityPositionGet( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Rebuilds the local transform matrix from the cached position/rotation/scale, + * then marks this node and all descendants dirty. + */ +void entityPositionRebuild(entityposition_t *pos); + +/** + * Marks this node and all descendants as having a stale world transform. + */ +void entityPositionMarkDirty(entityposition_t *pos); + +/** + * Disposes this entity and all of its position-component descendants + * recursively. Detaches from any parent before destroying. + * + * @param entityId The root entity ID. + * @param componentId The root position component ID. + */ +void entityPositionDisposeDeep( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Decomposes the local transform matrix back into the position, rotation + * (XYZ euler, radians), and scale cache fields. + */ +void entityPositionDecompose(entityposition_t *pos); diff --git a/src/dusk/entity/component/display/entityrenderable.c b/src/dusk/entity/component/display/entityrenderable.c new file mode 100644 index 00000000..a93a04f8 --- /dev/null +++ b/src/dusk/entity/component/display/entityrenderable.c @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "entityrenderable.h" +#include "entity/entitymanager.h" +#include "display/shader/shaderunlit.h" +#include "display/mesh/cube.h" + +void entityRenderableInit( + const entityid_t entityId, + const componentid_t componentId +) { + entityrenderable_t *r = componentGetData( + entityId, componentId, COMPONENT_TYPE_RENDERABLE + ); + r->type = ENTITY_RENDERABLE_TYPE_MATERIAL; + r->mesh = &CUBE_MESH_SIMPLE; + r->shader = &SHADER_UNLIT; + r->material.unlit.color = COLOR_WHITE; +} + +entityrenderabletype_t entityRenderableGetType( + const entityid_t entityId, + const componentid_t componentId +) { + entityrenderable_t *r = componentGetData( + entityId, componentId, COMPONENT_TYPE_RENDERABLE + ); + return r->type; +} + +void entityRenderableSetType( + const entityid_t entityId, + const componentid_t componentId, + const entityrenderabletype_t type +) { + entityrenderable_t *r = componentGetData( + entityId, componentId, COMPONENT_TYPE_RENDERABLE + ); + r->type = type; +} + +mesh_t * entityRenderableGetMesh( + const entityid_t entityId, + const componentid_t componentId +) { + entityrenderable_t *r = componentGetData( + entityId, componentId, COMPONENT_TYPE_RENDERABLE + ); + return r->mesh; +} + +void entityRenderableSetMesh( + const entityid_t entityId, + const componentid_t componentId, + mesh_t *mesh +) { + entityrenderable_t *r = componentGetData( + entityId, componentId, COMPONENT_TYPE_RENDERABLE + ); + r->mesh = mesh; +} + +shader_t * entityRenderableGetShader( + const entityid_t entityId, + const componentid_t componentId +) { + entityrenderable_t *r = componentGetData( + entityId, componentId, COMPONENT_TYPE_RENDERABLE + ); + return r->shader; +} + +void entityRenderableSetShader( + const entityid_t entityId, + const componentid_t componentId, + shader_t *shader +) { + entityrenderable_t *r = componentGetData( + entityId, componentId, COMPONENT_TYPE_RENDERABLE + ); + r->shader = shader; +} + +shadermaterial_t * entityRenderableGetShaderMaterial( + const entityid_t entityId, + const componentid_t componentId +) { + entityrenderable_t *r = componentGetData( + entityId, componentId, COMPONENT_TYPE_RENDERABLE + ); + return &r->material; +} + +void entityRenderableSetColor( + const entityid_t entityId, + const componentid_t componentId, + const color_t color +) { + entityrenderable_t *r = componentGetData( + entityId, componentId, COMPONENT_TYPE_RENDERABLE + ); + r->material.unlit.color = color; +} + +void entityRenderableSpriteBatchAdd( + const entityid_t entityId, + const componentid_t componentId, + const spritebatchsprite_t *sprite +) { + entityrenderable_t *r = componentGetData( + entityId, componentId, COMPONENT_TYPE_RENDERABLE + ); + if(r->spritebatch.spriteCount >= ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX) return; + r->spritebatch.sprites[r->spritebatch.spriteCount++] = *sprite; +} + +void entityRenderableSpriteBatchClear( + const entityid_t entityId, + const componentid_t componentId +) { + entityrenderable_t *r = componentGetData( + entityId, componentId, COMPONENT_TYPE_RENDERABLE + ); + r->spritebatch.spriteCount = 0; +} + +void entityRenderableDispose( + const entityid_t entityId, + const componentid_t componentId +) { + entityrenderable_t *r = componentGetData( + entityId, componentId, COMPONENT_TYPE_RENDERABLE + ); + if( + r->type == ENTITY_RENDERABLE_TYPE_CALLBACK && + r->userFree && + r->user + ) { + r->userFree(r->user); + r->user = NULL; + } + r->mesh = NULL; + r->shader = NULL; +} diff --git a/src/dusk/entity/component/display/entityrenderable.h b/src/dusk/entity/component/display/entityrenderable.h new file mode 100644 index 00000000..7a1553c0 --- /dev/null +++ b/src/dusk/entity/component/display/entityrenderable.h @@ -0,0 +1,157 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "entity/entitybase.h" +#include "display/mesh/mesh.h" +#include "display/shader/shadermaterial.h" +#include "display/spritebatch/spritebatch.h" + +#define ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX 64 + +typedef enum { + ENTITY_RENDERABLE_TYPE_MATERIAL = 0, + ENTITY_RENDERABLE_TYPE_SPRITEBATCH, + ENTITY_RENDERABLE_TYPE_CALLBACK, +} entityrenderabletype_t; + +typedef errorret_t (*entityrenderablecallback_t)( + const entityid_t entityId, + const componentid_t componentId, + const mat4 view, + const mat4 proj, + const mat4 model, + void *user +); + +typedef struct { + spritebatchsprite_t sprites[ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX]; + uint16_t spriteCount; +} entityrenderablespritebatch_t; + +typedef struct { + entityrenderabletype_t type; + shader_t *shader; + union { + struct { + mesh_t *mesh; + shadermaterial_t material; + }; + entityrenderablespritebatch_t spritebatch; + struct { + entityrenderablecallback_t callback; + void (*userFree)(void *user); + void *user; + }; + }; +} entityrenderable_t; + +/** + * Initializes the entity renderable component. Defaults to + * ENTITY_RENDERABLE_TYPE_MATERIAL, the unlit shader, white color, no mesh. + */ +void entityRenderableInit( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Disposes the entity renderable component, freeing any callback user data. + */ +void entityRenderableDispose( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Gets the renderable type. + */ +entityrenderabletype_t entityRenderableGetType( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Sets the renderable type. + */ +void entityRenderableSetType( + const entityid_t entityId, + const componentid_t componentId, + const entityrenderabletype_t type +); + +/** + * Gets the mesh pointer (ENTITY_RENDERABLE_TYPE_MATERIAL only). + */ +mesh_t * entityRenderableGetMesh( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Sets the mesh pointer (ENTITY_RENDERABLE_TYPE_MATERIAL only). + */ +void entityRenderableSetMesh( + const entityid_t entityId, + const componentid_t componentId, + mesh_t *mesh +); + +/** + * Gets the shader pointer. + */ +shader_t * entityRenderableGetShader( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Sets the shader pointer. + */ +void entityRenderableSetShader( + const entityid_t entityId, + const componentid_t componentId, + shader_t *shader +); + +/** + * Gets a pointer to the shader material union + * (ENTITY_RENDERABLE_TYPE_MATERIAL only). + */ +shadermaterial_t * entityRenderableGetShaderMaterial( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Sets the unlit color (ENTITY_RENDERABLE_TYPE_MATERIAL only). + */ +void entityRenderableSetColor( + const entityid_t entityId, + const componentid_t componentId, + const color_t color +); + +/** + * Appends a sprite to the spritebatch renderable + * (ENTITY_RENDERABLE_TYPE_SPRITEBATCH only). + * Does nothing if the sprite buffer is full. + */ +void entityRenderableSpriteBatchAdd( + const entityid_t entityId, + const componentid_t componentId, + const spritebatchsprite_t *sprite +); + +/** + * Clears all buffered sprites from the spritebatch renderable + * (ENTITY_RENDERABLE_TYPE_SPRITEBATCH only). + */ +void entityRenderableSpriteBatchClear( + const entityid_t entityId, + const componentid_t componentId +); diff --git a/src/dusk/entity/component/physics/CMakeLists.txt b/src/dusk/entity/component/physics/CMakeLists.txt new file mode 100644 index 00000000..f3848009 --- /dev/null +++ b/src/dusk/entity/component/physics/CMakeLists.txt @@ -0,0 +1,10 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Sources +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + entityphysics.c +) \ No newline at end of file diff --git a/src/dusk/entity/component/physics/entityphysics.c b/src/dusk/entity/component/physics/entityphysics.c new file mode 100644 index 00000000..3705f8b7 --- /dev/null +++ b/src/dusk/entity/component/physics/entityphysics.c @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "entityphysics.h" +#include "entity/entitymanager.h" +#include "entity/component/display/entityposition.h" +#include "physics/physicsmanager.h" +#include "assert/assert.h" +#include "util/memory.h" + +void entityPhysicsInit( + const entityid_t entityId, + const componentid_t componentId +) { + entityphysics_t *phys = entityPhysicsGet(entityId, componentId); + assertNotNull(phys, "Failed to get physics component data"); + + memoryZero(phys, sizeof(entityphysics_t)); + + // Default to cube + phys->type = PHYSICS_BODY_DYNAMIC; + phys->shape.type = PHYSICS_SHAPE_CUBE; + phys->shape.data.cube.halfExtents[0] = 0.5f; + phys->shape.data.cube.halfExtents[1] = 0.5f; + phys->shape.data.cube.halfExtents[2] = 0.5f; + phys->gravityScale = 1.0f; + phys->onGround = false; +} + +entityphysics_t *entityPhysicsGet( + const entityid_t entityId, + const componentid_t componentId +) { + return componentGetData(entityId, componentId, COMPONENT_TYPE_PHYSICS); +} + +void entityPhysicsSetShape( + const entityid_t entityId, + const componentid_t componentId, + const physicsshape_t shape +) { + entityphysics_t *phys = entityPhysicsGet(entityId, componentId); + assertNotNull(phys, "Failed to get physics component data"); + phys->shape = shape; + // TODO: Do I need to reset the state for ground/active? +} + +physicsshape_t entityPhysicsGetShape( + const entityid_t entityId, + const componentid_t componentId +) { + entityphysics_t *phys = entityPhysicsGet(entityId, componentId); + assertNotNull(phys, "Failed to get physics component data"); + return phys->shape; +} + +void entityPhysicsGetVelocity( + const entityid_t entityId, + const componentid_t componentId, + vec3 dest +) { + entityphysics_t *phys = entityPhysicsGet(entityId, componentId); + assertNotNull(phys, "Failed to get physics component data"); + + glm_vec3_copy(phys->velocity, dest); +} + +void entityPhysicsSetVelocity( + const entityid_t entityId, + const componentid_t componentId, + vec3 velocity +) { + entityphysics_t *phys = entityPhysicsGet(entityId, componentId); + assertNotNull(phys, "Failed to get physics component data"); + glm_vec3_copy(velocity, phys->velocity); +} + +void entityPhysicsApplyImpulse( + const entityid_t entityId, + const componentid_t componentId, + vec3 impulse +) { + entityphysics_t *phys = entityPhysicsGet(entityId, componentId); + assertNotNull(phys, "Failed to get physics component data"); + + if(phys->type == PHYSICS_BODY_STATIC) return; + glm_vec3_add(phys->velocity, impulse, phys->velocity); +} + +bool_t entityPhysicsIsOnGround( + const entityid_t entityId, + const componentid_t componentId +) { + entityphysics_t *phys = entityPhysicsGet(entityId, componentId); + assertNotNull(phys, "Failed to get physics component data"); + return phys->onGround; +} + +void entityPhysicsSetBodyType( + const entityid_t entityId, + const componentid_t componentId, + const physicsbodytype_t type +) { + entityphysics_t *phys = entityPhysicsGet(entityId, componentId); + assertNotNull(phys, "Failed to get physics component data"); + phys->type = type; +} + +physicsbodytype_t entityPhysicsGetBodyType( + const entityid_t entityId, + const componentid_t componentId +) { + entityphysics_t *phys = entityPhysicsGet(entityId, componentId); + assertNotNull(phys, "Failed to get physics component data"); + return phys->type; +} + +void entityPhysicsDispose( + const entityid_t entityId, + const componentid_t componentId +) { +} diff --git a/src/dusk/entity/component/physics/entityphysics.h b/src/dusk/entity/component/physics/entityphysics.h new file mode 100644 index 00000000..6cccfca3 --- /dev/null +++ b/src/dusk/entity/component/physics/entityphysics.h @@ -0,0 +1,157 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "entity/entitybase.h" +#include "physics/physicsshape.h" +#include "physics/physicsbodytype.h" + +typedef struct { + physicsbodytype_t type; + physicsshape_t shape; + vec3 velocity; + float_t gravityScale; + bool_t onGround; +} entityphysics_t; + +/** + * Initializes the physics component: allocates a body in PHYSICS_WORLD. + * Asserts if the world body limit is reached. + */ +void entityPhysicsInit( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Gets the underlying physics structure (temporarily) for the given entity. + * This is really just intended for doing operations faster than using the + * getters and setters, but it is preferred that you use those. + * + * @param entityId The entity ID. + * @param componentId The component ID. + * @return The physics component data for the given entity and component ID. + */ +entityphysics_t *entityPhysicsGet( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Sets the shape of the entity's physics body. This will not reset the body + * state, so if you change from a cube to a sphere, it will keep the same + * velocity and onGround state. + * + * @param entityId The entity ID. + * @param componentId The component ID. + * @param shape The new shape to set on the physics body. + */ +void entityPhysicsSetShape( + const entityid_t entityId, + const componentid_t componentId, + const physicsshape_t shape +); + +/** + * Gets the shape of the entity's physics body. + * + * @param entityId The entity ID. + * @param componentId The component ID. + * @return The shape of the physics body. + */ +physicsshape_t entityPhysicsGetShape( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Gets the velocity of the entity's physics body. + * + * @param entityId The entity ID. + * @param componentId The component ID. + * @param dest The destination vec3 to write the velocity to. + */ +void entityPhysicsGetVelocity( + const entityid_t entityId, + const componentid_t componentId, + vec3 dest +); + +/** + * Sets the velocity of the entity's physics body. This is not an impulse, so + * it will be affected by mass and drag. + * + * @param entityId The entity ID. + * @param componentId The component ID. + * @param velocity The new velocity to set on the physics body. + */ +void entityPhysicsSetVelocity( + const entityid_t entityId, + const componentid_t componentId, + vec3 velocity +); + +/** + * Applies an impulse to the entity's physics body. This is an immediate + * velocity change that is not affected by mass or drag. No-op on STATIC bodies. + * + * @param entityId The entity ID. + * @param componentId The component ID. + * @param impulse The impulse to apply to the physics body. + */ +void entityPhysicsApplyImpulse( + const entityid_t entityId, + const componentid_t componentId, + vec3 impulse +); + +/** + * Returns true if the entity's physics body rested on a surface during the last + * step or move. + * + * @param entityId The entity ID. + * @param componentId The component ID. + * @return True if the body is on the ground, false otherwise. + */ +bool_t entityPhysicsIsOnGround( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Sets the body type of the entity's physics body. + * + * @param entityId The entity ID. + * @param componentId The component ID. + * @param type The body type to set. + */ +void entityPhysicsSetBodyType( + const entityid_t entityId, + const componentid_t componentId, + const physicsbodytype_t type +); + +/** + * Gets the body type of the entity's physics body. + * + * @param entityId The entity ID. + * @param componentId The component ID. + * @return The body type of the physics body. + */ +physicsbodytype_t entityPhysicsGetBodyType( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Releases the body slot back to PHYSICS_WORLD. Called automatically when + * the component is disposed via the component system. + */ +void entityPhysicsDispose( + const entityid_t entityId, + const componentid_t componentId +); diff --git a/src/dusk/entity/component/script/CMakeLists.txt b/src/dusk/entity/component/script/CMakeLists.txt new file mode 100644 index 00000000..5fa29add --- /dev/null +++ b/src/dusk/entity/component/script/CMakeLists.txt @@ -0,0 +1,7 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + + + diff --git a/src/dusk/entity/component/trigger/CMakeLists.txt b/src/dusk/entity/component/trigger/CMakeLists.txt new file mode 100644 index 00000000..b8c409f8 --- /dev/null +++ b/src/dusk/entity/component/trigger/CMakeLists.txt @@ -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 + entitytrigger.c +) diff --git a/src/dusk/entity/component/trigger/entitytrigger.c b/src/dusk/entity/component/trigger/entitytrigger.c new file mode 100644 index 00000000..bba57269 --- /dev/null +++ b/src/dusk/entity/component/trigger/entitytrigger.c @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "entity/entitymanager.h" + +void entityTriggerInit( + const entityid_t entityId, + const componentid_t componentId +) { + entitytrigger_t *t = componentGetData( + entityId, componentId, COMPONENT_TYPE_TRIGGER + ); + glm_vec3_zero(t->min); + glm_vec3_zero(t->max); +} + +entitytrigger_t * entityTriggerGet( + const entityid_t entityId, + const componentid_t componentId +) { + return componentGetData(entityId, componentId, COMPONENT_TYPE_TRIGGER); +} + +bool_t entityTriggerContains( + const entityid_t entityId, + const componentid_t componentId, + const vec3 point +) { + entitytrigger_t *t = componentGetData( + entityId, componentId, COMPONENT_TYPE_TRIGGER + ); + return ( + point[0] >= t->min[0] && point[0] <= t->max[0] && + point[1] >= t->min[1] && point[1] <= t->max[1] && + point[2] >= t->min[2] && point[2] <= t->max[2] + ); +} + +void entityTriggerSetBounds( + const entityid_t entityId, + const componentid_t componentId, + const vec3 min, + const vec3 max +) { + entitytrigger_t *t = componentGetData( + entityId, componentId, COMPONENT_TYPE_TRIGGER + ); + glm_vec3_copy((float_t*)min, t->min); + glm_vec3_copy((float_t*)max, t->max); +} diff --git a/src/dusk/entity/component/trigger/entitytrigger.h b/src/dusk/entity/component/trigger/entitytrigger.h new file mode 100644 index 00000000..fdbef2c8 --- /dev/null +++ b/src/dusk/entity/component/trigger/entitytrigger.h @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "entity/entitybase.h" + +typedef struct { + vec3 min; + vec3 max; +} entitytrigger_t; + +/** + * Initializes the trigger component with zeroed bounds. + */ +void entityTriggerInit( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Returns a pointer to the trigger component data. + */ +entitytrigger_t * entityTriggerGet( + const entityid_t entityId, + const componentid_t componentId +); + +/** + * Returns true if the given world-space point lies within [min, max]. + */ +bool_t entityTriggerContains( + const entityid_t entityId, + const componentid_t componentId, + const vec3 point +); + +/** + * Sets both bounds at once. + */ +void entityTriggerSetBounds( + const entityid_t entityId, + const componentid_t componentId, + const vec3 min, + const vec3 max +); diff --git a/src/dusk/entity/componentlist.h b/src/dusk/entity/componentlist.h new file mode 100644 index 00000000..753b129b --- /dev/null +++ b/src/dusk/entity/componentlist.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#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 "entity/component/trigger/entitytrigger.h" + +// Name (Uppercase) +// Structure +// Field name (lowercase) +// Init function (optional) +// Dispose function (optional) +// FixedUpdate function (optional) - called once per fixed timestep + +X(POSITION, entityposition_t, position, entityPositionInit, NULL, NULL) +X(CAMERA, entitycamera_t, camera, entityCameraInit, NULL, NULL) +X(RENDERABLE, entityrenderable_t, renderable, entityRenderableInit, entityRenderableDispose, NULL) +X(PHYSICS, entityphysics_t, physics, entityPhysicsInit, entityPhysicsDispose, NULL) +X(TRIGGER, entitytrigger_t, trigger, entityTriggerInit, NULL, NULL) diff --git a/src/dusk/entity/entity.c b/src/dusk/entity/entity.c new file mode 100644 index 00000000..332dd8df --- /dev/null +++ b/src/dusk/entity/entity.c @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "entitymanager.h" +#include "component/display/entityposition.h" +#include "util/memory.h" +#include "assert/assert.h" + +void entityInit(const entityid_t entityId) { + entity_t *ent = &ENTITY_MANAGER.entities[entityId]; + + memoryZero(ent, sizeof(entity_t)); + + // Mark all component types not using this entity. + for( + componenttype_t compType = 0; + compType < COMPONENT_TYPE_COUNT; + compType++ + ) { + ENTITY_MANAGER.entitiesWithComponent[ + compType * ENTITY_COUNT_MAX + entityId + ] = COMPONENT_ID_INVALID; + } + + ent->state |= ENTITY_STATE_ACTIVE; +} + +componentid_t entityAddComponent( + const entityid_t entityId, + const componenttype_t type +) { + componentindex_t compInd; + entity_t *ent = &ENTITY_MANAGER.entities[entityId]; + + for(componentid_t i = 0; i < ENTITY_COMPONENT_COUNT_MAX; i++) { + compInd = componentGetIndex(entityId, i); + if(ENTITY_MANAGER.components[compInd].type != COMPONENT_TYPE_NULL) { + assertTrue( + ENTITY_MANAGER.components[compInd].type != type, + "Entity already has component of this type" + ); + continue; + } + componentInit(entityId, i, type); + ENTITY_MANAGER.entitiesWithComponent[ + type * ENTITY_COUNT_MAX + entityId + ] = i; + return i; + } + + assertUnreachable("Entity has no more component slots available"); + return COMPONENT_ID_INVALID; +} + +componentid_t entityGetComponent( + const entityid_t entityId, + const componenttype_t type +) { + componentid_t compId = ENTITY_MANAGER.entitiesWithComponent[ + type * ENTITY_COUNT_MAX + entityId + ]; + if(compId == COMPONENT_ID_INVALID) return compId; + assertTrue( + ENTITY_MANAGER.components[componentGetIndex(entityId, compId)].type == type, + "Component type mismatch" + ); + return compId; +} + +void entityDisposeDeep(const entityid_t entityId) { + componentid_t posComp = entityGetComponent(entityId, COMPONENT_TYPE_POSITION); + if(posComp != COMPONENT_ID_INVALID) { + entityPositionDisposeDeep(entityId, posComp); + } else { + entityDispose(entityId); + } +} + +void entityDispose(const entityid_t entityId) { + componentindex_t compInd; + entity_t *ent = &ENTITY_MANAGER.entities[entityId]; + + for(componentid_t i = 0; i < ENTITY_COMPONENT_COUNT_MAX; i++) { + compInd = componentGetIndex(entityId, i); + componenttype_t type = ENTITY_MANAGER.components[compInd].type; + if(type == COMPONENT_TYPE_NULL) continue; + ENTITY_MANAGER.entitiesWithComponent[ + type * ENTITY_COUNT_MAX + entityId + ] = COMPONENT_ID_INVALID; + componentDispose(entityId, i); + } + + ent->state = 0; +} \ No newline at end of file diff --git a/src/dusk/entity/entity.h b/src/dusk/entity/entity.h new file mode 100644 index 00000000..de437669 --- /dev/null +++ b/src/dusk/entity/entity.h @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "component.h" + +#define ENTITY_STATE_ACTIVE (1 << 0) + +typedef struct { + uint8_t state; +} entity_t; + +/** + * Initializes an entity with the given ID. + * + * @param entityId The ID of the entity to initialize. + */ +void entityInit(const entityid_t entityId); + +/** + * Adds a component of the given type to the entity with the given ID. + * + * @param entityId The ID of the entity to add the component to. + * @param type The type of the component to add. + * @return The ID of the entity with component. + */ +componentid_t entityAddComponent( + const entityid_t entityId, + const componenttype_t type +); + +/** + * Gets the ID of the component of the given type on the entity with the given + * ID, or COMPONENT_ID_INVALID if the entity lacks the component. + * + * @param entityId The ID of the entity to get the component from. + * @param type The type of the component to get. + * @return The ID of the component. + */ +componentid_t entityGetComponent( + const entityid_t entityId, + const componenttype_t type +); + +/** + * Disposes of an entity with the given ID. + * + * @param entityId The ID of the entity to dispose of. + */ +void entityDispose(const entityid_t entityId); + +/** + * Disposes of an entity and all of its position-component descendants + * recursively. If the entity has no position component, behaves like + * entityDispose. + * + * @param entityId The root entity ID. + */ +void entityDisposeDeep(const entityid_t entityId); \ No newline at end of file diff --git a/src/dusk/entity/entitybase.h b/src/dusk/entity/entitybase.h new file mode 100644 index 00000000..a61f1170 --- /dev/null +++ b/src/dusk/entity/entitybase.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +#define ENTITY_COUNT_MAX 20 +#define ENTITY_COMPONENT_COUNT_MAX 8 + +#define ENTITY_ID_INVALID 0xFF +#define COMPONENT_ID_INVALID 0xFF + +typedef uint8_t entityid_t; +typedef uint8_t componentid_t; +typedef uint16_t componentindex_t; \ No newline at end of file diff --git a/src/dusk/entity/entitymanager.c b/src/dusk/entity/entitymanager.c new file mode 100644 index 00000000..df10ea48 --- /dev/null +++ b/src/dusk/entity/entitymanager.c @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "entitymanager.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "console/console.h" + +entitymanager_t ENTITY_MANAGER; + +void entityManagerInit(void) { + memoryZero(&ENTITY_MANAGER, sizeof(entitymanager_t)); + memorySet( + ENTITY_MANAGER.entitiesWithComponent, COMPONENT_ID_INVALID, + sizeof(entityid_t) * COMPONENT_TYPE_COUNT * ENTITY_COUNT_MAX + ); + + consolePrint( + "Entity Manager size: %zu bytes (%.2f KB)", + sizeof(entitymanager_t), + sizeof(entitymanager_t) / 1024.0f + ); +} + +entityid_t entityManagerAdd() { + for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) { + if((ENTITY_MANAGER.entities[i].state & ENTITY_STATE_ACTIVE) != 0) continue; + entityInit(i); + return i; + } + assertUnreachable("No more entity IDs available"); + return ENTITY_ID_INVALID; +} + +void entityManagerFixedUpdate(void) { + componentFixedUpdateAll(); +} + +void entityManagerDispose(void) { + for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) { + if((ENTITY_MANAGER.entities[i].state & ENTITY_STATE_ACTIVE) == 0) continue; + entityDispose(i); + } +} \ No newline at end of file diff --git a/src/dusk/entity/entitymanager.h b/src/dusk/entity/entitymanager.h new file mode 100644 index 00000000..c225d6c6 --- /dev/null +++ b/src/dusk/entity/entitymanager.h @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "entity.h" + +typedef struct { + entity_t entities[ENTITY_COUNT_MAX]; + component_t components[ENTITY_COUNT_MAX * ENTITY_COMPONENT_COUNT_MAX]; + componentid_t entitiesWithComponent[COMPONENT_TYPE_COUNT * ENTITY_COUNT_MAX]; +} entitymanager_t; + +extern entitymanager_t ENTITY_MANAGER; + +/** + * Initializes the entity manager. + */ +void entityManagerInit(void); + +/** + * Adds / Reserves a new entity ID. + * + * @return The new entity ID. + */ +entityid_t entityManagerAdd(); + +/** + * Runs fixedUpdate on every active component that defines one. Should be + * called once per fixed timestep (see time.h's DUSK_TIME_DYNAMIC/ + * TIME.dynamicUpdate) - i.e. every frame on platforms without dynamic + * timing, or only on non-dynamic frames on platforms with it. + */ +void entityManagerFixedUpdate(void); + +/** + * Disposes of the entity manager, in turn freeing all entities and components. + */ +void entityManagerDispose(void); \ No newline at end of file diff --git a/src/dusk/entity/entityrender.c b/src/dusk/entity/entityrender.c new file mode 100644 index 00000000..4f5610f5 --- /dev/null +++ b/src/dusk/entity/entityrender.c @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "entityrender.h" +#include "entity.h" +#include "entitymanager.h" +#include "entity/component/display/entityposition.h" +#include "entity/component/display/entitycamera.h" +#include "entity/component/display/entityrenderable.h" +#include "display/display.h" +#include "display/displaystate.h" +#include "display/shader/shaderunlit.h" +#include "display/mesh/mesh.h" + +errorret_t entityRenderAll(void) { + entityid_t camId = entityCameraGetCurrent(); + if(camId == ENTITY_COUNT_MAX) errorOk(); + + componentid_t camPosComp = entityGetComponent(camId, COMPONENT_TYPE_POSITION); + componentid_t camCompId = entityGetComponent(camId, COMPONENT_TYPE_CAMERA); + if(camPosComp == COMPONENT_ID_INVALID) errorOk(); + + mat4 view, proj; + entityPositionGetTransform(camId, camPosComp, view); + entityCameraGetProjection(camId, camCompId, proj); + + errorChain(shaderBind(&SHADER_UNLIT)); + errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, view)); + errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, proj)); + + errorChain(displaySetState((displaystate_t){ + .flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL + })); + + entityid_t rendEnts[ENTITY_COUNT_MAX]; + componentid_t rendComps[ENTITY_COUNT_MAX]; + entityid_t count = componentGetEntitiesWithComponent( + COMPONENT_TYPE_RENDERABLE, rendEnts, rendComps + ); + + for(entityid_t i = 0; i < count; i++) { + entityrenderable_t *r = (entityrenderable_t *)componentGetData( + rendEnts[i], rendComps[i], COMPONENT_TYPE_RENDERABLE + ); + if(r->type != ENTITY_RENDERABLE_TYPE_MATERIAL) continue; + if(r->mesh == NULL) continue; + + mat4 model; + componentid_t posComp = entityGetComponent(rendEnts[i], COMPONENT_TYPE_POSITION); + if(posComp != COMPONENT_ID_INVALID) { + entityPositionGetTransform(rendEnts[i], posComp, model); + } else { + glm_mat4_identity(model); + } + + errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, model)); + errorChain(shaderSetMaterial(&SHADER_UNLIT, &r->material)); + errorChain(meshDraw(r->mesh, 0, -1)); + } + + errorOk(); +} diff --git a/src/dusk/entity/entityrender.h b/src/dusk/entity/entityrender.h new file mode 100644 index 00000000..4971df59 --- /dev/null +++ b/src/dusk/entity/entityrender.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +/** + * Draws every entity with a RENDERABLE component, from the perspective of + * whichever entity currently holds the active CAMERA component. A no-op if + * no camera is active. Entities and their components are created and + * managed entirely from script - this is the native side of that contract. + * + * @return An error if rendering failed, or errorOk() if it succeeded. + */ +errorret_t entityRenderAll(void); diff --git a/src/dusk/physics/CMakeLists.txt b/src/dusk/physics/CMakeLists.txt new file mode 100644 index 00000000..cc51815a --- /dev/null +++ b/src/dusk/physics/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Sources +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + physicsmanager.c + physicsworld.c + physicstest.c +) \ No newline at end of file diff --git a/src/dusk/physics/physicsbodytype.h b/src/dusk/physics/physicsbodytype.h new file mode 100644 index 00000000..7c5ffd4e --- /dev/null +++ b/src/dusk/physics/physicsbodytype.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef enum { + /** + * Never moves. Acts as an immovable collision surface. + */ + PHYSICS_BODY_STATIC, + + /** + * Simulated by the world step: gravity, forces, and collision response. + */ + PHYSICS_BODY_DYNAMIC, + + /** + * Moved programmatically via physicsWorldMoveBody; collides but is not + * driven by the simulation. Typical use: player character controller. + */ + PHYSICS_BODY_KINEMATIC +} physicsbodytype_t; diff --git a/src/dusk/physics/physicsmanager.c b/src/dusk/physics/physicsmanager.c new file mode 100644 index 00000000..a25b43fe --- /dev/null +++ b/src/dusk/physics/physicsmanager.c @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "physicsmanager.h" +#include "time/time.h" + +void physicsManagerInit(void) { + physicsWorldInit(); +} + +void physicsManagerUpdate() { + #if DUSK_TIME_DYNAMIC + if(TIME.dynamicUpdate) return; // Don't update on dynamic updates. + #endif + + physicsWorldStep(TIME.delta); +} \ No newline at end of file diff --git a/src/dusk/physics/physicsmanager.h b/src/dusk/physics/physicsmanager.h new file mode 100644 index 00000000..9492a46b --- /dev/null +++ b/src/dusk/physics/physicsmanager.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "physicsworld.h" + +/** + * Initializes the physics manager. + */ +void physicsManagerInit(void); + +/** + * Advances the physics simulation. + */ +void physicsManagerUpdate(); \ No newline at end of file diff --git a/src/dusk/physics/physicsshape.h b/src/dusk/physics/physicsshape.h new file mode 100644 index 00000000..bcf22014 --- /dev/null +++ b/src/dusk/physics/physicsshape.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 "dusk.h" + +typedef enum { + PHYSICS_SHAPE_CUBE, + PHYSICS_SHAPE_SPHERE, + PHYSICS_SHAPE_CAPSULE, + PHYSICS_SHAPE_PLANE +} physicshapetype_t; + +typedef struct { + vec3 halfExtents; +} physicsshapecube_t; + +typedef struct { + float_t radius; +} physicsshapesphere_t; + +typedef struct { + float_t radius; + float_t halfHeight; +} physicsshapecapsule_t; + +typedef struct { + vec3 normal; + float_t distance; +} physicsshapeplane_t; + +typedef union { + physicsshapecube_t cube; + physicsshapesphere_t sphere; + physicsshapecapsule_t capsule; + physicsshapeplane_t plane; +} physicsshapedata_t; + +typedef struct { + physicshapetype_t type; + physicsshapedata_t data; +} physicsshape_t; diff --git a/src/dusk/physics/physicstest.c b/src/dusk/physics/physicstest.c new file mode 100644 index 00000000..53d85779 --- /dev/null +++ b/src/dusk/physics/physicstest.c @@ -0,0 +1,402 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "physicstest.h" + +bool physicsTestAabbVsAabb( + const vec3 ac, const vec3 ah, + const vec3 bc, const vec3 bh, + vec3 outNormal, float_t *outDepth +) { + float_t dx = ac[0] - bc[0]; + float_t dy = ac[1] - bc[1]; + float_t dz = ac[2] - bc[2]; + + float_t px = (ah[0] + bh[0]) - fabsf(dx); + float_t py = (ah[1] + bh[1]) - fabsf(dy); + float_t pz = (ah[2] + bh[2]) - fabsf(dz); + + if(px <= 0.0f || py <= 0.0f || pz <= 0.0f) return false; + + outNormal[0] = outNormal[1] = outNormal[2] = 0.0f; + if(px < py && px < pz) { + *outDepth = px; + outNormal[0] = dx >= 0.0f ? 1.0f : -1.0f; + } else if(py < pz) { + *outDepth = py; + outNormal[1] = dy >= 0.0f ? 1.0f : -1.0f; + } else { + *outDepth = pz; + outNormal[2] = dz >= 0.0f ? 1.0f : -1.0f; + } + return true; +} + +bool physicsTestSphereVsSphere( + const vec3 ac, const float_t ar, + const vec3 bc, const float_t br, + vec3 outNormal, float_t *outDepth +) { + vec3 diff; + glm_vec3_sub((float_t *)ac, (float_t *)bc, diff); + float_t dist2 = glm_vec3_norm2(diff); + float_t sumR = ar + br; + + if(dist2 >= sumR * sumR) return false; + + float_t dist = sqrtf(dist2); + *outDepth = sumR - dist; + + if(dist > 1e-6f) { + glm_vec3_scale(diff, 1.0f / dist, outNormal); + } else { + outNormal[0] = 0.0f; + outNormal[1] = 1.0f; + outNormal[2] = 0.0f; + } + return true; +} + +bool physicsTestSphereVsAabb( + const vec3 sc, const float_t sr, + const vec3 ac, const vec3 ah, + vec3 outNormal, float_t *outDepth +) { + vec3 closest = { + glm_clamp(sc[0], ac[0] - ah[0], ac[0] + ah[0]), + glm_clamp(sc[1], ac[1] - ah[1], ac[1] + ah[1]), + glm_clamp(sc[2], ac[2] - ah[2], ac[2] + ah[2]) + }; + + vec3 diff; + glm_vec3_sub((float_t *)sc, closest, diff); + float_t dist2 = glm_vec3_norm2(diff); + + bool inside = (dist2 < 1e-10f); + if(!inside && dist2 >= sr * sr) return false; + + if(!inside) { + float_t dist = sqrtf(dist2); + *outDepth = sr - dist; + glm_vec3_scale(diff, 1.0f / dist, outNormal); + } else { + float_t faces[6] = { + (ac[0] + ah[0]) - sc[0], + sc[0] - (ac[0] - ah[0]), + (ac[1] + ah[1]) - sc[1], + sc[1] - (ac[1] - ah[1]), + (ac[2] + ah[2]) - sc[2], + sc[2] - (ac[2] - ah[2]) + }; + const float_t normals[6][3] = { + {1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1} + }; + int mi = 0; + for(int k = 1; k < 6; k++) { + if(faces[k] < faces[mi]) mi = k; + } + *outDepth = sr + faces[mi]; + outNormal[0] = normals[mi][0]; + outNormal[1] = normals[mi][1]; + outNormal[2] = normals[mi][2]; + } + return true; +} + +bool physicsTestSphereVsPlane( + const vec3 sc, const float_t sr, + const vec3 pn, const float_t pd, + vec3 outNormal, float_t *outDepth +) { + float_t signedDist = glm_vec3_dot((float_t *)pn, (float_t *)sc) - pd; + *outDepth = sr - signedDist; + if(*outDepth <= 0.0f) return false; + glm_vec3_copy((float_t *)pn, outNormal); + return true; +} + +bool physicsTestAabbVsPlane( + const vec3 ac, const vec3 ah, + const vec3 pn, const float_t pd, + vec3 outNormal, float_t *outDepth +) { + float_t proj = fabsf(pn[0] * ah[0]) + + fabsf(pn[1] * ah[1]) + + fabsf(pn[2] * ah[2]); + float_t signedDist = glm_vec3_dot((float_t *)pn, (float_t *)ac) - pd; + *outDepth = proj - signedDist; + if(*outDepth <= 0.0f) return false; + glm_vec3_copy((float_t *)pn, outNormal); + return true; +} + +void physicsTestClosestPointOnSegment( + const vec3 a, const vec3 b, const vec3 p, vec3 out +) { + vec3 ab, ap; + glm_vec3_sub((float_t *)b, (float_t *)a, ab); + glm_vec3_sub((float_t *)p, (float_t *)a, ap); + float_t denom = glm_vec3_dot(ab, ab); + float_t t = (denom > 1e-10f) + ? glm_clamp(glm_vec3_dot(ap, ab) / denom, 0.0f, 1.0f) + : 0.0f; + glm_vec3_lerp((float_t *)a, (float_t *)b, t, out); +} + +void physicsTestClosestPointsBetweenSegments( + const vec3 a1, const vec3 b1, + const vec3 a2, const vec3 b2, + vec3 outP1, vec3 outP2 +) { + vec3 d1, d2, r; + glm_vec3_sub((float_t *)b1, (float_t *)a1, d1); + glm_vec3_sub((float_t *)b2, (float_t *)a2, d2); + glm_vec3_sub((float_t *)a1, (float_t *)a2, r); + + float_t a = glm_vec3_dot(d1, d1); + float_t e = glm_vec3_dot(d2, d2); + float_t f = glm_vec3_dot(d2, r); + float_t s, t; + + if(a <= 1e-10f && e <= 1e-10f) { + glm_vec3_copy((float_t *)a1, outP1); + glm_vec3_copy((float_t *)a2, outP2); + return; + } + if(a <= 1e-10f) { + t = 0.0f; + s = glm_clamp(f / e, 0.0f, 1.0f); + } else { + float_t c = glm_vec3_dot(d1, r); + if(e <= 1e-10f) { + s = 0.0f; + t = glm_clamp(-c / a, 0.0f, 1.0f); + } else { + float_t b = glm_vec3_dot(d1, d2); + float_t denom = a * e - b * b; + t = (fabsf(denom) > 1e-10f) + ? glm_clamp((b * f - c * e) / denom, 0.0f, 1.0f) + : 0.0f; + s = glm_clamp((b * t + f) / e, 0.0f, 1.0f); + t = glm_clamp((b * s - c) / a, 0.0f, 1.0f); + } + } + glm_vec3_lerp((float_t *)a1, (float_t *)b1, t, outP1); + glm_vec3_lerp((float_t *)a2, (float_t *)b2, s, outP2); +} + +bool physicsTestCapsuleVsSphere( + const vec3 cc, const float_t cr, const float_t chh, + const vec3 sc, const float_t sr, + vec3 outNormal, float_t *outDepth +) { + vec3 capA = { cc[0], cc[1] - chh, cc[2] }; + vec3 capB = { cc[0], cc[1] + chh, cc[2] }; + vec3 closest; + physicsTestClosestPointOnSegment(capA, capB, sc, closest); + return physicsTestSphereVsSphere( + closest, cr, sc, sr, outNormal, outDepth + ); +} + +bool physicsTestCapsuleVsAabb( + const vec3 cc, const float_t cr, const float_t chh, + const vec3 ac, const vec3 ah, + vec3 outNormal, float_t *outDepth +) { + vec3 capA = { cc[0], cc[1] - chh, cc[2] }; + vec3 capB = { cc[0], cc[1] + chh, cc[2] }; + vec3 closest; + physicsTestClosestPointOnSegment(capA, capB, ac, closest); + return physicsTestSphereVsAabb( + closest, cr, ac, ah, outNormal, outDepth + ); +} + +bool physicsTestCapsuleVsPlane( + const vec3 cc, const float_t cr, const float_t chh, + const vec3 pn, const float_t pd, + vec3 outNormal, float_t *outDepth +) { + vec3 capA = { cc[0], cc[1] - chh, cc[2] }; + vec3 capB = { cc[0], cc[1] + chh, cc[2] }; + float_t da = glm_vec3_dot((float_t *)pn, capA) - pd; + float_t db = glm_vec3_dot((float_t *)pn, capB) - pd; + float_t minDist = (da < db) ? da : db; + *outDepth = cr - minDist; + if(*outDepth <= 0.0f) return false; + glm_vec3_copy((float_t *)pn, outNormal); + return true; +} + +bool physicsTestCapsuleVsCapsule( + const vec3 c1, const float_t r1, const float_t hh1, + const vec3 c2, const float_t r2, const float_t hh2, + vec3 outNormal, float_t *outDepth +) { + vec3 a1 = { c1[0], c1[1] - hh1, c1[2] }; + vec3 b1 = { c1[0], c1[1] + hh1, c1[2] }; + vec3 a2 = { c2[0], c2[1] - hh2, c2[2] }; + vec3 b2 = { c2[0], c2[1] + hh2, c2[2] }; + vec3 p1, p2; + physicsTestClosestPointsBetweenSegments(a1, b1, a2, b2, p1, p2); + return physicsTestSphereVsSphere(p1, r1, p2, r2, outNormal, outDepth); +} + +bool physicsTestDispatch( + const vec3 aPos, const physicsshape_t aShape, + const vec3 bPos, const physicsshape_t bShape, + vec3 outNormal, float_t *outDepth +) { + physicshapetype_t ta = aShape.type; + physicshapetype_t tb = bShape.type; + + if(tb == PHYSICS_SHAPE_PLANE) { + const float_t *pn = bShape.data.plane.normal; + const float_t pd = bShape.data.plane.distance; + switch (ta) { + case PHYSICS_SHAPE_CUBE: + return physicsTestAabbVsPlane( + aPos, aShape.data.cube.halfExtents, + pn, pd, outNormal, outDepth + ); + case PHYSICS_SHAPE_SPHERE: + return physicsTestSphereVsPlane( + aPos, aShape.data.sphere.radius, + pn, pd, outNormal, outDepth + ); + case PHYSICS_SHAPE_CAPSULE: + return physicsTestCapsuleVsPlane( + aPos, + aShape.data.capsule.radius, + aShape.data.capsule.halfHeight, + pn, pd, outNormal, outDepth + ); + default: + return false; + } + } + + if(ta == PHYSICS_SHAPE_PLANE) { + vec3 tmp; float_t d; + if(!physicsTestDispatch( + bPos, bShape, aPos, aShape, tmp, &d + )) return false; + glm_vec3_scale(tmp, -1.0f, outNormal); + *outDepth = d; + return true; + } + + switch (ta) { + case PHYSICS_SHAPE_CUBE: { + const float_t *ac = aPos; + const float_t *ah = aShape.data.cube.halfExtents; + switch (tb) { + case PHYSICS_SHAPE_CUBE: + return physicsTestAabbVsAabb( + ac, ah, + bPos, bShape.data.cube.halfExtents, + outNormal, outDepth + ); + case PHYSICS_SHAPE_SPHERE: { + vec3 tmp; float_t d; + if(!physicsTestSphereVsAabb( + bPos, bShape.data.sphere.radius, + ac, ah, tmp, &d + )) return false; + glm_vec3_scale(tmp, -1.0f, outNormal); + *outDepth = d; + return true; + } + case PHYSICS_SHAPE_CAPSULE: { + vec3 tmp; float_t d; + if(!physicsTestCapsuleVsAabb( + bPos, + bShape.data.capsule.radius, + bShape.data.capsule.halfHeight, + ac, ah, tmp, &d + )) return false; + glm_vec3_scale(tmp, -1.0f, outNormal); + *outDepth = d; + return true; + } + default: return false; + } + } + + case PHYSICS_SHAPE_SPHERE: { + const float_t sr = aShape.data.sphere.radius; + switch (tb) { + case PHYSICS_SHAPE_CUBE: + return physicsTestSphereVsAabb( + aPos, sr, + bPos, bShape.data.cube.halfExtents, + outNormal, outDepth + ); + case PHYSICS_SHAPE_SPHERE: + return physicsTestSphereVsSphere( + aPos, sr, + bPos, bShape.data.sphere.radius, + outNormal, outDepth + ); + case PHYSICS_SHAPE_CAPSULE: { + vec3 tmp; float_t d; + if(!physicsTestCapsuleVsSphere( + bPos, + bShape.data.capsule.radius, + bShape.data.capsule.halfHeight, + aPos, sr, tmp, &d + )) return false; + glm_vec3_scale(tmp, -1.0f, outNormal); + *outDepth = d; + return true; + } + default: return false; + } + } + + case PHYSICS_SHAPE_CAPSULE: { + const float_t cr = aShape.data.capsule.radius; + const float_t chh = aShape.data.capsule.halfHeight; + switch (tb) { + case PHYSICS_SHAPE_CUBE: + return physicsTestCapsuleVsAabb( + aPos, cr, chh, + bPos, bShape.data.cube.halfExtents, + outNormal, outDepth + ); + case PHYSICS_SHAPE_SPHERE: + return physicsTestCapsuleVsSphere( + aPos, cr, chh, + bPos, bShape.data.sphere.radius, + outNormal, outDepth + ); + case PHYSICS_SHAPE_CAPSULE: + return physicsTestCapsuleVsCapsule( + aPos, cr, chh, + bPos, + bShape.data.capsule.radius, + bShape.data.capsule.halfHeight, + outNormal, outDepth + ); + default: return false; + } + } + + default: return false; + } +} + +bool_t physicsTestShapeVsShape( + const vec3 aPos, const physicsshape_t aShape, + const vec3 bPos, const physicsshape_t bShape, + vec3 outNormal, float_t *outDepth +) { + return physicsTestDispatch( + aPos, aShape, bPos, bShape, outNormal, outDepth + ); +} diff --git a/src/dusk/physics/physicstest.h b/src/dusk/physics/physicstest.h new file mode 100644 index 00000000..3c02697c --- /dev/null +++ b/src/dusk/physics/physicstest.h @@ -0,0 +1,247 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "physicsshape.h" + +/** + * Tests overlap between two axis-aligned bounding boxes. + * outNormal points from B toward A. + * + * @param ac Center of AABB A. + * @param ah Half-extents of AABB A. + * @param bc Center of AABB B. + * @param bh Half-extents of AABB B. + * @param outNormal Push-out normal (B toward A). + * @param outDepth Penetration depth. + * @return true if overlapping. + */ +bool_t physicsTestAabbVsAabb( + const vec3 ac, const vec3 ah, + const vec3 bc, const vec3 bh, + vec3 outNormal, float_t *outDepth +); + +/** + * Tests overlap between two spheres. + * outNormal points from B toward A. + * + * @param ac Center of sphere A. + * @param ar Radius of sphere A. + * @param bc Center of sphere B. + * @param br Radius of sphere B. + * @param outNormal Push-out normal (B toward A). + * @param outDepth Penetration depth. + * @return true if overlapping. + */ +bool_t physicsTestSphereVsSphere( + const vec3 ac, const float_t ar, + const vec3 bc, const float_t br, + vec3 outNormal, float_t *outDepth +); + +/** + * Tests overlap between a sphere and an axis-aligned bounding box. + * outNormal points from the AABB toward the sphere. + * + * @param sc Center of the sphere. + * @param sr Radius of the sphere. + * @param ac Center of the AABB. + * @param ah Half-extents of the AABB. + * @param outNormal Push-out normal (AABB toward sphere). + * @param outDepth Penetration depth. + * @return true if overlapping. + */ +bool_t physicsTestSphereVsAabb( + const vec3 sc, const float_t sr, + const vec3 ac, const vec3 ah, + vec3 outNormal, float_t *outDepth +); + +/** + * Tests overlap between a sphere and an infinite plane. + * outNormal equals the plane normal (pointing away from the surface). + * + * @param sc Center of the sphere. + * @param sr Radius of the sphere. + * @param pn Plane normal (unit vector, world-space). + * @param pd Plane offset: dot(pn, surfacePoint) == pd. + * @param outNormal Push-out normal (equals pn). + * @param outDepth Penetration depth. + * @return true if overlapping. + */ +bool_t physicsTestSphereVsPlane( + const vec3 sc, const float_t sr, + const vec3 pn, const float_t pd, + vec3 outNormal, float_t *outDepth +); + +/** + * Tests overlap between an AABB and an infinite plane. + * outNormal equals the plane normal. + * + * @param ac Center of the AABB. + * @param ah Half-extents of the AABB. + * @param pn Plane normal (unit vector, world-space). + * @param pd Plane offset (see physicsTestSphereVsPlane). + * @param outNormal Push-out normal (equals pn). + * @param outDepth Penetration depth. + * @return true if overlapping. + */ +bool_t physicsTestAabbVsPlane( + const vec3 ac, const vec3 ah, + const vec3 pn, const float_t pd, + vec3 outNormal, float_t *outDepth +); + +/** + * Finds the closest point on segment [a, b] to query point p. + * + * @param a Start of the segment. + * @param b End of the segment. + * @param p Query point. + * @param out Receives the closest point on [a, b] to p. + */ +void physicsTestClosestPointOnSegment( + const vec3 a, const vec3 b, const vec3 p, vec3 out +); + +/** + * Finds the closest points between two line segments. + * + * @param a1 Start of segment 1. + * @param b1 End of segment 1. + * @param a2 Start of segment 2. + * @param b2 End of segment 2. + * @param outP1 Receives the closest point on segment 1. + * @param outP2 Receives the closest point on segment 2. + */ +void physicsTestClosestPointsBetweenSegments( + const vec3 a1, const vec3 b1, + const vec3 a2, const vec3 b2, + vec3 outP1, vec3 outP2 +); + +/** + * Tests overlap between a Y-axis-aligned capsule and a sphere. + * outNormal points from the sphere toward the capsule. + * + * @param cc Center of the capsule. + * @param cr Radius of the capsule. + * @param chh Half-height of the capsule's cylindrical segment. + * @param sc Center of the sphere. + * @param sr Radius of the sphere. + * @param outNormal Push-out normal (sphere toward capsule). + * @param outDepth Penetration depth. + * @return true if overlapping. + */ +bool_t physicsTestCapsuleVsSphere( + const vec3 cc, const float_t cr, const float_t chh, + const vec3 sc, const float_t sr, + vec3 outNormal, float_t *outDepth +); + +/** + * Tests overlap between a Y-axis-aligned capsule and an AABB. + * outNormal points from the AABB toward the capsule. + * + * @param cc Center of the capsule. + * @param cr Radius of the capsule. + * @param chh Half-height of the capsule's cylindrical segment. + * @param ac Center of the AABB. + * @param ah Half-extents of the AABB. + * @param outNormal Push-out normal (AABB toward capsule). + * @param outDepth Penetration depth. + * @return true if overlapping. + */ +bool_t physicsTestCapsuleVsAabb( + const vec3 cc, const float_t cr, const float_t chh, + const vec3 ac, const vec3 ah, + vec3 outNormal, float_t *outDepth +); + +/** + * Tests overlap between a Y-axis-aligned capsule and an infinite plane. + * outNormal equals the plane normal. + * + * @param cc Center of the capsule. + * @param cr Radius of the capsule. + * @param chh Half-height of the capsule's cylindrical segment. + * @param pn Plane normal (unit vector, world-space). + * @param pd Plane offset (see physicsTestSphereVsPlane). + * @param outNormal Push-out normal (equals pn). + * @param outDepth Penetration depth. + * @return true if overlapping. + */ +bool_t physicsTestCapsuleVsPlane( + const vec3 cc, const float_t cr, const float_t chh, + const vec3 pn, const float_t pd, + vec3 outNormal, float_t *outDepth +); + +/** + * Tests overlap between two Y-axis-aligned capsules. + * outNormal points from capsule B toward capsule A. + * + * @param c1 Center of capsule A. + * @param r1 Radius of capsule A. + * @param hh1 Half-height of capsule A's cylindrical segment. + * @param c2 Center of capsule B. + * @param r2 Radius of capsule B. + * @param hh2 Half-height of capsule B's cylindrical segment. + * @param outNormal Push-out normal (B toward A). + * @param outDepth Penetration depth. + * @return true if overlapping. + */ +bool_t physicsTestCapsuleVsCapsule( + const vec3 c1, const float_t r1, const float_t hh1, + const vec3 c2, const float_t r2, const float_t hh2, + vec3 outNormal, float_t *outDepth +); + +/** + * Routes a shape-pair collision test to the correct primitive. + * When A is a plane, delegates with swapped arguments and negates + * the resulting normal. outNormal points from B toward A. + * + * @param aPos Position of shape A. + * @param aShape Shape descriptor of A. + * @param bPos Position of shape B. + * @param bShape Shape descriptor of B. + * @param outNormal Push-out normal (B toward A). + * @param outDepth Penetration depth. + * @return true if overlapping. + */ +bool_t physicsTestDispatch( + const vec3 aPos, const physicsshape_t aShape, + const vec3 bPos, const physicsshape_t bShape, + vec3 outNormal, float_t *outDepth +); + +/** + * Tests for collision between two shapes. Returns true if they + * overlap, and if so, outputs the push-out normal and depth. + * + * outNormal always points from shape B toward shape A, so adding + * (outNormal * outDepth) to A's position separates the two shapes. + * + * @param aPos Position of shape A. + * @param aShape Shape descriptor of A. + * @param bPos Position of shape B. + * @param bShape Shape descriptor of B. + * @param outNormal Push-out normal, pointing from B toward A. + * @param outDepth Penetration depth (positive when overlapping). + * @return true if the shapes overlap, false otherwise. + */ +bool_t physicsTestShapeVsShape( + const vec3 aPos, + const physicsshape_t aShape, + const vec3 bPos, + const physicsshape_t bShape, + vec3 outNormal, + float_t *outDepth +); diff --git a/src/dusk/physics/physicsworld.c b/src/dusk/physics/physicsworld.c new file mode 100644 index 00000000..02e24ec7 --- /dev/null +++ b/src/dusk/physics/physicsworld.c @@ -0,0 +1,151 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "physicsworld.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "entity/entity.h" +#include "entity/component.h" +#include "physicstest.h" + +physicsworld_t PHYSICS_WORLD; + +void physicsWorldInit() { + memoryZero(&PHYSICS_WORLD, sizeof(physicsworld_t)); + + PHYSICS_WORLD.gravity[0] = 0.0f; + PHYSICS_WORLD.gravity[1] = -9.81f; + PHYSICS_WORLD.gravity[2] = 0.0f; +} + +void physicsWorldStep(const float_t dt) { + assertTrue(dt > 0.0f, "Delta time must be positive"); + + entityid_t physEnts[ENTITY_COUNT_MAX]; + componentid_t physComps[ENTITY_COUNT_MAX]; + entityid_t physCount = componentGetEntitiesWithComponent( + COMPONENT_TYPE_PHYSICS, physEnts, physComps + ); + + /* Pre-fetch all position and physics pointers once. */ + entityposition_t *positions[ENTITY_COUNT_MAX]; + entityphysics_t *physBodies[ENTITY_COUNT_MAX]; + for(entityid_t i = 0; i < physCount; i++) { + componentid_t posComp = entityGetComponent( + physEnts[i], COMPONENT_TYPE_POSITION + ); + positions[i] = (posComp != 0xFF) + ? entityPositionGet(physEnts[i], posComp) + : NULL; + physBodies[i] = entityPhysicsGet(physEnts[i], physComps[i]); + } + + /* Phase 1: integrate dynamic bodies (gravity + velocity → position). + * Writes directly to pos->position, matrix rebuilt at end. */ + for(entityid_t i = 0; i < physCount; i++) { + if(!positions[i]) continue; + entityphysics_t *phys = physBodies[i]; + if(phys->type != PHYSICS_BODY_DYNAMIC) continue; + + phys->onGround = false; + + phys->velocity[0] += PHYSICS_WORLD.gravity[0] * phys->gravityScale * dt; + phys->velocity[1] += PHYSICS_WORLD.gravity[1] * phys->gravityScale * dt; + phys->velocity[2] += PHYSICS_WORLD.gravity[2] * phys->gravityScale * dt; + + float_t *pos = positions[i]->position; + pos[0] += phys->velocity[0] * dt; + pos[1] += phys->velocity[1] * dt; + pos[2] += phys->velocity[2] * dt; + } + + /* Phase 2: dynamic vs static/kinematic. */ + for(entityid_t i = 0; i < physCount; i++) { + if(!positions[i]) continue; + entityphysics_t *phys = physBodies[i]; + if(phys->type != PHYSICS_BODY_DYNAMIC) continue; + + float_t *pos = positions[i]->position; + + for(entityid_t j = 0; j < physCount; j++) { + if(i == j || !positions[j]) continue; + entityphysics_t *otherPhys = physBodies[j]; + if(otherPhys->type == PHYSICS_BODY_DYNAMIC) continue; + + vec3 normal; float_t depth; + if(!physicsTestShapeVsShape( + pos, phys->shape, + positions[j]->position, otherPhys->shape, + normal, &depth + )) continue; + + pos[0] += normal[0] * depth; + pos[1] += normal[1] * depth; + pos[2] += normal[2] * depth; + + float_t vn = glm_vec3_dot(phys->velocity, normal); + if(vn < 0.0f) { + phys->velocity[0] -= vn * normal[0]; + phys->velocity[1] -= vn * normal[1]; + phys->velocity[2] -= vn * normal[2]; + } + + if(normal[1] > PHYSICS_GROUND_THRESHOLD) phys->onGround = true; + } + } + + /* Phase 3: dynamic vs dynamic. */ + for(entityid_t i = 0; i < physCount; i++) { + if(!positions[i]) continue; + entityphysics_t *physA = physBodies[i]; + if(physA->type != PHYSICS_BODY_DYNAMIC) continue; + + float_t *posA = positions[i]->position; + + for(entityid_t j = i + 1; j < physCount; j++) { + if(!positions[j]) continue; + entityphysics_t *physB = physBodies[j]; + if(physB->type != PHYSICS_BODY_DYNAMIC) continue; + + float_t *posB = positions[j]->position; + + vec3 normal; float_t depth; + if(!physicsTestShapeVsShape( + posA, physA->shape, posB, physB->shape, normal, &depth + )) continue; + + posA[0] += normal[0] * depth * 0.5f; + posA[1] += normal[1] * depth * 0.5f; + posA[2] += normal[2] * depth * 0.5f; + + posB[0] -= normal[0] * depth * 0.5f; + posB[1] -= normal[1] * depth * 0.5f; + posB[2] -= normal[2] * depth * 0.5f; + + float_t v_rel = glm_vec3_dot(physA->velocity, normal) + - glm_vec3_dot(physB->velocity, normal); + if(v_rel < 0.0f) { + physA->velocity[0] -= v_rel * normal[0]; + physA->velocity[1] -= v_rel * normal[1]; + physA->velocity[2] -= v_rel * normal[2]; + physB->velocity[0] += v_rel * normal[0]; + physB->velocity[1] += v_rel * normal[1]; + physB->velocity[2] += v_rel * normal[2]; + } + + if( normal[1] > PHYSICS_GROUND_THRESHOLD) physA->onGround = true; + if(-normal[1] > PHYSICS_GROUND_THRESHOLD) physB->onGround = true; + } + } + + /* Rebuild transforms for all dynamic bodies once, after all phases. */ + for(entityid_t i = 0; i < physCount; i++) { + if(!positions[i]) continue; + if(physBodies[i]->type != PHYSICS_BODY_DYNAMIC) continue; + entityPositionRebuild(positions[i]); + } +} diff --git a/src/dusk/physics/physicsworld.h b/src/dusk/physics/physicsworld.h new file mode 100644 index 00000000..a06dc5c8 --- /dev/null +++ b/src/dusk/physics/physicsworld.h @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "physics/physicsshape.h" +#include "physics/physicsbodytype.h" + +#define PHYSICS_GROUND_THRESHOLD 0.707f + +typedef struct { + vec3 gravity; +} physicsworld_t; + +extern physicsworld_t PHYSICS_WORLD; + +/** + * Initializes the physics world. + */ +void physicsWorldInit(void); + +/** + * Steps the physics simulation forward. + * + * @param dt The time delta in seconds since the last step. + */ +void physicsWorldStep(const float_t dt); \ No newline at end of file diff --git a/src/dusk/script/CMakeLists.txt b/src/dusk/script/CMakeLists.txt new file mode 100644 index 00000000..751142f2 --- /dev/null +++ b/src/dusk/script/CMakeLists.txt @@ -0,0 +1,13 @@ +# Copyright (c) 2025 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Sources +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + scriptmanager.c + scriptproto.c +) + +# Subdirectories \ No newline at end of file diff --git a/src/dusk/script/module/animation/moduleeasing.h b/src/dusk/script/module/animation/moduleeasing.h new file mode 100644 index 00000000..f0f171b0 --- /dev/null +++ b/src/dusk/script/module/animation/moduleeasing.h @@ -0,0 +1,70 @@ +// 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 "animation/easing.h" + +static scriptproto_t MODULE_EASING_PROTO; + +// Generate one handler per easing curve. +#define EASING_MODULE_TABLE \ + X(Linear, "linear", EASING_LINEAR, easingLinear) \ + X(InSine, "inSine", EASING_IN_SINE, easingInSine) \ + X(OutSine, "outSine", EASING_OUT_SINE, easingOutSine) \ + X(InOutSine, "inOutSine", EASING_IN_OUT_SINE, easingInOutSine) \ + X(InQuad, "inQuad", EASING_IN_QUAD, easingInQuad) \ + X(OutQuad, "outQuad", EASING_OUT_QUAD, easingOutQuad) \ + X(InOutQuad, "inOutQuad", EASING_IN_OUT_QUAD, easingInOutQuad) \ + X(InCubic, "inCubic", EASING_IN_CUBIC, easingInCubic) \ + X(OutCubic, "outCubic", EASING_OUT_CUBIC, easingOutCubic) \ + X(InOutCubic, "inOutCubic", EASING_IN_OUT_CUBIC, easingInOutCubic) \ + X(InQuart, "inQuart", EASING_IN_QUART, easingInQuart) \ + X(OutQuart, "outQuart", EASING_OUT_QUART, easingOutQuart) \ + X(InOutQuart, "inOutQuart", EASING_IN_OUT_QUART, easingInOutQuart) \ + X(InBack, "inBack", EASING_IN_BACK, easingInBack) \ + X(OutBack, "outBack", EASING_OUT_BACK, easingOutBack) \ + X(InOutBack, "inOutBack", EASING_IN_OUT_BACK, easingInOutBack) + +#define X(CName, jsName, type, fn) \ + moduleBaseFunction(moduleEasing##CName) { \ + if(argc < 1 || !jerry_value_is_number(args[0])) { \ + return moduleBaseThrow("Expected number t"); \ + } \ + float_t t = (float_t)jerry_value_as_number(args[0]); \ + return jerry_number((double)fn(t)); \ + } +EASING_MODULE_TABLE +#undef X + +// Adds a callable easing function with a .type property to the Easing object. +static void moduleEasingRegister( + const char_t *jsName, + uint8_t type, + jerry_external_handler_t fn +) { + jerry_value_t fnVal = jerry_function_external(fn); + + jerry_value_t typeKey = jerry_string_sz("type"); + jerry_value_t typeVal = jerry_number((double)type); + jerry_object_set(fnVal, typeKey, typeVal); + jerry_value_free(typeKey); + jerry_value_free(typeVal); + + jerry_value_t nameKey = jerry_string_sz(jsName); + jerry_object_set(MODULE_EASING_PROTO.prototype, nameKey, fnVal); + jerry_value_free(nameKey); + jerry_value_free(fnVal); +} + +static void moduleEasing(void) { + scriptProtoInit(&MODULE_EASING_PROTO, "Easing", sizeof(uint8_t), NULL); + + #define X(CName, jsName, type, fn) \ + moduleEasingRegister(jsName, type, moduleEasing##CName); + EASING_MODULE_TABLE + #undef X +} diff --git a/src/dusk/script/module/console/moduleconsole.h b/src/dusk/script/module/console/moduleconsole.h new file mode 100644 index 00000000..c09de362 --- /dev/null +++ b/src/dusk/script/module/console/moduleconsole.h @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/module/modulebase.h" +#include "script/scriptproto.h" +#include "console/console.h" + +static scriptproto_t MODULE_CONSOLE_PROTO; + +moduleBaseFunction(moduleConsolePrint) { + char_t buf[512]; + char_t msg[4096]; + size_t msgLen = 0; + + for(jerry_length_t i = 0; i < argc; ++i) { + jerry_value_t strVal = jerry_value_to_string(args[i]); + moduleBaseToString(strVal, buf, sizeof(buf)); + jerry_value_free(strVal); + + size_t partLen = strlen(buf); + if(msgLen + partLen + 1 < sizeof(msg)) { + stringCopy(msg + msgLen, buf, sizeof(msg) - msgLen); + msgLen += partLen; + } + + if(i + 1 < argc && msgLen + 1 < sizeof(msg)) { + msg[msgLen++] = '\t'; + msg[msgLen] = '\0'; + } + } + + consolePrint("%s", msg); + return jerry_undefined(); +} + +moduleBaseFunction(moduleConsoleGetVisible) { + return jerry_boolean(CONSOLE.visible); +} + +moduleBaseFunction(moduleConsoleSetVisible) { + moduleBaseRequireArgs(1); + CONSOLE.visible = moduleBaseArgBool(0); + return jerry_undefined(); +} + +static void moduleConsole(void) { + scriptProtoInit( + &MODULE_CONSOLE_PROTO, "Console", + sizeof(uint8_t), NULL + ); + + scriptProtoDefineStaticFunc( + &MODULE_CONSOLE_PROTO, "print", moduleConsolePrint + ); + scriptProtoDefineStaticProp( + &MODULE_CONSOLE_PROTO, "visible", + moduleConsoleGetVisible, moduleConsoleSetVisible + ); +} diff --git a/src/dusk/script/module/display/modulecolor.h b/src/dusk/script/module/display/modulecolor.h new file mode 100644 index 00000000..0228b290 --- /dev/null +++ b/src/dusk/script/module/display/modulecolor.h @@ -0,0 +1,136 @@ +/** + * 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 "display/color.h" +#include "time/time.h" +#include "script/scriptproto.h" + +static scriptproto_t MODULE_COLOR_PROTO; + +static inline color_t * moduleColorGet( + const jerry_call_info_t *callInfo +) { + return (color_t*)scriptProtoGetValue( + &MODULE_COLOR_PROTO, callInfo->this_value + ); +} + +moduleBaseFunction(moduleColorGetR) { + moduleBaseGetOrReturn(color_t, c, moduleColorGet); + return jerry_number(c->r); +} + +moduleBaseFunction(moduleColorGetG) { + moduleBaseGetOrReturn(color_t, c, moduleColorGet); + return jerry_number(c->g); +} + +moduleBaseFunction(moduleColorGetB) { + moduleBaseGetOrReturn(color_t, c, moduleColorGet); + return jerry_number(c->b); +} + +moduleBaseFunction(moduleColorGetA) { + moduleBaseGetOrReturn(color_t, c, moduleColorGet); + return jerry_number(c->a); +} + +moduleBaseFunction(moduleColorSetR) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + moduleBaseGetOrReturn(color_t, c, moduleColorGet); + c->r = (colorchannel8_t)moduleBaseArgInt(0); + return args[0]; +} + +moduleBaseFunction(moduleColorSetG) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + moduleBaseGetOrReturn(color_t, c, moduleColorGet); + c->g = (colorchannel8_t)moduleBaseArgInt(0); + return args[0]; +} + +moduleBaseFunction(moduleColorSetB) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + moduleBaseGetOrReturn(color_t, c, moduleColorGet); + c->b = (colorchannel8_t)moduleBaseArgInt(0); + return args[0]; +} + +moduleBaseFunction(moduleColorSetA) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + moduleBaseGetOrReturn(color_t, c, moduleColorGet); + c->a = (colorchannel8_t)moduleBaseArgInt(0); + return args[0]; +} + +moduleBaseFunction(moduleColorToString) { + moduleBaseGetOrReturn(color_t, c, moduleColorGet); + char_t buf[64]; + stringFormat( + buf, sizeof(buf), + "{ \"r\": %d, \"g\": %d, \"b\": %d, \"a\": %d }", + (int32_t)c->r, (int32_t)c->g, + (int32_t)c->b, (int32_t)c->a + ); + return jerry_string_sz(buf); +} + +static jerry_value_t moduleColorMakeObject(color_t color) { + return scriptProtoCreateValue(&MODULE_COLOR_PROTO, &color); +} + +moduleBaseFunction(moduleColorConstructor) { + if(argc > 0 && !jerry_value_is_number(args[0])) return moduleBaseThrow("Color: r must be a number"); + if(argc > 1 && !jerry_value_is_number(args[1])) return moduleBaseThrow("Color: g must be a number"); + if(argc > 2 && !jerry_value_is_number(args[2])) return moduleBaseThrow("Color: b must be a number"); + if(argc > 3 && !jerry_value_is_number(args[3])) return moduleBaseThrow("Color: a must be a number"); + color_t c; + c.r = (colorchannel8_t)moduleBaseOptInt(0, 255); + c.g = (colorchannel8_t)moduleBaseOptInt(1, 255); + c.b = (colorchannel8_t)moduleBaseOptInt(2, 255); + c.a = (colorchannel8_t)moduleBaseOptInt(3, 255); + return moduleColorMakeObject(c); +} + +moduleBaseFunction(moduleColorRainbow) { + float_t t = moduleBaseOptFloat(0, TIME.time * 4.0f); + if(argc >= 2 && jerry_value_is_number(args[1])) t *= moduleBaseArgFloat(1); + + color_t c; + c.r = (colorchannel8_t)((sinf(t) + 1.0f) * 0.5f * 255.0f); + c.g = (colorchannel8_t)((sinf(t + 2.0f) + 1.0f) * 0.5f * 255.0f); + c.b = (colorchannel8_t)((sinf(t + 4.0f) + 1.0f) * 0.5f * 255.0f); + c.a = 255; + return moduleColorMakeObject(c); +} + +static void moduleColor(void) { + scriptProtoInit( + &MODULE_COLOR_PROTO, "Color", sizeof(color_t), moduleColorConstructor + ); + + scriptProtoDefineProp( + &MODULE_COLOR_PROTO, "r", moduleColorGetR, moduleColorSetR + ); + scriptProtoDefineProp( + &MODULE_COLOR_PROTO, "g", moduleColorGetG, moduleColorSetG + ); + scriptProtoDefineProp( + &MODULE_COLOR_PROTO, "b", moduleColorGetB, moduleColorSetB + ); + scriptProtoDefineProp( + &MODULE_COLOR_PROTO, "a", moduleColorGetA, moduleColorSetA + ); + + scriptProtoDefineStaticFunc( + &MODULE_COLOR_PROTO, "rainbow", moduleColorRainbow + ); + scriptProtoDefineToString(&MODULE_COLOR_PROTO, moduleColorToString); + moduleBaseEval(COLOR_SCRIPT); +} diff --git a/src/dusk/script/module/display/modulemesh.h b/src/dusk/script/module/display/modulemesh.h new file mode 100644 index 00000000..163b6772 --- /dev/null +++ b/src/dusk/script/module/display/modulemesh.h @@ -0,0 +1,420 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/module/modulebase.h" +#include "script/scriptproto.h" +#include "script/module/math/modulevec3ref.h" +#include "display/mesh/mesh.h" +#include "display/mesh/cube.h" +#include "display/mesh/quad.h" +#include "display/mesh/sphere.h" +#include "display/mesh/plane.h" +#include "display/mesh/capsule.h" +#include "display/mesh/triprism.h" + +typedef struct { + mesh_t mesh; + meshvertex_t *vertices; + int32_t vertexCount; + bool_t initialized; +} meshscript_t; + +typedef struct { + meshvertex_t *vertex; +} meshvertexscript_t; + +static scriptproto_t MODULE_MESH_PROTO; +static scriptproto_t MODULE_MESH_VERTEX_PROTO; + +static inline meshscript_t * moduleMeshGet( + const jerry_call_info_t *callInfo +) { + return (meshscript_t*)scriptProtoGetValue( + &MODULE_MESH_PROTO, callInfo->this_value + ); +} + +static inline meshscript_t * moduleMeshFrom(const jerry_value_t val) { + return (meshscript_t*)scriptProtoGetValue(&MODULE_MESH_PROTO, val); +} + +static void moduleMeshFreeData( + void *ptr, + jerry_object_native_info_t *info +) { + (void)info; + meshscript_t *ms = (meshscript_t*)ptr; + if(ms->initialized) (void)meshDispose(&ms->mesh); + if(ms->vertices) memoryFree(ms->vertices); + memoryFree(ptr); +} + +// Creates a new JS Mesh object from an already-filled heap-allocated meshscript_t. +static jerry_value_t moduleMeshWrapNew(meshscript_t *ms) { + jerry_value_t obj = jerry_object(); + jerry_object_set_native_ptr(obj, &MODULE_MESH_PROTO.info, ms); + jerry_object_set_proto(obj, MODULE_MESH_PROTO.prototype); + + jerry_value_t arr = jerry_array((jerry_length_t)ms->vertexCount); + for(int32_t i = 0; i < ms->vertexCount; i++) { + meshvertexscript_t mv = { .vertex = &ms->vertices[i] }; + jerry_value_t vobj = scriptProtoCreateValue(&MODULE_MESH_VERTEX_PROTO, &mv); + jerry_value_t res = jerry_object_set_index(arr, (uint32_t)i, vobj); + jerry_value_free(res); + jerry_value_free(vobj); + } + jerry_value_t key = jerry_string_sz("_verts"); + jerry_object_set(obj, key, arr); + jerry_value_free(key); + jerry_value_free(arr); + + return obj; +} + +// Allocates a meshscript_t for the given vertex count and buffers it. +// Caller is responsible for populating ms->vertices before calling meshInit. +static meshscript_t * moduleMeshAlloc(const int32_t vertexCount) { + meshscript_t *ms = (meshscript_t*)memoryAllocate(sizeof(meshscript_t)); + memoryZero(ms, sizeof(meshscript_t)); + ms->vertices = (meshvertex_t*)memoryAllocate( + (size_t)vertexCount * sizeof(meshvertex_t) + ); + memoryZero(ms->vertices, (size_t)vertexCount * sizeof(meshvertex_t)); + ms->vertexCount = vertexCount; + return ms; +} + +// Finalizes a meshscript_t: uploads to GPU and wraps in a JS object. +static jerry_value_t moduleMeshFinalize( + meshscript_t *ms, + const meshprimitivetype_t type +) { + (void)meshInit(&ms->mesh, type, ms->vertexCount, ms->vertices); + ms->initialized = true; + return moduleMeshWrapNew(ms); +} + +// ---- MeshVertex ---- + +static inline meshvertexscript_t * moduleMeshVertexGet( + const jerry_call_info_t *callInfo +) { + return (meshvertexscript_t*)scriptProtoGetValue( + &MODULE_MESH_VERTEX_PROTO, callInfo->this_value + ); +} + +moduleBaseFunction(moduleMeshVertexGetPosition) { + moduleBaseGetOrReturn(meshvertexscript_t, mv, moduleMeshVertexGet); + return moduleVec3RefPush(mv->vertex->pos, NULL, NULL); +} + +moduleBaseFunction(moduleMeshVertexSetPosition) { + moduleBaseRequireArgs(1); + moduleBaseGetOrReturn(meshvertexscript_t, mv, moduleMeshVertexGet); + vec3 v; + if(!moduleVec3AnyCheck(args[0], v)) return moduleBaseThrow("Expected Vec3"); + glm_vec3_copy(v, mv->vertex->pos); + return jerry_undefined(); +} + +// ---- Mesh instance ---- + +moduleBaseFunction(moduleMeshConstructor) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + + int32_t vertexCount = moduleBaseArgInt(0); + if(vertexCount <= 0) return moduleBaseThrow("Vertex count must be > 0"); + + meshscript_t *ms = moduleMeshAlloc(vertexCount); + jerry_object_set_native_ptr( + callInfo->this_value, &MODULE_MESH_PROTO.info, ms + ); + + jerry_value_t arr = jerry_array((jerry_length_t)vertexCount); + for(int32_t i = 0; i < vertexCount; i++) { + meshvertexscript_t mv = { .vertex = &ms->vertices[i] }; + jerry_value_t vobj = scriptProtoCreateValue(&MODULE_MESH_VERTEX_PROTO, &mv); + jerry_value_t res = jerry_object_set_index(arr, (uint32_t)i, vobj); + jerry_value_free(res); + jerry_value_free(vobj); + } + jerry_value_t key = jerry_string_sz("_verts"); + jerry_object_set(callInfo->this_value, key, arr); + jerry_value_free(key); + jerry_value_free(arr); + + return jerry_undefined(); +} + +moduleBaseFunction(moduleMeshGetVertices) { + jerry_value_t key = jerry_string_sz("_verts"); + jerry_value_t arr = jerry_object_get(callInfo->this_value, key); + jerry_value_free(key); + return arr; +} + +moduleBaseFunction(moduleMeshGetVertexCount) { + moduleBaseGetOrReturn(meshscript_t, ms, moduleMeshGet); + return jerry_number((double)ms->vertexCount); +} + +moduleBaseFunction(moduleMeshFlush) { + moduleBaseGetOrReturn(meshscript_t, ms, moduleMeshGet); + if(!ms->initialized) { + (void)meshInit( + &ms->mesh, + MESH_PRIMITIVE_TYPE_TRIANGLES, + ms->vertexCount, + ms->vertices + ); + ms->initialized = true; + } else { + (void)meshFlush(&ms->mesh, 0, -1); + } + return jerry_undefined(); +} + +moduleBaseFunction(moduleMeshDispose) { + moduleBaseGetOrReturn(meshscript_t, ms, moduleMeshGet); + if(ms->initialized) { + (void)meshDispose(&ms->mesh); + ms->initialized = false; + } + if(ms->vertices) { + memoryFree(ms->vertices); + ms->vertices = NULL; + } + return jerry_undefined(); +} + +moduleBaseFunction(moduleMeshToString) { + meshscript_t *ms = moduleMeshGet(callInfo); + if(!ms) return jerry_string_sz("Mesh(?)"); + char_t buf[64]; + stringFormat(buf, sizeof(buf), "Mesh(%d)", ms->vertexCount); + return jerry_string_sz(buf); +} + +// ---- Static defaults (engine-owned, not GC'd) ---- + +moduleBaseFunction(moduleMeshDefaultCube) { + return moduleBaseWrapPointer(&CUBE_MESH_SIMPLE); +} +moduleBaseFunction(moduleMeshDefaultQuad) { + return moduleBaseWrapPointer(&QUAD_MESH_SIMPLE); +} +moduleBaseFunction(moduleMeshDefaultSphere) { + return moduleBaseWrapPointer(&SPHERE_MESH_SIMPLE); +} +moduleBaseFunction(moduleMeshDefaultPlane) { + return moduleBaseWrapPointer(&PLANE_MESH_SIMPLE); +} +moduleBaseFunction(moduleMeshDefaultCapsule) { + return moduleBaseWrapPointer(&CAPSULE_MESH_SIMPLE); +} +moduleBaseFunction(moduleMeshDefaultTriPrism) { + return moduleBaseWrapPointer(&TRIPRISM_MESH_SIMPLE); +} + +// ---- Static factory methods ---- + +// Mesh.createCube(min?, max?) — defaults to (-0.5,-0.5,-0.5)..(0.5,0.5,0.5) +moduleBaseFunction(moduleMeshCreateCube) { + vec3 min = { -0.5f, -0.5f, -0.5f }; + vec3 max = { 0.5f, 0.5f, 0.5f }; + if(argc >= 2) { + if(!moduleVec3AnyCheck(args[0], min)) { + return moduleBaseThrow("Mesh.createCube: expected Vec3 min"); + } + if(!moduleVec3AnyCheck(args[1], max)) { + return moduleBaseThrow("Mesh.createCube: expected Vec3 max"); + } + } + meshscript_t *ms = moduleMeshAlloc(CUBE_VERTEX_COUNT); + cubeBuffer( + ms->vertices, min, max + #if MESH_ENABLE_COLOR + , COLOR_WHITE_4B + #endif + ); + return moduleMeshFinalize(ms, CUBE_PRIMITIVE_TYPE); +} + +// Mesh.createQuad(minX, minY, maxX, maxY) — 2D XY quad, u0-u1=0-1 v0-v1=0-1 +moduleBaseFunction(moduleMeshCreateQuad) { + float_t minX = -0.5f, minY = -0.5f, maxX = 0.5f, maxY = 0.5f; + if(argc >= 4) { + if(!jerry_value_is_number(args[0]) || !jerry_value_is_number(args[1]) || + !jerry_value_is_number(args[2]) || !jerry_value_is_number(args[3])) { + return moduleBaseThrow("Mesh.createQuad: expected (minX, minY, maxX, maxY)"); + } + minX = moduleBaseArgFloat(0); + minY = moduleBaseArgFloat(1); + maxX = moduleBaseArgFloat(2); + maxY = moduleBaseArgFloat(3); + } + meshscript_t *ms = moduleMeshAlloc(QUAD_VERTEX_COUNT); + quadBuffer( + ms->vertices, + minX, minY, maxX, maxY, + 0.0f, 0.0f, 1.0f, 1.0f + #if MESH_ENABLE_COLOR + , COLOR_WHITE_4B + #endif + ); + return moduleMeshFinalize(ms, QUAD_PRIMITIVE_TYPE); +} + +// Mesh.createSphere(radius?, stacks?, sectors?) +moduleBaseFunction(moduleMeshCreateSphere) { + float_t radius = moduleBaseOptFloat(0, 0.5f); + int32_t stacks = moduleBaseOptInt(1, SPHERE_STACKS); + int32_t sectors = moduleBaseOptInt(2, SPHERE_SECTORS); + int32_t vertexCount = stacks * sectors * 6; + vec3 center = { 0.0f, 0.0f, 0.0f }; + meshscript_t *ms = moduleMeshAlloc(vertexCount); + sphereBuffer( + ms->vertices, center, radius, stacks, sectors + #if MESH_ENABLE_COLOR + , COLOR_WHITE_4B + #endif + ); + return moduleMeshFinalize(ms, SPHERE_PRIMITIVE_TYPE); +} + +// Mesh.createPlane(width?, height?) — XZ-aligned, centered at origin +moduleBaseFunction(moduleMeshCreatePlane) { + float_t width = moduleBaseOptFloat(0, 1.0f); + float_t height = moduleBaseOptFloat(1, 1.0f); + vec3 min = { -width * 0.5f, 0.0f, -height * 0.5f }; + vec3 max = { width * 0.5f, 0.0f, height * 0.5f }; + vec2 uvMin = { 0.0f, 0.0f }; + vec2 uvMax = { 1.0f, 1.0f }; + meshscript_t *ms = moduleMeshAlloc(PLANE_VERTEX_COUNT); + planeBuffer( + ms->vertices, PLANE_AXIS_XZ, min, max + #if MESH_ENABLE_COLOR + , COLOR_WHITE_4B + #endif + , uvMin, uvMax + ); + return moduleMeshFinalize(ms, PLANE_PRIMITIVE_TYPE); +} + +// Mesh.createCapsule(radius?, halfHeight?, capRings?, sectors?) +moduleBaseFunction(moduleMeshCreateCapsule) { + float_t radius = moduleBaseOptFloat(0, 0.5f); + float_t halfHeight = moduleBaseOptFloat(1, 0.5f); + int32_t capRings = moduleBaseOptInt(2, CAPSULE_CAP_RINGS); + int32_t sectors = moduleBaseOptInt(3, CAPSULE_SECTORS); + int32_t vertexCount = (2 * capRings + 1) * sectors * 6; + vec3 center = { 0.0f, 0.0f, 0.0f }; + meshscript_t *ms = moduleMeshAlloc(vertexCount); + capsuleBuffer( + ms->vertices, center, radius, halfHeight, capRings, sectors + #if MESH_ENABLE_COLOR + , COLOR_WHITE_4B + #endif + ); + return moduleMeshFinalize(ms, CAPSULE_PRIMITIVE_TYPE); +} + +// Mesh.createTriPrism(x0, y0, x1, y1, x2, y2, minZ, maxZ) +moduleBaseFunction(moduleMeshCreateTriPrism) { + moduleBaseRequireArgs(8); + float_t x0 = moduleBaseArgFloat(0); + float_t y0 = moduleBaseArgFloat(1); + float_t x1 = moduleBaseArgFloat(2); + float_t y1 = moduleBaseArgFloat(3); + float_t x2 = moduleBaseArgFloat(4); + float_t y2 = moduleBaseArgFloat(5); + float_t minZ = moduleBaseArgFloat(6); + float_t maxZ = moduleBaseArgFloat(7); + meshscript_t *ms = moduleMeshAlloc(TRIPRISM_VERTEX_COUNT); + triPrismBuffer( + ms->vertices, x0, y0, x1, y1, x2, y2, minZ, maxZ + #if MESH_ENABLE_COLOR + , COLOR_WHITE_4B + #endif + ); + return moduleMeshFinalize(ms, TRIPRISM_PRIMITIVE_TYPE); +} + +// ---- Registration ---- + +static void moduleMesh(void) { + // MeshVertex - internal type, no global constructor + scriptProtoInit( + &MODULE_MESH_VERTEX_PROTO, NULL, + sizeof(meshvertexscript_t), NULL + ); + scriptProtoDefineProp( + &MODULE_MESH_VERTEX_PROTO, "position", + moduleMeshVertexGetPosition, moduleMeshVertexSetPosition + ); + + // Mesh - global constructor: new Mesh(vertexCount) + scriptProtoInit( + &MODULE_MESH_PROTO, "Mesh", + sizeof(meshscript_t), moduleMeshConstructor + ); + MODULE_MESH_PROTO.info.free_cb = moduleMeshFreeData; + + scriptProtoDefineToString(&MODULE_MESH_PROTO, moduleMeshToString); + scriptProtoDefineProp( + &MODULE_MESH_PROTO, "vertices", + moduleMeshGetVertices, NULL + ); + scriptProtoDefineProp( + &MODULE_MESH_PROTO, "vertexCount", + moduleMeshGetVertexCount, NULL + ); + scriptProtoDefineFunc(&MODULE_MESH_PROTO, "flush", moduleMeshFlush); + scriptProtoDefineFunc(&MODULE_MESH_PROTO, "dispose", moduleMeshDispose); + + // Static default mesh references + scriptProtoDefineStaticProp( + &MODULE_MESH_PROTO, "DEFAULT_CUBE", moduleMeshDefaultCube, NULL + ); + scriptProtoDefineStaticProp( + &MODULE_MESH_PROTO, "DEFAULT_QUAD", moduleMeshDefaultQuad, NULL + ); + scriptProtoDefineStaticProp( + &MODULE_MESH_PROTO, "DEFAULT_SPHERE", moduleMeshDefaultSphere, NULL + ); + scriptProtoDefineStaticProp( + &MODULE_MESH_PROTO, "DEFAULT_PLANE", moduleMeshDefaultPlane, NULL + ); + scriptProtoDefineStaticProp( + &MODULE_MESH_PROTO, "DEFAULT_CAPSULE", moduleMeshDefaultCapsule, NULL + ); + scriptProtoDefineStaticProp( + &MODULE_MESH_PROTO, "DEFAULT_TRIPRISM", moduleMeshDefaultTriPrism, NULL + ); + + // Static factory methods + scriptProtoDefineStaticFunc( + &MODULE_MESH_PROTO, "createCube", moduleMeshCreateCube + ); + scriptProtoDefineStaticFunc( + &MODULE_MESH_PROTO, "createQuad", moduleMeshCreateQuad + ); + scriptProtoDefineStaticFunc( + &MODULE_MESH_PROTO, "createSphere", moduleMeshCreateSphere + ); + scriptProtoDefineStaticFunc( + &MODULE_MESH_PROTO, "createPlane", moduleMeshCreatePlane + ); + scriptProtoDefineStaticFunc( + &MODULE_MESH_PROTO, "createCapsule", moduleMeshCreateCapsule + ); + scriptProtoDefineStaticFunc( + &MODULE_MESH_PROTO, "createTriPrism", moduleMeshCreateTriPrism + ); +} diff --git a/src/dusk/script/module/display/modulescreen.h b/src/dusk/script/module/display/modulescreen.h new file mode 100644 index 00000000..386ebaeb --- /dev/null +++ b/src/dusk/script/module/display/modulescreen.h @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/module/display/modulecolor.h" +#include "display/screen/screen.h" + +static scriptproto_t MODULE_SCREEN_PROTO; + +moduleBaseFunction(moduleScreenGetWidth) { + return jerry_number(SCREEN.width); +} + +moduleBaseFunction(moduleScreenGetHeight) { + return jerry_number(SCREEN.height); +} + +moduleBaseFunction(moduleScreenGetAspect) { + return jerry_number(SCREEN.aspect); +} + +moduleBaseFunction(moduleScreenGetBackground) { + return moduleColorMakeObject(SCREEN.background); +} + +moduleBaseFunction(moduleScreenSetBackground) { + moduleBaseRequireArgs(1); moduleBaseRequireObject(0); + color_t *color = (color_t*)scriptProtoGetValue(&MODULE_COLOR_PROTO, args[0]); + if(!color) return moduleBaseThrow("Background must be a valid color object"); + memoryCopy(&SCREEN.background, color, sizeof(color_t)); + return jerry_undefined(); +} + +moduleBaseFunction(moduleScreenToString) { + char_t buf[128]; + stringFormat( + buf, sizeof(buf), + "{ \"width\": %d, \"height\": %d, \"aspect\": %.2f }", + SCREEN.width, SCREEN.height, SCREEN.aspect + ); + return jerry_string_sz(buf); +} + +static void moduleScreen(void) { + scriptProtoInit( + &MODULE_SCREEN_PROTO, "Screen", sizeof(screen_t), NULL + ); + + scriptProtoDefineProp( + &MODULE_SCREEN_PROTO, "width", moduleScreenGetWidth, NULL + ); + scriptProtoDefineProp( + &MODULE_SCREEN_PROTO, "height", moduleScreenGetHeight, NULL + ); + scriptProtoDefineProp( + &MODULE_SCREEN_PROTO, "aspect", moduleScreenGetAspect, NULL + ); + scriptProtoDefineProp( + &MODULE_SCREEN_PROTO, "background", + moduleScreenGetBackground, moduleScreenSetBackground + ); + + scriptProtoDefineToString(&MODULE_SCREEN_PROTO, moduleScreenToString); +} diff --git a/src/dusk/script/module/display/modulespritebatch.h b/src/dusk/script/module/display/modulespritebatch.h new file mode 100644 index 00000000..34db28b5 --- /dev/null +++ b/src/dusk/script/module/display/modulespritebatch.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 "script/module/display/modulecolor.h" +#include "script/module/math/modulevec2.h" +#include "script/module/math/modulevec3.h" +#include "display/spritebatch/spritebatch.h" + +static scriptproto_t MODULE_SPRITEBATCH_PROTO; + +moduleBaseFunction(moduleSpriteBatchGetSpriteCount) { + return jerry_number(SPRITEBATCH.spriteCount); +} + +moduleBaseFunction(moduleSpriteBatchPush) { +#if MESH_ENABLE_COLOR + moduleBaseRequireArgs(9); +#else + moduleBaseRequireArgs(8); +#endif + + moduleBaseRequireNumber(0); + moduleBaseRequireNumber(1); + moduleBaseRequireNumber(2); + moduleBaseRequireNumber(3); + +#if MESH_ENABLE_COLOR + color_t *col = (color_t *)scriptProtoGetValue(&MODULE_COLOR_PROTO, args[4]); + if(!col) return moduleBaseThrow("color must be a Color object"); + moduleBaseRequireNumber(5); + moduleBaseRequireNumber(6); + moduleBaseRequireNumber(7); + moduleBaseRequireNumber(8); + spriteBatchPush( + moduleBaseArgFloat(0), moduleBaseArgFloat(1), + moduleBaseArgFloat(2), moduleBaseArgFloat(3), + *col, + moduleBaseArgFloat(5), moduleBaseArgFloat(6), + moduleBaseArgFloat(7), moduleBaseArgFloat(8) + ); +#else + moduleBaseRequireNumber(4); + moduleBaseRequireNumber(5); + moduleBaseRequireNumber(6); + moduleBaseRequireNumber(7); + spriteBatchPush( + moduleBaseArgFloat(0), moduleBaseArgFloat(1), + moduleBaseArgFloat(2), moduleBaseArgFloat(3), + moduleBaseArgFloat(4), moduleBaseArgFloat(5), + moduleBaseArgFloat(6), moduleBaseArgFloat(7) + ); +#endif + + return jerry_undefined(); +} + +moduleBaseFunction(moduleSpriteBatchPush3D) { +#if MESH_ENABLE_COLOR + moduleBaseRequireArgs(5); +#else + moduleBaseRequireArgs(4); +#endif + + float_t *min = moduleVec3From(args[0]); + if(!min) return moduleBaseThrow("min must be a Vec3"); + float_t *max = moduleVec3From(args[1]); + if(!max) return moduleBaseThrow("max must be a Vec3"); + +#if MESH_ENABLE_COLOR + color_t *col = (color_t *)scriptProtoGetValue(&MODULE_COLOR_PROTO, args[2]); + if(!col) return moduleBaseThrow("color must be a Color object"); + float_t *uvMin = moduleVec2From(args[3]); + if(!uvMin) return moduleBaseThrow("uvMin must be a Vec2"); + float_t *uvMax = moduleVec2From(args[4]); + if(!uvMax) return moduleBaseThrow("uvMax must be a Vec2"); + spriteBatchPush3D(min, max, *col, uvMin, uvMax); +#else + float_t *uvMin = moduleVec2From(args[2]); + if(!uvMin) return moduleBaseThrow("uvMin must be a Vec2"); + float_t *uvMax = moduleVec2From(args[3]); + if(!uvMax) return moduleBaseThrow("uvMax must be a Vec2"); + spriteBatchPush3D(min, max, uvMin, uvMax); +#endif + + return jerry_undefined(); +} + +moduleBaseFunction(moduleSpriteBatchClear) { + spriteBatchClear(); + return jerry_undefined(); +} + +moduleBaseFunction(moduleSpriteBatchFlush) { + spriteBatchFlush(); + return jerry_undefined(); +} + +static void moduleSpriteBatch(void) { + scriptProtoInit( + &MODULE_SPRITEBATCH_PROTO, "SpriteBatch", sizeof(uint8_t), NULL + ); + + scriptProtoDefineStaticProp( + &MODULE_SPRITEBATCH_PROTO, "spriteCount", + moduleSpriteBatchGetSpriteCount, NULL + ); + + scriptProtoDefineStaticFunc( + &MODULE_SPRITEBATCH_PROTO, "push", moduleSpriteBatchPush + ); + scriptProtoDefineStaticFunc( + &MODULE_SPRITEBATCH_PROTO, "push3D", moduleSpriteBatchPush3D + ); + scriptProtoDefineStaticFunc( + &MODULE_SPRITEBATCH_PROTO, "clear", moduleSpriteBatchClear + ); + scriptProtoDefineStaticFunc( + &MODULE_SPRITEBATCH_PROTO, "flush", moduleSpriteBatchFlush + ); +} diff --git a/src/dusk/script/module/display/moduletext.h b/src/dusk/script/module/display/moduletext.h new file mode 100644 index 00000000..55135bba --- /dev/null +++ b/src/dusk/script/module/display/moduletext.h @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/module/modulebase.h" +#include "script/scriptproto.h" +#include "script/module/display/modulecolor.h" +#include "display/text/text.h" + +static scriptproto_t MODULE_TEXT_PROTO; + +moduleBaseFunction(moduleTextDraw) { + moduleBaseRequireArgs(3); moduleBaseRequireNumber(0); moduleBaseRequireNumber(1); + moduleBaseRequireString(2); + + float_t x = moduleBaseArgFloat(0); + float_t y = moduleBaseArgFloat(1); + + char_t text[1024]; + moduleBaseToString(args[2], text, sizeof(text)); + + color_t col = COLOR_WHITE; + if(argc >= 4) { + color_t *c = (color_t *)scriptProtoGetValue(&MODULE_COLOR_PROTO, args[3]); + if(c) col = *c; + } + + errorret_t err = textDraw( + x, y, text, col, &FONT_DEFAULT + ); + if(err.code != ERROR_OK) { + errorCatch(errorPrint(err)); + return moduleBaseThrow("Text draw failed"); + } + + return jerry_undefined(); +} + +moduleBaseFunction(moduleTextMeasure) { + moduleBaseRequireArgs(1); moduleBaseRequireString(0); + + char_t text[1024]; + moduleBaseToString(args[0], text, sizeof(text)); + + int32_t w, h; + textMeasure(text, &FONT_DEFAULT, &w, &h); + + jerry_value_t obj = jerry_object(); + jerry_value_t wKey = jerry_string_sz("width"); + jerry_value_t hKey = jerry_string_sz("height"); + jerry_value_t wVal = jerry_number(w); + jerry_value_t hVal = jerry_number(h); + jerry_object_set(obj, wKey, wVal); + jerry_object_set(obj, hKey, hVal); + jerry_value_free(wKey); + jerry_value_free(hKey); + jerry_value_free(wVal); + jerry_value_free(hVal); + return obj; +} + +static void moduleText(void) { + scriptProtoInit(&MODULE_TEXT_PROTO, "Text", sizeof(uint8_t), NULL); + scriptProtoDefineStaticFunc(&MODULE_TEXT_PROTO, "draw", moduleTextDraw); + scriptProtoDefineStaticFunc(&MODULE_TEXT_PROTO, "measure", moduleTextMeasure); +} diff --git a/src/dusk/script/module/engine/moduleengine.h b/src/dusk/script/module/engine/moduleengine.h new file mode 100644 index 00000000..1f5855db --- /dev/null +++ b/src/dusk/script/module/engine/moduleengine.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/module/modulebase.h" +#include "script/scriptproto.h" +#include "engine/engine.h" + +static scriptproto_t MODULE_ENGINE_PROTO; + +moduleBaseFunction(moduleEngineExit) { + ENGINE.running = false; + return jerry_undefined(); +} + +static void moduleEngine(void) { + scriptProtoInit( + &MODULE_ENGINE_PROTO, "Engine", + sizeof(uint8_t), NULL + ); + + scriptProtoDefineStaticFunc( + &MODULE_ENGINE_PROTO, "exit", moduleEngineExit + ); +} diff --git a/src/dusk/script/module/entity/component/moduleentitycamera.h b/src/dusk/script/module/entity/component/moduleentitycamera.h new file mode 100644 index 00000000..c3500ece --- /dev/null +++ b/src/dusk/script/module/entity/component/moduleentitycamera.h @@ -0,0 +1,182 @@ +/** + * 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/entity.h" +#include "entity/component/display/entitycamera.h" +#include "moduleentityposition.h" + +static scriptproto_t MODULE_ENTITY_CAMERA_PROTO; + +static entitycamera_t * moduleEntityCameraGet( + const jerry_call_info_t *callInfo +) { + componenthandle_t *h = scriptProtoGetValue( + &MODULE_ENTITY_CAMERA_PROTO, callInfo->this_value + ); + if(!h) return NULL; + return (entitycamera_t*)componentGetData(h->eid, h->cid, COMPONENT_TYPE_CAMERA); +} + +// ---- Getters ---- + +moduleBaseFunction(moduleEntityCameraGetZNear) { + moduleBaseGetOrReturn(entitycamera_t, cam, moduleEntityCameraGet); + return jerry_number(cam->nearClip); +} + +moduleBaseFunction(moduleEntityCameraGetZFar) { + moduleBaseGetOrReturn(entitycamera_t, cam, moduleEntityCameraGet); + return jerry_number(cam->farClip); +} + +moduleBaseFunction(moduleEntityCameraGetFov) { + moduleBaseGetOrReturn(entitycamera_t, cam, moduleEntityCameraGet); + if( + cam->projType != ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE && + cam->projType != ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED + ) return jerry_undefined(); + return jerry_number(cam->perspective.fov); +} + +moduleBaseFunction(moduleEntityCameraGetProjectionType) { + moduleBaseGetOrReturn(entitycamera_t, cam, moduleEntityCameraGet); + return jerry_number(cam->projType); +} + +moduleBaseFunction(moduleEntityCameraGetOrthoTop) { + moduleBaseGetOrReturn(entitycamera_t, cam, moduleEntityCameraGet); + if(cam->projType != ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) return jerry_undefined(); + return jerry_number(cam->orthographic.top); +} + +moduleBaseFunction(moduleEntityCameraGetOrthoBottom) { + moduleBaseGetOrReturn(entitycamera_t, cam, moduleEntityCameraGet); + if(cam->projType != ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) return jerry_undefined(); + return jerry_number(cam->orthographic.bottom); +} + +moduleBaseFunction(moduleEntityCameraGetOrthoLeft) { + moduleBaseGetOrReturn(entitycamera_t, cam, moduleEntityCameraGet); + if(cam->projType != ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) return jerry_undefined(); + return jerry_number(cam->orthographic.left); +} + +moduleBaseFunction(moduleEntityCameraGetOrthoRight) { + moduleBaseGetOrReturn(entitycamera_t, cam, moduleEntityCameraGet); + if(cam->projType != ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) return jerry_undefined(); + return jerry_number(cam->orthographic.right); +} + +// ---- Setters ---- + +moduleBaseFunction(moduleEntityCameraSetZNear) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + moduleBaseGetOrReturn(entitycamera_t, cam, moduleEntityCameraGet); + cam->nearClip = moduleBaseArgFloat(0); + return args[0]; +} + +moduleBaseFunction(moduleEntityCameraSetZFar) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + moduleBaseGetOrReturn(entitycamera_t, cam, moduleEntityCameraGet); + cam->farClip = moduleBaseArgFloat(0); + return args[0]; +} + +moduleBaseFunction(moduleEntityCameraSetFov) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + moduleBaseGetOrReturn(entitycamera_t, cam, moduleEntityCameraGet); + if( + cam->projType != ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE && + cam->projType != ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED + ) return jerry_undefined(); + cam->perspective.fov = moduleBaseArgFloat(0); + return args[0]; +} + +moduleBaseFunction(moduleEntityCameraSetProjectionType) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + moduleBaseGetOrReturn(entitycamera_t, cam, moduleEntityCameraGet); + int32_t t = moduleBaseArgInt(0); + if( + t < ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE || + t > ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC + ) return moduleBaseThrow("Invalid projection type"); + cam->projType = (entitycameraprojectiontype_t)t; + return args[0]; +} + +moduleBaseFunction(moduleEntityCameraSetOrthoTop) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + moduleBaseGetOrReturn(entitycamera_t, cam, moduleEntityCameraGet); + if(cam->projType != ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) return jerry_undefined(); + cam->orthographic.top = moduleBaseArgFloat(0); + return args[0]; +} + +moduleBaseFunction(moduleEntityCameraSetOrthoBottom) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + moduleBaseGetOrReturn(entitycamera_t, cam, moduleEntityCameraGet); + if(cam->projType != ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) return jerry_undefined(); + cam->orthographic.bottom = moduleBaseArgFloat(0); + return args[0]; +} + +moduleBaseFunction(moduleEntityCameraSetOrthoLeft) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + moduleBaseGetOrReturn(entitycamera_t, cam, moduleEntityCameraGet); + if(cam->projType != ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) return jerry_undefined(); + cam->orthographic.left = moduleBaseArgFloat(0); + return args[0]; +} + +moduleBaseFunction(moduleEntityCameraSetOrthoRight) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + moduleBaseGetOrReturn(entitycamera_t, cam, moduleEntityCameraGet); + if(cam->projType != ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) return jerry_undefined(); + cam->orthographic.right = moduleBaseArgFloat(0); + return args[0]; +} + +moduleBaseFunction(moduleEntityCameraAdd) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + entityid_t id = (entityid_t)moduleBaseArgInt(0); + componentid_t comp = entityAddComponent(id, COMPONENT_TYPE_CAMERA); + componenthandle_t h = { .eid = id, .cid = comp }; + return scriptProtoCreateValue(&MODULE_ENTITY_CAMERA_PROTO, &h); +} + +static void moduleEntityCAMERA(void) { + scriptProtoInit( + &MODULE_ENTITY_CAMERA_PROTO, NULL, sizeof(componenthandle_t), NULL + ); + + scriptProtoDefineProp(&MODULE_ENTITY_CAMERA_PROTO, "zNear", + moduleEntityCameraGetZNear, moduleEntityCameraSetZNear); + scriptProtoDefineProp(&MODULE_ENTITY_CAMERA_PROTO, "zFar", + moduleEntityCameraGetZFar, moduleEntityCameraSetZFar); + scriptProtoDefineProp(&MODULE_ENTITY_CAMERA_PROTO, "fov", + moduleEntityCameraGetFov, moduleEntityCameraSetFov); + scriptProtoDefineProp(&MODULE_ENTITY_CAMERA_PROTO, "projectionType", + moduleEntityCameraGetProjectionType, moduleEntityCameraSetProjectionType); + scriptProtoDefineProp(&MODULE_ENTITY_CAMERA_PROTO, "orthoTop", + moduleEntityCameraGetOrthoTop, moduleEntityCameraSetOrthoTop); + scriptProtoDefineProp(&MODULE_ENTITY_CAMERA_PROTO, "orthoBottom", + moduleEntityCameraGetOrthoBottom, moduleEntityCameraSetOrthoBottom); + scriptProtoDefineProp(&MODULE_ENTITY_CAMERA_PROTO, "orthoLeft", + moduleEntityCameraGetOrthoLeft, moduleEntityCameraSetOrthoLeft); + scriptProtoDefineProp(&MODULE_ENTITY_CAMERA_PROTO, "orthoRight", + moduleEntityCameraGetOrthoRight, moduleEntityCameraSetOrthoRight); + + moduleBaseSetInt("CAMERA_TYPE_ORTHOGRAPHIC", + ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC); + moduleBaseSetInt("CAMERA_TYPE_PERSPECTIVE", + ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE); +} diff --git a/src/dusk/script/module/entity/component/moduleentityphysics.h b/src/dusk/script/module/entity/component/moduleentityphysics.h new file mode 100644 index 00000000..c75d73d5 --- /dev/null +++ b/src/dusk/script/module/entity/component/moduleentityphysics.h @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/module/modulebase.h" +#include "script/module/math/modulevec3ref.h" +#include "script/scriptproto.h" +#include "entity/entity.h" +#include "entity/component/physics/entityphysics.h" +#include "moduleentityposition.h" + +static scriptproto_t MODULE_ENTITY_PHYSICS_PROTO; + +static entityphysics_t * moduleEntityPhysicsGet( + const jerry_call_info_t *callInfo +) { + componenthandle_t *h = scriptProtoGetValue( + &MODULE_ENTITY_PHYSICS_PROTO, callInfo->this_value + ); + if(!h) return NULL; + return entityPhysicsGet(h->eid, h->cid); +} + +moduleBaseFunction(moduleEntityPhysicsGetVelocity) { + moduleBaseGetOrReturn(entityphysics_t, phys, moduleEntityPhysicsGet); + return moduleVec3RefPush(phys->velocity, NULL, NULL); +} + +moduleBaseFunction(moduleEntityPhysicsSetVelocity) { + moduleBaseRequireArgs(1); + moduleBaseGetOrReturn(entityphysics_t, phys, moduleEntityPhysicsGet); + vec3 v; + if(!moduleVec3AnyCheck(args[0], v)) return moduleBaseThrow("Expected Vec3"); + glm_vec3_copy(v, phys->velocity); + return jerry_undefined(); +} + +moduleBaseFunction(moduleEntityPhysicsGetOnGround) { + moduleBaseGetOrReturn(entityphysics_t, phys, moduleEntityPhysicsGet); + return jerry_boolean(phys->onGround); +} + +moduleBaseFunction(moduleEntityPhysicsGetBodyType) { + moduleBaseGetOrReturn(entityphysics_t, phys, moduleEntityPhysicsGet); + return jerry_number(phys->type); +} + +moduleBaseFunction(moduleEntityPhysicsSetBodyType) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + moduleBaseGetOrReturn(entityphysics_t, phys, moduleEntityPhysicsGet); + phys->type = (physicsbodytype_t)moduleBaseArgInt(0); + return jerry_undefined(); +} + +moduleBaseFunction(moduleEntityPhysicsApplyImpulse) { + moduleBaseRequireArgs(1); + moduleBaseGetOrReturn(entityphysics_t, phys, moduleEntityPhysicsGet); + if(phys->type == PHYSICS_BODY_STATIC) return jerry_undefined(); + vec3 impulse; + if(!moduleVec3Check(args[0], impulse)) return moduleBaseThrow("Expected Vec3 impulse"); + glm_vec3_add(phys->velocity, impulse, phys->velocity); + return jerry_undefined(); +} + +moduleBaseFunction(moduleEntityPhysicsSetShapeCube) { + moduleBaseRequireArgs(1); + moduleBaseGetOrReturn(entityphysics_t, phys, moduleEntityPhysicsGet); + vec3 half; + if(!moduleVec3Check(args[0], half)) return moduleBaseThrow("Expected Vec3 halfExtents"); + phys->shape.type = PHYSICS_SHAPE_CUBE; + glm_vec3_copy(half, phys->shape.data.cube.halfExtents); + return jerry_undefined(); +} + +moduleBaseFunction(moduleEntityPhysicsSetShapeSphere) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + moduleBaseGetOrReturn(entityphysics_t, phys, moduleEntityPhysicsGet); + phys->shape.type = PHYSICS_SHAPE_SPHERE; + phys->shape.data.sphere.radius = moduleBaseArgFloat(0); + return jerry_undefined(); +} + +moduleBaseFunction(moduleEntityPhysicsSetShapeCapsule) { + moduleBaseRequireArgs(2); moduleBaseRequireNumber(0); moduleBaseRequireNumber(1); + moduleBaseGetOrReturn(entityphysics_t, phys, moduleEntityPhysicsGet); + phys->shape.type = PHYSICS_SHAPE_CAPSULE; + phys->shape.data.capsule.radius = moduleBaseArgFloat(0); + phys->shape.data.capsule.halfHeight = moduleBaseArgFloat(1); + return jerry_undefined(); +} + +moduleBaseFunction(moduleEntityPhysicsSetShapePlane) { + moduleBaseRequireArgs(2); moduleBaseRequireNumber(1); + moduleBaseGetOrReturn(entityphysics_t, phys, moduleEntityPhysicsGet); + vec3 normal; + if(!moduleVec3Check(args[0], normal)) return moduleBaseThrow("Expected Vec3 normal"); + phys->shape.type = PHYSICS_SHAPE_PLANE; + glm_vec3_copy(normal, phys->shape.data.plane.normal); + phys->shape.data.plane.distance = moduleBaseArgFloat(1); + return jerry_undefined(); +} + +moduleBaseFunction(moduleEntityPhysicsAdd) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + entityid_t id = (entityid_t)moduleBaseArgInt(0); + componentid_t comp = entityAddComponent(id, COMPONENT_TYPE_PHYSICS); + componenthandle_t h = { .eid = id, .cid = comp }; + return scriptProtoCreateValue(&MODULE_ENTITY_PHYSICS_PROTO, &h); +} + +static void moduleEntityPHYSICS(void) { + scriptProtoInit( + &MODULE_ENTITY_PHYSICS_PROTO, NULL, sizeof(componenthandle_t), NULL + ); + + scriptProtoDefineProp(&MODULE_ENTITY_PHYSICS_PROTO, "velocity", + moduleEntityPhysicsGetVelocity, moduleEntityPhysicsSetVelocity); + scriptProtoDefineProp(&MODULE_ENTITY_PHYSICS_PROTO, "onGround", + moduleEntityPhysicsGetOnGround, NULL); + scriptProtoDefineProp(&MODULE_ENTITY_PHYSICS_PROTO, "bodyType", + moduleEntityPhysicsGetBodyType, moduleEntityPhysicsSetBodyType); + + scriptProtoDefineFunc(&MODULE_ENTITY_PHYSICS_PROTO, "applyImpulse", + moduleEntityPhysicsApplyImpulse); + scriptProtoDefineFunc(&MODULE_ENTITY_PHYSICS_PROTO, "setShapeCube", + moduleEntityPhysicsSetShapeCube); + scriptProtoDefineFunc(&MODULE_ENTITY_PHYSICS_PROTO, "setShapeSphere", + moduleEntityPhysicsSetShapeSphere); + scriptProtoDefineFunc(&MODULE_ENTITY_PHYSICS_PROTO, "setShapeCapsule", + moduleEntityPhysicsSetShapeCapsule); + scriptProtoDefineFunc(&MODULE_ENTITY_PHYSICS_PROTO, "setShapePlane", + moduleEntityPhysicsSetShapePlane); + + 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); +} diff --git a/src/dusk/script/module/entity/component/moduleentityposition.h b/src/dusk/script/module/entity/component/moduleentityposition.h new file mode 100644 index 00000000..9786e28f --- /dev/null +++ b/src/dusk/script/module/entity/component/moduleentityposition.h @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/module/modulebase.h" +#include "script/module/math/modulevec3ref.h" +#include "script/scriptproto.h" +#include "entity/entity.h" +#include "entity/component/display/entityposition.h" + +#ifndef COMPONENT_HANDLE_DEFINED +#define COMPONENT_HANDLE_DEFINED +typedef struct { + entityid_t eid; + componentid_t cid; +} componenthandle_t; +#endif + +static scriptproto_t MODULE_ENTITY_POSITION_PROTO; + +static entityposition_t * moduleEntityPositionGet( + const jerry_call_info_t *callInfo +) { + componenthandle_t *h = scriptProtoGetValue( + &MODULE_ENTITY_POSITION_PROTO, callInfo->this_value + ); + if(!h) return NULL; + return entityPositionGet(h->eid, h->cid); +} + +moduleBaseFunction(moduleEntityPositionGetPosition) { + moduleBaseGetOrReturn(entityposition_t, pos, moduleEntityPositionGet); + return moduleVec3RefPush(pos->position, (void(*)(void*))entityPositionRebuild, pos); +} + +moduleBaseFunction(moduleEntityPositionSetPosition) { + moduleBaseRequireArgs(1); + moduleBaseGetOrReturn(entityposition_t, pos, moduleEntityPositionGet); + vec3 v; + if(!moduleVec3AnyCheck(args[0], v)) return moduleBaseThrow("Expected Vec3"); + glm_vec3_copy(v, pos->position); + entityPositionRebuild(pos); + return jerry_undefined(); +} + +moduleBaseFunction(moduleEntityPositionGetRotation) { + moduleBaseGetOrReturn(entityposition_t, pos, moduleEntityPositionGet); + return moduleVec3RefPush(pos->rotation, (void(*)(void*))entityPositionRebuild, pos); +} + +moduleBaseFunction(moduleEntityPositionSetRotation) { + moduleBaseRequireArgs(1); + moduleBaseGetOrReturn(entityposition_t, pos, moduleEntityPositionGet); + vec3 v; + if(!moduleVec3AnyCheck(args[0], v)) return moduleBaseThrow("Expected Vec3"); + glm_vec3_copy(v, pos->rotation); + entityPositionRebuild(pos); + return jerry_undefined(); +} + +moduleBaseFunction(moduleEntityPositionGetScale) { + moduleBaseGetOrReturn(entityposition_t, pos, moduleEntityPositionGet); + return moduleVec3RefPush(pos->scale, (void(*)(void*))entityPositionRebuild, pos); +} + +moduleBaseFunction(moduleEntityPositionSetScale) { + moduleBaseRequireArgs(1); + moduleBaseGetOrReturn(entityposition_t, pos, moduleEntityPositionGet); + vec3 v; + if(!moduleVec3AnyCheck(args[0], v)) return moduleBaseThrow("Expected Vec3"); + glm_vec3_copy(v, pos->scale); + entityPositionRebuild(pos); + return jerry_undefined(); +} + +moduleBaseFunction(moduleEntityPositionLookAt) { + moduleBaseRequireArgs(1); + moduleBaseGetOrReturn(entityposition_t, pos, moduleEntityPositionGet); + vec3 target; + if(!moduleVec3AnyCheck(args[0], target)) return moduleBaseThrow("Expected Vec3 target"); + vec3 up = { 0.0f, 1.0f, 0.0f }; + if(argc >= 2 && !moduleVec3AnyCheck(args[1], up)) return moduleBaseThrow("Expected Vec3 up"); + componenthandle_t *h = scriptProtoGetValue( + &MODULE_ENTITY_POSITION_PROTO, callInfo->this_value + ); + entityPositionLookAt(h->eid, h->cid, target, up, pos->position); + return jerry_undefined(); +} + +moduleBaseFunction(moduleEntityPositionGetParent) { + moduleBaseGetOrReturn(entityposition_t, pos, moduleEntityPositionGet); + if(pos->parentEntityId == ENTITY_ID_INVALID) return jerry_null(); + componenthandle_t ph = { .eid = pos->parentEntityId, .cid = pos->parentComponentId }; + return scriptProtoCreateValue(&MODULE_ENTITY_POSITION_PROTO, &ph); +} + +moduleBaseFunction(moduleEntityPositionSetParentProp) { + componenthandle_t *h = scriptProtoGetValue( + &MODULE_ENTITY_POSITION_PROTO, callInfo->this_value + ); + if(!h) return jerry_undefined(); + if(argc < 1 || jerry_value_is_null(args[0]) || jerry_value_is_undefined(args[0])) { + entityPositionSetParent(h->eid, h->cid, ENTITY_ID_INVALID, COMPONENT_ID_INVALID); + return jerry_undefined(); + } + componenthandle_t *ph = scriptProtoGetValue(&MODULE_ENTITY_POSITION_PROTO, args[0]); + if(!ph) return moduleBaseThrow("Expected EntityPosition"); + entityPositionSetParent(h->eid, h->cid, ph->eid, ph->cid); + return jerry_undefined(); +} + +moduleBaseFunction(moduleEntityPositionAdd) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + entityid_t id = (entityid_t)moduleBaseArgInt(0); + componentid_t comp = entityAddComponent(id, COMPONENT_TYPE_POSITION); + componenthandle_t h = { .eid = id, .cid = comp }; + return scriptProtoCreateValue(&MODULE_ENTITY_POSITION_PROTO, &h); +} + +static void moduleEntityPOSITION(void) { + scriptProtoInit( + &MODULE_ENTITY_POSITION_PROTO, NULL, sizeof(componenthandle_t), NULL + ); + scriptProtoDefineProp(&MODULE_ENTITY_POSITION_PROTO, "position", + moduleEntityPositionGetPosition, moduleEntityPositionSetPosition); + scriptProtoDefineProp(&MODULE_ENTITY_POSITION_PROTO, "rotation", + moduleEntityPositionGetRotation, moduleEntityPositionSetRotation); + scriptProtoDefineProp(&MODULE_ENTITY_POSITION_PROTO, "scale", + moduleEntityPositionGetScale, moduleEntityPositionSetScale); + scriptProtoDefineProp(&MODULE_ENTITY_POSITION_PROTO, "parent", + moduleEntityPositionGetParent, moduleEntityPositionSetParentProp); + scriptProtoDefineFunc(&MODULE_ENTITY_POSITION_PROTO, "lookAt", + moduleEntityPositionLookAt); +} diff --git a/src/dusk/script/module/entity/component/moduleentityrenderable.h b/src/dusk/script/module/entity/component/moduleentityrenderable.h new file mode 100644 index 00000000..bab96a41 --- /dev/null +++ b/src/dusk/script/module/entity/component/moduleentityrenderable.h @@ -0,0 +1,197 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/module/modulebase.h" +#include "script/module/display/modulemesh.h" +#include "script/module/display/modulecolor.h" +#include "script/scriptproto.h" +#include "entity/entity.h" +#include "entity/component/display/entityrenderable.h" +#include "moduleentityposition.h" + +static scriptproto_t MODULE_ENTITY_RENDERABLE_PROTO; + +static entityrenderable_t * moduleEntityRenderableGet( + const jerry_call_info_t *callInfo +) { + componenthandle_t *h = scriptProtoGetValue( + &MODULE_ENTITY_RENDERABLE_PROTO, callInfo->this_value + ); + if(!h) return NULL; + return (entityrenderable_t*)componentGetData( + h->eid, h->cid, COMPONENT_TYPE_RENDERABLE + ); +} + +moduleBaseFunction(moduleEntityRenderableGetType) { + moduleBaseGetOrReturn(entityrenderable_t, r, moduleEntityRenderableGet); + return jerry_number(r->type); +} + +moduleBaseFunction(moduleEntityRenderableSetType) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + moduleBaseGetOrReturn(entityrenderable_t, r, moduleEntityRenderableGet); + r->type = (entityrenderabletype_t)moduleBaseArgInt(0); + return jerry_undefined(); +} + +moduleBaseFunction(moduleEntityRenderableGetMesh) { + moduleBaseGetOrReturn(entityrenderable_t, r, moduleEntityRenderableGet); + if(!r->mesh) return jerry_undefined(); + return moduleBaseWrapPointer(r->mesh); +} + +moduleBaseFunction(moduleEntityRenderableSetMesh) { + moduleBaseRequireArgs(1); + moduleBaseGetOrReturn(entityrenderable_t, r, moduleEntityRenderableGet); + meshscript_t *ms = moduleMeshFrom(args[0]); + if(ms) { r->mesh = &ms->mesh; return jerry_undefined(); } + mesh_t *raw = (mesh_t*)moduleBaseUnwrapPointer(args[0]); + if(raw) { r->mesh = raw; return jerry_undefined(); } + return moduleBaseThrow("Expected a Mesh object or mesh constant"); +} + +moduleBaseFunction(moduleEntityRenderableGetColor) { + moduleBaseGetOrReturn(entityrenderable_t, r, moduleEntityRenderableGet); + return moduleColorMakeObject(r->material.unlit.color); +} + +moduleBaseFunction(moduleEntityRenderableSetColor) { + moduleBaseRequireArgs(1); moduleBaseRequireObject(0); + color_t *color = (color_t*)scriptProtoGetValue(&MODULE_COLOR_PROTO, args[0]); + if(!color) return moduleBaseThrow("Renderable.color: expected valid color object"); + moduleBaseGetOrReturn(entityrenderable_t, r, moduleEntityRenderableGet); + memoryCopy(&r->material.unlit.color, color, sizeof(color_t)); + return jerry_undefined(); +} + +moduleBaseFunction(moduleEntityRenderableSpriteBatchAdd) { + moduleBaseRequireArgs(1); + componenthandle_t *h = scriptProtoGetValue( + &MODULE_ENTITY_RENDERABLE_PROTO, callInfo->this_value + ); + if(!h) return jerry_undefined(); + + spritebatchsprite_t sprite; + glm_vec3_zero(sprite.min); + glm_vec3_zero(sprite.max); + glm_vec2_zero(sprite.uvMin); + glm_vec2_zero(sprite.uvMax); + + jerry_value_t obj = args[0]; +#define getVecField(name, dst, n) do { \ + jerry_value_t _v = jerry_object_get_sz(obj, name); \ + if(!jerry_value_is_undefined(_v)) moduleVec##n##AnyCheck(_v, dst); \ + jerry_value_free(_v); \ + } while(0) + getVecField("min", sprite.min, 3); + getVecField("max", sprite.max, 3); + getVecField("uvMin", sprite.uvMin, 2); + getVecField("uvMax", sprite.uvMax, 2); +#undef getVecField + + entityRenderableSpriteBatchAdd(h->eid, h->cid, &sprite); + return jerry_undefined(); +} + +moduleBaseFunction(moduleEntityRenderableSpriteBatchClear) { + componenthandle_t *h = scriptProtoGetValue( + &MODULE_ENTITY_RENDERABLE_PROTO, callInfo->this_value + ); + if(!h) return jerry_undefined(); + entityRenderableSpriteBatchClear(h->eid, h->cid); + return jerry_undefined(); +} + +static void jsRenderCallbackFree(void *user) { + jerry_value_t *cb = (jerry_value_t *)user; + jerry_value_free(*cb); + memoryFree(cb); +} + +static errorret_t jsRenderCallbackBridge( + const entityid_t entityId, + const componentid_t componentId, + const mat4 view, + const mat4 proj, + const mat4 model, + void *user +) { + (void)entityId; (void)componentId; + (void)view; (void)proj; (void)model; + jerry_value_t *cb = (jerry_value_t *)user; + jerry_value_t ret = jerry_call(*cb, jerry_undefined(), NULL, 0); + if(jerry_value_is_exception(ret)) { + jerry_value_free(ret); + errorThrow("Renderable callback threw a JS exception"); + } + jerry_value_free(ret); + errorOk(); +} + +moduleBaseFunction(moduleEntityRenderableSetCallback) { + componenthandle_t *h = scriptProtoGetValue( + &MODULE_ENTITY_RENDERABLE_PROTO, callInfo->this_value + ); + if(!h) return jerry_undefined(); + + entityrenderable_t *r = (entityrenderable_t *)componentGetData( + h->eid, h->cid, COMPONENT_TYPE_RENDERABLE + ); + + if( + r->type == ENTITY_RENDERABLE_TYPE_CALLBACK && + r->userFree && + r->user + ) { + r->userFree(r->user); + r->user = NULL; + } + + if(argc >= 1 && jerry_value_is_function(args[0])) { + jerry_value_t *cb = (jerry_value_t *)memoryAllocate(sizeof(jerry_value_t)); + *cb = jerry_value_copy(args[0]); + r->type = ENTITY_RENDERABLE_TYPE_CALLBACK; + r->callback = jsRenderCallbackBridge; + r->userFree = jsRenderCallbackFree; + r->user = cb; + } else { + r->type = ENTITY_RENDERABLE_TYPE_CALLBACK; + r->callback = NULL; + r->userFree = NULL; + r->user = NULL; + } + + return jerry_undefined(); +} + +static void moduleEntityRENDERABLE(void) { + scriptProtoInit( + &MODULE_ENTITY_RENDERABLE_PROTO, NULL, sizeof(componenthandle_t), NULL + ); + scriptProtoDefineProp(&MODULE_ENTITY_RENDERABLE_PROTO, "type", + moduleEntityRenderableGetType, moduleEntityRenderableSetType); + scriptProtoDefineProp(&MODULE_ENTITY_RENDERABLE_PROTO, "mesh", + moduleEntityRenderableGetMesh, moduleEntityRenderableSetMesh); + scriptProtoDefineProp(&MODULE_ENTITY_RENDERABLE_PROTO, "color", + moduleEntityRenderableGetColor, moduleEntityRenderableSetColor); + scriptProtoDefineFunc(&MODULE_ENTITY_RENDERABLE_PROTO, "addSprite", + moduleEntityRenderableSpriteBatchAdd); + scriptProtoDefineFunc(&MODULE_ENTITY_RENDERABLE_PROTO, "clearSprites", + moduleEntityRenderableSpriteBatchClear); + + scriptProtoDefineFunc(&MODULE_ENTITY_RENDERABLE_PROTO, "setCallback", + moduleEntityRenderableSetCallback); + + moduleBaseSetInt("ENTITY_RENDERABLE_TYPE_MATERIAL", + ENTITY_RENDERABLE_TYPE_MATERIAL); + moduleBaseSetInt("ENTITY_RENDERABLE_TYPE_SPRITEBATCH", + ENTITY_RENDERABLE_TYPE_SPRITEBATCH); + moduleBaseSetInt("ENTITY_RENDERABLE_TYPE_CALLBACK", + ENTITY_RENDERABLE_TYPE_CALLBACK); +} diff --git a/src/dusk/script/module/entity/component/moduleentitytrigger.h b/src/dusk/script/module/entity/component/moduleentitytrigger.h new file mode 100644 index 00000000..8de39bc3 --- /dev/null +++ b/src/dusk/script/module/entity/component/moduleentitytrigger.h @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/module/modulebase.h" +#include "script/module/math/modulevec3.h" +#include "script/scriptproto.h" +#include "entity/entity.h" +#include "entity/component/trigger/entitytrigger.h" +#include "moduleentityposition.h" + +static scriptproto_t MODULE_ENTITY_TRIGGER_PROTO; + +static entitytrigger_t * moduleEntityTriggerGet( + const jerry_call_info_t *callInfo +) { + componenthandle_t *h = scriptProtoGetValue( + &MODULE_ENTITY_TRIGGER_PROTO, callInfo->this_value + ); + if(!h) return NULL; + return entityTriggerGet(h->eid, h->cid); +} + +moduleBaseFunction(moduleEntityTriggerGetMin) { + moduleBaseGetOrReturn(entitytrigger_t, t, moduleEntityTriggerGet); + return moduleVec3Push(t->min); +} + +moduleBaseFunction(moduleEntityTriggerSetMin) { + moduleBaseRequireArgs(1); + moduleBaseGetOrReturn(entitytrigger_t, t, moduleEntityTriggerGet); + vec3 v; + if(!moduleVec3AnyCheck(args[0], v)) return moduleBaseThrow("Expected Vec3"); + glm_vec3_copy(v, t->min); + return jerry_undefined(); +} + +moduleBaseFunction(moduleEntityTriggerGetMax) { + moduleBaseGetOrReturn(entitytrigger_t, t, moduleEntityTriggerGet); + return moduleVec3Push(t->max); +} + +moduleBaseFunction(moduleEntityTriggerSetMax) { + moduleBaseRequireArgs(1); + moduleBaseGetOrReturn(entitytrigger_t, t, moduleEntityTriggerGet); + vec3 v; + if(!moduleVec3AnyCheck(args[0], v)) return moduleBaseThrow("Expected Vec3"); + glm_vec3_copy(v, t->max); + return jerry_undefined(); +} + +moduleBaseFunction(moduleEntityTriggerSetBounds) { + moduleBaseRequireArgs(2); + componenthandle_t *h = scriptProtoGetValue( + &MODULE_ENTITY_TRIGGER_PROTO, callInfo->this_value + ); + if(!h) return jerry_undefined(); + vec3 mn, mx; + if(!moduleVec3AnyCheck(args[0], mn)) return moduleBaseThrow("Expected Vec3 min"); + if(!moduleVec3AnyCheck(args[1], mx)) return moduleBaseThrow("Expected Vec3 max"); + entityTriggerSetBounds(h->eid, h->cid, mn, mx); + return jerry_undefined(); +} + +moduleBaseFunction(moduleEntityTriggerContains) { + moduleBaseRequireArgs(1); + componenthandle_t *h = scriptProtoGetValue( + &MODULE_ENTITY_TRIGGER_PROTO, callInfo->this_value + ); + if(!h) return jerry_boolean(false); + vec3 point; + if(!moduleVec3AnyCheck(args[0], point)) return moduleBaseThrow("Expected Vec3"); + return jerry_boolean(entityTriggerContains(h->eid, h->cid, point)); +} + +static void moduleEntityTRIGGER(void) { + scriptProtoInit( + &MODULE_ENTITY_TRIGGER_PROTO, NULL, sizeof(componenthandle_t), NULL + ); + scriptProtoDefineProp(&MODULE_ENTITY_TRIGGER_PROTO, "min", + moduleEntityTriggerGetMin, moduleEntityTriggerSetMin); + scriptProtoDefineProp(&MODULE_ENTITY_TRIGGER_PROTO, "max", + moduleEntityTriggerGetMax, moduleEntityTriggerSetMax); + scriptProtoDefineFunc(&MODULE_ENTITY_TRIGGER_PROTO, "setBounds", + moduleEntityTriggerSetBounds); + scriptProtoDefineFunc(&MODULE_ENTITY_TRIGGER_PROTO, "contains", + moduleEntityTriggerContains); +} diff --git a/src/dusk/script/module/entity/moduleentity.h b/src/dusk/script/module/entity/moduleentity.h new file mode 100644 index 00000000..1009adb8 --- /dev/null +++ b/src/dusk/script/module/entity/moduleentity.h @@ -0,0 +1,196 @@ +/** + * 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/entity.h" +#include "entity/entitymanager.h" + +#include "component/moduleentityposition.h" +#include "component/moduleentitycamera.h" +#include "component/moduleentityrenderable.h" +#include "component/moduleentityphysics.h" +#include "component/moduleentitytrigger.h" + +typedef struct { + entityid_t id; +} entityscript_t; + +static scriptproto_t MODULE_ENTITY_PROTO; + +static inline entityscript_t * moduleEntityGet( + const jerry_call_info_t *callInfo +) { + return (entityscript_t*)scriptProtoGetValue( + &MODULE_ENTITY_PROTO, callInfo->this_value + ); +} + +// Getters +moduleBaseFunction(moduleEntityGetId) { + moduleBaseGetOrReturn(entityscript_t, inst, moduleEntityGet); + return jerry_number(inst->id); +} + +// Getter defined for each component type +static jerry_value_t moduleEntityGetComponent( + const jerry_call_info_t *callInfo, + const jerry_value_t args[], + const jerry_length_t argc, + const componenttype_t type, + scriptproto_t *proto +) { + assertNotNull(callInfo, "Call info must not be null"); + assertTrue(argc >= 0, "Argc must be non-negative"); + assertTrue( + type > COMPONENT_TYPE_NULL && type < COMPONENT_TYPE_COUNT, + "Invalid component type" + ); + + entityid_t entityId; + componentid_t compId; + + entityscript_t *inst = moduleEntityGet(callInfo); + if(!inst) return jerry_undefined(); + entityId = inst->id; + + // Find the component ID of the requested type. + compId = entityGetComponent(entityId, type); + if(compId == COMPONENT_ID_INVALID) { + return jerry_undefined(); + } + + componenthandle_t h = { .eid = entityId, .cid = compId }; + return scriptProtoCreateValue(proto, &h); +} + +#define X(enumName, type, field, init, dispose, fixedUpdate) \ + moduleBaseFunction(moduleEntityGet##enumName) { \ + return moduleEntityGetComponent( \ + callInfo, \ + args, \ + argc, \ + COMPONENT_TYPE_##enumName, \ + &MODULE_ENTITY_##enumName##_PROTO \ + ); \ + } +#include "entity/componentlist.h" +#undef X + +moduleBaseFunction(moduleEntityToString) { + entityscript_t *inst = moduleEntityGet(callInfo); + if(!inst) return jerry_string_sz("Entity(?)"); + + char_t components[128]; + size_t clen = 0; + bool_t first = true; + + for(componenttype_t t = 1; t < COMPONENT_TYPE_COUNT; t++) { + if(entityGetComponent(inst->id, t) == COMPONENT_ID_INVALID) continue; + if(!first) { + stringCopy(components + clen, ", ", sizeof(components) - clen); + clen += 2; + } + const char_t *name = COMPONENT_DEFINITIONS[t].enumName; + stringCopy(components + clen, name, sizeof(components) - clen); + clen += strlen(name); + first = false; + } + + char_t buf[256]; + if(first) { + stringFormat( + buf, sizeof(buf), + "{ \"id\": %d, \"components\": [] }", inst->id + ); + } else { + stringFormat( + buf, sizeof(buf), + "{ \"id\": %d, \"components\": [ %s ] }", inst->id, components + ); + } + return jerry_string_sz(buf); +} + +// Methods +moduleBaseFunction(moduleEntityConstructor) { + entityscript_t *inst = (entityscript_t*)memoryAllocate( + sizeof(entityscript_t) + ); + inst->id = entityManagerAdd(); + jerry_object_set_native_ptr( + callInfo->this_value, &MODULE_ENTITY_PROTO.info, inst + ); + return jerry_undefined(); +} + +moduleBaseFunction(moduleEntityAddComponent) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + componenttype_t type = (componenttype_t)moduleBaseArgInt(0); + if(type <= COMPONENT_TYPE_NULL || type >= COMPONENT_TYPE_COUNT) { + return moduleBaseThrow("Entity.add: invalid component type"); + } + + entityscript_t *inst = moduleEntityGet(callInfo); + if(!inst) return moduleBaseThrow("Entity.add: invalid entity"); + + componentid_t id = entityAddComponent(inst->id, type); + componenthandle_t h = { .eid = inst->id, .cid = id }; + switch(type) { + #define X(enumName, stype, field, init, dispose, fixedUpdate) \ + case COMPONENT_TYPE_##enumName: \ + return scriptProtoCreateValue(&MODULE_ENTITY_##enumName##_PROTO, &h); + #include "entity/componentlist.h" + #undef X + default: return jerry_number(id); + } +} + +moduleBaseFunction(moduleEntityDisposeMethod) { + moduleBaseGetOrReturn(entityscript_t, inst, moduleEntityGet); + entityDispose(inst->id); + return jerry_undefined(); +} + +static void moduleEntity(void) { + // Init the entity prototype + scriptProtoInit( + &MODULE_ENTITY_PROTO, + "Entity", + sizeof(entityscript_t), + moduleEntityConstructor + ); + + scriptProtoDefineToString(&MODULE_ENTITY_PROTO, moduleEntityToString); + + // Entity Methods + scriptProtoDefineFunc( + &MODULE_ENTITY_PROTO, "add", moduleEntityAddComponent + ); + scriptProtoDefineFunc( + &MODULE_ENTITY_PROTO, "dispose", moduleEntityDisposeMethod + ); + + // Entity props + scriptProtoDefineProp( + &MODULE_ENTITY_PROTO, "id", moduleEntityGetId, NULL + ); + + // Init component type modules. + #define X(enumName, type, field, iMethod, dMethod, fMethod) \ + moduleEntity##enumName(); \ + scriptProtoDefineProp( \ + &MODULE_ENTITY_PROTO, \ + COMPONENT_DEFINITIONS[COMPONENT_TYPE_##enumName].name, \ + moduleEntityGet##enumName, \ + NULL \ + ); \ + moduleBaseSetInt(#enumName, COMPONENT_TYPE_##enumName); + #include "entity/componentlist.h" + #undef X +} diff --git a/src/dusk/script/module/input/moduleinput.h b/src/dusk/script/module/input/moduleinput.h new file mode 100644 index 00000000..5ff4cf59 --- /dev/null +++ b/src/dusk/script/module/input/moduleinput.h @@ -0,0 +1,158 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/module/modulebase.h" +#include "script/scriptproto.h" +#include "script/module/math/modulevec2.h" +#include "input/input.h" + +static scriptproto_t MODULE_INPUT_PROTO; + +// Static Methods +moduleBaseFunction(moduleInputBind) { + moduleBaseRequireArgs(2); moduleBaseRequireString(0); moduleBaseRequireNumber(1); + + char_t strBtn[128]; + moduleBaseToString(args[0], strBtn, sizeof(strBtn)); + if(strBtn[0] == '\0') { + return moduleBaseThrow("Input.bind: button name cannot be empty"); + } + + const inputaction_t action = (inputaction_t)moduleBaseArgInt(1); + if(action <= INPUT_ACTION_NULL || action >= INPUT_ACTION_COUNT) { + return moduleBaseThrow("Input.bind: invalid action"); + } + + inputbutton_t btn = inputButtonGetByName(strBtn); + if(btn.type == INPUT_BUTTON_TYPE_NONE) { + return moduleBaseThrow("Input.bind: invalid button name"); + } + + inputBind(btn, action); + return jerry_undefined(); +} + +moduleBaseFunction(moduleInputIsDown) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + const inputaction_t action = (inputaction_t)moduleBaseArgInt(0); + if(action <= INPUT_ACTION_NULL || action >= INPUT_ACTION_COUNT) { + return moduleBaseThrow("Input.isDown: invalid action"); + } + return jerry_boolean(inputIsDown(action)); +} + +moduleBaseFunction(moduleInputPressed) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + const inputaction_t action = (inputaction_t)moduleBaseArgInt(0); + if(action <= INPUT_ACTION_NULL || action >= INPUT_ACTION_COUNT) { + return moduleBaseThrow("Input.pressed: invalid action"); + } + return jerry_boolean(inputPressed(action)); +} + +moduleBaseFunction(moduleInputReleased) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + const inputaction_t action = (inputaction_t)moduleBaseArgInt(0); + if(action <= INPUT_ACTION_NULL || action >= INPUT_ACTION_COUNT) { + return moduleBaseThrow("Input.released: invalid action"); + } + return jerry_boolean(inputReleased(action)); +} + +moduleBaseFunction(moduleInputGetValue) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + const inputaction_t action = (inputaction_t)moduleBaseArgInt(0); + if(action <= INPUT_ACTION_NULL || action >= INPUT_ACTION_COUNT) { + return moduleBaseThrow("Input.getValue: invalid action"); + } + return jerry_number(inputGetCurrentValue(action)); +} + +moduleBaseFunction(moduleInputAxis) { + moduleBaseRequireArgs(2); moduleBaseRequireNumber(0); moduleBaseRequireNumber(1); + const inputaction_t neg = (inputaction_t)moduleBaseArgInt(0); + const inputaction_t pos = (inputaction_t)moduleBaseArgInt(1); + if(neg <= INPUT_ACTION_NULL || neg >= INPUT_ACTION_COUNT) { + return moduleBaseThrow("Input.axis: invalid negative action"); + } + if(pos <= INPUT_ACTION_NULL || pos >= INPUT_ACTION_COUNT) { + return moduleBaseThrow("Input.axis: invalid positive action"); + } + return jerry_number(inputAxis(neg, pos)); +} + +moduleBaseFunction(moduleInputAxis2D) { + moduleBaseRequireArgs(4); + moduleBaseRequireNumber(0); moduleBaseRequireNumber(1); + moduleBaseRequireNumber(2); moduleBaseRequireNumber(3); + const inputaction_t negX = (inputaction_t)moduleBaseArgInt(0); + const inputaction_t posX = (inputaction_t)moduleBaseArgInt(1); + const inputaction_t negY = (inputaction_t)moduleBaseArgInt(2); + const inputaction_t posY = (inputaction_t)moduleBaseArgInt(3); + if(negX <= INPUT_ACTION_NULL || negX >= INPUT_ACTION_COUNT) { + return moduleBaseThrow("Input.axis2D: invalid negX action"); + } + if(posX <= INPUT_ACTION_NULL || posX >= INPUT_ACTION_COUNT) { + return moduleBaseThrow("Input.axis2D: invalid posX action"); + } + if(negY <= INPUT_ACTION_NULL || negY >= INPUT_ACTION_COUNT) { + return moduleBaseThrow("Input.axis2D: invalid negY action"); + } + if(posY <= INPUT_ACTION_NULL || posY >= INPUT_ACTION_COUNT) { + return moduleBaseThrow("Input.axis2D: invalid posY action"); + } + vec2 result; + inputAxis2D(negX, posX, negY, posY, result); + return moduleVec2Push(result); +} + +static void moduleInput(void) { + moduleBaseEval(INPUT_ACTION_SCRIPT); + + moduleBaseEval( + "" + #ifdef DUSK_INPUT_KEYBOARD + "var INPUT_KEYBOARD = true;\n" + #endif + #ifdef DUSK_INPUT_GAMEPAD + "var INPUT_GAMEPAD = true;\n" + #endif + #ifdef DUSK_INPUT_POINTER + "var INPUT_POINTER = true;\n" + #endif + #ifdef DUSK_INPUT_TOUCH + "var INPUT_TOUCH = true;\n" + #endif + ); + + scriptProtoInit( + &MODULE_INPUT_PROTO, "Input", sizeof(uint8_t), NULL + ); + + scriptProtoDefineStaticFunc( + &MODULE_INPUT_PROTO, "bind", moduleInputBind + ); + scriptProtoDefineStaticFunc( + &MODULE_INPUT_PROTO, "isDown", moduleInputIsDown + ); + scriptProtoDefineStaticFunc( + &MODULE_INPUT_PROTO, "pressed", moduleInputPressed + ); + scriptProtoDefineStaticFunc( + &MODULE_INPUT_PROTO, "released", moduleInputReleased + ); + scriptProtoDefineStaticFunc( + &MODULE_INPUT_PROTO, "getValue", moduleInputGetValue + ); + scriptProtoDefineStaticFunc( + &MODULE_INPUT_PROTO, "axis", moduleInputAxis + ); + scriptProtoDefineStaticFunc( + &MODULE_INPUT_PROTO, "axis2D", moduleInputAxis2D + ); +} diff --git a/src/dusk/script/module/math/modulemat4.h b/src/dusk/script/module/math/modulemat4.h new file mode 100644 index 00000000..11baa97c --- /dev/null +++ b/src/dusk/script/module/math/modulemat4.h @@ -0,0 +1,226 @@ +/** + * 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 "cglm/cglm.h" +#include "modulevec3.h" +#include "modulevec4.h" + +static scriptproto_t MODULE_MAT4_PROTO; + +static inline void * moduleMatGet(const jerry_call_info_t *callInfo) { + return scriptProtoGetValue(&MODULE_MAT4_PROTO, callInfo->this_value); +} + +moduleBaseFunction(moduleMatConstructor) { + float_t (*ptr)[4] = (float_t (*)[4])memoryAllocate(sizeof(mat4)); + glm_mat4_identity(ptr); + jerry_object_set_native_ptr( + callInfo->this_value, &MODULE_MAT4_PROTO.info, ptr + ); + return jerry_undefined(); +} + +moduleBaseFunction(moduleMatMul) { + moduleBaseRequireArgs(1); + float_t (*a)[4] = (float_t (*)[4])moduleMatGet(callInfo); + if(!a) return moduleBaseThrow("Mat4.mul: invalid this"); + float_t (*b)[4] = (float_t (*)[4])scriptProtoGetValue( + &MODULE_MAT4_PROTO, args[0] + ); + if(!b) return moduleBaseThrow("Mat4.mul: argument must be a Mat4"); + mat4 r; + glm_mat4_mul(a, b, r); + return scriptProtoCreateValue(&MODULE_MAT4_PROTO, r); +} + +moduleBaseFunction(moduleMatTranspose) { + float_t (*m)[4] = (float_t (*)[4])moduleMatGet(callInfo); + if(!m) return moduleBaseThrow("Mat4.transpose: invalid this"); + mat4 r; + glm_mat4_transpose_to(m, r); + return scriptProtoCreateValue(&MODULE_MAT4_PROTO, r); +} + +moduleBaseFunction(moduleMatInverse) { + float_t (*m)[4] = (float_t (*)[4])moduleMatGet(callInfo); + if(!m) return moduleBaseThrow("Mat4.inverse: invalid this"); + mat4 r; + glm_mat4_inv(m, r); + return scriptProtoCreateValue(&MODULE_MAT4_PROTO, r); +} + +moduleBaseFunction(moduleMatDeterminant) { + float_t (*m)[4] = (float_t (*)[4])moduleMatGet(callInfo); + if(!m) return moduleBaseThrow("Mat4.determinant: invalid this"); + return jerry_number(glm_mat4_det(m)); +} + +moduleBaseFunction(moduleMatMulVec3) { + moduleBaseRequireArgs(1); + float_t (*m)[4] = (float_t (*)[4])moduleMatGet(callInfo); + if(!m) return moduleBaseThrow("Mat4.mulVec3: invalid this"); + vec3 vin; + if(!moduleVec3Check(args[0], vin)) { + return moduleBaseThrow("Mat4.mulVec3: argument must be a Vec3"); + } + float_t w = moduleBaseOptFloat(1, 1.0f); + vec3 vout; + glm_mat4_mulv3(m, vin, w, vout); + return moduleVec3Push(vout); +} + +moduleBaseFunction(moduleMatMulVec4) { + moduleBaseRequireArgs(1); + float_t (*m)[4] = (float_t (*)[4])moduleMatGet(callInfo); + if(!m) return moduleBaseThrow("Mat4.mulVec4: invalid this"); + vec4 vin; + if(!moduleVec4Check(args[0], vin)) { + return moduleBaseThrow("Mat4.mulVec4: argument must be a Vec4"); + } + vec4 vout; + glm_mat4_mulv(m, vin, vout); + return moduleVec4Push(vout); +} + +moduleBaseFunction(moduleMatTranslate) { + moduleBaseRequireArgs(1); + float_t (*m)[4] = (float_t (*)[4])moduleMatGet(callInfo); + if(!m) return moduleBaseThrow("Mat4.translate: invalid this"); + vec3 tv; + if(!moduleVec3Check(args[0], tv)) { + return moduleBaseThrow("Mat4.translate: argument must be a Vec3"); + } + mat4 r; + glm_mat4_copy(m, r); + glm_translate(r, tv); + return scriptProtoCreateValue(&MODULE_MAT4_PROTO, r); +} + +moduleBaseFunction(moduleMatScale) { + moduleBaseRequireArgs(1); + float_t (*m)[4] = (float_t (*)[4])moduleMatGet(callInfo); + if(!m) return moduleBaseThrow("Mat4.scale: invalid this"); + vec3 sv; + if(!moduleVec3Check(args[0], sv)) { + return moduleBaseThrow("Mat4.scale: argument must be a Vec3"); + } + mat4 r; + glm_mat4_copy(m, r); + glm_scale(r, sv); + return scriptProtoCreateValue(&MODULE_MAT4_PROTO, r); +} + +moduleBaseFunction(moduleMatStaticIdentity) { + mat4 r; + glm_mat4_identity(r); + return scriptProtoCreateValue(&MODULE_MAT4_PROTO, r); +} + +moduleBaseFunction(moduleMatStaticPerspective) { + moduleBaseRequireArgs(4); + moduleBaseRequireNumber(0); moduleBaseRequireNumber(1); + moduleBaseRequireNumber(2); moduleBaseRequireNumber(3); + mat4 r; + glm_perspective( + moduleBaseArgFloat(0), moduleBaseArgFloat(1), + moduleBaseArgFloat(2), moduleBaseArgFloat(3), + r + ); + return scriptProtoCreateValue(&MODULE_MAT4_PROTO, r); +} + +moduleBaseFunction(moduleMatStaticLookAt) { + moduleBaseRequireArgs(3); + vec3 eye, center, up; + if(!moduleVec3Check(args[0], eye)) { + return moduleBaseThrow("Mat4.lookAt: eye must be a Vec3"); + } + if(!moduleVec3Check(args[1], center)) { + return moduleBaseThrow("Mat4.lookAt: center must be a Vec3"); + } + if(!moduleVec3Check(args[2], up)) { + return moduleBaseThrow("Mat4.lookAt: up must be a Vec3"); + } + mat4 r; + glm_lookat(eye, center, up, r); + return scriptProtoCreateValue(&MODULE_MAT4_PROTO, r); +} + +moduleBaseFunction(moduleMatToString) { + float_t (*m)[4] = (float_t (*)[4])moduleMatGet(callInfo); + if(!m) return jerry_string_sz("Mat4(?)"); + char_t buf[256]; + stringFormat( + buf, sizeof(buf), + "Mat4([%g,%g,%g,%g], [%g,%g,%g,%g]," + " [%g,%g,%g,%g], [%g,%g,%g,%g])", + m[0][0], m[0][1], m[0][2], m[0][3], + m[1][0], m[1][1], m[1][2], m[1][3], + m[2][0], m[2][1], m[2][2], m[2][3], + m[3][0], m[3][1], m[3][2], m[3][3] + ); + return jerry_string_sz(buf); +} + +static inline jerry_value_t moduleMat4Push(float (*m)[4]) { + return scriptProtoCreateValue(&MODULE_MAT4_PROTO, m); +} + +static inline bool_t moduleMat4Check(jerry_value_t val, float (*out)[4]) { + float_t (*m)[4] = (float_t (*)[4])scriptProtoGetValue( + &MODULE_MAT4_PROTO, val + ); + if(!m) return false; + glm_mat4_copy(m, out); + return true; +} + +static void moduleMat4(void) { + scriptProtoInit( + &MODULE_MAT4_PROTO, "Mat4", sizeof(mat4), moduleMatConstructor + ); + + scriptProtoDefineToString(&MODULE_MAT4_PROTO, moduleMatToString); + + scriptProtoDefineFunc( + &MODULE_MAT4_PROTO, "mul", moduleMatMul + ); + scriptProtoDefineFunc( + &MODULE_MAT4_PROTO, "transpose", moduleMatTranspose + ); + scriptProtoDefineFunc( + &MODULE_MAT4_PROTO, "inverse", moduleMatInverse + ); + scriptProtoDefineFunc( + &MODULE_MAT4_PROTO, "determinant", moduleMatDeterminant + ); + scriptProtoDefineFunc( + &MODULE_MAT4_PROTO, "mulVec3", moduleMatMulVec3 + ); + scriptProtoDefineFunc( + &MODULE_MAT4_PROTO, "mulVec4", moduleMatMulVec4 + ); + scriptProtoDefineFunc( + &MODULE_MAT4_PROTO, "translate", moduleMatTranslate + ); + scriptProtoDefineFunc( + &MODULE_MAT4_PROTO, "scale", moduleMatScale + ); + + scriptProtoDefineStaticFunc( + &MODULE_MAT4_PROTO, "identity", moduleMatStaticIdentity + ); + scriptProtoDefineStaticFunc( + &MODULE_MAT4_PROTO, "perspective", moduleMatStaticPerspective + ); + scriptProtoDefineStaticFunc( + &MODULE_MAT4_PROTO, "lookAt", moduleMatStaticLookAt + ); +} diff --git a/src/dusk/script/module/math/modulemath.h b/src/dusk/script/module/math/modulemath.h new file mode 100644 index 00000000..906c1f0d --- /dev/null +++ b/src/dusk/script/module/math/modulemath.h @@ -0,0 +1,22 @@ +/** + * 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 "modulevec2.h" +#include "modulevec3.h" +#include "modulevec3ref.h" +#include "modulevec4.h" +#include "modulemat4.h" + +static void moduleMath(void) { + moduleVec2(); + moduleVec3(); + moduleVec3Ref(); + moduleVec4(); + moduleMat4(); +} diff --git a/src/dusk/script/module/math/modulevec2.h b/src/dusk/script/module/math/modulevec2.h new file mode 100644 index 00000000..b63fa248 --- /dev/null +++ b/src/dusk/script/module/math/modulevec2.h @@ -0,0 +1,193 @@ +/** + * 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 "cglm/cglm.h" + +static scriptproto_t MODULE_VEC2_PROTO; + +static inline float_t * moduleVec2Get(const jerry_call_info_t *callInfo) { + return (float_t *)scriptProtoGetValue( + &MODULE_VEC2_PROTO, callInfo->this_value + ); +} + +static inline float_t * moduleVec2From(jerry_value_t val) { + return (float_t *)scriptProtoGetValue(&MODULE_VEC2_PROTO, val); +} + +moduleBaseFunction(moduleVec2Constructor) { + float_t *ptr = (float_t *)memoryAllocate(sizeof(vec2)); + ptr[0] = moduleBaseOptFloat(0, 0.0f); + ptr[1] = moduleBaseOptFloat(1, 0.0f); + jerry_object_set_native_ptr( + callInfo->this_value, &MODULE_VEC2_PROTO.info, ptr + ); + return jerry_undefined(); +} + +moduleBaseFunction(moduleVec2GetX) { + moduleBaseGetOrReturn(float_t, v, moduleVec2Get); + return jerry_number(v[0]); +} +moduleBaseFunction(moduleVec2SetX) { + moduleBaseRequireArgs(1); + moduleBaseGetOrReturn(float_t, v, moduleVec2Get); + v[0] = moduleBaseArgFloat(0); + return jerry_undefined(); +} + +moduleBaseFunction(moduleVec2GetY) { + moduleBaseGetOrReturn(float_t, v, moduleVec2Get); + return jerry_number(v[1]); +} +moduleBaseFunction(moduleVec2SetY) { + moduleBaseRequireArgs(1); + moduleBaseGetOrReturn(float_t, v, moduleVec2Get); + v[1] = moduleBaseArgFloat(0); + return jerry_undefined(); +} + +moduleBaseFunction(moduleVec2Dot) { + moduleBaseRequireArgs(1); + float_t *a = moduleVec2Get(callInfo); + if(!a) return moduleBaseThrow("Vec2.dot: invalid this"); + float_t *b = moduleVec2From(args[0]); + if(!b) return moduleBaseThrow("Vec2.dot: argument must be a Vec2"); + return jerry_number(glm_vec2_dot(a, b)); +} + +moduleBaseFunction(moduleVec2Length) { + float_t *v = moduleVec2Get(callInfo); + if(!v) return moduleBaseThrow("Vec2.length: invalid this"); + return jerry_number(glm_vec2_norm(v)); +} + +moduleBaseFunction(moduleVec2LengthSq) { + float_t *v = moduleVec2Get(callInfo); + if(!v) return moduleBaseThrow("Vec2.lengthSq: invalid this"); + return jerry_number(glm_vec2_norm2(v)); +} + +moduleBaseFunction(moduleVec2Normalize) { + float_t *v = moduleVec2Get(callInfo); + if(!v) return moduleBaseThrow("Vec2.normalize: invalid this"); + vec2 r; + glm_vec2_normalize_to(v, r); + return scriptProtoCreateValue(&MODULE_VEC2_PROTO, r); +} + +moduleBaseFunction(moduleVec2Negate) { + float_t *v = moduleVec2Get(callInfo); + if(!v) return moduleBaseThrow("Vec2.negate: invalid this"); + vec2 r; + glm_vec2_negate_to(v, r); + return scriptProtoCreateValue(&MODULE_VEC2_PROTO, r); +} + +moduleBaseFunction(moduleVec2Add) { + moduleBaseRequireArgs(1); + float_t *a = moduleVec2Get(callInfo); + if(!a) return moduleBaseThrow("Vec2.add: invalid this"); + float_t *b = moduleVec2From(args[0]); + if(!b) return moduleBaseThrow("Vec2.add: argument must be a Vec2"); + vec2 r; + glm_vec2_add(a, b, r); + return scriptProtoCreateValue(&MODULE_VEC2_PROTO, r); +} + +moduleBaseFunction(moduleVec2Sub) { + moduleBaseRequireArgs(1); + float_t *a = moduleVec2Get(callInfo); + if(!a) return moduleBaseThrow("Vec2.sub: invalid this"); + float_t *b = moduleVec2From(args[0]); + if(!b) return moduleBaseThrow("Vec2.sub: argument must be a Vec2"); + vec2 r; + glm_vec2_sub(a, b, r); + return scriptProtoCreateValue(&MODULE_VEC2_PROTO, r); +} + +moduleBaseFunction(moduleVec2Scale) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + float_t *v = moduleVec2Get(callInfo); + if(!v) return moduleBaseThrow("Vec2.scale: invalid this"); + vec2 r; + glm_vec2_scale(v, moduleBaseArgFloat(0), r); + return scriptProtoCreateValue(&MODULE_VEC2_PROTO, r); +} + +moduleBaseFunction(moduleVec2Lerp) { + moduleBaseRequireArgs(2); moduleBaseRequireNumber(1); + float_t *a = moduleVec2Get(callInfo); + if(!a) return moduleBaseThrow("Vec2.lerp: invalid this"); + float_t *b = moduleVec2From(args[0]); + if(!b) return moduleBaseThrow("Vec2.lerp: first argument must be a Vec2"); + vec2 r; + glm_vec2_lerp(a, b, moduleBaseArgFloat(1), r); + return scriptProtoCreateValue(&MODULE_VEC2_PROTO, r); +} + +moduleBaseFunction(moduleVec2Distance) { + moduleBaseRequireArgs(1); + float_t *a = moduleVec2Get(callInfo); + if(!a) return moduleBaseThrow("Vec2.distance: invalid this"); + float_t *b = moduleVec2From(args[0]); + if(!b) return moduleBaseThrow("Vec2.distance: argument must be a Vec2"); + return jerry_number(glm_vec2_distance(a, b)); +} + +moduleBaseFunction(moduleVec2ToString) { + float_t *v = moduleVec2Get(callInfo); + if(!v) return jerry_string_sz("Vec2(?, ?)"); + char_t buf[64]; + stringFormat(buf, sizeof(buf), "Vec2(%g, %g)", v[0], v[1]); + return jerry_string_sz(buf); +} + +static inline jerry_value_t moduleVec2Push(const float_t *v) { + return scriptProtoCreateValue(&MODULE_VEC2_PROTO, v); +} + +static inline bool_t moduleVec2Check(jerry_value_t val, float_t *out) { + float_t *v = moduleVec2From(val); + if(!v) return false; + out[0] = v[0]; + out[1] = v[1]; + return true; +} + +static inline bool_t moduleVec2AnyCheck(jerry_value_t val, float_t *out) { + return moduleVec2Check(val, out); +} + +static void moduleVec2(void) { + scriptProtoInit( + &MODULE_VEC2_PROTO, "Vec2", sizeof(vec2), moduleVec2Constructor + ); + + scriptProtoDefineProp( + &MODULE_VEC2_PROTO, "x", moduleVec2GetX, moduleVec2SetX + ); + scriptProtoDefineProp( + &MODULE_VEC2_PROTO, "y", moduleVec2GetY, moduleVec2SetY + ); + + scriptProtoDefineToString(&MODULE_VEC2_PROTO, moduleVec2ToString); + + scriptProtoDefineFunc(&MODULE_VEC2_PROTO, "dot", moduleVec2Dot); + scriptProtoDefineFunc(&MODULE_VEC2_PROTO, "length", moduleVec2Length); + scriptProtoDefineFunc(&MODULE_VEC2_PROTO, "lengthSq", moduleVec2LengthSq); + scriptProtoDefineFunc(&MODULE_VEC2_PROTO, "normalize", moduleVec2Normalize); + scriptProtoDefineFunc(&MODULE_VEC2_PROTO, "negate", moduleVec2Negate); + scriptProtoDefineFunc(&MODULE_VEC2_PROTO, "add", moduleVec2Add); + scriptProtoDefineFunc(&MODULE_VEC2_PROTO, "sub", moduleVec2Sub); + scriptProtoDefineFunc(&MODULE_VEC2_PROTO, "scale", moduleVec2Scale); + scriptProtoDefineFunc(&MODULE_VEC2_PROTO, "lerp", moduleVec2Lerp); + scriptProtoDefineFunc(&MODULE_VEC2_PROTO, "distance", moduleVec2Distance); +} diff --git a/src/dusk/script/module/math/modulevec3.h b/src/dusk/script/module/math/modulevec3.h new file mode 100644 index 00000000..165d577f --- /dev/null +++ b/src/dusk/script/module/math/modulevec3.h @@ -0,0 +1,217 @@ +/** + * 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 "cglm/cglm.h" + +static scriptproto_t MODULE_VEC3_PROTO; + +static inline float_t * moduleVec3Get(const jerry_call_info_t *callInfo) { + return (float_t *)scriptProtoGetValue( + &MODULE_VEC3_PROTO, callInfo->this_value + ); +} + +static inline float_t * moduleVec3From(jerry_value_t val) { + return (float_t *)scriptProtoGetValue(&MODULE_VEC3_PROTO, val); +} + +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) { + moduleBaseGetOrReturn(float_t, v, moduleVec3Get); + return jerry_number(v[0]); +} +moduleBaseFunction(moduleVec3SetX) { + moduleBaseRequireArgs(1); + moduleBaseGetOrReturn(float_t, v, moduleVec3Get); + v[0] = moduleBaseArgFloat(0); + return jerry_undefined(); +} + +moduleBaseFunction(moduleVec3GetY) { + moduleBaseGetOrReturn(float_t, v, moduleVec3Get); + return jerry_number(v[1]); +} +moduleBaseFunction(moduleVec3SetY) { + moduleBaseRequireArgs(1); + moduleBaseGetOrReturn(float_t, v, moduleVec3Get); + v[1] = moduleBaseArgFloat(0); + return jerry_undefined(); +} + +moduleBaseFunction(moduleVec3GetZ) { + moduleBaseGetOrReturn(float_t, v, moduleVec3Get); + return jerry_number(v[2]); +} +moduleBaseFunction(moduleVec3SetZ) { + moduleBaseRequireArgs(1); + moduleBaseGetOrReturn(float_t, v, moduleVec3Get); + v[2] = moduleBaseArgFloat(0); + return jerry_undefined(); +} + +moduleBaseFunction(moduleVec3Dot) { + moduleBaseRequireArgs(1); + float_t *a = moduleVec3Get(callInfo); + if(!a) return moduleBaseThrow("Vec3.dot: invalid this"); + float_t *b = moduleVec3From(args[0]); + if(!b) return moduleBaseThrow("Vec3.dot: argument must be a Vec3"); + return jerry_number(glm_vec3_dot(a, b)); +} + +moduleBaseFunction(moduleVec3Cross) { + moduleBaseRequireArgs(1); + float_t *a = moduleVec3Get(callInfo); + if(!a) return moduleBaseThrow("Vec3.cross: invalid this"); + float_t *b = moduleVec3From(args[0]); + if(!b) return moduleBaseThrow("Vec3.cross: argument must be a Vec3"); + vec3 r; + glm_vec3_cross(a, b, r); + return scriptProtoCreateValue(&MODULE_VEC3_PROTO, r); +} + +moduleBaseFunction(moduleVec3Length) { + float_t *v = moduleVec3Get(callInfo); + if(!v) return moduleBaseThrow("Vec3.length: invalid this"); + return jerry_number(glm_vec3_norm(v)); +} + +moduleBaseFunction(moduleVec3LengthSq) { + float_t *v = moduleVec3Get(callInfo); + if(!v) return moduleBaseThrow("Vec3.lengthSq: invalid this"); + return jerry_number(glm_vec3_norm2(v)); +} + +moduleBaseFunction(moduleVec3Normalize) { + float_t *v = moduleVec3Get(callInfo); + if(!v) return moduleBaseThrow("Vec3.normalize: invalid this"); + vec3 r; + glm_vec3_normalize_to(v, r); + return scriptProtoCreateValue(&MODULE_VEC3_PROTO, r); +} + +moduleBaseFunction(moduleVec3Negate) { + float_t *v = moduleVec3Get(callInfo); + if(!v) return moduleBaseThrow("Vec3.negate: invalid this"); + vec3 r; + glm_vec3_negate_to(v, r); + return scriptProtoCreateValue(&MODULE_VEC3_PROTO, r); +} + +moduleBaseFunction(moduleVec3Add) { + moduleBaseRequireArgs(1); + float_t *a = moduleVec3Get(callInfo); + if(!a) return moduleBaseThrow("Vec3.add: invalid this"); + float_t *b = moduleVec3From(args[0]); + if(!b) return moduleBaseThrow("Vec3.add: argument must be a Vec3"); + vec3 r; + glm_vec3_add(a, b, r); + return scriptProtoCreateValue(&MODULE_VEC3_PROTO, r); +} + +moduleBaseFunction(moduleVec3Sub) { + moduleBaseRequireArgs(1); + float_t *a = moduleVec3Get(callInfo); + if(!a) return moduleBaseThrow("Vec3.sub: invalid this"); + float_t *b = moduleVec3From(args[0]); + if(!b) return moduleBaseThrow("Vec3.sub: argument must be a Vec3"); + vec3 r; + glm_vec3_sub(a, b, r); + return scriptProtoCreateValue(&MODULE_VEC3_PROTO, r); +} + +moduleBaseFunction(moduleVec3Scale) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + float_t *v = moduleVec3Get(callInfo); + if(!v) return moduleBaseThrow("Vec3.scale: invalid this"); + vec3 r; + glm_vec3_scale(v, moduleBaseArgFloat(0), r); + return scriptProtoCreateValue(&MODULE_VEC3_PROTO, r); +} + +moduleBaseFunction(moduleVec3Lerp) { + moduleBaseRequireArgs(2); moduleBaseRequireNumber(1); + float_t *a = moduleVec3Get(callInfo); + if(!a) return moduleBaseThrow("Vec3.lerp: invalid this"); + float_t *b = moduleVec3From(args[0]); + if(!b) return moduleBaseThrow("Vec3.lerp: first argument must be a Vec3"); + vec3 r; + glm_vec3_lerp(a, b, moduleBaseArgFloat(1), r); + return scriptProtoCreateValue(&MODULE_VEC3_PROTO, r); +} + +moduleBaseFunction(moduleVec3Distance) { + moduleBaseRequireArgs(1); + float_t *a = moduleVec3Get(callInfo); + if(!a) return moduleBaseThrow("Vec3.distance: invalid this"); + float_t *b = moduleVec3From(args[0]); + if(!b) return moduleBaseThrow("Vec3.distance: argument must be a Vec3"); + return jerry_number(glm_vec3_distance(a, b)); +} + +moduleBaseFunction(moduleVec3ToString) { + float_t *v = moduleVec3Get(callInfo); + if(!v) return jerry_string_sz("Vec3(?, ?, ?)"); + char_t buf[80]; + stringFormat(buf, sizeof(buf), "Vec3(%g, %g, %g)", v[0], v[1], v[2]); + return jerry_string_sz(buf); +} + +static inline jerry_value_t moduleVec3Push(const float_t *v) { + return scriptProtoCreateValue(&MODULE_VEC3_PROTO, v); +} + +static inline bool_t moduleVec3Check(jerry_value_t val, float_t *out) { + float_t *v = moduleVec3From(val); + if(!v) return false; + out[0] = v[0]; + out[1] = v[1]; + out[2] = v[2]; + return true; +} + +static void moduleVec3(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); + + scriptProtoDefineFunc(&MODULE_VEC3_PROTO, "dot", moduleVec3Dot); + scriptProtoDefineFunc(&MODULE_VEC3_PROTO, "cross", moduleVec3Cross); + scriptProtoDefineFunc(&MODULE_VEC3_PROTO, "length", moduleVec3Length); + scriptProtoDefineFunc(&MODULE_VEC3_PROTO, "lengthSq", moduleVec3LengthSq); + scriptProtoDefineFunc(&MODULE_VEC3_PROTO, "normalize", moduleVec3Normalize); + scriptProtoDefineFunc(&MODULE_VEC3_PROTO, "negate", moduleVec3Negate); + scriptProtoDefineFunc(&MODULE_VEC3_PROTO, "add", moduleVec3Add); + scriptProtoDefineFunc(&MODULE_VEC3_PROTO, "sub", moduleVec3Sub); + scriptProtoDefineFunc(&MODULE_VEC3_PROTO, "scale", moduleVec3Scale); + scriptProtoDefineFunc(&MODULE_VEC3_PROTO, "lerp", moduleVec3Lerp); + scriptProtoDefineFunc(&MODULE_VEC3_PROTO, "distance", moduleVec3Distance); +} diff --git a/src/dusk/script/module/math/modulevec3ref.h b/src/dusk/script/module/math/modulevec3ref.h new file mode 100644 index 00000000..d7c8cf85 --- /dev/null +++ b/src/dusk/script/module/math/modulevec3ref.h @@ -0,0 +1,123 @@ +/** + * 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 "cglm/cglm.h" +#include "modulevec3.h" + +typedef struct { + float_t *data; + void (*onChange)(void *ctx); + void *ctx; +} vec3ref_t; + +static scriptproto_t MODULE_VEC3_REF_PROTO; + +static inline vec3ref_t * moduleVec3RefGet( + const jerry_call_info_t *callInfo +) { + return (vec3ref_t*)scriptProtoGetValue( + &MODULE_VEC3_REF_PROTO, callInfo->this_value + ); +} + +static inline vec3ref_t * moduleVec3RefFrom(jerry_value_t val) { + return (vec3ref_t*)scriptProtoGetValue(&MODULE_VEC3_REF_PROTO, val); +} + +static inline void moduleVec3RefNotify(vec3ref_t *ref) { + if(ref->onChange) ref->onChange(ref->ctx); +} + +moduleBaseFunction(moduleVec3RefGetX) { + moduleBaseGetOrReturn(vec3ref_t, ref, moduleVec3RefGet); + return jerry_number(ref->data[0]); +} +moduleBaseFunction(moduleVec3RefSetX) { + moduleBaseRequireArgs(1); + moduleBaseGetOrReturn(vec3ref_t, ref, moduleVec3RefGet); + ref->data[0] = moduleBaseArgFloat(0); + moduleVec3RefNotify(ref); + return jerry_undefined(); +} + +moduleBaseFunction(moduleVec3RefGetY) { + moduleBaseGetOrReturn(vec3ref_t, ref, moduleVec3RefGet); + return jerry_number(ref->data[1]); +} +moduleBaseFunction(moduleVec3RefSetY) { + moduleBaseRequireArgs(1); + moduleBaseGetOrReturn(vec3ref_t, ref, moduleVec3RefGet); + ref->data[1] = moduleBaseArgFloat(0); + moduleVec3RefNotify(ref); + return jerry_undefined(); +} + +moduleBaseFunction(moduleVec3RefGetZ) { + moduleBaseGetOrReturn(vec3ref_t, ref, moduleVec3RefGet); + return jerry_number(ref->data[2]); +} +moduleBaseFunction(moduleVec3RefSetZ) { + moduleBaseRequireArgs(1); + moduleBaseGetOrReturn(vec3ref_t, ref, moduleVec3RefGet); + ref->data[2] = moduleBaseArgFloat(0); + moduleVec3RefNotify(ref); + return jerry_undefined(); +} + +moduleBaseFunction(moduleVec3RefToString) { + vec3ref_t *ref = moduleVec3RefGet(callInfo); + if(!ref) return jerry_string_sz("Vec3(?, ?, ?)"); + char_t buf[80]; + stringFormat( + buf, sizeof(buf), + "Vec3(%g, %g, %g)", ref->data[0], ref->data[1], ref->data[2] + ); + return jerry_string_sz(buf); +} + +static inline jerry_value_t moduleVec3RefPush( + float_t *data, void (*onChange)(void *), void *ctx +) { + vec3ref_t ref = { .data = data, .onChange = onChange, .ctx = ctx }; + return scriptProtoCreateValue(&MODULE_VEC3_REF_PROTO, &ref); +} + +static inline bool_t moduleVec3RefCheck(jerry_value_t val, float_t *out) { + vec3ref_t *ref = moduleVec3RefFrom(val); + if(!ref) return false; + out[0] = ref->data[0]; + out[1] = ref->data[1]; + out[2] = ref->data[2]; + return true; +} + +static inline bool_t moduleVec3AnyCheck(jerry_value_t val, float_t *out) { + return moduleVec3Check(val, out) || moduleVec3RefCheck(val, out); +} + +static void moduleVec3Ref(void) { + scriptProtoInit( + &MODULE_VEC3_REF_PROTO, NULL, sizeof(vec3ref_t), NULL + ); + scriptProtoDefineToString(&MODULE_VEC3_REF_PROTO, moduleVec3RefToString); + + scriptProtoDefineProp( + &MODULE_VEC3_REF_PROTO, "x", + moduleVec3RefGetX, moduleVec3RefSetX + ); + scriptProtoDefineProp( + &MODULE_VEC3_REF_PROTO, "y", + moduleVec3RefGetY, moduleVec3RefSetY + ); + scriptProtoDefineProp( + &MODULE_VEC3_REF_PROTO, "z", + moduleVec3RefGetZ, moduleVec3RefSetZ + ); +} diff --git a/src/dusk/script/module/math/modulevec4.h b/src/dusk/script/module/math/modulevec4.h new file mode 100644 index 00000000..52751b56 --- /dev/null +++ b/src/dusk/script/module/math/modulevec4.h @@ -0,0 +1,248 @@ +/** + * 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 "cglm/cglm.h" + +static scriptproto_t MODULE_VEC4_PROTO; + +static inline float_t * moduleVec4Get(const jerry_call_info_t *callInfo) { + return (float_t *)scriptProtoGetValue( + &MODULE_VEC4_PROTO, callInfo->this_value + ); +} + +static inline float_t * moduleVec4From(jerry_value_t val) { + return (float_t *)scriptProtoGetValue(&MODULE_VEC4_PROTO, val); +} + +moduleBaseFunction(moduleVec4Constructor) { + float_t *ptr = (float_t *)memoryAllocate(sizeof(vec4)); + ptr[0] = moduleBaseOptFloat(0, 0.0f); + ptr[1] = moduleBaseOptFloat(1, 0.0f); + ptr[2] = moduleBaseOptFloat(2, 0.0f); + ptr[3] = moduleBaseOptFloat(3, 0.0f); + jerry_object_set_native_ptr( + callInfo->this_value, &MODULE_VEC4_PROTO.info, ptr + ); + return jerry_undefined(); +} + +// x/y/z/w +moduleBaseFunction(moduleVec4GetX) { + moduleBaseGetOrReturn(float_t, v, moduleVec4Get); return jerry_number(v[0]); +} +moduleBaseFunction(moduleVec4SetX) { + moduleBaseRequireArgs(1); moduleBaseGetOrReturn(float_t, v, moduleVec4Get); + v[0] = moduleBaseArgFloat(0); return jerry_undefined(); +} + +moduleBaseFunction(moduleVec4GetY) { + moduleBaseGetOrReturn(float_t, v, moduleVec4Get); return jerry_number(v[1]); +} +moduleBaseFunction(moduleVec4SetY) { + moduleBaseRequireArgs(1); moduleBaseGetOrReturn(float_t, v, moduleVec4Get); + v[1] = moduleBaseArgFloat(0); return jerry_undefined(); +} + +moduleBaseFunction(moduleVec4GetZ) { + moduleBaseGetOrReturn(float_t, v, moduleVec4Get); return jerry_number(v[2]); +} +moduleBaseFunction(moduleVec4SetZ) { + moduleBaseRequireArgs(1); moduleBaseGetOrReturn(float_t, v, moduleVec4Get); + v[2] = moduleBaseArgFloat(0); return jerry_undefined(); +} + +moduleBaseFunction(moduleVec4GetW) { + moduleBaseGetOrReturn(float_t, v, moduleVec4Get); return jerry_number(v[3]); +} +moduleBaseFunction(moduleVec4SetW) { + moduleBaseRequireArgs(1); moduleBaseGetOrReturn(float_t, v, moduleVec4Get); + v[3] = moduleBaseArgFloat(0); return jerry_undefined(); +} + +// u0/v0/u1/v1 aliases for UV coordinates +moduleBaseFunction(moduleVec4GetU0) { + moduleBaseGetOrReturn(float_t, v, moduleVec4Get); return jerry_number(v[0]); +} +moduleBaseFunction(moduleVec4SetU0) { + moduleBaseRequireArgs(1); moduleBaseGetOrReturn(float_t, v, moduleVec4Get); + v[0] = moduleBaseArgFloat(0); return jerry_undefined(); +} + +moduleBaseFunction(moduleVec4GetV0) { + moduleBaseGetOrReturn(float_t, v, moduleVec4Get); return jerry_number(v[1]); +} +moduleBaseFunction(moduleVec4SetV0) { + moduleBaseRequireArgs(1); moduleBaseGetOrReturn(float_t, v, moduleVec4Get); + v[1] = moduleBaseArgFloat(0); return jerry_undefined(); +} + +moduleBaseFunction(moduleVec4GetU1) { + moduleBaseGetOrReturn(float_t, v, moduleVec4Get); return jerry_number(v[2]); +} +moduleBaseFunction(moduleVec4SetU1) { + moduleBaseRequireArgs(1); moduleBaseGetOrReturn(float_t, v, moduleVec4Get); + v[2] = moduleBaseArgFloat(0); return jerry_undefined(); +} + +moduleBaseFunction(moduleVec4GetV1) { + moduleBaseGetOrReturn(float_t, v, moduleVec4Get); return jerry_number(v[3]); +} +moduleBaseFunction(moduleVec4SetV1) { + moduleBaseRequireArgs(1); moduleBaseGetOrReturn(float_t, v, moduleVec4Get); + v[3] = moduleBaseArgFloat(0); return jerry_undefined(); +} + +moduleBaseFunction(moduleVec4Dot) { + moduleBaseRequireArgs(1); + float_t *a = moduleVec4Get(callInfo); + if(!a) return moduleBaseThrow("Vec4.dot: invalid this"); + float_t *b = moduleVec4From(args[0]); + if(!b) return moduleBaseThrow("Vec4.dot: argument must be a Vec4"); + return jerry_number(glm_vec4_dot(a, b)); +} + +moduleBaseFunction(moduleVec4Length) { + float_t *v = moduleVec4Get(callInfo); + if(!v) return moduleBaseThrow("Vec4.length: invalid this"); + return jerry_number(glm_vec4_norm(v)); +} + +moduleBaseFunction(moduleVec4LengthSq) { + float_t *v = moduleVec4Get(callInfo); + if(!v) return moduleBaseThrow("Vec4.lengthSq: invalid this"); + return jerry_number(glm_vec4_norm2(v)); +} + +moduleBaseFunction(moduleVec4Normalize) { + float_t *v = moduleVec4Get(callInfo); + if(!v) return moduleBaseThrow("Vec4.normalize: invalid this"); + vec4 r; + glm_vec4_normalize_to(v, r); + return scriptProtoCreateValue(&MODULE_VEC4_PROTO, r); +} + +moduleBaseFunction(moduleVec4Negate) { + float_t *v = moduleVec4Get(callInfo); + if(!v) return moduleBaseThrow("Vec4.negate: invalid this"); + vec4 r; + glm_vec4_negate_to(v, r); + return scriptProtoCreateValue(&MODULE_VEC4_PROTO, r); +} + +moduleBaseFunction(moduleVec4Add) { + moduleBaseRequireArgs(1); + float_t *a = moduleVec4Get(callInfo); + if(!a) return moduleBaseThrow("Vec4.add: invalid this"); + float_t *b = moduleVec4From(args[0]); + if(!b) return moduleBaseThrow("Vec4.add: argument must be a Vec4"); + vec4 r; + glm_vec4_add(a, b, r); + return scriptProtoCreateValue(&MODULE_VEC4_PROTO, r); +} + +moduleBaseFunction(moduleVec4Sub) { + moduleBaseRequireArgs(1); + float_t *a = moduleVec4Get(callInfo); + if(!a) return moduleBaseThrow("Vec4.sub: invalid this"); + float_t *b = moduleVec4From(args[0]); + if(!b) return moduleBaseThrow("Vec4.sub: argument must be a Vec4"); + vec4 r; + glm_vec4_sub(a, b, r); + return scriptProtoCreateValue(&MODULE_VEC4_PROTO, r); +} + +moduleBaseFunction(moduleVec4Scale) { + moduleBaseRequireArgs(1); moduleBaseRequireNumber(0); + float_t *v = moduleVec4Get(callInfo); + if(!v) return moduleBaseThrow("Vec4.scale: invalid this"); + vec4 r; + glm_vec4_scale(v, moduleBaseArgFloat(0), r); + return scriptProtoCreateValue(&MODULE_VEC4_PROTO, r); +} + +moduleBaseFunction(moduleVec4Lerp) { + moduleBaseRequireArgs(2); moduleBaseRequireNumber(1); + float_t *a = moduleVec4Get(callInfo); + if(!a) return moduleBaseThrow("Vec4.lerp: invalid this"); + float_t *b = moduleVec4From(args[0]); + if(!b) return moduleBaseThrow("Vec4.lerp: first argument must be a Vec4"); + vec4 r; + glm_vec4_lerp(a, b, moduleBaseArgFloat(1), r); + return scriptProtoCreateValue(&MODULE_VEC4_PROTO, r); +} + +moduleBaseFunction(moduleVec4ToString) { + float_t *v = moduleVec4Get(callInfo); + if(!v) return jerry_string_sz("Vec4(?, ?, ?, ?)"); + char_t buf[96]; + stringFormat( + buf, sizeof(buf), + "Vec4(%g, %g, %g, %g)", v[0], v[1], v[2], v[3] + ); + return jerry_string_sz(buf); +} + +static inline jerry_value_t moduleVec4Push(const float_t *v) { + return scriptProtoCreateValue(&MODULE_VEC4_PROTO, v); +} + +static inline bool_t moduleVec4Check(jerry_value_t val, float_t *out) { + float_t *v = moduleVec4From(val); + if(!v) return false; + out[0] = v[0]; + out[1] = v[1]; + out[2] = v[2]; + out[3] = v[3]; + return true; +} + +static void moduleVec4(void) { + scriptProtoInit( + &MODULE_VEC4_PROTO, "Vec4", sizeof(vec4), moduleVec4Constructor + ); + + scriptProtoDefineProp( + &MODULE_VEC4_PROTO, "x", moduleVec4GetX, moduleVec4SetX + ); + scriptProtoDefineProp( + &MODULE_VEC4_PROTO, "y", moduleVec4GetY, moduleVec4SetY + ); + scriptProtoDefineProp( + &MODULE_VEC4_PROTO, "z", moduleVec4GetZ, moduleVec4SetZ + ); + scriptProtoDefineProp( + &MODULE_VEC4_PROTO, "w", moduleVec4GetW, moduleVec4SetW + ); + scriptProtoDefineProp( + &MODULE_VEC4_PROTO, "u0", moduleVec4GetU0, moduleVec4SetU0 + ); + scriptProtoDefineProp( + &MODULE_VEC4_PROTO, "v0", moduleVec4GetV0, moduleVec4SetV0 + ); + scriptProtoDefineProp( + &MODULE_VEC4_PROTO, "u1", moduleVec4GetU1, moduleVec4SetU1 + ); + scriptProtoDefineProp( + &MODULE_VEC4_PROTO, "v1", moduleVec4GetV1, moduleVec4SetV1 + ); + + scriptProtoDefineToString(&MODULE_VEC4_PROTO, moduleVec4ToString); + + scriptProtoDefineFunc(&MODULE_VEC4_PROTO, "dot", moduleVec4Dot); + scriptProtoDefineFunc(&MODULE_VEC4_PROTO, "length", moduleVec4Length); + scriptProtoDefineFunc(&MODULE_VEC4_PROTO, "lengthSq", moduleVec4LengthSq); + scriptProtoDefineFunc(&MODULE_VEC4_PROTO, "normalize", moduleVec4Normalize); + scriptProtoDefineFunc(&MODULE_VEC4_PROTO, "negate", moduleVec4Negate); + scriptProtoDefineFunc(&MODULE_VEC4_PROTO, "add", moduleVec4Add); + scriptProtoDefineFunc(&MODULE_VEC4_PROTO, "sub", moduleVec4Sub); + scriptProtoDefineFunc(&MODULE_VEC4_PROTO, "scale", moduleVec4Scale); + scriptProtoDefineFunc(&MODULE_VEC4_PROTO, "lerp", moduleVec4Lerp); +} diff --git a/src/dusk/script/module/module.h b/src/dusk/script/module/module.h new file mode 100644 index 00000000..e384bc48 --- /dev/null +++ b/src/dusk/script/module/module.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/module/script/moduleinclude.h" +#include "script/module/math/modulemath.h" +#include "script/module/display/modulecolor.h" +#include "script/module/display/modulemesh.h" +#include "script/module/entity/moduleentity.h" +#include "script/module/input/moduleinput.h" +#include "script/module/moduleplatform.h" +#include "script/module/time/moduletime.h" + +static void moduleRegister(void) { + moduleInclude(); + moduleMath(); + moduleColor(); + moduleMesh(); + moduleEntity(); + moduleInput(); + modulePlatform(); + moduleTime(); +} diff --git a/src/dusk/script/module/modulebase.h b/src/dusk/script/module/modulebase.h new file mode 100644 index 00000000..cad94673 --- /dev/null +++ b/src/dusk/script/module/modulebase.h @@ -0,0 +1,513 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/scriptmanager.h" +#include "assert/assert.h" +#include "util/string.h" +#include "util/memory.h" +#include + +/** + * Define a function for a module in JavaScript. + * + * @param name Name of the method (in C, not JS) + * @return A C function with that name, containing the standard JS sig. + */ +#define moduleBaseFunction(name) \ + static jerry_value_t name( \ + const jerry_call_info_t *callInfo, \ + const jerry_value_t args[], \ + const jerry_length_t argc) + +/** + * Define a standard JS prototype for a module. + * This creates the INFO struct, and a prototype reference value. + * + * Values are; + * {NAME}_INFO and {NAME_PROTOTYPE} + * + * @param name Name of the module. + * @return See above. + */ +#define moduleBaseProtoDefine(name) \ + static const jerry_object_native_info_t name##_INFO = { \ + .free_cb = moduleBaseFreeProto, \ + .number_of_references = 0, \ + .offset_of_references = 0 \ + }; \ + static jerry_value_t name##_PROTOTYPE = 0; + +/** + * Gets the pointer to what is otherwise a prototype in the JerryScript code. + * This, for example, allows you to define a pointer in C and have it be a + * prototype for objects in JavaScript. + * + * @param object The JavaScript object to get the native pointer from. + * @param info The native info "prototype" struct used to get the pointer. + * @return The pointer to the proto (or NULL). + */ +static void* moduleBaseGetProto( + const jerry_value_t object, + const jerry_object_native_info_t *info +) { + assertNotNull(info, "Native info must not be null"); + if(!jerry_value_is_object(object)) return NULL; + + return (void*)jerry_object_get_native_ptr(object, info); +} + +/** + * Create an object based on a C value. + * + * @param input The pointer to the value to create a JS object for. + * @param size The size of the data provided in input. + * @param info The native info "prototype" struct used to create the object. + * @return A JS object wrapping the provided C value. + */ +static jerry_value_t moduleBaseCreateProto( + void *input, + const size_t size, + const jerry_object_native_info_t *info, + const jerry_value_t prototype +) { + assertNotNull(input, "Input pointer must not be null"); + assertTrue(size > 0, "Struct size must be greater than 0"); + assertNotNull(info, "Native info must not be null"); + + void *ptr = memoryAllocate(size); + memoryCopy(ptr, input, size); + jerry_value_t proto = jerry_object(); + jerry_object_set_native_ptr(proto, info, ptr); + jerry_object_set_proto(proto, prototype); + return proto; +} + +/** + * Standard JerryScript free callback. + * + * @param ptr The pointer to free. + * @param info The native info struct associated with the pointer. + */ +static void moduleBaseFreeProto(void *ptr, jerry_object_native_info_t *info) { + assertNotNull(ptr, "Pointer must not be null"); + assertNotNull(info, "Native info must not be null"); + memoryFree(ptr); +} + +/** + * Quickly defines a property on a prototype with a getter and setter. + */ +static void moduleBaseProtoDefineProp( + const jerry_value_t prototype, + const char_t *name, + jerry_external_handler_t getter, + jerry_external_handler_t setter +) { + assertTrue(prototype != 0, "Prototype must not be null"); + assertNotNull(name, "Property name must not be null"); + assertNotNull(getter, "Getter must not be null"); + + jerry_property_descriptor_t desc; + memset(&desc, 0, sizeof(desc)); + desc.flags = (uint16_t)( + JERRY_PROP_IS_GET_DEFINED | + JERRY_PROP_IS_ENUMERABLE_DEFINED | JERRY_PROP_IS_ENUMERABLE | + JERRY_PROP_IS_CONFIGURABLE_DEFINED | JERRY_PROP_IS_CONFIGURABLE + ); + + desc.getter = jerry_function_external(getter); + + if(setter != NULL) { + desc.flags |= JERRY_PROP_IS_SET_DEFINED; + desc.setter = jerry_function_external(setter); + } + + jerry_value_t key = jerry_string_sz(name); + jerry_value_t result = jerry_object_define_own_prop(prototype, key, &desc); + jerry_value_free(result); + jerry_value_free(key); + jerry_value_free(desc.getter); + if(setter != NULL) jerry_value_free(desc.setter); +} + +/** + * Register a global function for a module. + * + * @param name The name of the function as seen in JavaScript. + * @param fn The C handler function for the method. + */ +static void moduleBaseFunctionRegister( + const char_t *name, + jerry_external_handler_t fn +) { + assertNotNull(name, "Function name must not be null"); + assertNotNull(fn, "Function handler must not be null"); + + jerry_value_t global = jerry_current_realm(); + jerry_value_t key = jerry_string_sz(name); + jerry_value_t func = jerry_function_external(fn); + jerry_object_set(global, key, func); + jerry_value_free(func); + jerry_value_free(key); + jerry_value_free(global); +} + +/** + * Evaluates a script in the global scope. + * + * @param script The script to evaluate. + */ +static void moduleBaseEval(const char_t *script) { + assertNotNull(script, "Script must not be null"); + + jerry_value_t result = jerry_eval( + (const jerry_char_t *)script, + strlen(script), + JERRY_PARSE_NO_OPTS + ); + jerry_value_free(result); +} + +/** + * Throw a type error from a module function. + * + * @param message The error message to throw. + * @return A JerryScript error value. + */ +static jerry_value_t moduleBaseThrow(const char_t *message) { + assertStrLenMin(message, 1, "Error message must not be empty"); + return jerry_throw_sz(JERRY_ERROR_TYPE, message); +} + +/** + * Converts a C errorret_t into a JS exception, forwarding the error message + * so that try/catch in JS sees the real error text. Clears the C error state. + * + * @param err The errorret_t returned by a failing C function. + * @return A JerryScript error value carrying the C error message. + */ +static jerry_value_t moduleBaseThrowError(const errorret_t err) { + assertNotNull(err.state, "Error state must not be NULL"); + assertNotNull(err.state->message, "Error message must not be NULL"); + jerry_value_t jsErr = jerry_throw_sz(JERRY_ERROR_TYPE, err.state->message); + errorCatch(err); + return jsErr; +} + +/** + * Set a global string constant. + */ +static void moduleBaseSetString(const char_t *name, const char_t *value) { + jerry_value_t global = jerry_current_realm(); + jerry_value_t key = jerry_string_sz(name); + jerry_value_t val = jerry_string_sz(value); + jerry_object_set(global, key, val); + jerry_value_free(val); + jerry_value_free(key); + jerry_value_free(global); +} + +/** + * Defines a global object with the provided name and prototype. + * + * @param name The name of the global object to create. + * @param prototype The prototype to set on the global object. + */ +static void moduleBaseCreateGlobalObject( + const char_t *name, jerry_value_t prototype +) { + jerry_value_t global = jerry_current_realm(); + jerry_value_t key = jerry_string_sz(name); + jerry_object_set(global, key, prototype); + jerry_value_free(key); + jerry_value_free(global); +} + +/** + * Assert an argument is a number; return type error if not. + */ +#define moduleBaseRequireNumber(i) do { \ + if(!jerry_value_is_number(args[(i)])) { \ + return moduleBaseThrow("Expected number argument"); \ + } \ +} while(0) + +/** + * Assert an argument is a string; return type error if not. + */ +#define moduleBaseRequireString(i) do { \ + if(!jerry_value_is_string(args[(i)])) { \ + return moduleBaseThrow("Expected string argument"); \ + } \ +} while(0) + +/** + * Assert an argument is a function; return type error if not. + */ +#define moduleBaseRequireFunction(i) do { \ + if(!jerry_value_is_function(args[(i)])) { \ + return moduleBaseThrow("Expected function argument"); \ + } \ +} while(0) + +/** + * Assert an argument is an object; return type error if not. + */ +#define moduleBaseRequireObject(i) do { \ + if(!jerry_value_is_object(args[(i)])) { \ + return moduleBaseThrow("Expected object argument"); \ + } \ +} while(0) + +/** + * Require at least N arguments; throw a TypeError if fewer were provided. + * + * Example: moduleBaseRequireArgs(2); + */ +#define moduleBaseRequireArgs(n) do { \ + if(argc < (jerry_length_t)(n)) { \ + return moduleBaseThrow("Expected at least " #n " argument(s)"); \ + } \ +} while(0) + +/** + * Declare a typed pointer from a getter and immediately return undefined if it + * is NULL. The named variable is available for the rest of the function. + * + * Example: + * moduleBaseGetOrReturn(entitycamera_t, cam, moduleEntityCameraGet); + * return jerry_number(cam->nearClip); + */ +#define moduleBaseGetOrReturn(type, var, getter) \ + type *var = (getter)(callInfo); \ + if(!(var)) return jerry_undefined() + +/** + * Cast argument i to float_t. Call after validating the arg is a number. + */ +#define moduleBaseArgFloat(i) ((float_t)jerry_value_as_number(args[(i)])) + +/** + * Cast argument i to int32_t. Call after validating the arg is a number. + */ +#define moduleBaseArgInt(i) ((int32_t)jerry_value_as_number(args[(i)])) + +/** + * Read argument i as a boolean (true/false). + */ +#define moduleBaseArgBool(i) (jerry_value_is_true(args[(i)])) + +/** + * Read optional argument i as float_t. Returns def if the argument is missing + * or not a number. + */ +#define moduleBaseOptFloat(i, def) \ + ((jerry_length_t)(i) < argc && jerry_value_is_number(args[(i)]) \ + ? (float_t)jerry_value_as_number(args[(i)]) : (def)) + +/** + * Read optional argument i as int32_t. Returns def if the argument is missing + * or not a number. + */ +#define moduleBaseOptInt(i, def) \ + ((jerry_length_t)(i) < argc && jerry_value_is_number(args[(i)]) \ + ? (int32_t)jerry_value_as_number(args[(i)]) : (def)) + +/** + * Set a global numeric constant. + */ +static inline void moduleBaseSetNumber(const char_t *name, double value) { + jerry_value_t global = jerry_current_realm(); + jerry_value_t key = jerry_string_sz(name); + jerry_value_t val = jerry_number(value); + jerry_object_set(global, key, val); + jerry_value_free(val); + jerry_value_free(key); + jerry_value_free(global); +} + +/** + * Set a global integer constant. + */ +static inline void moduleBaseSetInt(const char_t *name, int32_t value) { + moduleBaseSetNumber(name, (double)value); +} + +/** + * Set a global JS value. Caller retains ownership of the value and must free + * it independently. + */ +static inline void moduleBaseSetValue(const char_t *name, jerry_value_t value) { + jerry_value_t global = jerry_current_realm(); + jerry_value_t key = jerry_string_sz(name); + jerry_object_set(global, key, value); + jerry_value_free(key); + jerry_value_free(global); +} + +/** + * Wrap an engine-owned C pointer as a JS object (no GC free callback). + * Used for global singletons like INPUT_EVENT_PRESSED, SHADER_UNLIT, etc. + */ +static inline jerry_value_t moduleBaseWrapPointer(void *ptr) { + jerry_value_t obj = jerry_object(); + jerry_object_set_native_ptr(obj, &JS_PTR_NATIVE_INFO, ptr); + return obj; +} + +/** + * Set a named global to a wrapped engine-owned C pointer. + * Combines moduleBaseWrapPointer and moduleBaseSetValue in one call. + */ +static inline void moduleBaseSetWrappedPointer(const char_t *name, void *ptr) { + jerry_value_t val = moduleBaseWrapPointer(ptr); + moduleBaseSetValue(name, val); + jerry_value_free(val); +} + +/** + * Unwrap a C pointer from a JS object created by moduleBaseWrapPointer. + * Returns NULL if the object does not carry a matching native pointer. + */ +static inline void *moduleBaseUnwrapPointer(jerry_value_t val) { + if(!jerry_value_is_object(val)) return NULL; + return jerry_object_get_native_ptr(val, &JS_PTR_NATIVE_INFO); +} + +/** + * Copy a JerryScript string value into a C buffer (null-terminated). + * + * @param val Jerry string value. + * @param buf Output buffer. + * @param buflen Buffer capacity including the null terminator. + */ +static inline void moduleBaseToString( + jerry_value_t val, + char_t *buf, + jerry_size_t buflen +) { + jerry_size_t len = jerry_string_to_buffer( + val, JERRY_ENCODING_UTF8, (jerry_char_t *)buf, buflen - 1 + ); + buf[len] = '\0'; +} + +/** + * Define a named property on a JS object with getter and optional setter. + * + * @param obj Target object (e.g. a prototype). + * @param name Property name. + * @param getter C getter handler. + * @param setter C setter handler, or NULL for read-only property. + */ +static inline void moduleBaseDefineProperty( + jerry_value_t obj, + const char_t *name, + jerry_external_handler_t getter, + jerry_external_handler_t setter +) { + jerry_property_descriptor_t desc; + memset(&desc, 0, sizeof(desc)); + desc.flags = (uint16_t)( + JERRY_PROP_IS_GET_DEFINED | + JERRY_PROP_IS_ENUMERABLE_DEFINED | JERRY_PROP_IS_ENUMERABLE | + JERRY_PROP_IS_CONFIGURABLE_DEFINED | JERRY_PROP_IS_CONFIGURABLE + ); + desc.getter = jerry_function_external(getter); + if(setter != NULL) { + desc.flags |= JERRY_PROP_IS_SET_DEFINED; + desc.setter = jerry_function_external(setter); + } + jerry_value_t key = jerry_string_sz(name); + jerry_value_t result = jerry_object_define_own_prop(obj, key, &desc); + jerry_value_free(result); + jerry_value_free(key); + jerry_value_free(desc.getter); + if(setter != NULL) jerry_value_free(desc.setter); +} + +/** + * Set a named method (C function) on a JS object. + * + * @param obj Target object (e.g. a prototype). + * @param name Method name. + * @param fn C handler function. + */ +static inline void moduleBaseDefineMethod( + jerry_value_t obj, + const char_t *name, + jerry_external_handler_t fn +) { + jerry_value_t key = jerry_string_sz(name); + jerry_value_t func = jerry_function_external(fn); + jerry_object_set(obj, key, func); + jerry_value_free(func); + jerry_value_free(key); +} + +/** + * Format an error message from a JerryScript exception value. + * Caller must ensure buf is large enough. + */ +static inline void moduleBaseExceptionMessage( + jerry_value_t exception, + char_t *buf, + size_t buflen +) { + jerry_value_t errVal = jerry_exception_value(exception, false); + jerry_value_t errStr = jerry_value_to_string(errVal); + jerry_size_t len = jerry_string_to_buffer( + errStr, JERRY_ENCODING_UTF8, (jerry_char_t *)buf, (jerry_size_t)(buflen - 1) + ); + buf[len] = '\0'; + jerry_value_free(errStr); + jerry_value_free(errVal); +} + +/** + * Define a named global function. + * + * @param name The name of the function as seen in JavaScript. + * @param fn The C handler function for the method. + */ +static inline void moduleBaseDefineGlobalMethod( + const char_t *name, + jerry_external_handler_t fn +) { + jerry_value_t global = jerry_current_realm(); + moduleBaseDefineMethod(global, name, fn); + jerry_value_free(global); +} + +/** + * Get a named property from a JS object. Caller must free the returned value. + */ +static inline jerry_value_t moduleBaseGetProp( + jerry_value_t obj, const char_t *name +) { + jerry_value_t key = jerry_string_sz(name); + jerry_value_t val = jerry_object_get(obj, key); + jerry_value_free(key); + return val; +} + +/** + * Cast a JS value to float_t. Use for non-args[] values (e.g. object + * properties). For args[], prefer moduleBaseArgFloat. + */ +static inline float_t moduleBaseValueFloat(jerry_value_t val) { + return (float_t)jerry_value_as_number(val); +} + +/** + * Cast a JS value to int32_t. Use for non-args[] values (e.g. object + * properties). For args[], prefer moduleBaseArgInt. + */ +static inline int32_t moduleBaseValueInt(jerry_value_t val) { + return (int32_t)jerry_value_as_number(val); +} \ No newline at end of file diff --git a/src/dusk/script/module/moduleplatform.h b/src/dusk/script/module/moduleplatform.h new file mode 100644 index 00000000..fcd3b46c --- /dev/null +++ b/src/dusk/script/module/moduleplatform.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/module/modulebase.h" +#include "script/module/moduleplatformplatform.h" + +#ifndef DUSK_TARGET_SYSTEM + #error "DUSK_TARGET_SYSTEM must be defined" +#endif + +#define MODULE_PLATFORM_VALUE "var PLATFORM = '" DUSK_TARGET_SYSTEM "';\n" + +static void modulePlatform(void) { + moduleBaseEval(MODULE_PLATFORM_VALUE); + + #ifdef modulePlatformPlatform + modulePlatformPlatform(); + #endif +} \ No newline at end of file diff --git a/src/dusk/script/module/scene/modulescene.h b/src/dusk/script/module/scene/modulescene.h new file mode 100644 index 00000000..a520ef8e --- /dev/null +++ b/src/dusk/script/module/scene/modulescene.h @@ -0,0 +1,109 @@ +/** + * 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; + +moduleBaseFunction(moduleSceneDefaultUpdate) { + return jerry_undefined(); +} + +moduleBaseFunction(moduleSceneDefaultDispose) { + return jerry_undefined(); +} + +moduleBaseFunction(moduleSceneDefaultConstructor) { + return jerry_undefined(); +} + +moduleBaseFunction(moduleSceneSet) { + moduleBaseRequireArgs(1); moduleBaseRequireString(0); + + char_t name[ASSET_FILE_NAME_MAX]; + moduleBaseToString(args[0], name, sizeof(name)); + if(name[0] == '\0') return moduleBaseThrow("Scene.set: name cannot be empty"); + + sceneSet(name); + return jerry_undefined(); +} + +moduleBaseFunction(moduleSceneGetCurrent) { + if(SCENE.sceneCurrent[0] == '\0') return jerry_undefined(); + return jerry_string_sz(SCENE.sceneCurrent); +} + +static void moduleSceneReset(void) { + if(SCENE.scriptRef != SCENE_SCRIPT_REF_NONE) { + jerry_value_free(SCENE.scriptRef); + SCENE.scriptRef = SCENE_SCRIPT_REF_NONE; + } + + // Drop the 'module' global reference to the scene class so JerryScript's + // GC can collect it and all associated closures. + jerry_value_t global = jerry_current_realm(); + jerry_value_t key = jerry_string_sz("module"); + jerry_value_t undef = jerry_undefined(); + jerry_object_set(global, key, undef); + jerry_value_free(undef); + jerry_value_free(key); + jerry_value_free(global); +} + +static errorret_t moduleSceneCall(const char_t *method) { + assertStrLenMin(method, 1, "Method name cannot be empty"); + + if(SCENE.scriptRef == SCENE_SCRIPT_REF_NONE) { + errorThrow("No active scene script to call method on"); + } + + jerry_value_t key = jerry_string_sz(method); + jerry_value_t fn = jerry_object_get(SCENE.scriptRef, key); + jerry_value_free(key); + + if(!jerry_value_is_function(fn)) { + jerry_value_free(fn); + errorThrow("Scene method '%s' not found", method); + } + + jerry_value_t result = jerry_call(fn, SCENE.scriptRef, NULL, 0); + jerry_value_free(fn); + + if(jerry_value_is_exception(result)) { + char_t errMsg[512]; + moduleBaseExceptionMessage(result, errMsg, sizeof(errMsg)); + jerry_value_free(result); + errorThrow("Scene:%s failed: %s", method, errMsg); + } + + jerry_value_free(result); + errorOk(); +} + +static void moduleScene(void) { + scriptProtoInit( + &MODULE_SCENE_PROTO, + "Scene", + sizeof(uint8_t), + moduleSceneDefaultConstructor + ); + + scriptProtoDefineFunc( + &MODULE_SCENE_PROTO, "update", moduleSceneDefaultUpdate + ); + scriptProtoDefineFunc( + &MODULE_SCENE_PROTO, "dispose", moduleSceneDefaultDispose + ); + + scriptProtoDefineStaticFunc(&MODULE_SCENE_PROTO, "set", moduleSceneSet); + scriptProtoDefineStaticProp( + &MODULE_SCENE_PROTO, "current", moduleSceneGetCurrent, NULL + ); +} diff --git a/src/dusk/script/module/script/moduleinclude.h b/src/dusk/script/module/script/moduleinclude.h new file mode 100644 index 00000000..a40ed8e4 --- /dev/null +++ b/src/dusk/script/module/script/moduleinclude.h @@ -0,0 +1,50 @@ +/** + * 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/scriptmanager.h" +#include "asset/asset.h" +#include "asset/loader/assetloader.h" + +// include() always returns a Promise, and must be awaited (or .then()'d). +// Multiple include() calls for the same file share a single underlying +// asset entry, and therefore a single promise: the first call creates it, +// every later call for that same file is handed a copy of that same +// promise instead of starting a second load. It resolves with the script's +// exported `module` value, or rejects with the script's exception, exactly +// once - see assetScriptLoaderSync(). +moduleBaseFunction(moduleIncludeInclude) { + moduleBaseRequireArgs(1); moduleBaseRequireString(0); + + char_t filename[ASSET_FILE_NAME_MAX]; + moduleBaseToString(args[0], filename, sizeof(filename)); + + if(filename[0] == '\0') { + return moduleBaseThrow("include: filename cannot be empty"); + } + + size_t len = strlen(filename); + if(len < 3 || stringCompare(&filename[len - 3], ".js") != 0) { + return moduleBaseThrow("include: filename must end with .js"); + } + + assetentry_t *entry = assetGetEntry(filename, ASSET_LOADER_TYPE_SCRIPT, NULL); + assetscriptoutput_t *output = &entry->data.script; + + if(output->promise == 0) { + // First request for this file - this is what kicks off the load; + // assetUpdate() picks up entries sitting in NOT_STARTED every frame. + output->promise = jerry_promise(); + } + + return jerry_value_copy(output->promise); +} + +static void moduleInclude(void) { + moduleBaseDefineGlobalMethod("include", moduleIncludeInclude); +} diff --git a/src/dusk/script/module/time/moduletime.h b/src/dusk/script/module/time/moduletime.h new file mode 100644 index 00000000..2d034654 --- /dev/null +++ b/src/dusk/script/module/time/moduletime.h @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/module/modulebase.h" +#include "time/time.h" + +moduleBaseFunction(moduleTimeGetDelta) { + return jerry_number(TIME.delta); +} + +moduleBaseFunction(moduleTimeGetTime) { + return jerry_number(TIME.time); +} + +static void moduleTime(void) { + jerry_value_t obj = jerry_object(); + moduleBaseDefineProperty(obj, "delta", moduleTimeGetDelta, NULL); + moduleBaseDefineProperty(obj, "time", moduleTimeGetTime, NULL); + moduleBaseSetValue("TIME", obj); + jerry_value_free(obj); +} diff --git a/src/dusk/script/scriptmanager.c b/src/dusk/script/scriptmanager.c new file mode 100644 index 00000000..6f057b23 --- /dev/null +++ b/src/dusk/script/scriptmanager.c @@ -0,0 +1,181 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scriptmanager.h" +#include "assert/assert.h" +#include "asset/asset.h" +#include "util/memory.h" +#include "scriptproto.h" +#include "script/module/module.h" + +#ifdef SCRIPT_GAME_INIT + #include "script/scriptgame.h" +#endif + +scriptmanager_t SCRIPT_MANAGER; + +const jerry_object_native_info_t JS_PTR_NATIVE_INFO = { + .free_cb = NULL, + .number_of_references = 0, + .offset_of_references = 0 +}; + +errorret_t scriptManagerInit(void) { + memoryZero(&SCRIPT_MANAGER, sizeof(scriptmanager_t)); + + jerry_init(JERRY_INIT_EMPTY); + + moduleRegister(); + + #ifdef SCRIPT_GAME_INIT + SCRIPT_GAME_INIT(); + #endif + + errorOk(); +} + +errorret_t scriptManagerExec(const char_t *script, jerry_value_t *resultOut) { + assertNotNull(script, "Script cannot be NULL"); + + jerry_value_t result = jerry_eval( + (const jerry_char_t *)script, + strlen(script), + JERRY_PARSE_NO_OPTS + ); + + if(jerry_value_is_exception(result)) { + jerry_value_t errVal = jerry_exception_value(result, false); + jerry_value_t errStr = jerry_value_to_string(errVal); + char_t buf[256]; + jerry_size_t len = jerry_string_to_buffer( + errStr, JERRY_ENCODING_UTF8, (jerry_char_t *)buf, sizeof(buf) - 1 + ); + buf[len] = '\0'; + jerry_value_free(errStr); + jerry_value_free(errVal); + jerry_value_free(result); + errorThrow("Failed to execute script: %s", buf); + } + + if(resultOut != NULL) { + *resultOut = result; + } else { + jerry_value_free(result); + } + errorOk(); +} + +errorret_t scriptManagerExecFile( + const char_t *fname, + jerry_value_t *resultOut +) { + assertNotNull(fname, "Filename cannot be NULL"); + + assetfile_t file; + errorChain(assetFileInit(&file, fname, NULL, NULL)); + + uint8_t *buffer = NULL; + size_t size = 0; + errorChain(assetFileReadEntire(&file, &buffer, &size)); + errorChain(assetFileDispose(&file)); + + char_t *src = (char_t *)memoryAllocate(size + 1); + memoryCopy(src, buffer, size); + src[size] = '\0'; + memoryFree(buffer); + + errorret_t ret = scriptManagerExec(src, resultOut); + memoryFree(src); + errorChain(ret); + errorOk(); +} + +static errorret_t scriptManagerFormatValueError( + const char_t *context, + const char_t *name, + jerry_value_t value +) { + jerry_value_t errStr = jerry_value_to_string(value); + char_t buf[256]; + jerry_size_t len = jerry_string_to_buffer( + errStr, JERRY_ENCODING_UTF8, (jerry_char_t *)buf, sizeof(buf) - 1 + ); + buf[len] = '\0'; + jerry_value_free(errStr); + errorThrow("%s '%s': %s", context, name, buf); +} + +static errorret_t scriptManagerFormatException( + const char_t *context, + const char_t *name, + jerry_value_t exception +) { + jerry_value_t errVal = jerry_exception_value(exception, false); + errorret_t err = scriptManagerFormatValueError(context, name, errVal); + jerry_value_free(errVal); + return err; +} + +errorret_t scriptManagerCallGlobal(const char_t *name) { + assertNotNull(name, "Function name cannot be NULL"); + + jerry_value_t global = jerry_current_realm(); + jerry_value_t key = jerry_string_sz(name); + jerry_value_t fn = jerry_object_get(global, key); + jerry_value_free(key); + jerry_value_free(global); + + if(!jerry_value_is_function(fn)) { + jerry_value_free(fn); + errorOk(); + } + + jerry_value_t result = jerry_call(fn, jerry_undefined(), NULL, 0); + jerry_value_free(fn); + + if(jerry_value_is_exception(result)) { + errorret_t err = scriptManagerFormatException("Global function", name, result); + jerry_value_free(result); + errorChain(err); + } + + // If this was an `async function`, its work (including anything it + // awaited) isn't actually done yet - it just returned a pending promise. + // Drive both the JerryScript job queue and the asset system forward + // (an awaited include() only progresses via assetUpdate()) until it + // settles, so the caller can rely on the function being fully complete. + if(jerry_value_is_promise(result)) { + while(jerry_promise_state(result) == JERRY_PROMISE_STATE_PENDING) { + errorret_t updateErr = assetUpdate(); + if(errorIsNotOk(updateErr)) { + jerry_value_free(result); + errorChain(updateErr); + } + jerry_value_t jobsResult = jerry_run_jobs(); + jerry_value_free(jobsResult); + } + + if(jerry_promise_state(result) == JERRY_PROMISE_STATE_REJECTED) { + jerry_value_t rejectVal = jerry_promise_result(result); + errorret_t err = scriptManagerFormatValueError( + "Global async function", name, rejectVal + ); + jerry_value_free(rejectVal); + jerry_value_free(result); + errorChain(err); + } + } + + jerry_value_free(result); + errorOk(); +} + +errorret_t scriptManagerDispose(void) { + scriptProtoDisposeAll(); + jerry_cleanup(); + errorOk(); +} diff --git a/src/dusk/script/scriptmanager.h b/src/dusk/script/scriptmanager.h new file mode 100644 index 00000000..bd6fd64f --- /dev/null +++ b/src/dusk/script/scriptmanager.h @@ -0,0 +1,81 @@ +/** + * 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 "scriptvalue.h" +#include + +#define SCRIPT_MANAGER_MAX_EVENT_SUBSCRIPTIONS 64 + +typedef struct { + void* nothing; +} scriptmanager_t; + +extern scriptmanager_t SCRIPT_MANAGER; + +/** + * Singleton native-info tag for engine-owned C pointers wrapped in JS objects. + * A single global instance ensures jerry_object_get_native_ptr() matches across + * all compilation units (including event.c and module headers). + */ +extern const jerry_object_native_info_t JS_PTR_NATIVE_INFO; + +/** + * Initialize the script manager (and the underlying JerryScript context). + * + * @return The error return value. + */ +errorret_t scriptManagerInit(void); + +/** + * Execute a JS string in the active script context. + * + * @param script The JS source to execute. + * @param result Optional out-parameter for the script's return value. + * Caller must call jerry_value_free() on it when done. + * Pass NULL to discard the return value. + * @return The error return value. + */ +errorret_t scriptManagerExec(const char_t *script, jerry_value_t *result); + +/** + * Execute a JS file in the active script context. + * + * @param fname The filename of the script to execute. + * @param result Optional out-parameter for the script's return value. + * Caller must call jerry_value_free() on it when done. + * Pass NULL to discard the return value. + * @return The error return value. + */ +errorret_t scriptManagerExecFile( + const char_t *fname, + jerry_value_t *result +); + +/** + * Calls a global JS function by name, if one is defined. Silently does + * nothing if the global isn't a function (e.g. the script never defined it). + * + * If the function returns a Promise (e.g. it's an `async function`), the + * JerryScript job queue is pumped (alongside assetUpdate(), so pending + * include()s can actually progress) until it settles, and a rejection is + * surfaced as an error - callers can rely on the function's work, including + * anything it awaited, being complete by the time this returns. + * + * @param name The name of the global function to call. + * @return The error return value. An error is thrown if the JS function + * itself throws, or if its returned promise rejects. + */ +errorret_t scriptManagerCallGlobal(const char_t *name); + +/** + * Dispose of the script manager. + * + * @return The error return value. + */ +errorret_t scriptManagerDispose(void); diff --git a/src/dusk/script/scriptproto.c b/src/dusk/script/scriptproto.c new file mode 100644 index 00000000..a386a8e9 --- /dev/null +++ b/src/dusk/script/scriptproto.c @@ -0,0 +1,215 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scriptproto.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "script/module/modulebase.h" + +#define SCRIPT_PROTO_REGISTRY_MAX 64 + +static scriptproto_t *SCRIPT_PROTO_REGISTRY[SCRIPT_PROTO_REGISTRY_MAX]; +static size_t SCRIPT_PROTO_REGISTRY_COUNT = 0; + +void scriptProtoInit( + scriptproto_t *proto, + const char_t *name, + const size_t size, + jerry_external_handler_t constructor +) { + assertNotNull(proto, "Script prototype struct must not be null"); + memoryZero(proto, sizeof(scriptproto_t)); + + assertTrue( + SCRIPT_PROTO_REGISTRY_COUNT < SCRIPT_PROTO_REGISTRY_MAX, + "Script prototype registry capacity exceeded" + ); + SCRIPT_PROTO_REGISTRY[SCRIPT_PROTO_REGISTRY_COUNT++] = proto; + + proto->info = (jerry_object_native_info_t){ + .free_cb = moduleBaseFreeProto, + .number_of_references = 0, + .offset_of_references = 0 + }; + proto->prototype = jerry_object(); + proto->size = size; + + if(constructor != NULL) { + proto->constructor = jerry_function_external(constructor); + jerry_value_t protoKey = jerry_string_sz("prototype"); + jerry_object_set(proto->constructor, protoKey, proto->prototype); + jerry_value_free(protoKey); + jerry_value_t ctorKey = jerry_string_sz("constructor"); + jerry_object_set(proto->prototype, ctorKey, proto->constructor); + jerry_value_free(ctorKey); + } + + if(name != NULL) { + jerry_value_t global = jerry_current_realm(); + jerry_value_t key = jerry_string_sz(name); + jerry_value_t val; + if(proto->constructor) { + val = proto->constructor; + } else { + val = proto->prototype; + } + jerry_object_set(global, key, val); + jerry_value_free(key); + jerry_value_free(global); + } +} + +void scriptProtoDefineProp( + scriptproto_t *proto, + const char_t *name, + jerry_external_handler_t getter, + jerry_external_handler_t setter +) { + assertNotNull(proto, "Script prototype struct must not be null"); + assertStrLenMin(name, 1, "Property name must not be empty"); + assertNotNull(getter, "Getter must not be null"); + + jerry_property_descriptor_t desc; + memoryZero(&desc, sizeof(desc)); + desc.flags = (uint16_t)( + JERRY_PROP_IS_GET_DEFINED | + JERRY_PROP_IS_ENUMERABLE_DEFINED | JERRY_PROP_IS_ENUMERABLE | + JERRY_PROP_IS_CONFIGURABLE_DEFINED | JERRY_PROP_IS_CONFIGURABLE + ); + + desc.getter = jerry_function_external(getter); + + if(setter != NULL) { + desc.flags |= JERRY_PROP_IS_SET_DEFINED; + desc.setter = jerry_function_external(setter); + } + + jerry_value_t key = jerry_string_sz(name); + jerry_value_t result = jerry_object_define_own_prop( + proto->prototype, key, &desc + ); + jerry_value_free(result); + jerry_value_free(key); + jerry_value_free(desc.getter); + if(setter != NULL) jerry_value_free(desc.setter); +} + +void scriptProtoDefineFunc( + scriptproto_t *proto, + const char_t *name, + jerry_external_handler_t fn +) { + assertNotNull(proto, "Script prototype struct must not be null"); + assertStrLenMin(name, 1, "Method name must not be empty"); + assertNotNull(fn, "Function handler must not be null"); + + jerry_value_t key = jerry_string_sz(name); + jerry_value_t func = jerry_function_external(fn); + jerry_object_set(proto->prototype, key, func); + jerry_value_free(func); + jerry_value_free(key); +} + +void scriptProtoDefineStaticProp( + scriptproto_t *proto, + const char_t *name, + jerry_external_handler_t getter, + jerry_external_handler_t setter +) { + assertNotNull(proto, "Script prototype struct must not be null"); + assertStrLenMin(name, 1, "Property name must not be empty"); + assertNotNull(getter, "Getter must not be null"); + + jerry_value_t target = ( + proto->constructor ? proto->constructor : proto->prototype + ); + + jerry_property_descriptor_t desc; + memoryZero(&desc, sizeof(desc)); + desc.flags = (uint16_t)( + JERRY_PROP_IS_GET_DEFINED | + JERRY_PROP_IS_ENUMERABLE_DEFINED | JERRY_PROP_IS_ENUMERABLE | + JERRY_PROP_IS_CONFIGURABLE_DEFINED | JERRY_PROP_IS_CONFIGURABLE + ); + + desc.getter = jerry_function_external(getter); + + if(setter != NULL) { + desc.flags |= JERRY_PROP_IS_SET_DEFINED; + desc.setter = jerry_function_external(setter); + } + + jerry_value_t key = jerry_string_sz(name); + jerry_value_t result = jerry_object_define_own_prop(target, key, &desc); + jerry_value_free(result); + jerry_value_free(key); + jerry_value_free(desc.getter); + if(setter != NULL) jerry_value_free(desc.setter); +} + +void scriptProtoDefineStaticFunc( + scriptproto_t *proto, + const char_t *name, + jerry_external_handler_t fn +) { + assertNotNull(proto, "Script prototype struct must not be null"); + assertStrLenMin(name, 1, "Method name must not be empty"); + assertNotNull(fn, "Function handler must not be null"); + + jerry_value_t target = ( + proto->constructor ? proto->constructor : proto->prototype + ); + jerry_value_t key = jerry_string_sz(name); + jerry_value_t func = jerry_function_external(fn); + jerry_object_set(target, key, func); + jerry_value_free(func); + jerry_value_free(key); +} + +jerry_value_t scriptProtoCreateValue( + const scriptproto_t *proto, + const void *value +) { + assertNotNull(proto, "Script prototype struct must not be null"); + assertNotNull(value, "Value pointer must not be null"); + + void *ptr = memoryAllocate(proto->size); + memoryCopy(ptr, value, proto->size); + jerry_value_t obj = jerry_object(); + jerry_object_set_native_ptr(obj, &proto->info, ptr); + jerry_object_set_proto(obj, proto->prototype); + return obj; +} + +void *scriptProtoGetValue( + const scriptproto_t *proto, + const jerry_value_t obj +) { + assertNotNull(proto, "Script prototype struct must not be null"); + if(!jerry_value_is_object(obj)) return NULL; + return jerry_object_get_native_ptr(obj, &proto->info); +} + +void scriptProtoDefineToString( + scriptproto_t *proto, + jerry_external_handler_t fn +) { + scriptProtoDefineFunc(proto, "toString", fn); +} + +void scriptProtoDispose(scriptproto_t *proto) { + assertNotNull(proto, "Script prototype struct must not be null"); + jerry_value_free(proto->prototype); + if(proto->constructor) jerry_value_free(proto->constructor); +} + +void scriptProtoDisposeAll(void) { + for(size_t i = 0; i < SCRIPT_PROTO_REGISTRY_COUNT; i++) { + scriptProtoDispose(SCRIPT_PROTO_REGISTRY[i]); + } + SCRIPT_PROTO_REGISTRY_COUNT = 0; +} diff --git a/src/dusk/script/scriptproto.h b/src/dusk/script/scriptproto.h new file mode 100644 index 00000000..728744f2 --- /dev/null +++ b/src/dusk/script/scriptproto.h @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" +#include + +typedef struct { + jerry_object_native_info_t info; + jerry_value_t prototype; + jerry_value_t constructor; + size_t size; +} scriptproto_t; + +/** + * Initialize a JS class prototype. + * + * If name is non-NULL the class is registered as a global. When ctor is also + * non-NULL the global is the constructor function (enabling `new Name(...)`); + * otherwise the prototype object itself becomes the global. + * + * @param proto The struct to initialize. + * @param name JS global name, or NULL to skip global registration. + * @param size sizeof the C struct this class wraps. + * @param ctor Constructor handler, or NULL if the class has no constructor. + */ +void scriptProtoInit( + scriptproto_t *proto, + const char_t *name, + const size_t size, + jerry_external_handler_t ctor +); + +/** + * Define an instance property with a getter and optional setter. + * + * @param proto The class prototype. + * @param name Property name. + * @param getter Getter handler (must not be NULL). + * @param setter Setter handler, or NULL for a read-only property. + */ +void scriptProtoDefineProp( + scriptproto_t *proto, + const char_t *name, + jerry_external_handler_t getter, + jerry_external_handler_t setter +); + +/** + * Define an instance method on the class prototype. + * + * @param proto The class prototype. + * @param name Method name. + * @param fn C handler called when the method is invoked. + */ +void scriptProtoDefineFunc( + scriptproto_t *proto, + const char_t *name, + jerry_external_handler_t fn +); + +/** + * Define a static property on the class (e.g. Scene.current). + * + * Attaches to the constructor function when one exists, otherwise attaches + * directly to the prototype object (which is the global in that case). + * + * @param proto The class prototype. + * @param name Property name. + * @param getter Getter handler (must not be NULL). + * @param setter Setter handler, or NULL for a read-only property. + */ +void scriptProtoDefineStaticProp( + scriptproto_t *proto, + const char_t *name, + jerry_external_handler_t getter, + jerry_external_handler_t setter +); + +/** + * Define a static method on the class (e.g. Color.fromRGBA). + * + * Attaches to the constructor function when one exists, otherwise attaches + * directly to the prototype object (which is the global in that case). + * + * @param proto The class prototype. + * @param name Method name. + * @param fn C handler called when the static method is invoked. + */ +void scriptProtoDefineStaticFunc( + scriptproto_t *proto, + const char_t *name, + jerry_external_handler_t fn +); + +/** + * Create a JS instance wrapping a copy of a C value. + * + * @param proto The class prototype. + * @param value Pointer to the C value to copy into the new JS object. + * @return A new JS object with the class prototype and native pointer set. + */ +jerry_value_t scriptProtoCreateValue( + const scriptproto_t *proto, + const void *value +); + +/** + * Unwrap the native C pointer from a JS object. + * + * @param proto The class prototype. + * @param obj The JS object to inspect. + * @return Pointer to the wrapped C value, or NULL if not an instance. + */ +void *scriptProtoGetValue( + const scriptproto_t *proto, + const jerry_value_t obj +); + +/** + * Define the toString() method on the class prototype. + * + * @param proto The class prototype. + * @param fn C handler called when toString() is invoked on an instance. + */ +void scriptProtoDefineToString( + scriptproto_t *proto, + jerry_external_handler_t fn +); + +/** + * Release all JerryScript resources held by the prototype. + * + * @param proto The class prototype to dispose. + */ +void scriptProtoDispose(scriptproto_t *proto); + +/** + * Disposes every prototype ever initialized via scriptProtoInit. Must be + * called before jerry_cleanup() - this JerryScript build fatally asserts + * during cleanup if any jerry_value_t handle is still held by the embedder, + * and every scriptproto_t retains its prototype/constructor handles for the + * lifetime of the module system. + */ +void scriptProtoDisposeAll(void); diff --git a/src/dusk/script/scriptvalue.h b/src/dusk/script/scriptvalue.h new file mode 100644 index 00000000..fec219ea --- /dev/null +++ b/src/dusk/script/scriptvalue.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +#define SCRIPT_VALUE_TYPE_NIL 0 +#define SCRIPT_VALUE_TYPE_INT 1 +#define SCRIPT_VALUE_TYPE_FLOAT 2 +#define SCRIPT_VALUE_TYPE_STRING 3 +#define SCRIPT_VALUE_TYPE_BOOL 4 +#define SCRIPT_VALUE_TYPE_USERDATA 5 + +typedef struct scriptvalue_s { + uint8_t type; + + union { + int32_t intValue; + float_t floatValue; + char_t *strValue; + bool_t boolValue; + } value; +} scriptvalue_t; \ No newline at end of file