From 4670aa7af9cd15c3768290c38d34658976b3a007 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Fri, 4 Sep 2026 23:45:08 +0300 Subject: [PATCH] Scene Script added. Hide verbose error message. --- src/features/editScene/AGENTS.md | 39 ++++++ src/features/editScene/CMakeLists.txt | 6 + src/features/editScene/EditorApp.cpp | 28 +++++ .../editScene/components/SceneScript.hpp | 28 +++++ .../components/SceneScriptModule.cpp | 52 ++++++++ .../lua-examples/scene_script_example.lua | 84 +++++++++++++ .../editScene/lua/LuaComponentApi.cpp | 15 +++ .../editScene/systems/BuoyancySystem.cpp | 7 +- .../editScene/systems/BuoyancySystem.hpp | 3 + .../editScene/systems/EditorUISystem.cpp | 3 + .../editScene/systems/SceneScriptSystem.cpp | 116 ++++++++++++++++++ .../editScene/systems/SceneScriptSystem.hpp | 46 +++++++ .../editScene/systems/SceneSerializer.cpp | 36 ++++++ .../editScene/systems/SceneSerializer.hpp | 3 + .../editScene/tests/component_lua_test.cpp | 27 ++++ .../editScene/ui/SceneScriptEditor.cpp | 32 +++++ .../editScene/ui/SceneScriptEditor.hpp | 20 +++ 17 files changed, 543 insertions(+), 2 deletions(-) create mode 100644 src/features/editScene/components/SceneScript.hpp create mode 100644 src/features/editScene/components/SceneScriptModule.cpp create mode 100644 src/features/editScene/lua-examples/scene_script_example.lua create mode 100644 src/features/editScene/systems/SceneScriptSystem.cpp create mode 100644 src/features/editScene/systems/SceneScriptSystem.hpp create mode 100644 src/features/editScene/ui/SceneScriptEditor.cpp create mode 100644 src/features/editScene/ui/SceneScriptEditor.hpp diff --git a/src/features/editScene/AGENTS.md b/src/features/editScene/AGENTS.md index b97ff00..2155a16 100644 --- a/src/features/editScene/AGENTS.md +++ b/src/features/editScene/AGENTS.md @@ -342,6 +342,45 @@ uses `EditorApp::getPlayerCharacterEntity()` to know which character performs th action. Actions run through `BehaviorTreeSystem` and lock player input while running (`PlayerControllerComponent::inputLocked`). +### SceneScriptComponent & SceneScriptSystem + +Attaches a Lua script to a scene or prefab entity. Fields: + +- `scriptPath` – Lua file resolved from the `LuaScripts` resource group; + wins over `inlineScript` when set. +- `inlineScript` – Lua source stored directly on the component. The editor + adder prefills it with a commented template subscribing to the load events. + +`SceneScriptSystem` is a static helper (no per-frame update): + +- `init(lua_State *)` stores the shared `m_lua` state; called once from + `EditorApp::setup()` after the Lua APIs are registered. +- `loadPendingScripts(world)` executes every not-yet-executed + `SceneScriptComponent` exactly once in that shared state. Dedupe keys: the + file path for `scriptPath`, `"inline:" + std::hash(source)` for inline + scripts. Scripts are never unloaded; a script that fails (open error, load + error or runtime error) is logged via `Ogre::LogManager` and NOT marked + executed, so the next load retries it. +- `sendSceneLoaded(scenePath)` sends `"scene_loaded"` with param `scenePath`; + `sendPrefabLoaded(prefabPath, entityId)` sends `"prefab_loaded"` with + params `prefabPath` and `entityId` (both via `EventBus` + + `editScene::EventParams`). + +Wiring call sites (each calls `loadPendingScripts` then sends the event): + +- `EditorApp::setup()` – after Lua init, for the `startup_menu.json` loaded + earlier in game mode (the menu scene loads before the Lua state exists, so + its scripts run here, just before `game_start` is sent; their subscriptions + therefore see `scene_loaded` but miss the menu's `prefab_loaded` events). +- `EditorApp::startNewGame()` – after `resolveInstances()`. +- `EditorApp::loadGame()` – after `resolveInstances()`. +- `EditorUISystem::loadScene()` – editor scene loads. +- `PrefabSystem::instantiatePrefab()` (in `SceneSerializer.cpp`) – after a + prefab instance is created, sends `prefab_loaded`. + +Lua binding: `ecs.get_component`/`set_component` with `"SceneScript"` +(`scriptPath`, `inlineScript`). Example: `lua-examples/scene_script_example.lua`. + ### Save/Load Game-mode saves are JSON files in the OS user-data directory (see diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index 3f900d7..ca9139a 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -82,6 +82,9 @@ set(EDITSCENE_SOURCES systems/EventHandlerSystem.cpp ui/EventHandlerEditor.cpp components/EventHandlerModule.cpp + systems/SceneScriptSystem.cpp + ui/SceneScriptEditor.cpp + components/SceneScriptModule.cpp systems/PrefabSystem.cpp ui/PrefabInstanceEditor.cpp @@ -285,6 +288,9 @@ set(EDITSCENE_HEADERS components/EventHandler.hpp systems/EventHandlerSystem.hpp ui/EventHandlerEditor.hpp + components/SceneScript.hpp + systems/SceneScriptSystem.hpp + ui/SceneScriptEditor.hpp components/PrefabInstance.hpp components/TerrainPrefabSpawner.hpp ui/PrefabInstanceEditor.hpp diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 2f959ee..f4e820d 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -102,8 +102,10 @@ #include "systems/ActuatorSystem.hpp" #include "systems/EventHandlerSystem.hpp" #include "systems/EventBus.hpp" +#include "systems/SceneScriptSystem.hpp" #include "systems/ItemSystem.hpp" #include "components/EventHandler.hpp" +#include "components/SceneScript.hpp" #include "components/Item.hpp" #include "components/Inventory.hpp" @@ -730,6 +732,10 @@ void EditorApp::setup() std::make_unique( m_world, m_sceneMgr, this); + /* Set when startup_menu.json loaded successfully; its scene + * scripts run after Lua init below. */ + bool startupMenuLoaded = false; + if (m_gameMode == GameMode::Game) { // Load startup menu scene configured in editor. // This must happen before show() so the @@ -741,6 +747,7 @@ void EditorApp::setup() m_uiSystem.get())) { PrefabSystem prefabSys(m_world, m_sceneMgr); prefabSys.resolveInstances(); + startupMenuLoaded = true; Ogre::LogManager::getSingleton().logMessage( "Game mode: startup_menu.json loaded"); } else { @@ -812,6 +819,9 @@ void EditorApp::setup() editScene::registerLuaItemApi(L); editScene::registerLuaTerrainApi(L); + // Scene scripts execute in this shared Lua state. + SceneScriptSystem::init(L); + // Run late setup: load data.lua and initial scripts. m_lua.lateSetup(); @@ -828,6 +838,17 @@ void EditorApp::setup() } if (m_gameMode == GameMode::Game) { + // Run pending scene scripts (from startup_menu.json) so + // their event subscriptions are active before + // scene_loaded and game_start are sent. The startup + // menu scene loads before the Lua state is initialized, + // so this happens here instead of at load time. + if (startupMenuLoaded) { + SceneScriptSystem::loadPendingScripts(m_world); + SceneScriptSystem::sendSceneLoaded( + "startup_menu.json"); + } + // Queue "game_start" event after Lua scripts are loaded // so Lua subscribers registered via ecs.subscribe_event() // will receive the event. @@ -951,6 +972,8 @@ void EditorApp::startNewGame(const Ogre::String &scenePath) if (serializer.loadFromFile(scenePath, m_uiSystem.get())) { PrefabSystem prefabSys(m_world, m_sceneMgr); prefabSys.resolveInstances(); + SceneScriptSystem::loadPendingScripts(m_world); + SceneScriptSystem::sendSceneLoaded(scenePath); setGamePlayState(GamePlayState::Playing); m_currentBaseScene = scenePath; m_playTime = 0.0f; @@ -1169,6 +1192,8 @@ void EditorApp::loadGame(const std::string &slotPath) } PrefabSystem prefabSys(m_world, m_sceneMgr); prefabSys.resolveInstances(); + SceneScriptSystem::loadPendingScripts(m_world); + SceneScriptSystem::sendSceneLoaded(baseScene); /* Restore registries */ auto ®istry = CharacterRegistry::getSingleton(); @@ -1461,6 +1486,9 @@ void EditorApp::setupECS() // Register Event Handler component m_world.component(); + // Register Scene Script component + m_world.component(); + // Register GOAP Planner component m_world.component(); diff --git a/src/features/editScene/components/SceneScript.hpp b/src/features/editScene/components/SceneScript.hpp new file mode 100644 index 0000000..9fbfb66 --- /dev/null +++ b/src/features/editScene/components/SceneScript.hpp @@ -0,0 +1,28 @@ +#ifndef EDITSCENE_SCENE_SCRIPT_HPP +#define EDITSCENE_SCENE_SCRIPT_HPP +#pragma once + +#include + +/** + * Scene/prefab Lua script component. + * + * References a Lua script that is executed exactly once in the shared + * lua_State owned by EditorApp when the scene or prefab containing this + * entity is loaded. Scripts are never unloaded; their usual purpose is + * to subscribe event handlers (ecs.subscribe_event) specific to the + * scene or prefab. + * + * If scriptPath is non-empty it wins and the file is resolved in the + * "LuaScripts" OGRE resource group (same resolution as + * LuaState::luaLibraryLoader). Otherwise inlineScript is executed. + * Execution is deduplicated: by path for file scripts, by content hash + * for inline scripts. On a Lua error the script is NOT marked executed, + * so a later load can retry. + */ +struct SceneScriptComponent { + std::string scriptPath; + std::string inlineScript; +}; + +#endif // EDITSCENE_SCENE_SCRIPT_HPP diff --git a/src/features/editScene/components/SceneScriptModule.cpp b/src/features/editScene/components/SceneScriptModule.cpp new file mode 100644 index 0000000..e4031b7 --- /dev/null +++ b/src/features/editScene/components/SceneScriptModule.cpp @@ -0,0 +1,52 @@ +#include "SceneScript.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/SceneScriptEditor.hpp" + +/** + * Template prefilled into inlineScript when the component is added in + * the editor. Shows the most common event handlers; everything is + * commented out so a fresh component executes as a no-op. + */ +static const char kSceneScriptTemplate[] = + "-- Scene script: executed once when this scene or prefab is loaded.\n" + "-- Lives in the shared Lua environment and is never unloaded.\n" + "-- Its purpose is to subscribe event handlers specific to this\n" + "-- scene/prefab. Uncomment the handlers you need:\n" + "\n" + "-- ecs.subscribe_event(\"scene_loaded\", function(event, params)\n" + "-- -- params.scenePath identifies the loaded scene\n" + "-- end)\n" + "\n" + "-- ecs.subscribe_event(\"prefab_loaded\", function(event, params)\n" + "-- -- params.prefabPath and params.entityId identify the instance\n" + "-- end)\n" + "\n" + "-- ecs.subscribe_event(\"game_start\", function(event, params) end)\n" + "-- ecs.subscribe_event(\"game_saved\", function(event, params) end)\n" + "-- ecs.subscribe_event(\"game_loaded\", function(event, params) end)\n" + "\n" + "-- Custom events:\n" + "-- local id = ecs.subscribe_event(\"my_event\", function(event, params) end)\n" + "-- ecs.unsubscribe_event(id)\n" + "-- ecs.send_event(\"my_event\", { key = \"value\" })\n"; + +REGISTER_COMPONENT_GROUP("Scene Script", "Game", SceneScriptComponent, + SceneScriptEditor) +{ + registry.registerComponent( + "Scene Script", "Game", + std::make_unique(), + // Adder + [](flecs::entity e) { + if (!e.has()) { + SceneScriptComponent script; + script.inlineScript = kSceneScriptTemplate; + e.set(script); + } + }, + // Remover + [](flecs::entity e) { + if (e.has()) + e.remove(); + }); +} diff --git a/src/features/editScene/lua-examples/scene_script_example.lua b/src/features/editScene/lua-examples/scene_script_example.lua new file mode 100644 index 0000000..3dcd395 --- /dev/null +++ b/src/features/editScene/lua-examples/scene_script_example.lua @@ -0,0 +1,84 @@ +-- ============================================================================= +-- Scene Script Component Examples +-- ============================================================================= +-- This file shows what a script attached to a SceneScript component typically +-- looks like. Put code like this either into the component's "Inline Script" +-- field or into a .lua file referenced by "Script Path" (resolved from the +-- "LuaScripts" resource group; the path wins when both fields are set). +-- +-- Semantics: +-- - The script executes exactly ONCE, in the shared Lua state, when the +-- owning scene or prefab finishes loading. +-- - File scripts are deduplicated by path, inline scripts by content hash, +-- so reloading a scene does not re-run them. +-- - A script that raises a Lua error is logged and will be retried on the +-- next load. +-- - Scripts are never unloaded; subscriptions made here stay active. +-- +-- Events sent by the engine (subscribe via ecs.subscribe_event): +-- "scene_loaded" - params.scenePath (string) +-- "prefab_loaded" - params.prefabPath (string), params.entityId (entity id) +-- Also useful: "game_start", "game_saved", "game_loaded", "scene_ready". +-- +-- Caveat: scripts in startup_menu.json execute right before "game_start" is +-- sent, so they can rely on "scene_loaded" but will miss the "prefab_loaded" +-- events of prefabs instantiated while the menu scene was loading. +-- ============================================================================= + +print("Scene script executed") + +-- ============================================================================= +-- Reacting to scene_loaded +-- ============================================================================= + +ecs.subscribe_event("scene_loaded", function(event, params) + local path = params and params.scenePath or "?" + print("scene_loaded: " .. path) + -- Per-scene setup can go here, e.g.: + -- if path:match("town%.json$") then ... end +end) + +-- ============================================================================= +-- Reacting to prefab_loaded +-- ============================================================================= + +ecs.subscribe_event("prefab_loaded", function(event, params) + if not params then + return + end + print("prefab_loaded: " .. tostring(params.prefabPath) .. + " entity=" .. tostring(params.entityId)) + -- The instantiated root entity is available as params.entityId, e.g.: + -- if ecs.has_component(params.entityId, "Transform") then + -- local t = ecs.get_component(params.entityId, "Transform") + -- print(" at " .. t.position[1] .. ", " .. t.position[3]) + -- end +end) + +-- ============================================================================= +-- Other useful game events +-- ============================================================================= + +ecs.subscribe_event("game_start", function(event, params) + print("game_start: the player pressed New Game on the startup menu") +end) + +ecs.subscribe_event("game_saved", function(event, params) + print("game_saved") +end) + +ecs.subscribe_event("game_loaded", function(event, params) + print("game_loaded") +end) + +-- ============================================================================= +-- Custom events still work the same way +-- ============================================================================= + +ecs.subscribe_event("my_custom_event", function(event, params) + print("my_custom_event received") +end) + +ecs.send_event("my_custom_event", { source = "scene_script_example" }) + +print("Scene script example completed") diff --git a/src/features/editScene/lua/LuaComponentApi.cpp b/src/features/editScene/lua/LuaComponentApi.cpp index c3a964b..2e19758 100644 --- a/src/features/editScene/lua/LuaComponentApi.cpp +++ b/src/features/editScene/lua/LuaComponentApi.cpp @@ -54,6 +54,7 @@ #include "components/GoapRunner.hpp" #include "components/PathFollowing.hpp" #include "components/EventHandler.hpp" +#include "components/SceneScript.hpp" #include "components/Item.hpp" #include "components/Inventory.hpp" #include "components/GeneratedPhysicsTag.hpp" @@ -759,6 +760,20 @@ static void registerAllComponents() c.enabled = lua_toboolean(L, -1) != 0; lua_pop(L, 1);); + // --- SceneScript --- + REGISTER_COMPONENT( + SceneScriptComponent, "SceneScript", + lua_pushstring(L, c.scriptPath.c_str()); + lua_setfield(L, -2, "scriptPath"); + lua_pushstring(L, c.inlineScript.c_str()); + lua_setfield(L, -2, "inlineScript"); + , if (lua_getfield(L, idx, "scriptPath"), lua_isstring(L, -1)) + c.scriptPath = lua_tostring(L, -1); + lua_pop(L, 1); + if (lua_getfield(L, idx, "inlineScript"), lua_isstring(L, -1)) + c.inlineScript = lua_tostring(L, -1); + lua_pop(L, 1);); + // --- PathFollowing --- REGISTER_COMPONENT( PathFollowingComponent, "PathFollowing", diff --git a/src/features/editScene/systems/BuoyancySystem.cpp b/src/features/editScene/systems/BuoyancySystem.cpp index fb9b2a1..ca75d39 100644 --- a/src/features/editScene/systems/BuoyancySystem.cpp +++ b/src/features/editScene/systems/BuoyancySystem.cpp @@ -26,8 +26,11 @@ void BuoyancySystem::update(float deltaTime) }); if (!waterPhysics) { - Ogre::LogManager::getSingleton().logMessage( - "BuoyancySystem: No WaterPhysics component found"); + if (m_debugEnabled && !m_loggedMissingWater) { + Ogre::LogManager::getSingleton().logMessage( + "BuoyancySystem: No WaterPhysics component found"); + m_loggedMissingWater = true; + } return; } diff --git a/src/features/editScene/systems/BuoyancySystem.hpp b/src/features/editScene/systems/BuoyancySystem.hpp index b56bdc7..3e27d26 100644 --- a/src/features/editScene/systems/BuoyancySystem.hpp +++ b/src/features/editScene/systems/BuoyancySystem.hpp @@ -83,6 +83,9 @@ private: // Debug mode bool m_debugEnabled = false; + // Set once the missing-WaterPhysics message has been logged + bool m_loggedMissingWater = false; + // Camera position for water detection area Ogre::Vector3 m_cameraPosition = Ogre::Vector3::ZERO; diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index 93ef14f..86ec537 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -58,6 +58,7 @@ #include "BuoyancySystem.hpp" #include "NavMeshSystem.hpp" #include "NormalDebugSystem.hpp" +#include "SceneScriptSystem.hpp" #include #include #include @@ -1348,6 +1349,8 @@ void EditorUISystem::loadScene(const std::string &filepath) if (m_serializer->loadFromFile(filepath, this)) { m_navigationPanel.setBookmarks(m_serializer->getBookmarks()); + SceneScriptSystem::loadPendingScripts(m_world); + SceneScriptSystem::sendSceneLoaded(filepath); Ogre::LogManager::getSingleton().logMessage( "Scene loaded from: " + filepath); } else { diff --git a/src/features/editScene/systems/SceneScriptSystem.cpp b/src/features/editScene/systems/SceneScriptSystem.cpp new file mode 100644 index 0000000..9139cf3 --- /dev/null +++ b/src/features/editScene/systems/SceneScriptSystem.cpp @@ -0,0 +1,116 @@ +#include "SceneScriptSystem.hpp" +#include "EventBus.hpp" +#include "../components/SceneScript.hpp" +#include "../components/EventParams.hpp" +#include +#include +#include +#include +#include +#include +#include + +/* Shared Lua state owned by EditorApp; set once via init(). */ +static lua_State *s_luaState = nullptr; + +/* Keys of scripts that executed successfully (path, or "inline:"). */ +static std::unordered_set s_executedScripts; + +void SceneScriptSystem::init(lua_State *L) +{ + s_luaState = L; +} + +bool SceneScriptSystem::isInitialized() +{ + return s_luaState != nullptr; +} + +static bool executeScriptSource(const std::string &source, + const std::string &chunkName) +{ + if (luaL_loadbufferx(s_luaState, source.c_str(), source.size(), + chunkName.c_str(), "t") != LUA_OK) { + Ogre::LogManager::getSingleton().stream() + << "SceneScriptSystem: failed to load " << chunkName + << ": " << lua_tostring(s_luaState, -1); + lua_pop(s_luaState, 1); + return false; + } + if (lua_pcall(s_luaState, 0, 0, 0) != LUA_OK) { + Ogre::LogManager::getSingleton().stream() + << "SceneScriptSystem: error running " << chunkName + << ": " << lua_tostring(s_luaState, -1); + lua_pop(s_luaState, 1); + return false; + } + return true; +} + +void SceneScriptSystem::loadPendingScripts(flecs::world &world) +{ + if (!s_luaState) + return; + + world.query().each( + [&](flecs::entity entity, SceneScriptComponent &script) { + std::string key, source, chunkName; + + if (!script.scriptPath.empty()) { + key = script.scriptPath; + if (s_executedScripts.count(key)) + return; + try { + Ogre::DataStreamPtr stream = + Ogre::ResourceGroupManager:: + getSingleton() + .openResource( + script.scriptPath, + "LuaScripts"); + source = stream->getAsString(); + } catch (const Ogre::Exception &e) { + Ogre::LogManager::getSingleton() + .stream() + << "SceneScriptSystem: cannot open " + << script.scriptPath << ": " + << e.getDescription(); + return; + } + chunkName = "@" + script.scriptPath; + } else { + if (script.inlineScript.empty()) + return; + key = "inline:" + std::to_string( + std::hash()( + script.inlineScript)); + if (s_executedScripts.count(key)) + return; + source = script.inlineScript; + chunkName = "@" + key; + } + + if (executeScriptSource(source, chunkName)) { + s_executedScripts.insert(key); + Ogre::LogManager::getSingleton().stream() + << "SceneScriptSystem: executed " + << chunkName << " (entity " + << entity.id() << ")"; + } + }); +} + +void SceneScriptSystem::sendSceneLoaded(const std::string &scenePath) +{ + editScene::EventParams params; + params.setString("scenePath", scenePath); + EventBus::getInstance().send("scene_loaded", params); +} + +void SceneScriptSystem::sendPrefabLoaded(const std::string &prefabPath, + uint64_t entityId) +{ + editScene::EventParams params; + params.setString("prefabPath", prefabPath); + params.setEntityId("entityId", entityId); + EventBus::getInstance().send("prefab_loaded", params); +} diff --git a/src/features/editScene/systems/SceneScriptSystem.hpp b/src/features/editScene/systems/SceneScriptSystem.hpp new file mode 100644 index 0000000..ea31fed --- /dev/null +++ b/src/features/editScene/systems/SceneScriptSystem.hpp @@ -0,0 +1,46 @@ +#ifndef EDITSCENE_SCENE_SCRIPT_SYSTEM_HPP +#define EDITSCENE_SCENE_SCRIPT_SYSTEM_HPP +#pragma once + +#include +#include +#include + +struct lua_State; + +/** + * Executes SceneScriptComponent scripts and sends the scene/prefab + * load events. + * + * init() stores the shared lua_State owned by EditorApp and must be + * called once after the Lua APIs are registered. + * loadPendingScripts() runs every SceneScriptComponent script that has + * not been executed yet (dedupe: by path for file scripts, by content + * hash for inline scripts) and is called from every scene/prefab load + * site. Scripts are never unloaded; a script that fails with a Lua + * error is logged and NOT marked executed so a later load can retry. + * + * Events sent via the global EventBus: + * "scene_loaded" - param scenePath (string) + * "prefab_loaded" - params prefabPath (string), entityId (entity id) + */ +class SceneScriptSystem { +public: + /** Store the shared lua_State. Called once from EditorApp::setup(). */ + static void init(lua_State *L); + + /** True after init() — scripts can only execute when initialized. */ + static bool isInitialized(); + + /** Execute all not-yet-executed SceneScriptComponent scripts. */ + static void loadPendingScripts(flecs::world &world); + + /** Send "scene_loaded" with named param scenePath. */ + static void sendSceneLoaded(const std::string &scenePath); + + /** Send "prefab_loaded" with named params prefabPath and entityId. */ + static void sendPrefabLoaded(const std::string &prefabPath, + uint64_t entityId); +}; + +#endif // EDITSCENE_SCENE_SCRIPT_SYSTEM_HPP diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 9afcff8..c968b57 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -40,6 +40,7 @@ #include "../components/Sun.hpp" #include "../components/Skybox.hpp" #include "../components/EventHandler.hpp" +#include "../components/SceneScript.hpp" #include "../components/ActionDatabase.hpp" #include "../components/ActionDebug.hpp" #include "../components/SmartObject.hpp" @@ -54,6 +55,7 @@ #include "../components/NavMesh.hpp" #include "../components/PrefabInstance.hpp" #include "EditorUISystem.hpp" +#include "SceneScriptSystem.hpp" #include #include #include @@ -405,6 +407,9 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) if (entity.has()) { json["eventHandler"] = serializeEventHandler(entity); } + if (entity.has()) { + json["sceneScript"] = serializeSceneScript(entity); + } if (entity.has()) { json["goapPlanner"] = serializeGoapPlanner(entity); } @@ -644,6 +649,9 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json, if (json.contains("eventHandler")) { deserializeEventHandler(entity, json["eventHandler"]); } + if (json.contains("sceneScript")) { + deserializeSceneScript(entity, json["sceneScript"]); + } if (json.contains("goapPlanner")) { deserializeGoapPlanner(entity, json["goapPlanner"]); } @@ -956,6 +964,9 @@ void SceneSerializer::deserializeEntityComponents( deserializeEventHandler(entity, json["eventHandler"]); } } + if (json.contains("sceneScript")) { + deserializeSceneScript(entity, json["sceneScript"]); + } if (json.contains("goapPlanner")) { deserializeGoapPlanner(entity, json["goapPlanner"]); } @@ -1121,6 +1132,13 @@ bool SceneSerializer::instantiatePrefab(flecs::entity instanceEntity, // Restore main scene entity map m_entityMap = savedMap; + /* Run any scene scripts contained in the prefab subtree + * (pending-only, so repeat instantiations are cheap), then + * notify listeners that this instance finished loading. */ + SceneScriptSystem::loadPendingScripts(m_world); + SceneScriptSystem::sendPrefabLoaded(filepath, + instanceEntity.id()); + return true; } catch (const std::exception &e) { m_lastError = @@ -4131,6 +4149,24 @@ void SceneSerializer::deserializeEventHandler(flecs::entity entity, entity.set(handler); } +nlohmann::json SceneSerializer::serializeSceneScript(flecs::entity entity) +{ + const SceneScriptComponent &script = entity.get(); + nlohmann::json json; + json["scriptPath"] = script.scriptPath; + json["inlineScript"] = script.inlineScript; + return json; +} + +void SceneSerializer::deserializeSceneScript(flecs::entity entity, + const nlohmann::json &json) +{ + SceneScriptComponent script; + script.scriptPath = json.value("scriptPath", ""); + script.inlineScript = json.value("inlineScript", ""); + entity.set(script); +} + nlohmann::json SceneSerializer::serializeItem(flecs::entity entity) { const ItemComponent &item = entity.get(); diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index 1ea3f5a..1b21d02 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -277,6 +277,7 @@ private: nlohmann::json serializeSmartObject(flecs::entity entity); nlohmann::json serializeActuator(flecs::entity entity); nlohmann::json serializeEventHandler(flecs::entity entity); + nlohmann::json serializeSceneScript(flecs::entity entity); nlohmann::json serializeGoapPlanner(flecs::entity entity); nlohmann::json serializeGoapRunner(flecs::entity entity); nlohmann::json serializeBehaviorTree(flecs::entity entity); @@ -291,6 +292,8 @@ private: const nlohmann::json &json); void deserializeEventHandler(flecs::entity entity, const nlohmann::json &json); + void deserializeSceneScript(flecs::entity entity, + const nlohmann::json &json); void deserializeGoapPlanner(flecs::entity entity, const nlohmann::json &json); void deserializeGoapRunner(flecs::entity entity, diff --git a/src/features/editScene/tests/component_lua_test.cpp b/src/features/editScene/tests/component_lua_test.cpp index f4fbe6d..8455ad4 100644 --- a/src/features/editScene/tests/component_lua_test.cpp +++ b/src/features/editScene/tests/component_lua_test.cpp @@ -965,6 +965,32 @@ static int testEventHandlerComponent(lua_State *L) return 0; } +// --------------------------------------------------------------------------- +// Test 29b: SceneScript component +// --------------------------------------------------------------------------- + +static int testSceneScriptComponent(lua_State *L) +{ + TEST("SceneScript component"); + + bool ok = runLua( + L, + "local id = ecs.create_entity();" + "ecs.set_component(id, 'SceneScript', {" + " scriptPath = 'scripts/my_scene.lua'," + " inlineScript = 'print(1)'" + "});" + "local s = ecs.get_component(id, 'SceneScript');" + "assert(s ~= nil, 'SceneScript should exist');" + "assert(s.scriptPath == 'scripts/my_scene.lua', 'wrong scriptPath');" + "assert(s.inlineScript == 'print(1)', 'wrong inlineScript')"); + if (!ok) + FAIL("SceneScript component assertion failed"); + + PASS(); + return 0; +} + // --------------------------------------------------------------------------- // Test 30: GoapBlackboard component // --------------------------------------------------------------------------- @@ -1917,6 +1943,7 @@ int main() failures += testSmartObjectComponent(L); failures += testActuatorComponent(L); failures += testEventHandlerComponent(L); + failures += testSceneScriptComponent(L); failures += testGoapBlackboardComponent(L); failures += testPrefabInstanceComponent(L); failures += testCellGridComponent(L); diff --git a/src/features/editScene/ui/SceneScriptEditor.cpp b/src/features/editScene/ui/SceneScriptEditor.cpp new file mode 100644 index 0000000..76fcb85 --- /dev/null +++ b/src/features/editScene/ui/SceneScriptEditor.cpp @@ -0,0 +1,32 @@ +#include "SceneScriptEditor.hpp" +#include + +bool SceneScriptEditor::renderComponent(flecs::entity entity, + SceneScriptComponent &script) +{ + bool modified = false; + ImGui::PushID("SceneScript"); + (void)entity; + + char pathBuf[512]; + snprintf(pathBuf, sizeof(pathBuf), "%s", script.scriptPath.c_str()); + if (ImGui::InputText("Script Path", pathBuf, sizeof(pathBuf))) { + script.scriptPath = pathBuf; + modified = true; + } + ImGui::TextDisabled( + "File in the LuaScripts resource group; overrides the inline script"); + + static char scriptBuf[16384]; + snprintf(scriptBuf, sizeof(scriptBuf), "%s", + script.inlineScript.c_str()); + if (ImGui::InputTextMultiline("Inline Script", scriptBuf, + sizeof(scriptBuf), + ImVec2(-1.0f, 240.0f))) { + script.inlineScript = scriptBuf; + modified = true; + } + + ImGui::PopID(); + return modified; +} diff --git a/src/features/editScene/ui/SceneScriptEditor.hpp b/src/features/editScene/ui/SceneScriptEditor.hpp new file mode 100644 index 0000000..d52c4e6 --- /dev/null +++ b/src/features/editScene/ui/SceneScriptEditor.hpp @@ -0,0 +1,20 @@ +#ifndef EDITSCENE_SCENE_SCRIPT_EDITOR_HPP +#define EDITSCENE_SCENE_SCRIPT_EDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/SceneScript.hpp" + +class SceneScriptEditor : public ComponentEditor { +public: + const char *getName() const override + { + return "Scene Script"; + } + +protected: + bool renderComponent(flecs::entity entity, + SceneScriptComponent &script) override; +}; + +#endif // EDITSCENE_SCENE_SCRIPT_EDITOR_HPP