1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
|
#ifndef COMPONENTS_LUA_SCRIPTSCONTAINER_H
#define COMPONENTS_LUA_SCRIPTSCONTAINER_H
#include <map>
#include <set>
#include <string>
#include <variant>
#include <components/debug/debuglog.hpp>
#include <components/esm/luascripts.hpp>
#include "luastate.hpp"
#include "serialization.hpp"
namespace LuaUtil
{
class ScriptTracker;
// ScriptsContainer is a base class for all scripts containers (LocalScripts,
// GlobalScripts, PlayerScripts, etc). Each script runs in a separate sandbox.
// Scripts from different containers can interact to each other only via events.
// Scripts within one container can interact via interfaces.
// All scripts from one container have the same set of API packages available.
//
// Each script should return a table in a specific format that describes its
// handlers and interfaces. Every section of the table is optional. Basic structure:
//
// local function update(dt)
// print("Update")
// end
//
// local function someEventHandler(eventData)
// print("'SomeEvent' received")
// end
//
// return {
// -- Provides interface for other scripts in the same container
// interfaceName = "InterfaceName",
// interface = {
// someFunction = function() print("someFunction was called from another script") end,
// },
//
// -- Script interface for the engine. Not available for other script.
// -- An error is printed if unknown handler is specified.
// engineHandlers = {
// onUpdate = update,
// onInit = function(initData) ... end, -- used when the script is just created (not loaded)
// onSave = function() return ... end,
// onLoad = function(state, initData) ... end, -- "state" is the data that was earlier returned by
// onSave
//
// -- Works only if a child class has passed a EngineHandlerList
// -- for 'onSomethingElse' to ScriptsContainer::registerEngineHandlers.
// onSomethingElse = function() print("something else") end
// },
//
// -- Handlers for events, sent from other scripts. Engine itself never sent events. Any name can be used
// for an event. eventHandlers = {
// SomeEvent = someEventHandler
// }
// }
class ScriptsContainer
{
public:
// ScriptId of each script is stored with this key in Script::mHiddenData.
// Removed from mHiddenData when the script if removed.
constexpr static std::string_view sScriptIdKey = "_id";
// Debug identifier of each script is stored with this key in Script::mHiddenData.
// Present in mHiddenData even after removal of the script from ScriptsContainer.
constexpr static std::string_view sScriptDebugNameKey = "_name";
using TimerType = ESM::LuaTimer::Type;
// `namePrefix` is a common prefix for all scripts in the container. Used in logs for error messages and `print`
// output. `tracker` is a tracker for managing the container's state. `load` specifies whether the container
// should be constructed in a loaded state.
ScriptsContainer(
LuaState* lua, std::string_view namePrefix, ScriptTracker* tracker = nullptr, bool load = true);
ScriptsContainer(const ScriptsContainer&) = delete;
ScriptsContainer(ScriptsContainer&&) = delete;
virtual ~ScriptsContainer();
// `conf` specifies the list of scripts that should be autostarted in this container; the script
// names themselves are stored in ScriptsConfiguration.
void setAutoStartConf(ScriptIdsWithInitializationData conf) { mAutoStartScripts = std::move(conf); }
const ScriptIdsWithInitializationData& getAutoStartConf() const { return mAutoStartScripts; }
// Adds package that will be available (via `require`) for all scripts in the container.
// Automatically applies LuaUtil::makeReadOnly to the package.
void addPackage(std::string packageName, sol::main_object package);
// Gets script with given id from ScriptsConfiguration, finds the source in the virtual file system, starts as a
// new script, adds it to the container, and calls onInit for this script. Returns `true` if the script was
// successfully added. The script should have CUSTOM flag. If the flag is not set, or file not found, or has
// syntax errors, returns false. If such script already exists in the container, then also returns false.
bool addCustomScript(int scriptId, std::string_view initData = {});
bool hasScript(int scriptId) const;
void removeScript(int scriptId);
void processTimers(double simulationTime, double gameTime);
// Calls `onUpdate` (if present) for every script in the container.
// Handlers are called in the same order as scripts were added.
void update(float dt) { callEngineHandlers(mUpdateHandlers, dt); }
// Calls event handlers `eventName` (if present) for every script.
// If several scripts register handlers for `eventName`, they are called in reverse order.
// If some handler returns `false`, all remaining handlers are ignored. Any other return value
// (including `nil`) has no effect.
void receiveEvent(std::string_view eventName, std::string_view eventData);
// Serializer defines how to serialize/deserialize userdata. If serializer is not provided,
// only built-in types and types from util package can be serialized.
void setSerializer(const UserdataSerializer* serializer) { mSerializer = serializer; }
// Special deserializer to use when load data from saves. Can be used to remap content files in Refnums.
void setSavedDataDeserializer(const UserdataSerializer* serializer) { mSavedDataDeserializer = serializer; }
// Starts scripts according to `autoStartMode` and calls `onInit` for them. Not needed if `load` is used.
void addAutoStartedScripts();
// Removes all scripts including the auto started.
void removeAllScripts();
// Calls engineHandler "onSave" for every script and saves the list of the scripts with serialized data to
// ESM::LuaScripts.
void save(ESM::LuaScripts&);
// Removes all scripts; starts scripts according to `autoStartMode` and
// loads the savedScripts. Runs "onLoad" for each script.
void load(const ESM::LuaScripts& savedScripts);
// Callbacks for serializable timers should be registered in advance.
// The script with the given path should already present in the container.
void registerTimerCallback(int scriptId, std::string_view callbackName, sol::main_protected_function callback);
// Sets up a timer, that can be automatically saved and loaded.
// type - the type of timer, either SIMULATION_TIME or GAME_TIME.
// time - the absolute game time (in seconds or in hours) when the timer should be executed.
// scriptPath - script path in VFS is used as script id. The script with the given path should already present
// in the container. callbackName - callback (should be registered in advance) for this timer. callbackArg -
// parameter for the callback (should be serializable).
void setupSerializableTimer(
TimerType type, double time, int scriptId, std::string_view callbackName, sol::main_object callbackArg);
// Creates a timer. `callback` is an arbitrary Lua function. These timers are called "unsavable"
// because they can not be stored in saves. I.e. loading a saved game will not fully restore the state.
void setupUnsavableTimer(TimerType type, double time, int scriptId, sol::main_protected_function callback);
// Informs that new frame is started. Needed to track Lua instruction count per frame.
void statsNextFrame();
struct ScriptStats
{
float mAvgInstructionCount = 0; // averaged number of Lua instructions per frame
int64_t mMemoryUsage = 0; // bytes
};
void collectStats(std::vector<ScriptStats>& stats) const;
static int64_t getInstanceCount() { return sInstanceCount; }
virtual bool isActive() const { return false; }
protected:
struct Handler
{
int mScriptId;
sol::main_function mFn;
};
struct EngineHandlerList
{
std::string_view mName;
std::vector<Handler> mList;
// "name" must be string literal
explicit EngineHandlerList(std::string_view name)
: mName(name)
{
}
};
// Calls given handlers in direct order.
template <typename... Args>
void callEngineHandlers(EngineHandlerList& handlers, const Args&... args)
{
ensureLoaded();
for (Handler& handler : handlers.mList)
{
try
{
LuaUtil::call({ this, handler.mScriptId }, handler.mFn, args...);
}
catch (std::exception& e)
{
Log(Debug::Error) << mNamePrefix << "[" << scriptPath(handler.mScriptId) << "] " << handlers.mName
<< " failed. " << e.what();
}
}
}
// To add a new engine handler a derived class should register the corresponding EngineHandlerList and define
// a public function (see how ScriptsContainer::update is implemented) that calls `callEngineHandlers`.
void registerEngineHandlers(std::initializer_list<EngineHandlerList*> handlers);
const std::string mNamePrefix;
LuaUtil::LuaState& mLua;
private:
struct Script
{
std::optional<sol::main_function> mOnSave;
std::optional<sol::main_function> mOnOverride;
std::optional<sol::main_table> mInterface;
std::string mInterfaceName;
sol::main_table mHiddenData;
std::map<std::string, sol::main_protected_function> mRegisteredCallbacks;
std::map<int64_t, sol::main_protected_function> mTemporaryCallbacks;
VFS::Path::Normalized mPath;
ScriptStats mStats;
~Script();
};
struct Timer
{
double mTime;
bool mSerializable;
int mScriptId;
std::variant<std::string, int64_t> mCallback; // string if serializable, integer otherwise
sol::main_object mArg;
std::string mSerializedArg;
bool operator<(const Timer& t) const { return mTime > t.mTime; }
};
using EventHandlerList = std::vector<Handler>;
friend class LuaState;
void addInstructionCount(int scriptId, int64_t instructionCount);
void addMemoryUsage(int scriptId, int64_t memoryDelta);
// Add to container without calling onInit/onLoad.
bool addScript(
LuaView& view, int scriptId, std::optional<sol::function>& onInit, std::optional<sol::function>& onLoad);
// Returns script by id (throws an exception if doesn't exist)
Script& getScript(int scriptId);
void printError(int scriptId, std::string_view msg, const std::exception& e);
const VFS::Path::Normalized& scriptPath(int scriptId) const
{
return mLua.getConfiguration()[scriptId].mScriptPath;
}
void callOnInit(LuaView& view, int scriptId, const sol::function& onInit, std::string_view data);
void callTimer(const Timer& t);
void updateTimerQueue(std::vector<Timer>& timerQueue, double time);
static void insertTimer(std::vector<Timer>& timerQueue, Timer&& t);
static void insertHandler(std::vector<Handler>& list, int scriptId, sol::function fn);
static void removeHandler(std::vector<Handler>& list, int scriptId);
void insertInterface(int scriptId, const Script& script);
void removeInterface(int scriptId, const Script& script);
ScriptIdsWithInitializationData mAutoStartScripts;
const UserdataSerializer* mSerializer = nullptr;
const UserdataSerializer* mSavedDataDeserializer = nullptr;
std::map<std::string, sol::main_object> mAPI;
struct LoadedData
{
std::map<int, Script> mScripts;
sol::main_table mPublicInterfaces;
std::map<std::string, EventHandlerList, std::less<>> mEventHandlers;
std::vector<Timer> mSimulationTimersQueue;
std::vector<Timer> mGameTimersQueue;
};
using UnloadedData = ESM::LuaScripts;
// Unloads the container to free resources held by the shared Lua state. This method serializes the container's
// state. The serialized data is automatically restored to the Lua state as required. Unloading and reloading
// the container is functionally equivalent to saving and loading the game, meaning the appropriate engine
// handlers are invoked.
UnloadedData& ensureUnloaded(LuaView& lua);
LoadedData& ensureLoaded();
EngineHandlerList mUpdateHandlers{ "onUpdate" };
std::map<std::string_view, EngineHandlerList*> mEngineHandlers;
std::variant<UnloadedData, LoadedData> mData;
int64_t mTemporaryCallbackCounter = 0;
std::map<int, int64_t> mRemovedScriptsMemoryUsage;
using WeakPtr = std::shared_ptr<ScriptsContainer*>;
WeakPtr mThis; // used by LuaState to track ownership of memory allocations
ScriptTracker* mTracker;
bool mRequiredLoading = false;
friend class ScriptTracker;
static int64_t sInstanceCount; // debug information, shown in Lua profiler
};
}
#endif // COMPONENTS_LUA_SCRIPTSCONTAINER_H
|