#pragma once #include "StarPeriodic.hpp" #include "StarLogging.hpp" #include "StarListener.hpp" #include "StarWorld.hpp" #include "StarWorldLuaBindings.hpp" namespace Star { STAR_EXCEPTION(LuaComponentException, LuaException); // Basic lua component that can be initialized (takes and then owns a script // context, calls the script context's init function) and uninitialized // (releases the context, calls the context 'uninit' function). // // Callbacks can be added and removed whether or not the context is initialized // or not, they will be added back during a call to init. 'root' callbacks are // available by default as well as an ephemeral 'self' table. // // All script function calls (init / uninit / invoke) guard against missing // functions. If the function is missing, it will do nothing and return // nothing. If the function exists but throws an error, the error will be // logged and the component will go into the error state. // // Whenever an error is set, all function calls or eval will fail until the // error is cleared by re-initializing. // // If 'autoReInit' is set, Monitors Root for reloads, and if a root reload // occurs, will automatically (on the next call to invoke) uninit and then // re-init the script before calling invoke. 'autoReInit' defaults to true. class LuaBaseComponent { public: LuaBaseComponent(); // The LuaBaseComponent destructor does NOT call the 'unint' entry point in // the script. In order to do so, uninit() must be called manually before // destruction. This is because during destruction, it is highly likely that // callbacks may not be valid, and highly likely that exceptions could be // thrown. virtual ~LuaBaseComponent(); LuaBaseComponent(LuaBaseComponent const& component) = delete; LuaBaseComponent& operator=(LuaBaseComponent const& component) = delete; StringList const& scripts() const; void setScript(String script); void setScripts(StringList scripts); void addCallbacks(String groupName, LuaCallbacks callbacks); bool removeCallbacks(String const& groupName); // If true, component will automatically uninit and re-init when root is // reloaded. bool autoReInit() const; void setAutoReInit(bool autoReInit); // Lua components require access to a LuaRoot object to initialize / // uninitialize. void setLuaRoot(LuaRootPtr luaRoot); LuaRootPtr const& luaRoot(); // init returns true on success, false if there has been an error // initializing the script. LuaRoot must be set before calling or this will // always fail. Calls the 'init' entry point on the script context. bool init(); // uninit will uninitialize the LuaComponent if it is currently initialized. // This calls the 'uninit' entry point on the script context before // destroying the context. void uninit(); bool initialized() const; template Maybe invoke(String const& name, V&&... args); template Maybe eval(String const& code); // Returns last error, if there has been an error. Errors can only be // cleared by re-initializing the context. Maybe const& error() const; Maybe const& context() const; Maybe& context(); protected: virtual void contextSetup(); virtual void contextShutdown(); void setError(String error); // Checks the initialization state of the script, while also reloading the // script and clearing the error state if a root reload has occurred. bool checkInitialization(); private: StringList m_scripts; StringMap m_callbacks; LuaRootPtr m_luaRoot; TrackerListenerPtr m_reloadTracker; Maybe m_context; Maybe m_error; }; // Wraps a basic lua component to add a persistent storage table translated // into JSON that can be stored outside of the script context. template class LuaStorableComponent : public Base { public: JsonObject getScriptStorage() const; void setScriptStorage(JsonObject storage); protected: virtual void contextSetup() override; virtual void contextShutdown() override; private: JsonObject m_storage; }; // Wraps a basic lua component with an 'update' method and an embedded tick // rate. Every call to 'update' here will only call the internal script // 'update' at the configured delta. Adds a update tick controls under the // 'script' callback table. template class LuaUpdatableComponent : public Base { public: LuaUpdatableComponent(); unsigned updateDelta() const; float updateDt(float dt) const; float updateDt() const; void setUpdateDelta(unsigned updateDelta); // Returns true if the next update will call the internal script update // method. bool updateReady() const; template Maybe update(V&&... args); private: Periodic m_updatePeriodic; mutable float m_lastDt; }; // Wraps a basic lua component so that world callbacks are added on init, and // removed on uninit, and sets the world LuaRoot as the LuaBaseComponent // LuaRoot automatically. template class LuaWorldComponent : public Base { public: void init(World* world); void uninit(); protected: using Base::setLuaRoot; using Base::init; }; // Component for scripts which can be used as entity message handlers, provides // a 'message' table with 'setHandler' callback to set message handlers. template class LuaMessageHandlingComponent : public Base { public: LuaMessageHandlingComponent(); Maybe handleMessage(String const& message, bool localMessage, JsonArray const& args = {}); protected: virtual void contextShutdown() override; private: struct MessageHandler { Maybe function; String name; bool passName = true; bool localOnly = false; }; StringMap m_messageHandlers; }; template Maybe LuaBaseComponent::invoke(String const& name, V&&... args) { if (!checkInitialization()) return {}; try { auto method = m_context->getPath(name); if (method == LuaNil) return {}; return m_context->luaTo(std::move(method)).invoke(std::forward(args)...); } catch (LuaException const& e) { Logger::error("Exception while invoking lua function '{}'. {}", name, outputException(e, true)); setError(printException(e, false)); return {}; } } template Maybe LuaBaseComponent::eval(String const& code) { if (!checkInitialization()) return {}; try { return m_context->eval(code); } catch (LuaException const& e) { Logger::error("Exception while evaluating lua in context: {}", outputException(e, true)); return {}; } } template JsonObject LuaStorableComponent::getScriptStorage() const { if (Base::initialized()) return Base::context()->template getPath("storage"); else return m_storage; } template void LuaStorableComponent::setScriptStorage(JsonObject storage) { if (Base::initialized()) Base::context()->setPath("storage", std::move(storage)); else m_storage = std::move(storage); } template void LuaStorableComponent::contextSetup() { Base::contextSetup(); Base::context()->setPath("storage", std::move(m_storage)); } template void LuaStorableComponent::contextShutdown() { m_storage = Base::context()->template getPath("storage"); Base::contextShutdown(); } template LuaUpdatableComponent::LuaUpdatableComponent() { m_updatePeriodic.setStepCount(1); LuaCallbacks scriptCallbacks; scriptCallbacks.registerCallback("updateDt", [this]() { return updateDt(); }); scriptCallbacks.registerCallback("setUpdateDelta", [this](unsigned d) { setUpdateDelta(d); }); m_lastDt = GlobalTimestep * GlobalTimescale; Base::addCallbacks("script", std::move(scriptCallbacks)); } template unsigned LuaUpdatableComponent::updateDelta() const { return m_updatePeriodic.stepCount(); } template float LuaUpdatableComponent::updateDt(float dt) const { m_lastDt = dt; return m_updatePeriodic.stepCount() * dt; } template float LuaUpdatableComponent::updateDt() const { return m_updatePeriodic.stepCount() * m_lastDt; } template void LuaUpdatableComponent::setUpdateDelta(unsigned updateDelta) { m_updatePeriodic.setStepCount(updateDelta); } template bool LuaUpdatableComponent::updateReady() const { return m_updatePeriodic.ready(); } template template Maybe LuaUpdatableComponent::update(V&&... args) { if (!m_updatePeriodic.tick()) return {}; return Base::template invoke("update", std::forward(args)...); } template void LuaWorldComponent::init(World* world) { if (Base::initialized()) uninit(); Base::setLuaRoot(world->luaRoot()); Base::addCallbacks("world", LuaBindings::makeWorldCallbacks(world)); Base::init(); } template void LuaWorldComponent::uninit() { Base::uninit(); Base::removeCallbacks("world"); } template LuaMessageHandlingComponent::LuaMessageHandlingComponent() { LuaCallbacks scriptCallbacks; scriptCallbacks.registerCallback("setHandler", [this](Variant message, Maybe handler) { MessageHandler handlerInfo = {}; if (Json* config = message.ptr()) { handlerInfo.passName = config->getBool("passName", false); handlerInfo.localOnly = config->getBool("localOnly", false); handlerInfo.name = config->getString("name"); } else { handlerInfo.passName = true; handlerInfo.localOnly = false; handlerInfo.name = message.get(); } if (handler) { handlerInfo.function.emplace(handler.take()); m_messageHandlers.set(handlerInfo.name, handlerInfo); } else m_messageHandlers.remove(handlerInfo.name); }); Base::addCallbacks("message", std::move(scriptCallbacks)); } template Maybe LuaMessageHandlingComponent::handleMessage( String const& message, bool localMessage, JsonArray const& args) { if (!Base::initialized()) return {}; if (auto handler = m_messageHandlers.ptr(message)) { try { if (handler->localOnly) { if (!localMessage) return {}; else if (handler->passName) return handler->function->template invoke(message, luaUnpack(args)); else return handler->function->template invoke(luaUnpack(args)); } else if (handler->passName) return handler->function->template invoke(message, localMessage, luaUnpack(args)); else return handler->function->template invoke(localMessage, luaUnpack(args)); } catch (LuaException const& e) { Logger::error( "Exception while invoking lua message handler for message '{}'. {}", message, outputException(e, true)); Base::setError(String(printException(e, false))); } } return {}; } template void LuaMessageHandlingComponent::contextShutdown() { m_messageHandlers.clear(); Base::contextShutdown(); } }