Scene Script added. Hide verbose error message.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<PlayerControllerSystem>(
|
||||
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<EventHandlerComponent>();
|
||||
|
||||
// Register Scene Script component
|
||||
m_world.component<SceneScriptComponent>();
|
||||
|
||||
// Register GOAP Planner component
|
||||
m_world.component<GoapPlannerComponent>();
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef EDITSCENE_SCENE_SCRIPT_HPP
|
||||
#define EDITSCENE_SCENE_SCRIPT_HPP
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -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<SceneScriptComponent>(
|
||||
"Scene Script", "Game",
|
||||
std::make_unique<SceneScriptEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<SceneScriptComponent>()) {
|
||||
SceneScriptComponent script;
|
||||
script.inlineScript = kSceneScriptTemplate;
|
||||
e.set<SceneScriptComponent>(script);
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<SceneScriptComponent>())
|
||||
e.remove<SceneScriptComponent>();
|
||||
});
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
#include "BuoyancySystem.hpp"
|
||||
#include "NavMeshSystem.hpp"
|
||||
#include "NormalDebugSystem.hpp"
|
||||
#include "SceneScriptSystem.hpp"
|
||||
#include <imgui.h>
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
#include "SceneScriptSystem.hpp"
|
||||
#include "EventBus.hpp"
|
||||
#include "../components/SceneScript.hpp"
|
||||
#include "../components/EventParams.hpp"
|
||||
#include <lua.hpp>
|
||||
#include <OgreLogManager.h>
|
||||
#include <OgreResourceGroupManager.h>
|
||||
#include <OgreDataStream.h>
|
||||
#include <OgreException.h>
|
||||
#include <functional>
|
||||
#include <unordered_set>
|
||||
|
||||
/* 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:<hash>"). */
|
||||
static std::unordered_set<std::string> 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<SceneScriptComponent>().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<std::string>()(
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#ifndef EDITSCENE_SCENE_SCRIPT_SYSTEM_HPP
|
||||
#define EDITSCENE_SCENE_SCRIPT_SYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
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
|
||||
@@ -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 <random>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
@@ -405,6 +407,9 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
|
||||
if (entity.has<EventHandlerComponent>()) {
|
||||
json["eventHandler"] = serializeEventHandler(entity);
|
||||
}
|
||||
if (entity.has<SceneScriptComponent>()) {
|
||||
json["sceneScript"] = serializeSceneScript(entity);
|
||||
}
|
||||
if (entity.has<GoapPlannerComponent>()) {
|
||||
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<EventHandlerComponent>(handler);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeSceneScript(flecs::entity entity)
|
||||
{
|
||||
const SceneScriptComponent &script = entity.get<SceneScriptComponent>();
|
||||
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<SceneScriptComponent>(script);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeItem(flecs::entity entity)
|
||||
{
|
||||
const ItemComponent &item = entity.get<ItemComponent>();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "SceneScriptEditor.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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<SceneScriptComponent> {
|
||||
public:
|
||||
const char *getName() const override
|
||||
{
|
||||
return "Scene Script";
|
||||
}
|
||||
|
||||
protected:
|
||||
bool renderComponent(flecs::entity entity,
|
||||
SceneScriptComponent &script) override;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_SCENE_SCRIPT_EDITOR_HPP
|
||||
Reference in New Issue
Block a user