From d6f2cc1ea32427f480ffa4208e1cd30448846251 Mon Sep 17 00:00:00 2001 From: Nick Koirala Date: Sun, 8 Mar 2026 21:48:40 +1300 Subject: [PATCH] feat: example entity script --- .gitea/workflows/sync-docs-to-wiki.yml | 28 ++-- docs/Editor.md | 15 +++ imgui.ini | 4 +- include/Application.h | 20 +++ include/ECSComponents.h | 7 + include/SceneLoader.h | 5 + include/gui/GuiManager.h | 6 + include/scripting/ScriptEngine.h | 2 + scenes/demo.toml | 1 + scripts/ball.as | 104 +++++++++++++++ src/Application.cpp | 169 ++++++++++++++++++++++++- src/SceneLoader.cpp | 90 ++++++++++++- src/gui/GuiManager.cpp | 87 +++++++++++-- src/scripting/ScriptEngine.cpp | 85 ++++++++++++- 14 files changed, 588 insertions(+), 35 deletions(-) create mode 100644 scripts/ball.as diff --git a/.gitea/workflows/sync-docs-to-wiki.yml b/.gitea/workflows/sync-docs-to-wiki.yml index 3d78a9e..d1fdcdd 100644 --- a/.gitea/workflows/sync-docs-to-wiki.yml +++ b/.gitea/workflows/sync-docs-to-wiki.yml @@ -12,8 +12,6 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 - with: - fetch-depth: 0 # Full history - name: Start SSH agent uses: webfactory/ssh-agent@v0.9.0 @@ -29,27 +27,25 @@ jobs: env: WIKI_REPO: git@gitea.appstack.me:nick/simian.wiki.git run: | - sudo apt-get update -qq - sudo apt-get install -y rsync - git config --global user.name "Simian CI" git config --global user.email "ci@simian.local" - # Clone wiki to TEMP dir (not workspace) TEMP_WIKI=$(mktemp -d) git clone "$WIKI_REPO" "$TEMP_WIKI" - # Copy docs into wiki clone - rsync -av --delete docs/ "$TEMP_WIKI/" - cd "$TEMP_WIKI" - git checkout main 2>/dev/null || git checkout -b main - git add -A - if ! git diff --staged --quiet; then + # Git-safe clean: delete everything except .git + find . -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} + + + # Copy docs files + cp -r ../docs/* . 2>/dev/null || true + + git add -A + if git diff --staged --quiet; then + echo "No wiki changes" + else git commit -m "Update wiki from docs (CI) [skip ci]" git push origin main - echo "✅ Pushed to simian.wiki main" - else - echo "No wiki changes" - fi + echo "✅ Synced to simian.wiki main" + fi \ No newline at end of file diff --git a/docs/Editor.md b/docs/Editor.md index a8c8d6a..dde22b2 100644 --- a/docs/Editor.md +++ b/docs/Editor.md @@ -53,6 +53,7 @@ This document outlines the in-app editor UI, its panels, and common workflows. - Transform: position, scale, rotation (degrees in UI). - Velocity: linear and angular velocity. - Sprite: model id, shader id, color, outline. +- Script: per-entity script path and enabled toggle. - Camera: position, target, up, FOV, projection, helper actions. - Light: direction, color, intensity, helper presets. - Material: material id. @@ -76,3 +77,17 @@ This document outlines the in-app editor UI, its panels, and common workflows. - Scene Save and Load use file path text fields. - Component IDs are numeric and map to runtime resource managers. - World Transform is computed from hierarchy and local transforms and is read-only. + +## Scene Scripts + +- Scene scripts are loaded by the scene system and run once per scene. +- The editor lets you set a scene-level script path in the Scene panel. +- Preferred scene callbacks: `SceneInit()`, `SceneUpdate(float dt)`, `SceneShutdown()`. +- Legacy callbacks still work: `Init()`, `Update(float dt)`, `Shutdown()`. + +## ScriptComponent + +- ScriptComponent runs a per-entity script module. +- Set the script path in the Inspector and toggle enabled. +- Preferred callbacks: `EntityInit(uint entity)`, `EntityUpdate(uint entity, float dt)`, `EntityShutdown(uint entity)`. +- Legacy callbacks still work: `Init(uint entity)`, `Update(uint entity, float dt)`, `Shutdown(uint entity)`. diff --git a/imgui.ini b/imgui.ini index 8d1eaf3..6310b76 100644 --- a/imgui.ini +++ b/imgui.ini @@ -26,8 +26,8 @@ Size=225,63 Collapsed=0 [Window][##TOAST1] -Pos=1704,835 -Size=196,63 +Pos=1035,564 +Size=225,63 Collapsed=0 [Window][##TOAST2] diff --git a/include/Application.h b/include/Application.h index b726922..452831c 100644 --- a/include/Application.h +++ b/include/Application.h @@ -8,6 +8,8 @@ #include "MaterialManager.h" #include "InputManager.h" #include +#include +#include class Application { public: @@ -24,6 +26,16 @@ public: entt::registry& GetRegistry() { return registry; } private: + struct EntityScriptModule { + std::string path; + std::string moduleName; + asIScriptModule* module = nullptr; + asIScriptFunction* initFunc = nullptr; + asIScriptFunction* updateFunc = nullptr; + asIScriptFunction* shutdownFunc = nullptr; + bool valid = false; + }; + ScriptEngine scriptEngine; HotReload* hotReload; bool scriptCompilationError; @@ -41,6 +53,10 @@ private: ModelManager modelManager; MaterialManager materialManager; InputManager inputManager; + std::unordered_map entityScriptModules; + std::unordered_map entityScriptBindings; + unsigned int entityScriptModuleCounter = 0; + std::string currentSceneScriptPath; static const int WINDOW_WIDTH = 1280; static const int WINDOW_HEIGHT = 720; @@ -50,6 +66,10 @@ private: void Update(float deltaTime); void UpdateSystems(float deltaTime); + void UpdateEntityScripts(float deltaTime); + void ShutdownEntityScripts(); + void ProcessSceneScriptChanges(); + EntityScriptModule* GetEntityScriptModule(const std::string& path); void RenderScene(); void Draw(); }; \ No newline at end of file diff --git a/include/ECSComponents.h b/include/ECSComponents.h index ef7b56e..6acb29f 100644 --- a/include/ECSComponents.h +++ b/include/ECSComponents.h @@ -56,6 +56,13 @@ struct Tag { Tag(const std::string& n) : name(n) {} }; +// ScriptComponent - per-entity script reference +struct ScriptComponent { + std::string scriptPath; + bool enabled = true; + bool started = false; +}; + // SceneEntity component - marks entities owned by the scene struct SceneEntity { bool persistent = false; diff --git a/include/SceneLoader.h b/include/SceneLoader.h index b35d79c..b5497d1 100644 --- a/include/SceneLoader.h +++ b/include/SceneLoader.h @@ -18,3 +18,8 @@ bool SaveSceneToFile(const std::string& path); // Clear all entities from the registry void ClearScene(); + +// Scene script path helpers +void SetSceneScriptPath(const std::string& path); +const std::string& GetSceneScriptPath(); +bool ConsumeSceneScriptPath(std::string& outPath); diff --git a/include/gui/GuiManager.h b/include/gui/GuiManager.h index ed5fde5..a17acf7 100644 --- a/include/gui/GuiManager.h +++ b/include/gui/GuiManager.h @@ -6,6 +6,7 @@ #include "gui/ImGuiNotify.hpp" #include "LogWindow.h" // Include the new LogWindow class #include +#include class Application; @@ -30,6 +31,7 @@ private: bool IsEntityAncestor(entt::entity ancestor, entt::entity entity) const; const char* GetEntityLabel(entt::entity entity); void SyncTagBuffer(entt::entity entity); + void SyncScriptPathBuffer(entt::entity entity); bool showLogWindow = true; bool showSceneWindow = true; @@ -39,6 +41,10 @@ private: entt::entity selectedEntity = entt::null; entt::entity tagBufferEntity = entt::null; char tagBuffer[128] = {0}; + entt::entity scriptPathEntity = entt::null; + char scriptPathBuffer[260] = {0}; + char sceneScriptPathBuffer[260] = {0}; + std::string lastSceneScriptPath; ImVec2 desiredRenderSize = ImVec2(0.0f, 0.0f); bool requestSceneNew = false; bool requestSceneLoad = false; diff --git a/include/scripting/ScriptEngine.h b/include/scripting/ScriptEngine.h index 650936d..649c1c4 100644 --- a/include/scripting/ScriptEngine.h +++ b/include/scripting/ScriptEngine.h @@ -12,7 +12,9 @@ public: bool Initialize(); void Shutdown(); bool CompileScript(const std::string& filename); + bool CompileModule(const std::string& moduleName, const std::string& filename, asIScriptModule** outModule); void CallScriptFunction(asIScriptFunction* func, float dt = 0.0f); + void CallScriptFunction(asIScriptFunction* func, unsigned int entityId, float dt); void GarbageCollect(); // Add a path used to resolve #include directives in scripts diff --git a/scenes/demo.toml b/scenes/demo.toml index 5dd1eea..90059b1 100644 --- a/scenes/demo.toml +++ b/scenes/demo.toml @@ -50,4 +50,5 @@ color = 0x4523BAFF outline_size = 0.000 shader_vs = "shaders/toon.vs" shader_fs = "shaders/toon.fs" +script = "scripts/ball.as" diff --git a/scripts/ball.as b/scripts/ball.as new file mode 100644 index 0000000..f21e1c1 --- /dev/null +++ b/scripts/ball.as @@ -0,0 +1,104 @@ +// ScriptComponent example: vertical sine movement + +array trackedEntities; +array baseX; +array baseY; +array baseZ; +array phase; + +float amplitude = 0.5f; +float speed = 1.5f; + +int FindEntityIndex(uint entity) +{ + for (uint i = 0; i < trackedEntities.length(); ++i) + { + if (trackedEntities[i] == entity) + { + return int(i); + } + } + return -1; +} + +void TrackEntity(uint entity) +{ + if (FindEntityIndex(entity) != -1) + { + return; + } + + if (!ECS::HasTransform(entity)) + { + ECS::AddTransform(entity, 0.0f, 0.0f, 0.0f); + } + + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + ECS::GetPosition(entity, x, y, z); + + trackedEntities.insertLast(entity); + baseX.insertLast(x); + baseY.insertLast(y); + baseZ.insertLast(z); + phase.insertLast(0.0f); +} + +void UntrackEntity(uint entity) +{ + int idx = FindEntityIndex(entity); + if (idx < 0) + { + return; + } + + trackedEntities.removeAt(idx); + baseX.removeAt(idx); + baseY.removeAt(idx); + baseZ.removeAt(idx); + phase.removeAt(idx); +} + +void EntityInit(uint entity) +{ + if (!ECS::IsValid(entity)) + { + return; + } + + TrackEntity(entity); +} + +void EntityUpdate(uint entity, float dt) +{ + if (!ECS::IsValid(entity)) + { + return; + } + + int idx = FindEntityIndex(entity); + if (idx < 0) + { + TrackEntity(entity); + idx = FindEntityIndex(entity); + if (idx < 0) + { + return; + } + } + + if (!ECS::HasTransform(entity)) + { + return; + } + + phase[idx] += dt; + float offset = Math::Sin(phase[idx] * speed + float(entity) * 0.1f) * amplitude; + ECS::SetPosition(entity, baseX[idx], baseY[idx] + offset, baseZ[idx]); +} + +void EntityShutdown(uint entity) +{ + UntrackEntity(entity); +} diff --git a/src/Application.cpp b/src/Application.cpp index c120722..b3af0ff 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -107,6 +107,7 @@ bool Application::Initialize(int argc, char *argv[]) // Compile initial script scriptCompilationError = !scriptEngine.CompileScript(SCRIPT_FILE); + currentSceneScriptPath = SCRIPT_FILE; if (enableEditor) { @@ -134,10 +135,12 @@ void Application::Run() void Application::Update(float deltaTime) { + ProcessSceneScriptChanges(); + // Check for hot reload if (hotReload && hotReload->CheckForChanges()) { - bool success = scriptEngine.CompileScript(SCRIPT_FILE); + bool success = scriptEngine.CompileScript(currentSceneScriptPath); scriptCompilationError = !success; if (!success) { @@ -155,6 +158,8 @@ void Application::Update(float deltaTime) // Call script Update function scriptEngine.CallScriptFunction(scriptEngine.GetUpdateFunction(), deltaTime); + UpdateEntityScripts(deltaTime); + // Update ECS systems UpdateSystems(deltaTime); } @@ -176,6 +181,167 @@ void Application::UpdateSystems(float deltaTime) UpdateWorldTransforms(registry); } +Application::EntityScriptModule* Application::GetEntityScriptModule(const std::string& path) +{ + if (path.empty()) { + return nullptr; + } + + auto it = entityScriptModules.find(path); + if (it != entityScriptModules.end()) { + return &it->second; + } + + EntityScriptModule moduleInfo; + moduleInfo.path = path; + moduleInfo.moduleName = "entity_script_" + std::to_string(entityScriptModuleCounter++); + + if (!scriptEngine.CompileModule(moduleInfo.moduleName, path, &moduleInfo.module)) { + log_warn("Failed to compile entity script: %s", path.c_str()); + entityScriptModules.emplace(path, moduleInfo); + return &entityScriptModules[path]; + } + + if (moduleInfo.module) { + moduleInfo.initFunc = moduleInfo.module->GetFunctionByDecl("void EntityInit(uint)"); + if (!moduleInfo.initFunc) { + moduleInfo.initFunc = moduleInfo.module->GetFunctionByDecl("void Init(uint)"); + } + + moduleInfo.updateFunc = moduleInfo.module->GetFunctionByDecl("void EntityUpdate(uint, float)"); + if (!moduleInfo.updateFunc) { + moduleInfo.updateFunc = moduleInfo.module->GetFunctionByDecl("void Update(uint, float)"); + } + + moduleInfo.shutdownFunc = moduleInfo.module->GetFunctionByDecl("void EntityShutdown(uint)"); + if (!moduleInfo.shutdownFunc) { + moduleInfo.shutdownFunc = moduleInfo.module->GetFunctionByDecl("void Shutdown(uint)"); + } + + moduleInfo.valid = true; + } + + entityScriptModules.emplace(path, moduleInfo); + return &entityScriptModules[path]; +} + +void Application::UpdateEntityScripts(float deltaTime) +{ + auto view = registry.view(); + std::vector invalidBindings; + + for (auto entity : view) { + auto& script = view.get(entity); + if (!script.enabled || script.scriptPath.empty()) { + auto existingBinding = entityScriptBindings.find(entity); + if (script.started && existingBinding != entityScriptBindings.end()) { + auto* moduleInfo = GetEntityScriptModule(existingBinding->second); + if (moduleInfo && moduleInfo->valid && moduleInfo->shutdownFunc) { + scriptEngine.CallScriptFunction(moduleInfo->shutdownFunc, static_cast(entity), 0.0f); + } + } + script.started = false; + entityScriptBindings.erase(entity); + continue; + } + + std::string previousPath; + auto existingBinding = entityScriptBindings.find(entity); + if (existingBinding != entityScriptBindings.end()) { + previousPath = existingBinding->second; + } + + if (previousPath != script.scriptPath) { + if (!previousPath.empty()) { + auto* oldModule = GetEntityScriptModule(previousPath); + if (oldModule && oldModule->valid && oldModule->shutdownFunc) { + scriptEngine.CallScriptFunction(oldModule->shutdownFunc, static_cast(entity), 0.0f); + } + } + entityScriptBindings[entity] = script.scriptPath; + script.started = false; + } + + auto* moduleInfo = GetEntityScriptModule(script.scriptPath); + if (!moduleInfo || !moduleInfo->valid) { + continue; + } + + if (!script.started) { + if (moduleInfo->initFunc) { + scriptEngine.CallScriptFunction(moduleInfo->initFunc, static_cast(entity), 0.0f); + } + script.started = true; + } + + if (moduleInfo->updateFunc) { + scriptEngine.CallScriptFunction(moduleInfo->updateFunc, static_cast(entity), deltaTime); + } + } + + for (const auto& [entity, path] : entityScriptBindings) { + if (!registry.valid(entity)) { + invalidBindings.push_back(entity); + } + } + + for (auto entity : invalidBindings) { + entityScriptBindings.erase(entity); + } +} + +void Application::ShutdownEntityScripts() +{ + auto view = registry.view(); + for (auto entity : view) { + auto& script = view.get(entity); + if (!script.started || script.scriptPath.empty()) { + continue; + } + + auto* moduleInfo = GetEntityScriptModule(script.scriptPath); + if (moduleInfo && moduleInfo->valid && moduleInfo->shutdownFunc) { + scriptEngine.CallScriptFunction(moduleInfo->shutdownFunc, static_cast(entity), 0.0f); + } + script.started = false; + } +} + +void Application::ProcessSceneScriptChanges() +{ + std::string sceneScriptPath; + if (!ConsumeSceneScriptPath(sceneScriptPath)) { + return; + } + + if (sceneScriptPath.empty() || sceneScriptPath == currentSceneScriptPath) { + return; + } + + currentSceneScriptPath = sceneScriptPath; + bool success = scriptEngine.CompileScript(currentSceneScriptPath); + scriptCompilationError = !success; + if (!success) { + log_warn("Scene script compilation failed - keeping previous version"); + Toast::Warning("Scene script compilation failed - check console for details."); + return; + } + + if (hotReload) { + delete hotReload; + hotReload = nullptr; + } + + std::filesystem::path p(currentSceneScriptPath); + std::string watchPath = p.parent_path().string(); + if (watchPath.empty()) { + watchPath = "."; + } + hotReload = new HotReload(watchPath); + + scriptEngine.CallScriptFunction(scriptEngine.GetInitFunction()); +} + void Application::RenderScene() { // Query for active camera (first one found with Camera3DComponent) @@ -305,6 +471,7 @@ void Application::Shutdown() } + ShutdownEntityScripts(); scriptEngine.Shutdown(); if (enableEditor) { diff --git a/src/SceneLoader.cpp b/src/SceneLoader.cpp index d77ed53..d731d96 100644 --- a/src/SceneLoader.cpp +++ b/src/SceneLoader.cpp @@ -17,6 +17,8 @@ static entt::registry* g_sceneRegistry = nullptr; static ModelManager* g_sceneModelManager = nullptr; static ShaderManager* g_sceneShaderManager = nullptr; +static std::string g_sceneScriptPath; +static bool g_sceneScriptDirty = false; void SetSceneContext(entt::registry* registry, ModelManager* modelManager, ShaderManager* shaderManager) { g_sceneRegistry = registry; @@ -30,6 +32,26 @@ void ClearScene() { } } +void SetSceneScriptPath(const std::string& path) { + if (g_sceneScriptPath != path) { + g_sceneScriptPath = path; + g_sceneScriptDirty = true; + } +} + +const std::string& GetSceneScriptPath() { + return g_sceneScriptPath; +} + +bool ConsumeSceneScriptPath(std::string& outPath) { + if (!g_sceneScriptDirty) { + return false; + } + outPath = g_sceneScriptPath; + g_sceneScriptDirty = false; + return true; +} + struct SceneEntityDef { std::string id; std::string parent; @@ -53,6 +75,10 @@ struct SceneEntityDef { std::string shaderFs; unsigned int shaderId = 0; bool hasShaderId = false; + + bool hasScript = false; + std::string scriptPath; + bool scriptEnabled = true; }; static std::string Trim(const std::string& value) { @@ -131,6 +157,24 @@ static bool ParseString(const std::string& value, std::string& out) { return !out.empty(); } +static bool ParseBool(const std::string& value, bool& out) { + std::string trimmed = Trim(value); + std::string lower; + lower.reserve(trimmed.size()); + for (char c : trimmed) { + lower.push_back(static_cast(std::tolower(static_cast(c)))); + } + if (lower == "true" || lower == "1") { + out = true; + return true; + } + if (lower == "false" || lower == "0") { + out = false; + return true; + } + return false; +} + static std::string EscapeString(const std::string& value) { std::string out; out.reserve(value.size()); @@ -251,7 +295,23 @@ entt::entity LoadSceneFromFile(const std::string& path, bool clearExisting) { } if (!current) { - log_warn("Scene parse: key/value outside entity section at line %d", lineNumber); + size_t eq = line.find('='); + if (eq == std::string::npos) { + log_warn("Scene parse: invalid line %d", lineNumber); + continue; + } + + std::string key = Trim(line.substr(0, eq)); + std::string value = Trim(line.substr(eq + 1)); + + if (key == "scene_script") { + std::string sceneScript; + if (ParseString(value, sceneScript)) { + SetSceneScriptPath(sceneScript); + } + } else { + log_warn("Scene parse: key/value outside entity section at line %d", lineNumber); + } continue; } @@ -308,6 +368,14 @@ entt::entity LoadSceneFromFile(const std::string& path, bool clearExisting) { current->shaderId = static_cast(std::stoul(value, nullptr, 0)); current->hasShaderId = true; current->hasSprite = true; + } else if (key == "script") { + if (ParseString(value, current->scriptPath)) { + current->hasScript = true; + } + } else if (key == "script_enabled") { + if (ParseBool(value, current->scriptEnabled)) { + current->hasScript = true; + } } else { log_warn("Scene parse: unknown key '%s' at line %d", key.c_str(), lineNumber); } @@ -377,6 +445,13 @@ entt::entity LoadSceneFromFile(const std::string& path, bool clearExisting) { } g_sceneRegistry->emplace(entity, static_cast(modelId), col, def.outlineSize, shaderId); } + + if (def.hasScript || !def.scriptPath.empty()) { + auto& script = g_sceneRegistry->emplace(entity); + script.scriptPath = def.scriptPath; + script.enabled = def.scriptEnabled; + script.started = false; + } } for (size_t i = 0; i < defs.size(); ++i) { @@ -412,6 +487,10 @@ bool SaveSceneToFile(const std::string& path) { return false; } + if (!g_sceneScriptPath.empty()) { + file << "scene_script = \"" << EscapeString(g_sceneScriptPath) << "\"\n\n"; + } + auto& registry = *g_sceneRegistry; std::vector entities; auto& storage = registry.storage(); @@ -533,6 +612,15 @@ bool SaveSceneToFile(const std::string& path) { } } + if (auto* script = registry.try_get(entity)) { + if (!script->scriptPath.empty()) { + file << "script = \"" << EscapeString(script->scriptPath) << "\"\n"; + } + if (!script->enabled) { + file << "script_enabled = false\n"; + } + } + file << "\n"; } diff --git a/src/gui/GuiManager.cpp b/src/gui/GuiManager.cpp index 7d99b92..1e30512 100644 --- a/src/gui/GuiManager.cpp +++ b/src/gui/GuiManager.cpp @@ -328,6 +328,19 @@ void GuiManager::SyncTagBuffer(entt::entity entity) } } +void GuiManager::SyncScriptPathBuffer(entt::entity entity) +{ + if (!app) return; + if (entity == scriptPathEntity) return; + + scriptPathEntity = entity; + scriptPathBuffer[0] = '\0'; + auto& registry = app->GetRegistry(); + if (auto* script = registry.try_get(entity)) { + std::snprintf(scriptPathBuffer, sizeof(scriptPathBuffer), "%s", script->scriptPath.c_str()); + } +} + void GuiManager::RenderEntityNode(entt::entity entity) { if (!app) return; @@ -395,6 +408,24 @@ void GuiManager::RenderSceneWindow() auto& registry = app->GetRegistry(); + const std::string& sceneScriptPath = GetSceneScriptPath(); + if (sceneScriptPath != lastSceneScriptPath) { + std::snprintf(sceneScriptPathBuffer, sizeof(sceneScriptPathBuffer), "%s", sceneScriptPath.c_str()); + lastSceneScriptPath = sceneScriptPath; + } + + ImGui::Text("Scene Script"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(240.0f); + ImGui::InputText("##SceneScriptPath", sceneScriptPathBuffer, sizeof(sceneScriptPathBuffer)); + ImGui::SameLine(); + if (ImGui::Button("Apply")) { + SetSceneScriptPath(sceneScriptPathBuffer); + lastSceneScriptPath = sceneScriptPathBuffer; + } + + ImGui::Separator(); + if (ImGui::Button("Create Entity")) { entt::entity entity = registry.create(); std::string name = "Entity " + std::to_string(static_cast(ToId(entity))); @@ -492,10 +523,11 @@ void GuiManager::RenderInspectorWindow() {"Transform", 1}, {"Velocity", 2}, {"Sprite", 3}, - {"Camera", 4}, - {"Light", 5}, - {"Material", 6}, - {"Render Pass", 7} + {"Script", 4}, + {"Camera", 5}, + {"Light", 6}, + {"Material", 7}, + {"Render Pass", 8} }; auto hasComponent = [&](int id) { @@ -504,10 +536,11 @@ void GuiManager::RenderInspectorWindow() case 1: return registry.any_of(selectedEntity); case 2: return registry.any_of(selectedEntity); case 3: return registry.any_of(selectedEntity); - case 4: return registry.any_of(selectedEntity); - case 5: return registry.any_of(selectedEntity); - case 6: return registry.any_of(selectedEntity); - case 7: return registry.any_of(selectedEntity); + case 4: return registry.any_of(selectedEntity); + case 5: return registry.any_of(selectedEntity); + case 6: return registry.any_of(selectedEntity); + case 7: return registry.any_of(selectedEntity); + case 8: return registry.any_of(selectedEntity); default: return false; } }; @@ -560,15 +593,19 @@ void GuiManager::RenderInspectorWindow() registry.emplace(selectedEntity, 0, WHITE, 0.0f, 0); break; case 4: - registry.emplace(selectedEntity); + registry.emplace(selectedEntity); + SyncScriptPathBuffer(selectedEntity); break; case 5: - registry.emplace(selectedEntity); + registry.emplace(selectedEntity); break; case 6: - registry.emplace(selectedEntity); + registry.emplace(selectedEntity); break; case 7: + registry.emplace(selectedEntity); + break; + case 8: registry.emplace(selectedEntity); break; default: @@ -705,6 +742,34 @@ void GuiManager::RenderInspectorWindow() } } + // Script + if (registry.any_of(selectedEntity)) { + auto& script = registry.get(selectedEntity); + ImGui::Separator(); + ImGui::Text("Script"); + ImGui::PushID("ScriptComponent"); + + SyncScriptPathBuffer(selectedEntity); + if (ImGui::InputText("Path", scriptPathBuffer, sizeof(scriptPathBuffer))) { + script.scriptPath = scriptPathBuffer; + script.started = false; + } + + if (ImGui::Checkbox("Enabled", &script.enabled)) { + if (!script.enabled) { + script.started = false; + } + } + + if (ImGui::Button("Remove Script")) { + registry.remove(selectedEntity); + scriptPathEntity = entt::null; + scriptPathBuffer[0] = '\0'; + } + + ImGui::PopID(); + } + // Camera if (registry.any_of(selectedEntity)) { ImGui::PushID("Camera"); diff --git a/src/scripting/ScriptEngine.cpp b/src/scripting/ScriptEngine.cpp index f44a8be..68a29aa 100644 --- a/src/scripting/ScriptEngine.cpp +++ b/src/scripting/ScriptEngine.cpp @@ -165,15 +165,62 @@ bool ScriptEngine::CompileScript(const std::string& filename) { // Get the newly created module currentModule = engine->GetModule("main"); - // Cache new functions - initFunc = currentModule->GetFunctionByName("Init"); - updateFunc = currentModule->GetFunctionByName("Update"); - shutdownFunc = currentModule->GetFunctionByName("Shutdown"); + // Cache new functions (scene-specific names preferred) + initFunc = currentModule->GetFunctionByDecl("void SceneInit()"); + if (!initFunc) { + initFunc = currentModule->GetFunctionByDecl("void Init()"); + } + + updateFunc = currentModule->GetFunctionByDecl("void SceneUpdate(float)"); + if (!updateFunc) { + updateFunc = currentModule->GetFunctionByDecl("void Update(float)"); + } + + shutdownFunc = currentModule->GetFunctionByDecl("void SceneShutdown()"); + if (!shutdownFunc) { + shutdownFunc = currentModule->GetFunctionByDecl("void Shutdown()"); + } hasValidScript = true; log_info("Script compiled and cached: %s", filename.c_str()); return true; } +bool ScriptEngine::CompileModule(const std::string& moduleName, const std::string& filename, asIScriptModule** outModule) { + if (!engine) return false; + + if (engine->GetModule(moduleName.c_str())) { + engine->DiscardModule(moduleName.c_str()); + } + + CScriptBuilder builder; + builder.SetIncludeCallback(IncludeCallback, nullptr); + + int r = builder.StartNewModule(engine, moduleName.c_str()); + if (r < 0) { + log_error("Failed to start module: %s", moduleName.c_str()); + return false; + } + + r = builder.AddSectionFromFile(filename.c_str()); + if (r < 0) { + log_error("Failed to add script section from file: %s", filename.c_str()); + engine->DiscardModule(moduleName.c_str()); + return false; + } + + r = builder.BuildModule(); + if (r < 0) { + log_error("Failed to build module: %s", moduleName.c_str()); + engine->DiscardModule(moduleName.c_str()); + return false; + } + + if (outModule) { + *outModule = engine->GetModule(moduleName.c_str()); + } + return true; +} + void ScriptEngine::CallScriptFunction(asIScriptFunction* func, float dt) { if (!func || !engine || !hasValidScript) return; @@ -200,6 +247,36 @@ void ScriptEngine::CallScriptFunction(asIScriptFunction* func, float dt) { ctx->Release(); } +void ScriptEngine::CallScriptFunction(asIScriptFunction* func, unsigned int entityId, float dt) { + if (!func || !engine) return; + + asIScriptContext* ctx = engine->CreateContext(); + if (!ctx) return; + + int r = ctx->Prepare(func); + if (r < 0) { + ctx->Release(); + return; + } + + const asUINT paramCount = func->GetParamCount(); + if (paramCount >= 1) { + ctx->SetArgDWord(0, entityId); + } + if (paramCount >= 2) { + ctx->SetArgFloat(1, dt); + } + + r = ctx->Execute(); + if (r != asEXECUTION_FINISHED) { + if (r == asEXECUTION_EXCEPTION) { + log_error("Script exception: %s", ctx->GetExceptionString()); + } + } + + ctx->Release(); +} + void ScriptEngine::ClearCachedFunctions() { updateFunc = nullptr; hasValidScript = false;