From 7c3386cf3eb1556a59f3282f18941204ec529ea6 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Mon, 20 Apr 2026 17:01:12 -0500 Subject: [PATCH] Add entity scripting --- src/dusk/entity/component/CMakeLists.txt | 3 +- .../entity/component/script/CMakeLists.txt | 9 ++ .../entity/component/script/entityscript.c | 115 ++++++++++++++++++ .../entity/component/script/entityscript.h | 77 ++++++++++++ src/dusk/entity/componentlist.h | 4 +- src/dusk/script/module/entity/CMakeLists.txt | 1 + src/dusk/script/module/entity/moduleentity.c | 13 ++ .../module/entity/script/CMakeLists.txt | 9 ++ .../module/entity/script/moduleentityscript.c | 85 +++++++++++++ .../module/entity/script/moduleentityscript.h | 16 +++ src/dusk/script/scriptmodule.c | 2 + 11 files changed, 332 insertions(+), 2 deletions(-) create mode 100644 src/dusk/entity/component/script/CMakeLists.txt create mode 100644 src/dusk/entity/component/script/entityscript.c create mode 100644 src/dusk/entity/component/script/entityscript.h create mode 100644 src/dusk/script/module/entity/script/CMakeLists.txt create mode 100644 src/dusk/script/module/entity/script/moduleentityscript.c create mode 100644 src/dusk/script/module/entity/script/moduleentityscript.h diff --git a/src/dusk/entity/component/CMakeLists.txt b/src/dusk/entity/component/CMakeLists.txt index e2bf96f6..e5d8792f 100644 --- a/src/dusk/entity/component/CMakeLists.txt +++ b/src/dusk/entity/component/CMakeLists.txt @@ -4,4 +4,5 @@ # https://opensource.org/licenses/MIT add_subdirectory(display) -add_subdirectory(physics) \ No newline at end of file +add_subdirectory(physics) +add_subdirectory(script) \ No newline at end of file diff --git a/src/dusk/entity/component/script/CMakeLists.txt b/src/dusk/entity/component/script/CMakeLists.txt new file mode 100644 index 00000000..bd179301 --- /dev/null +++ b/src/dusk/entity/component/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 + entityscript.c +) diff --git a/src/dusk/entity/component/script/entityscript.c b/src/dusk/entity/component/script/entityscript.c new file mode 100644 index 00000000..ebbd7323 --- /dev/null +++ b/src/dusk/entity/component/script/entityscript.c @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "entityscript.h" +#include "entity/entitymanager.h" +#include "assert/assert.h" +#include "util/memory.h" + +void entityScriptInit( + const entityid_t entityId, + const componentid_t compId +) { + entityscript_t *data = componentGetData( + entityId, compId, COMPONENT_TYPE_ENTITYSCRIPT + ); + memoryZero(data, sizeof(entityscript_t)); +} + +void entityScriptUnload( + const entityid_t entityId, + const componentid_t compId +) { + entityscript_t *data = componentGetData( + entityId, compId, COMPONENT_TYPE_ENTITYSCRIPT + ); + + if(!data->active) return; + + lua_getglobal(data->context.luaState, "entityDispose"); + if(lua_isfunction(data->context.luaState, -1)) { + if(lua_pcall(data->context.luaState, 0, 0, 0) != LUA_OK) { + lua_pop(data->context.luaState, 1); + } + } else { + lua_pop(data->context.luaState, 1); + } + + scriptContextDispose(&data->context); + data->active = false; +} + +errorret_t entityScriptSetScript( + const entityid_t entityId, + const componentid_t compId, + const char_t *filename +) { + assertNotNull(filename, "Entity script filename cannot be NULL"); + + entityScriptUnload(entityId, compId); + + entityscript_t *data = componentGetData( + entityId, compId, COMPONENT_TYPE_ENTITYSCRIPT + ); + + errorChain(scriptContextInit(&data->context)); + data->active = true; + + // Expose entity identity to the script as globals + lua_pushnumber(data->context.luaState, (lua_Number)entityId); + lua_setglobal(data->context.luaState, "ENTITY_ID"); + lua_pushnumber(data->context.luaState, (lua_Number)compId); + lua_setglobal(data->context.luaState, "ENTITY_COMPONENT_ID"); + + errorChain(scriptContextExecFile(&data->context, filename)); + + lua_getglobal(data->context.luaState, "entityInit"); + if(lua_isfunction(data->context.luaState, -1)) { + if(lua_pcall(data->context.luaState, 0, 0, 0) != LUA_OK) { + errorThrow( + "Failed to call entityInit: %s", + lua_tostring(data->context.luaState, -1) + ); + } + } else { + lua_pop(data->context.luaState, 1); + } + + errorOk(); +} + +errorret_t entityScriptUpdate( + const entityid_t entityId, + const componentid_t compId +) { + entityscript_t *data = componentGetData( + entityId, compId, COMPONENT_TYPE_ENTITYSCRIPT + ); + + if(!data->active) errorOk(); + + lua_getglobal(data->context.luaState, "entityUpdate"); + if(lua_isfunction(data->context.luaState, -1)) { + if(lua_pcall(data->context.luaState, 0, 0, 0) != LUA_OK) { + errorThrow( + "Failed to call entityUpdate: %s", + lua_tostring(data->context.luaState, -1) + ); + } + } else { + lua_pop(data->context.luaState, 1); + } + + errorOk(); +} + +void entityScriptDispose( + const entityid_t entityId, + const componentid_t compId +) { + entityScriptUnload(entityId, compId); +} diff --git a/src/dusk/entity/component/script/entityscript.h b/src/dusk/entity/component/script/entityscript.h new file mode 100644 index 00000000..c133899c --- /dev/null +++ b/src/dusk/entity/component/script/entityscript.h @@ -0,0 +1,77 @@ +/** + * 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 "script/scriptcontext.h" + +typedef struct { + scriptcontext_t context; + bool_t active; +} entityscript_t; + +/** + * Initializes the entity script component (empty context, not yet loaded). + * + * @param entityId The entity ID. + * @param compId The component ID. + */ +void entityScriptInit( + const entityid_t entityId, + const componentid_t compId +); + +/** + * Unloads the currently active script: calls entityDispose if defined and + * disposes the context. No-op if no script is loaded. + * + * @param entityId The entity ID. + * @param compId The component ID. + */ +void entityScriptUnload( + const entityid_t entityId, + const componentid_t compId +); + +/** + * Sets the script for this component. If a script is already loaded it is + * unloaded first. Then loads and executes the new file, sets entityId and + * entityScriptId as Lua globals, and calls entityInit if defined. + * + * @param entityId The entity ID. + * @param compId The component ID. + * @param filename The script file to load. + * @return The error return value. + */ +errorret_t entityScriptSetScript( + const entityid_t entityId, + const componentid_t compId, + const char_t *filename +); + +/** + * Calls the entityUpdate Lua function in the entity script context, if defined. + * + * @param entityId The entity ID. + * @param compId The component ID. + * @return The error return value. + */ +errorret_t entityScriptUpdate( + const entityid_t entityId, + const componentid_t compId +); + +/** + * Unloads the script if one is active, then disposes the component. + * + * @param entityId The entity ID. + * @param compId The component ID. + */ +void entityScriptDispose( + const entityid_t entityId, + const componentid_t compId +); diff --git a/src/dusk/entity/componentlist.h b/src/dusk/entity/componentlist.h index c8991eb8..7ce59b84 100644 --- a/src/dusk/entity/componentlist.h +++ b/src/dusk/entity/componentlist.h @@ -10,9 +10,11 @@ #include "entity/component/display/entitymesh.h" #include "entity/component/display/entitymaterial.h" #include "entity/component/physics/entityphysics.h" +#include "entity/component/script/entityscript.h" X(POSITION, entityposition_t, position, entityPositionInit, NULL) X(CAMERA, entitycamera_t, camera, entityCameraInit, NULL) X(MESH, entitymesh_t, mesh, entityMeshInit, NULL) X(MATERIAL, entitymaterial_t, material, entityMaterialInit, NULL) -X(PHYSICS, entityphysics_t, physics, entityPhysicsInit, entityPhysicsDispose) \ No newline at end of file +X(PHYSICS, entityphysics_t, physics, entityPhysicsInit, entityPhysicsDispose) +X(ENTITYSCRIPT, entityscript_t, entityscript, entityScriptInit, entityScriptDispose) \ No newline at end of file diff --git a/src/dusk/script/module/entity/CMakeLists.txt b/src/dusk/script/module/entity/CMakeLists.txt index 4bc0238e..5a511cb0 100644 --- a/src/dusk/script/module/entity/CMakeLists.txt +++ b/src/dusk/script/module/entity/CMakeLists.txt @@ -12,3 +12,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} # Subdirectories add_subdirectory(display) add_subdirectory(physics) +add_subdirectory(script) diff --git a/src/dusk/script/module/entity/moduleentity.c b/src/dusk/script/module/entity/moduleentity.c index 8291b1fe..82a8b3f3 100644 --- a/src/dusk/script/module/entity/moduleentity.c +++ b/src/dusk/script/module/entity/moduleentity.c @@ -9,12 +9,25 @@ #include "assert/assert.h" #include "entity/entitymanager.h" #include "entity/entity.h" +#include "display/moduleentityposition.h" +#include "display/moduleentitycamera.h" +#include "display/moduleentitymesh.h" +#include "display/moduleentitymaterial.h" +#include "physics/moduleentityphysics.h" +#include "script/moduleentityscript.h" void moduleEntity(scriptcontext_t *ctx) { assertNotNull(ctx, "Script context cannot be NULL"); lua_register(ctx->luaState, "entityAdd", moduleEntityAdd); lua_register(ctx->luaState, "entityRemove", moduleEntityRemove); + + moduleEntityPosition(ctx); + moduleEntityCamera(ctx); + moduleEntityMesh(ctx); + moduleEntityMaterial(ctx); + moduleEntityPhysics(ctx); + moduleEntityScript(ctx); } int moduleEntityAdd(lua_State *L) { diff --git a/src/dusk/script/module/entity/script/CMakeLists.txt b/src/dusk/script/module/entity/script/CMakeLists.txt new file mode 100644 index 00000000..d1fa1a37 --- /dev/null +++ b/src/dusk/script/module/entity/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 + moduleentityscript.c +) diff --git a/src/dusk/script/module/entity/script/moduleentityscript.c b/src/dusk/script/module/entity/script/moduleentityscript.c new file mode 100644 index 00000000..d9c85553 --- /dev/null +++ b/src/dusk/script/module/entity/script/moduleentityscript.c @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "moduleentityscript.h" +#include "assert/assert.h" +#include "entity/entity.h" +#include "entity/entitymanager.h" +#include "entity/component/script/entityscript.h" + +void moduleEntityScript(scriptcontext_t *ctx) { + assertNotNull(ctx, "Script context cannot be NULL"); + + #define REG(name, func) lua_register(ctx->luaState, name, func) + REG("entityScriptAdd", moduleEntityScriptAdd); + REG("entityScriptSetScript", moduleEntityScriptSetScript); + REG("entityScriptUpdate", moduleEntityScriptUpdate); + REG("entityAddScripted", moduleEntityAddScripted); + #undef REG +} + +int moduleEntityScriptAdd(lua_State *L) { + assertNotNull(L, "Lua state cannot be NULL"); + + entityid_t entityId = (entityid_t)luaL_checknumber(L, 1); + componentid_t compId = entityAddComponent(entityId, COMPONENT_TYPE_ENTITYSCRIPT); + lua_pushnumber(L, (lua_Number)compId); + return 1; +} + +int moduleEntityScriptSetScript(lua_State *L) { + assertNotNull(L, "Lua state cannot be NULL"); + + entityid_t entityId = (entityid_t)luaL_checknumber(L, 1); + componentid_t compId = (componentid_t)luaL_checknumber(L, 2); + const char_t *filename = luaL_checkstring(L, 3); + + errorret_t err = entityScriptSetScript(entityId, compId, filename); + if(err.code != ERROR_OK) { + luaL_error(L, "Failed to set entity script: %s", filename); + errorCatch(errorPrint(err)); + return 0; + } + + return 0; +} + +int moduleEntityAddScripted(lua_State *L) { + assertNotNull(L, "Lua state cannot be NULL"); + + const char_t *filename = luaL_checkstring(L, 1); + + entityid_t entityId = entityManagerAdd(); + componentid_t compId = entityAddComponent(entityId, COMPONENT_TYPE_ENTITYSCRIPT); + + errorret_t err = entityScriptSetScript(entityId, compId, filename); + if(err.code != ERROR_OK) { + luaL_error(L, "Failed to set entity script: %s", filename); + errorCatch(errorPrint(err)); + return 0; + } + + lua_pushnumber(L, (lua_Number)entityId); + lua_pushnumber(L, (lua_Number)compId); + return 2; +} + +int moduleEntityScriptUpdate(lua_State *L) { + assertNotNull(L, "Lua state cannot be NULL"); + + entityid_t entityId = (entityid_t)luaL_checknumber(L, 1); + componentid_t compId = (componentid_t)luaL_checknumber(L, 2); + + errorret_t err = entityScriptUpdate(entityId, compId); + if(err.code != ERROR_OK) { + luaL_error(L, "Failed to update entity script"); + errorCatch(errorPrint(err)); + return 0; + } + + return 0; +} diff --git a/src/dusk/script/module/entity/script/moduleentityscript.h b/src/dusk/script/module/entity/script/moduleentityscript.h new file mode 100644 index 00000000..00ab45ab --- /dev/null +++ b/src/dusk/script/module/entity/script/moduleentityscript.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/scriptcontext.h" + +void moduleEntityScript(scriptcontext_t *ctx); + +int moduleEntityScriptAdd(lua_State *L); +int moduleEntityScriptSetScript(lua_State *L); +int moduleEntityScriptUpdate(lua_State *L); +int moduleEntityAddScripted(lua_State *L); diff --git a/src/dusk/script/scriptmodule.c b/src/dusk/script/scriptmodule.c index 973d6a05..d0e7abe2 100644 --- a/src/dusk/script/scriptmodule.c +++ b/src/dusk/script/scriptmodule.c @@ -28,6 +28,7 @@ #include "script/module/entity/display/moduleentitymesh.h" #include "script/module/entity/display/moduleentitymaterial.h" #include "script/module/entity/physics/moduleentityphysics.h" +#include "script/module/entity/script/moduleentityscript.h" #include "script/scriptgame.h" #include "util/string.h" @@ -40,6 +41,7 @@ const scriptmodule_t SCRIPT_MODULE_LIST[] = { REG("entitymesh", moduleEntityMesh) REG("entitymaterial", moduleEntityMaterial) REG("entityphysics", moduleEntityPhysics) + REG("entityscript", moduleEntityScript) REG("entity", moduleEntity) REG("input", moduleInput) REG("platform", modulePlatform)