File: scriptscontainer.hpp

package info (click to toggle)
openmw 0.49.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 33,992 kB
  • sloc: cpp: 372,479; xml: 2,149; sh: 1,403; python: 797; makefile: 26
file content (308 lines) | stat: -rw-r--r-- 14,171 bytes parent folder | download
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