mirror of
https://github.com/OpenMW/openmw.git
synced 2025-03-03 02:09:41 +00:00
Merge branch 'omwscripts' into 'master'
Change format of "*.omwscripts" files See merge request OpenMW/openmw!1271
This commit is contained in:
commit
34b63bf142
50 changed files with 1240 additions and 630 deletions
|
@ -495,11 +495,6 @@ void OMW::Engine::addGroundcoverFile(const std::string& file)
|
|||
mGroundcoverFiles.emplace_back(file);
|
||||
}
|
||||
|
||||
void OMW::Engine::addLuaScriptListFile(const std::string& file)
|
||||
{
|
||||
mLuaScriptListFiles.push_back(file);
|
||||
}
|
||||
|
||||
void OMW::Engine::setSkipMenu (bool skipMenu, bool newGame)
|
||||
{
|
||||
mSkipMenu = skipMenu;
|
||||
|
@ -674,7 +669,7 @@ void OMW::Engine::setWindowIcon()
|
|||
void OMW::Engine::prepareEngine (Settings::Manager & settings)
|
||||
{
|
||||
mEnvironment.setStateManager (
|
||||
new MWState::StateManager (mCfgMgr.getUserDataPath() / "saves", mContentFiles.at (0)));
|
||||
new MWState::StateManager (mCfgMgr.getUserDataPath() / "saves", mContentFiles));
|
||||
|
||||
createWindow(settings);
|
||||
|
||||
|
@ -714,7 +709,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings)
|
|||
|
||||
mViewer->addEventHandler(mScreenCaptureHandler);
|
||||
|
||||
mLuaManager = new MWLua::LuaManager(mVFS.get(), mLuaScriptListFiles);
|
||||
mLuaManager = new MWLua::LuaManager(mVFS.get());
|
||||
mEnvironment.setLuaManager(mLuaManager);
|
||||
|
||||
// Create input and UI first to set up a bootstrapping environment for
|
||||
|
|
|
@ -72,7 +72,6 @@ namespace OMW
|
|||
std::string mCellName;
|
||||
std::vector<std::string> mContentFiles;
|
||||
std::vector<std::string> mGroundcoverFiles;
|
||||
std::vector<std::string> mLuaScriptListFiles;
|
||||
bool mSkipMenu;
|
||||
bool mUseSound;
|
||||
bool mCompileAll;
|
||||
|
@ -146,7 +145,6 @@ namespace OMW
|
|||
*/
|
||||
void addContentFile(const std::string& file);
|
||||
void addGroundcoverFile(const std::string& file);
|
||||
void addLuaScriptListFile(const std::string& file);
|
||||
|
||||
/// Disable or enable all sounds
|
||||
void setSoundUsage(bool soundUsage);
|
||||
|
|
|
@ -124,9 +124,11 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat
|
|||
engine.addGroundcoverFile(file);
|
||||
}
|
||||
|
||||
StringsVector luaScriptLists = variables["lua-scripts"].as<Files::EscapeStringVector>().toStdStringVector();
|
||||
for (const auto& file : luaScriptLists)
|
||||
engine.addLuaScriptListFile(file);
|
||||
if (variables.count("lua-scripts"))
|
||||
{
|
||||
Log(Debug::Warning) << "Lua scripts have been specified via the old lua-scripts option and will not be loaded. "
|
||||
"Please update them to a version which uses the new omwscripts format.";
|
||||
}
|
||||
|
||||
// startup-settings
|
||||
engine.setCell(variables["start"].as<Files::EscapeHashString>().toStdString());
|
||||
|
|
|
@ -30,6 +30,7 @@ namespace MWBase
|
|||
virtual ~LuaManager() = default;
|
||||
|
||||
virtual void newGameStarted() = 0;
|
||||
virtual void gameLoaded() = 0;
|
||||
virtual void registerObject(const MWWorld::Ptr& ptr) = 0;
|
||||
virtual void deregisterObject(const MWWorld::Ptr& ptr) = 0;
|
||||
virtual void objectAddedToScene(const MWWorld::Ptr& ptr) = 0;
|
||||
|
|
|
@ -23,7 +23,7 @@ namespace MWLua
|
|||
sol::usertype<AsyncPackageId> api = context.mLua->sol().new_usertype<AsyncPackageId>("AsyncPackage");
|
||||
api["registerTimerCallback"] = [](const AsyncPackageId& asyncId, std::string_view name, sol::function callback)
|
||||
{
|
||||
asyncId.mContainer->registerTimerCallback(asyncId.mScript, name, std::move(callback));
|
||||
asyncId.mContainer->registerTimerCallback(asyncId.mScriptId, name, std::move(callback));
|
||||
return TimerCallback{asyncId, std::string(name)};
|
||||
};
|
||||
api["newTimerInSeconds"] = [world=context.mWorldView](const AsyncPackageId&, double delay,
|
||||
|
@ -31,24 +31,24 @@ namespace MWLua
|
|||
{
|
||||
callback.mAsyncId.mContainer->setupSerializableTimer(
|
||||
TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay,
|
||||
callback.mAsyncId.mScript, callback.mName, std::move(callbackArg));
|
||||
callback.mAsyncId.mScriptId, callback.mName, std::move(callbackArg));
|
||||
};
|
||||
api["newTimerInHours"] = [world=context.mWorldView](const AsyncPackageId&, double delay,
|
||||
const TimerCallback& callback, sol::object callbackArg)
|
||||
{
|
||||
callback.mAsyncId.mContainer->setupSerializableTimer(
|
||||
TimeUnit::HOURS, world->getGameTimeInHours() + delay,
|
||||
callback.mAsyncId.mScript, callback.mName, std::move(callbackArg));
|
||||
callback.mAsyncId.mScriptId, callback.mName, std::move(callbackArg));
|
||||
};
|
||||
api["newUnsavableTimerInSeconds"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback)
|
||||
{
|
||||
asyncId.mContainer->setupUnsavableTimer(
|
||||
TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay, asyncId.mScript, std::move(callback));
|
||||
TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay, asyncId.mScriptId, std::move(callback));
|
||||
};
|
||||
api["newUnsavableTimerInHours"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback)
|
||||
{
|
||||
asyncId.mContainer->setupUnsavableTimer(
|
||||
TimeUnit::HOURS, world->getGameTimeInHours() + delay, asyncId.mScript, std::move(callback));
|
||||
TimeUnit::HOURS, world->getGameTimeInHours() + delay, asyncId.mScriptId, std::move(callback));
|
||||
};
|
||||
api["callback"] = [](const AsyncPackageId& asyncId, sol::function fn)
|
||||
{
|
||||
|
@ -59,7 +59,7 @@ namespace MWLua
|
|||
{
|
||||
LuaUtil::ScriptsContainer::ScriptId id = hiddenData[LuaUtil::ScriptsContainer::ScriptId::KEY];
|
||||
hiddenData[Callback::SCRIPT_NAME_KEY] = id.toString();
|
||||
return AsyncPackageId{id.mContainer, id.mPath, hiddenData};
|
||||
return AsyncPackageId{id.mContainer, id.mIndex, hiddenData};
|
||||
};
|
||||
return sol::make_object(context.mLua->sol(), initializer);
|
||||
}
|
||||
|
|
|
@ -16,7 +16,8 @@ namespace MWLua
|
|||
class GlobalScripts : public LuaUtil::ScriptsContainer
|
||||
{
|
||||
public:
|
||||
GlobalScripts(LuaUtil::LuaState* lua) : LuaUtil::ScriptsContainer(lua, "Global")
|
||||
GlobalScripts(LuaUtil::LuaState* lua) :
|
||||
LuaUtil::ScriptsContainer(lua, "Global", ESM::LuaScriptCfg::sGlobal)
|
||||
{
|
||||
registerEngineHandlers({&mActorActiveHandlers, &mNewGameHandlers, &mPlayerAddedHandlers});
|
||||
}
|
||||
|
|
|
@ -82,14 +82,14 @@ namespace MWLua
|
|||
};
|
||||
}
|
||||
|
||||
LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj)
|
||||
: LuaUtil::ScriptsContainer(lua, "L" + idToString(obj.id())), mData(obj)
|
||||
LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj, ESM::LuaScriptCfg::Flags autoStartMode)
|
||||
: LuaUtil::ScriptsContainer(lua, "L" + idToString(obj.id()), autoStartMode), mData(obj)
|
||||
{
|
||||
this->addPackage("openmw.self", sol::make_object(lua->sol(), &mData));
|
||||
registerEngineHandlers({&mOnActiveHandlers, &mOnInactiveHandlers, &mOnConsumeHandlers});
|
||||
}
|
||||
|
||||
void LocalScripts::receiveEngineEvent(const EngineEvent& event, ObjectRegistry*)
|
||||
void LocalScripts::receiveEngineEvent(const EngineEvent& event)
|
||||
{
|
||||
std::visit([this](auto&& arg)
|
||||
{
|
||||
|
|
|
@ -20,7 +20,7 @@ namespace MWLua
|
|||
{
|
||||
public:
|
||||
static void initializeSelfPackage(const Context&);
|
||||
LocalScripts(LuaUtil::LuaState* lua, const LObject& obj);
|
||||
LocalScripts(LuaUtil::LuaState* lua, const LObject& obj, ESM::LuaScriptCfg::Flags autoStartMode);
|
||||
|
||||
MWBase::LuaManager::ActorControls* getActorControls() { return &mData.mControls; }
|
||||
|
||||
|
@ -39,7 +39,7 @@ namespace MWLua
|
|||
};
|
||||
using EngineEvent = std::variant<OnActive, OnInactive, OnConsume>;
|
||||
|
||||
void receiveEngineEvent(const EngineEvent&, ObjectRegistry*);
|
||||
void receiveEngineEvent(const EngineEvent&);
|
||||
|
||||
protected:
|
||||
SelfObject mData;
|
||||
|
|
|
@ -25,7 +25,7 @@ namespace MWLua
|
|||
{
|
||||
auto* lua = context.mLua;
|
||||
sol::table api(lua->sol(), sol::create);
|
||||
api["API_REVISION"] = 7;
|
||||
api["API_REVISION"] = 8;
|
||||
api["quit"] = [lua]()
|
||||
{
|
||||
std::string traceback = lua->sol()["debug"]["traceback"]().get<std::string>();
|
||||
|
|
|
@ -48,7 +48,7 @@ namespace MWLua
|
|||
struct AsyncPackageId
|
||||
{
|
||||
LuaUtil::ScriptsContainer* mContainer;
|
||||
std::string mScript;
|
||||
int mScriptId;
|
||||
sol::table mHiddenData;
|
||||
};
|
||||
sol::function getAsyncPackageInitializer(const Context&);
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
#include <components/esm/luascripts.hpp>
|
||||
|
||||
#include <components/lua/utilpackage.hpp>
|
||||
#include <components/lua/omwscriptsparser.hpp>
|
||||
|
||||
#include "../mwbase/windowmanager.hpp"
|
||||
|
||||
#include "../mwworld/class.hpp"
|
||||
#include "../mwworld/esmstore.hpp"
|
||||
#include "../mwworld/ptr.hpp"
|
||||
|
||||
#include "luabindings.hpp"
|
||||
|
@ -20,10 +20,9 @@
|
|||
namespace MWLua
|
||||
{
|
||||
|
||||
LuaManager::LuaManager(const VFS::Manager* vfs, const std::vector<std::string>& scriptLists) : mLua(vfs)
|
||||
LuaManager::LuaManager(const VFS::Manager* vfs) : mLua(vfs, &mConfiguration)
|
||||
{
|
||||
Log(Debug::Info) << "Lua version: " << LuaUtil::getLuaVersion();
|
||||
mGlobalScriptList = LuaUtil::parseOMWScriptsFiles(vfs, scriptLists);
|
||||
|
||||
mGlobalSerializer = createUserdataSerializer(false, mWorldView.getObjectRegistry());
|
||||
mLocalSerializer = createUserdataSerializer(true, mWorldView.getObjectRegistry());
|
||||
|
@ -33,6 +32,14 @@ namespace MWLua
|
|||
mGlobalScripts.setSerializer(mGlobalSerializer.get());
|
||||
}
|
||||
|
||||
void LuaManager::initConfiguration()
|
||||
{
|
||||
mConfiguration.init(MWBase::Environment::get().getWorld()->getStore().getLuaScriptsCfg());
|
||||
Log(Debug::Verbose) << "Lua scripts configuration (" << mConfiguration.size() << " scripts):";
|
||||
for (size_t i = 0; i < mConfiguration.size(); ++i)
|
||||
Log(Debug::Verbose) << "#" << i << " " << LuaUtil::scriptCfgToString(mConfiguration[i]);
|
||||
}
|
||||
|
||||
void LuaManager::init()
|
||||
{
|
||||
Context context;
|
||||
|
@ -67,10 +74,7 @@ namespace MWLua
|
|||
mLocalSettingsPackage = initLocalSettingsPackage(localContext);
|
||||
mPlayerSettingsPackage = initPlayerSettingsPackage(localContext);
|
||||
|
||||
mInputEvents.clear();
|
||||
for (const std::string& path : mGlobalScriptList)
|
||||
if (mGlobalScripts.addNewScript(path))
|
||||
Log(Debug::Info) << "Global script started: " << path;
|
||||
initConfiguration();
|
||||
mInitialized = true;
|
||||
}
|
||||
|
||||
|
@ -160,7 +164,7 @@ namespace MWLua
|
|||
}
|
||||
LocalScripts* scripts = obj.ptr().getRefData().getLuaScripts();
|
||||
if (scripts)
|
||||
scripts->receiveEngineEvent(e.mEvent, objectRegistry);
|
||||
scripts->receiveEngineEvent(e.mEvent);
|
||||
}
|
||||
mLocalEngineEvents.clear();
|
||||
|
||||
|
@ -173,6 +177,11 @@ namespace MWLua
|
|||
mPlayerChanged = false;
|
||||
mGlobalScripts.playerAdded(GObject(getId(mPlayer), objectRegistry));
|
||||
}
|
||||
if (mNewGameStarted)
|
||||
{
|
||||
mNewGameStarted = false;
|
||||
mGlobalScripts.newGameStarted();
|
||||
}
|
||||
|
||||
for (ObjectId id : mActorAddedEvents)
|
||||
mGlobalScripts.actorActive(GObject(id, objectRegistry));
|
||||
|
@ -205,8 +214,11 @@ namespace MWLua
|
|||
mInputEvents.clear();
|
||||
mActorAddedEvents.clear();
|
||||
mLocalEngineEvents.clear();
|
||||
mNewGameStarted = false;
|
||||
mPlayerChanged = false;
|
||||
mWorldView.clear();
|
||||
mGlobalScripts.removeAllScripts();
|
||||
mGlobalScriptsStarted = false;
|
||||
if (!mPlayer.isEmpty())
|
||||
{
|
||||
mPlayer.getCellRef().unsetRefNum();
|
||||
|
@ -225,17 +237,38 @@ namespace MWLua
|
|||
mPlayer = ptr;
|
||||
LocalScripts* localScripts = ptr.getRefData().getLuaScripts();
|
||||
if (!localScripts)
|
||||
localScripts = createLocalScripts(ptr);
|
||||
localScripts = createLocalScripts(ptr, ESM::LuaScriptCfg::sPlayer);
|
||||
mActiveLocalScripts.insert(localScripts);
|
||||
mLocalEngineEvents.push_back({getId(ptr), LocalScripts::OnActive{}});
|
||||
mPlayerChanged = true;
|
||||
}
|
||||
|
||||
void LuaManager::newGameStarted()
|
||||
{
|
||||
mNewGameStarted = true;
|
||||
mInputEvents.clear();
|
||||
mGlobalScripts.addAutoStartedScripts();
|
||||
mGlobalScriptsStarted = true;
|
||||
}
|
||||
|
||||
void LuaManager::gameLoaded()
|
||||
{
|
||||
if (!mGlobalScriptsStarted)
|
||||
mGlobalScripts.addAutoStartedScripts();
|
||||
mGlobalScriptsStarted = true;
|
||||
}
|
||||
|
||||
void LuaManager::objectAddedToScene(const MWWorld::Ptr& ptr)
|
||||
{
|
||||
mWorldView.objectAddedToScene(ptr); // assigns generated RefNum if it is not set yet.
|
||||
|
||||
LocalScripts* localScripts = ptr.getRefData().getLuaScripts();
|
||||
if (!localScripts)
|
||||
{
|
||||
ESM::LuaScriptCfg::Flags flag = getLuaScriptFlag(ptr);
|
||||
if (!mConfiguration.getListByFlag(flag).empty())
|
||||
localScripts = createLocalScripts(ptr, flag); // TODO: put to a queue and apply on next `update()`
|
||||
}
|
||||
if (localScripts)
|
||||
{
|
||||
mActiveLocalScripts.insert(localScripts);
|
||||
|
@ -281,26 +314,26 @@ namespace MWLua
|
|||
return localScripts->getActorControls();
|
||||
}
|
||||
|
||||
void LuaManager::addLocalScript(const MWWorld::Ptr& ptr, const std::string& scriptPath)
|
||||
void LuaManager::addCustomLocalScript(const MWWorld::Ptr& ptr, int scriptId)
|
||||
{
|
||||
LocalScripts* localScripts = ptr.getRefData().getLuaScripts();
|
||||
if (!localScripts)
|
||||
{
|
||||
localScripts = createLocalScripts(ptr);
|
||||
localScripts = createLocalScripts(ptr, getLuaScriptFlag(ptr));
|
||||
if (ptr.isInCell() && MWBase::Environment::get().getWorld()->isCellActive(ptr.getCell()))
|
||||
mActiveLocalScripts.insert(localScripts);
|
||||
}
|
||||
localScripts->addNewScript(scriptPath);
|
||||
localScripts->addCustomScript(scriptId);
|
||||
}
|
||||
|
||||
LocalScripts* LuaManager::createLocalScripts(const MWWorld::Ptr& ptr)
|
||||
LocalScripts* LuaManager::createLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScriptCfg::Flags flag)
|
||||
{
|
||||
assert(mInitialized);
|
||||
assert(flag != ESM::LuaScriptCfg::sGlobal);
|
||||
std::shared_ptr<LocalScripts> scripts;
|
||||
// When loading a game, it can be called before LuaManager::setPlayer,
|
||||
// so we can't just check ptr == mPlayer here.
|
||||
if (ptr.getCellRef().getRefIdRef() == "player")
|
||||
if (flag == ESM::LuaScriptCfg::sPlayer)
|
||||
{
|
||||
assert(ptr.getCellRef().getRefIdRef() == "player");
|
||||
scripts = std::make_shared<PlayerScripts>(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry()));
|
||||
scripts->addPackage("openmw.ui", mUserInterfacePackage);
|
||||
scripts->addPackage("openmw.camera", mCameraPackage);
|
||||
|
@ -309,11 +342,12 @@ namespace MWLua
|
|||
}
|
||||
else
|
||||
{
|
||||
scripts = std::make_shared<LocalScripts>(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry()));
|
||||
scripts = std::make_shared<LocalScripts>(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry()), flag);
|
||||
scripts->addPackage("openmw.settings", mLocalSettingsPackage);
|
||||
}
|
||||
scripts->addPackage("openmw.nearby", mNearbyPackage);
|
||||
scripts->setSerializer(mLocalSerializer.get());
|
||||
scripts->addAutoStartedScripts();
|
||||
|
||||
MWWorld::RefData& refData = ptr.getRefData();
|
||||
refData.setLuaScripts(std::move(scripts));
|
||||
|
@ -344,8 +378,9 @@ namespace MWLua
|
|||
loadEvents(mLua.sol(), reader, mGlobalEvents, mLocalEvents, mContentFileMapping, mGlobalLoader.get());
|
||||
|
||||
mGlobalScripts.setSerializer(mGlobalLoader.get());
|
||||
mGlobalScripts.load(globalScripts, false);
|
||||
mGlobalScripts.load(globalScripts);
|
||||
mGlobalScripts.setSerializer(mGlobalSerializer.get());
|
||||
mGlobalScriptsStarted = true;
|
||||
}
|
||||
|
||||
void LuaManager::saveLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScripts& data)
|
||||
|
@ -366,10 +401,10 @@ namespace MWLua
|
|||
}
|
||||
|
||||
mWorldView.getObjectRegistry()->registerPtr(ptr);
|
||||
LocalScripts* scripts = createLocalScripts(ptr);
|
||||
LocalScripts* scripts = createLocalScripts(ptr, getLuaScriptFlag(ptr));
|
||||
|
||||
scripts->setSerializer(mLocalLoader.get());
|
||||
scripts->load(data, true);
|
||||
scripts->load(data);
|
||||
scripts->setSerializer(mLocalSerializer.get());
|
||||
|
||||
// LiveCellRef is usually copied after loading, so this Ptr will become invalid and should be deregistered.
|
||||
|
@ -380,15 +415,12 @@ namespace MWLua
|
|||
{
|
||||
Log(Debug::Info) << "Reload Lua";
|
||||
mLua.dropScriptCache();
|
||||
initConfiguration();
|
||||
|
||||
{ // Reload global scripts
|
||||
ESM::LuaScripts data;
|
||||
mGlobalScripts.save(data);
|
||||
mGlobalScripts.removeAllScripts();
|
||||
for (const std::string& path : mGlobalScriptList)
|
||||
if (mGlobalScripts.addNewScript(path))
|
||||
Log(Debug::Info) << "Global script restarted: " << path;
|
||||
mGlobalScripts.load(data, false);
|
||||
mGlobalScripts.load(data);
|
||||
}
|
||||
|
||||
for (const auto& [id, ptr] : mWorldView.getObjectRegistry()->mObjectMapping)
|
||||
|
@ -398,8 +430,10 @@ namespace MWLua
|
|||
continue;
|
||||
ESM::LuaScripts data;
|
||||
scripts->save(data);
|
||||
scripts->load(data, true);
|
||||
scripts->load(data);
|
||||
}
|
||||
for (LocalScripts* scripts : mActiveLocalScripts)
|
||||
scripts->receiveEngineEvent(LocalScripts::OnActive());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -35,9 +35,9 @@ namespace MWLua
|
|||
class LuaManager : public MWBase::LuaManager
|
||||
{
|
||||
public:
|
||||
LuaManager(const VFS::Manager* vfs, const std::vector<std::string>& globalScriptLists);
|
||||
LuaManager(const VFS::Manager* vfs);
|
||||
|
||||
// Called by engine.cpp when environment is fully initialized.
|
||||
// Called by engine.cpp when the environment is fully initialized.
|
||||
void init();
|
||||
|
||||
// Called by engine.cpp every frame. For performance reasons it works in a separate
|
||||
|
@ -49,7 +49,8 @@ namespace MWLua
|
|||
|
||||
// Available everywhere through the MWBase::LuaManager interface.
|
||||
// LuaManager queues these events and propagates to scripts on the next `update` call.
|
||||
void newGameStarted() override { mGlobalScripts.newGameStarted(); }
|
||||
void newGameStarted() override;
|
||||
void gameLoaded() override;
|
||||
void objectAddedToScene(const MWWorld::Ptr& ptr) override;
|
||||
void objectRemovedFromScene(const MWWorld::Ptr& ptr) override;
|
||||
void registerObject(const MWWorld::Ptr& ptr) override;
|
||||
|
@ -62,8 +63,8 @@ namespace MWLua
|
|||
void clear() override; // should be called before loading game or starting a new game to reset internal state.
|
||||
void setupPlayer(const MWWorld::Ptr& ptr) override; // Should be called once after each "clear".
|
||||
|
||||
// Used only in luabindings
|
||||
void addLocalScript(const MWWorld::Ptr&, const std::string& scriptPath);
|
||||
// Used only in Lua bindings
|
||||
void addCustomLocalScript(const MWWorld::Ptr&, int scriptId);
|
||||
void addAction(std::unique_ptr<Action>&& action) { mActionQueue.push_back(std::move(action)); }
|
||||
void addTeleportPlayerAction(std::unique_ptr<TeleportAction>&& action) { mTeleportPlayerAction = std::move(action); }
|
||||
void addUIMessage(std::string_view message) { mUIMessages.emplace_back(message); }
|
||||
|
@ -93,9 +94,12 @@ namespace MWLua
|
|||
}
|
||||
|
||||
private:
|
||||
LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr);
|
||||
void initConfiguration();
|
||||
LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScriptCfg::Flags);
|
||||
|
||||
bool mInitialized = false;
|
||||
bool mGlobalScriptsStarted = false;
|
||||
LuaUtil::ScriptsConfiguration mConfiguration;
|
||||
LuaUtil::LuaState mLua;
|
||||
sol::table mNearbyPackage;
|
||||
sol::table mUserInterfacePackage;
|
||||
|
@ -104,12 +108,12 @@ namespace MWLua
|
|||
sol::table mLocalSettingsPackage;
|
||||
sol::table mPlayerSettingsPackage;
|
||||
|
||||
std::vector<std::string> mGlobalScriptList;
|
||||
GlobalScripts mGlobalScripts{&mLua};
|
||||
std::set<LocalScripts*> mActiveLocalScripts;
|
||||
WorldView mWorldView;
|
||||
|
||||
bool mPlayerChanged = false;
|
||||
bool mNewGameStarted = false;
|
||||
MWWorld::Ptr mPlayer;
|
||||
|
||||
GlobalEventQueue mGlobalEvents;
|
||||
|
|
|
@ -1,19 +1,6 @@
|
|||
#include "object.hpp"
|
||||
|
||||
#include "../mwclass/activator.hpp"
|
||||
#include "../mwclass/armor.hpp"
|
||||
#include "../mwclass/book.hpp"
|
||||
#include "../mwclass/clothing.hpp"
|
||||
#include "../mwclass/container.hpp"
|
||||
#include "../mwclass/creature.hpp"
|
||||
#include "../mwclass/door.hpp"
|
||||
#include "../mwclass/ingredient.hpp"
|
||||
#include "../mwclass/light.hpp"
|
||||
#include "../mwclass/misc.hpp"
|
||||
#include "../mwclass/npc.hpp"
|
||||
#include "../mwclass/potion.hpp"
|
||||
#include "../mwclass/static.hpp"
|
||||
#include "../mwclass/weapon.hpp"
|
||||
#include <unordered_map>
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
@ -23,28 +10,34 @@ namespace MWLua
|
|||
return std::to_string(id.mIndex) + "_" + std::to_string(id.mContentFile);
|
||||
}
|
||||
|
||||
const static std::map<std::type_index, std::string_view> classNames = {
|
||||
{typeid(MWClass::Activator), "Activator"},
|
||||
{typeid(MWClass::Armor), "Armor"},
|
||||
{typeid(MWClass::Book), "Book"},
|
||||
{typeid(MWClass::Clothing), "Clothing"},
|
||||
{typeid(MWClass::Container), "Container"},
|
||||
{typeid(MWClass::Creature), "Creature"},
|
||||
{typeid(MWClass::Door), "Door"},
|
||||
{typeid(MWClass::Ingredient), "Ingredient"},
|
||||
{typeid(MWClass::Light), "Light"},
|
||||
{typeid(MWClass::Miscellaneous), "Miscellaneous"},
|
||||
{typeid(MWClass::Npc), "NPC"},
|
||||
{typeid(MWClass::Potion), "Potion"},
|
||||
{typeid(MWClass::Static), "Static"},
|
||||
{typeid(MWClass::Weapon), "Weapon"},
|
||||
struct LuaObjectTypeInfo
|
||||
{
|
||||
std::string_view mName;
|
||||
ESM::LuaScriptCfg::Flags mFlag = 0;
|
||||
};
|
||||
|
||||
std::string_view getMWClassName(const std::type_index& cls_type, std::string_view fallback)
|
||||
const static std::unordered_map<ESM::RecNameInts, LuaObjectTypeInfo> luaObjectTypeInfo = {
|
||||
{ESM::REC_ACTI, {"Activator", ESM::LuaScriptCfg::sActivator}},
|
||||
{ESM::REC_ARMO, {"Armor", ESM::LuaScriptCfg::sArmor}},
|
||||
{ESM::REC_BOOK, {"Book", ESM::LuaScriptCfg::sBook}},
|
||||
{ESM::REC_CLOT, {"Clothing", ESM::LuaScriptCfg::sClothing}},
|
||||
{ESM::REC_CONT, {"Container", ESM::LuaScriptCfg::sContainer}},
|
||||
{ESM::REC_CREA, {"Creature", ESM::LuaScriptCfg::sCreature}},
|
||||
{ESM::REC_DOOR, {"Door", ESM::LuaScriptCfg::sDoor}},
|
||||
{ESM::REC_INGR, {"Ingredient", ESM::LuaScriptCfg::sIngredient}},
|
||||
{ESM::REC_LIGH, {"Light", ESM::LuaScriptCfg::sLight}},
|
||||
{ESM::REC_MISC, {"Miscellaneous", ESM::LuaScriptCfg::sMiscItem}},
|
||||
{ESM::REC_NPC_, {"NPC", ESM::LuaScriptCfg::sNPC}},
|
||||
{ESM::REC_ALCH, {"Potion", ESM::LuaScriptCfg::sPotion}},
|
||||
{ESM::REC_STAT, {"Static"}},
|
||||
{ESM::REC_WEAP, {"Weapon", ESM::LuaScriptCfg::sWeapon}},
|
||||
};
|
||||
|
||||
std::string_view getLuaObjectTypeName(ESM::RecNameInts type, std::string_view fallback)
|
||||
{
|
||||
auto it = classNames.find(cls_type);
|
||||
if (it != classNames.end())
|
||||
return it->second;
|
||||
auto it = luaObjectTypeInfo.find(type);
|
||||
if (it != luaObjectTypeInfo.end())
|
||||
return it->second.mName;
|
||||
else
|
||||
return fallback;
|
||||
}
|
||||
|
@ -55,13 +48,31 @@ namespace MWLua
|
|||
return id == "prisonmarker" || id == "divinemarker" || id == "templemarker" || id == "northmarker";
|
||||
}
|
||||
|
||||
std::string_view getMWClassName(const MWWorld::Ptr& ptr)
|
||||
std::string_view getLuaObjectTypeName(const MWWorld::Ptr& ptr)
|
||||
{
|
||||
// Behaviour of this function is a part of OpenMW Lua API. We can not just return
|
||||
// `ptr.getTypeDescription()` because its implementation is distributed over many files
|
||||
// and can be accidentally changed. We use `ptr.getTypeDescription()` only as a fallback
|
||||
// for types that are not present in `luaObjectTypeInfo` (for such types result stability
|
||||
// is not necessary because they are not listed in OpenMW Lua documentation).
|
||||
if (ptr.getCellRef().getRefIdRef() == "player")
|
||||
return "Player";
|
||||
if (isMarker(ptr))
|
||||
return "Marker";
|
||||
return getMWClassName(typeid(ptr.getClass()));
|
||||
return getLuaObjectTypeName(static_cast<ESM::RecNameInts>(ptr.getType()), /*fallback=*/ptr.getTypeDescription());
|
||||
}
|
||||
|
||||
ESM::LuaScriptCfg::Flags getLuaScriptFlag(const MWWorld::Ptr& ptr)
|
||||
{
|
||||
if (ptr.getCellRef().getRefIdRef() == "player")
|
||||
return ESM::LuaScriptCfg::sPlayer;
|
||||
if (isMarker(ptr))
|
||||
return 0;
|
||||
auto it = luaObjectTypeInfo.find(static_cast<ESM::RecNameInts>(ptr.getType()));
|
||||
if (it != luaObjectTypeInfo.end())
|
||||
return it->second.mFlag;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string ptrToString(const MWWorld::Ptr& ptr)
|
||||
|
@ -69,7 +80,7 @@ namespace MWLua
|
|||
std::string res = "object";
|
||||
res.append(idToString(getId(ptr)));
|
||||
res.append(" (");
|
||||
res.append(getMWClassName(ptr));
|
||||
res.append(getLuaObjectTypeName(ptr));
|
||||
res.append(", ");
|
||||
res.append(ptr.getCellRef().getRefIdRef());
|
||||
res.append(")");
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
#include <typeindex>
|
||||
|
||||
#include <components/esm/cellref.hpp>
|
||||
#include <components/esm/defs.hpp>
|
||||
#include <components/esm/luascripts.hpp>
|
||||
|
||||
#include "../mwbase/environment.hpp"
|
||||
#include "../mwbase/world.hpp"
|
||||
|
@ -19,8 +21,12 @@ namespace MWLua
|
|||
std::string idToString(const ObjectId& id);
|
||||
std::string ptrToString(const MWWorld::Ptr& ptr);
|
||||
bool isMarker(const MWWorld::Ptr& ptr);
|
||||
std::string_view getMWClassName(const std::type_index& cls_type, std::string_view fallback = "Unknown");
|
||||
std::string_view getMWClassName(const MWWorld::Ptr& ptr);
|
||||
std::string_view getLuaObjectTypeName(ESM::RecNameInts recordType, std::string_view fallback = "Unknown");
|
||||
std::string_view getLuaObjectTypeName(const MWWorld::Ptr& ptr);
|
||||
|
||||
// Each script has a set of flags that controls to which objects the script should be
|
||||
// automatically attached. This function maps each object types to one of the flags.
|
||||
ESM::LuaScriptCfg::Flags getLuaScriptFlag(const MWWorld::Ptr& ptr);
|
||||
|
||||
// Holds a mapping ObjectId -> MWWord::Ptr.
|
||||
class ObjectRegistry
|
||||
|
@ -64,7 +70,7 @@ namespace MWLua
|
|||
ObjectId id() const { return mId; }
|
||||
|
||||
std::string toString() const;
|
||||
std::string_view type() const { return getMWClassName(ptr()); }
|
||||
std::string_view type() const { return getLuaObjectTypeName(ptr()); }
|
||||
|
||||
// Updates and returns the underlying Ptr. Throws an exception if object is not available.
|
||||
const MWWorld::Ptr& ptr() const;
|
||||
|
|
|
@ -42,13 +42,12 @@ namespace MWLua
|
|||
template <typename ObjT>
|
||||
using Cell = std::conditional_t<std::is_same_v<ObjT, LObject>, LCell, GCell>;
|
||||
|
||||
template <class Class>
|
||||
static const MWWorld::Ptr& requireClass(const MWWorld::Ptr& ptr)
|
||||
static const MWWorld::Ptr& requireRecord(ESM::RecNameInts recordType, const MWWorld::Ptr& ptr)
|
||||
{
|
||||
if (typeid(Class) != typeid(ptr.getClass()))
|
||||
if (ptr.getType() != recordType)
|
||||
{
|
||||
std::string msg = "Requires type '";
|
||||
msg.append(getMWClassName(typeid(Class)));
|
||||
msg.append(getLuaObjectTypeName(recordType));
|
||||
msg.append("', but applied to ");
|
||||
msg.append(ptrToString(ptr));
|
||||
throw std::runtime_error(msg);
|
||||
|
@ -141,9 +140,43 @@ namespace MWLua
|
|||
|
||||
if constexpr (std::is_same_v<ObjectT, GObject>)
|
||||
{ // Only for global scripts
|
||||
objectT["addScript"] = [luaManager=context.mLuaManager](const GObject& object, const std::string& path)
|
||||
objectT["addScript"] = [lua=context.mLua, luaManager=context.mLuaManager](const GObject& object, std::string_view path)
|
||||
{
|
||||
luaManager->addLocalScript(object.ptr(), path);
|
||||
const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration();
|
||||
std::optional<int> scriptId = cfg.findId(path);
|
||||
if (!scriptId)
|
||||
throw std::runtime_error("Unknown script: " + std::string(path));
|
||||
if (!(cfg[*scriptId].mFlags & ESM::LuaScriptCfg::sCustom))
|
||||
throw std::runtime_error("Script without CUSTOM tag can not be added dynamically: " + std::string(path));
|
||||
luaManager->addCustomLocalScript(object.ptr(), *scriptId);
|
||||
};
|
||||
objectT["hasScript"] = [lua=context.mLua](const GObject& object, std::string_view path)
|
||||
{
|
||||
const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration();
|
||||
std::optional<int> scriptId = cfg.findId(path);
|
||||
if (!scriptId)
|
||||
return false;
|
||||
MWWorld::Ptr ptr = object.ptr();
|
||||
LocalScripts* localScripts = ptr.getRefData().getLuaScripts();
|
||||
if (localScripts)
|
||||
return localScripts->hasScript(*scriptId);
|
||||
else
|
||||
return false;
|
||||
};
|
||||
objectT["removeScript"] = [lua=context.mLua](const GObject& object, std::string_view path)
|
||||
{
|
||||
const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration();
|
||||
std::optional<int> scriptId = cfg.findId(path);
|
||||
if (!scriptId)
|
||||
throw std::runtime_error("Unknown script: " + std::string(path));
|
||||
MWWorld::Ptr ptr = object.ptr();
|
||||
LocalScripts* localScripts = ptr.getRefData().getLuaScripts();
|
||||
if (!localScripts || !localScripts->hasScript(*scriptId))
|
||||
throw std::runtime_error("There is no script " + std::string(path) + " on " + ptrToString(ptr));
|
||||
ESM::LuaScriptCfg::Flags flags = cfg[*scriptId].mFlags;
|
||||
if ((flags & (localScripts->getAutoStartMode() | ESM::LuaScriptCfg::sCustom)) != ESM::LuaScriptCfg::sCustom)
|
||||
throw std::runtime_error("Autostarted script can not be removed: " + std::string(path));
|
||||
localScripts->removeScript(*scriptId);
|
||||
};
|
||||
|
||||
objectT["teleport"] = [luaManager=context.mLuaManager](const GObject& object, std::string_view cell,
|
||||
|
@ -189,7 +222,7 @@ namespace MWLua
|
|||
template <class ObjectT>
|
||||
static void addDoorBindings(sol::usertype<ObjectT>& objectT, const Context& context)
|
||||
{
|
||||
auto ptr = [](const ObjectT& o) -> const MWWorld::Ptr& { return requireClass<MWClass::Door>(o.ptr()); };
|
||||
auto ptr = [](const ObjectT& o) -> const MWWorld::Ptr& { return requireRecord(ESM::REC_DOOR, o.ptr()); };
|
||||
|
||||
objectT["isTeleport"] = sol::readonly_property([ptr](const ObjectT& o)
|
||||
{
|
||||
|
|
|
@ -13,7 +13,7 @@ namespace MWLua
|
|||
class PlayerScripts : public LocalScripts
|
||||
{
|
||||
public:
|
||||
PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj)
|
||||
PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj, ESM::LuaScriptCfg::sPlayer)
|
||||
{
|
||||
registerEngineHandlers({&mKeyPressHandlers, &mKeyReleaseHandlers,
|
||||
&mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers,
|
||||
|
|
|
@ -13,6 +13,15 @@ bool MWState::operator< (const Slot& left, const Slot& right)
|
|||
return left.mTimeStamp<right.mTimeStamp;
|
||||
}
|
||||
|
||||
std::string MWState::getFirstGameFile(const std::vector<std::string>& contentFiles)
|
||||
{
|
||||
for (const std::string& c : contentFiles)
|
||||
{
|
||||
if (Misc::StringUtils::ciEndsWith(c, ".esm") || Misc::StringUtils::ciEndsWith(c, ".omwgame"))
|
||||
return c;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void MWState::Character::addSlot (const boost::filesystem::path& path, const std::string& game)
|
||||
{
|
||||
|
@ -30,8 +39,7 @@ void MWState::Character::addSlot (const boost::filesystem::path& path, const std
|
|||
|
||||
slot.mProfile.load (reader);
|
||||
|
||||
if (Misc::StringUtils::lowerCase (slot.mProfile.mContentFiles.at (0))!=
|
||||
Misc::StringUtils::lowerCase (game))
|
||||
if (!Misc::StringUtils::ciEqual(getFirstGameFile(slot.mProfile.mContentFiles), game))
|
||||
return; // this file is for a different game -> ignore
|
||||
|
||||
mSlots.push_back (slot);
|
||||
|
|
|
@ -16,6 +16,8 @@ namespace MWState
|
|||
|
||||
bool operator< (const Slot& left, const Slot& right);
|
||||
|
||||
std::string getFirstGameFile(const std::vector<std::string>& contentFiles);
|
||||
|
||||
class Character
|
||||
{
|
||||
public:
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
#include <boost/filesystem.hpp>
|
||||
|
||||
MWState::CharacterManager::CharacterManager (const boost::filesystem::path& saves,
|
||||
const std::string& game)
|
||||
: mPath (saves), mCurrent (nullptr), mGame (game)
|
||||
const std::vector<std::string>& contentFiles)
|
||||
: mPath (saves), mCurrent (nullptr), mGame (getFirstGameFile(contentFiles))
|
||||
{
|
||||
if (!boost::filesystem::is_directory (mPath))
|
||||
{
|
||||
|
|
|
@ -29,7 +29,7 @@ namespace MWState
|
|||
|
||||
public:
|
||||
|
||||
CharacterManager (const boost::filesystem::path& saves, const std::string& game);
|
||||
CharacterManager (const boost::filesystem::path& saves, const std::vector<std::string>& contentFiles);
|
||||
|
||||
Character *getCurrentCharacter ();
|
||||
///< @note May return null
|
||||
|
|
|
@ -88,8 +88,8 @@ std::map<int, int> MWState::StateManager::buildContentFileIndexMap (const ESM::E
|
|||
return map;
|
||||
}
|
||||
|
||||
MWState::StateManager::StateManager (const boost::filesystem::path& saves, const std::string& game)
|
||||
: mQuitRequest (false), mAskLoadRecent(false), mState (State_NoGame), mCharacterManager (saves, game), mTimePlayed (0)
|
||||
MWState::StateManager::StateManager (const boost::filesystem::path& saves, const std::vector<std::string>& contentFiles)
|
||||
: mQuitRequest (false), mAskLoadRecent(false), mState (State_NoGame), mCharacterManager (saves, contentFiles), mTimePlayed (0)
|
||||
{
|
||||
|
||||
}
|
||||
|
@ -558,6 +558,8 @@ void MWState::StateManager::loadGame (const Character *character, const std::str
|
|||
// Since we passed "changeEvent=false" to changeCell, we shouldn't have triggered the cell change flag.
|
||||
// But make sure the flag is cleared anyway in case it was set from an earlier game.
|
||||
MWBase::Environment::get().getWorld()->markCellAsUnchanged();
|
||||
|
||||
MWBase::Environment::get().getLuaManager()->gameLoaded();
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
|
|
|
@ -31,7 +31,7 @@ namespace MWState
|
|||
|
||||
public:
|
||||
|
||||
StateManager (const boost::filesystem::path& saves, const std::string& game);
|
||||
StateManager (const boost::filesystem::path& saves, const std::vector<std::string>& contentFiles);
|
||||
|
||||
void requestQuit() override;
|
||||
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
#include "esmstore.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <set>
|
||||
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
|
||||
#include <components/debug/debuglog.hpp>
|
||||
#include <components/loadinglistener/loadinglistener.hpp>
|
||||
#include <components/esm/esmreader.hpp>
|
||||
#include <components/esm/esmwriter.hpp>
|
||||
#include <components/loadinglistener/loadinglistener.hpp>
|
||||
#include <components/lua/configuration.hpp>
|
||||
#include <components/misc/algorithm.hpp>
|
||||
|
||||
#include "../mwmechanics/spelllist.hpp"
|
||||
|
@ -166,7 +168,10 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener)
|
|||
std::string fname = mast.name;
|
||||
int index = ~0;
|
||||
for (int i = 0; i < esm.getIndex(); i++) {
|
||||
const std::string candidate = allPlugins->at(i).getContext().filename;
|
||||
ESM::ESMReader& reader = allPlugins->at(i);
|
||||
if (reader.getFileSize() == 0)
|
||||
continue; // Content file in non-ESM format
|
||||
const std::string candidate = reader.getContext().filename;
|
||||
std::string fnamecandidate = boost::filesystem::path(candidate).filename().string();
|
||||
if (Misc::StringUtils::ciEqual(fname, fnamecandidate)) {
|
||||
index = i;
|
||||
|
@ -213,6 +218,13 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener)
|
|||
// ignore project file only records
|
||||
esm.skipRecord();
|
||||
}
|
||||
else if (n.toInt() == ESM::REC_LUAL)
|
||||
{
|
||||
ESM::LuaScriptsCfg cfg;
|
||||
cfg.load(esm);
|
||||
// TODO: update refnums in cfg.mScripts[].mInitializationData according to load order
|
||||
mLuaContent.push_back(std::move(cfg));
|
||||
}
|
||||
else {
|
||||
throw std::runtime_error("Unknown record: " + n.toString());
|
||||
}
|
||||
|
@ -234,6 +246,32 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener)
|
|||
}
|
||||
}
|
||||
|
||||
ESM::LuaScriptsCfg ESMStore::getLuaScriptsCfg() const
|
||||
{
|
||||
ESM::LuaScriptsCfg cfg;
|
||||
for (const LuaContent& c : mLuaContent)
|
||||
{
|
||||
if (std::holds_alternative<std::string>(c))
|
||||
{
|
||||
// *.omwscripts are intentionally reloaded every time when `getLuaScriptsCfg` is called.
|
||||
// It is important for the `reloadlua` console command.
|
||||
try
|
||||
{
|
||||
auto file = std::ifstream(std::get<std::string>(c));
|
||||
std::string fileContent(std::istreambuf_iterator<char>(file), {});
|
||||
LuaUtil::parseOMWScripts(cfg, fileContent);
|
||||
}
|
||||
catch (std::exception& e) { Log(Debug::Error) << e.what(); }
|
||||
}
|
||||
else
|
||||
{
|
||||
const ESM::LuaScriptsCfg& addition = std::get<ESM::LuaScriptsCfg>(c);
|
||||
cfg.mScripts.insert(cfg.mScripts.end(), addition.mScripts.begin(), addition.mScripts.end());
|
||||
}
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
void ESMStore::setUp(bool validateRecords)
|
||||
{
|
||||
mIds.clear();
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <components/esm/luascripts.hpp>
|
||||
#include <components/esm/records.hpp>
|
||||
#include "store.hpp"
|
||||
|
||||
|
@ -92,7 +93,16 @@ namespace MWWorld
|
|||
|
||||
template<class T>
|
||||
void removeMissingObjects(Store<T>& store);
|
||||
|
||||
using LuaContent = std::variant<
|
||||
ESM::LuaScriptsCfg, // data from an omwaddon
|
||||
std::string>; // path to an omwscripts file
|
||||
std::vector<LuaContent> mLuaContent;
|
||||
|
||||
public:
|
||||
void addOMWScripts(std::string filePath) { mLuaContent.push_back(std::move(filePath)); }
|
||||
ESM::LuaScriptsCfg getLuaScriptsCfg() const;
|
||||
|
||||
/// \todo replace with SharedIterator<StoreBase>
|
||||
typedef std::map<int, StoreBase *>::const_iterator iterator;
|
||||
|
||||
|
|
|
@ -111,6 +111,17 @@ namespace MWWorld
|
|||
LoadersContainer mLoaders;
|
||||
};
|
||||
|
||||
struct OMWScriptsLoader : public ContentLoader
|
||||
{
|
||||
ESMStore& mStore;
|
||||
OMWScriptsLoader(Loading::Listener& listener, ESMStore& store) : ContentLoader(listener), mStore(store) {}
|
||||
void load(const boost::filesystem::path& filepath, int& index) override
|
||||
{
|
||||
ContentLoader::load(filepath.filename(), index);
|
||||
mStore.addOMWScripts(filepath.string());
|
||||
}
|
||||
};
|
||||
|
||||
void World::adjustSky()
|
||||
{
|
||||
if (mSky && (isCellExterior() || isCellQuasiExterior()))
|
||||
|
@ -156,6 +167,9 @@ namespace MWWorld
|
|||
gameContentLoader.addLoader(".omwaddon", &esmLoader);
|
||||
gameContentLoader.addLoader(".project", &esmLoader);
|
||||
|
||||
OMWScriptsLoader omwScriptsLoader(*listener, mStore);
|
||||
gameContentLoader.addLoader(".omwscripts", &omwScriptsLoader);
|
||||
|
||||
loadContentFiles(fileCollections, contentFiles, groundcoverFiles, gameContentLoader);
|
||||
|
||||
listener->loadingOff();
|
||||
|
|
|
@ -40,14 +40,11 @@ namespace OpenMW
|
|||
"set initial cell")
|
||||
|
||||
("content", bpo::value<Files::EscapeStringVector>()->default_value(Files::EscapeStringVector(), "")
|
||||
->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon")
|
||||
->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon/omwscripts")
|
||||
|
||||
("groundcover", bpo::value<Files::EscapeStringVector>()->default_value(Files::EscapeStringVector(), "")
|
||||
->multitoken()->composing(), "groundcover content file(s): esm/esp, or omwgame/omwaddon")
|
||||
|
||||
("lua-scripts", bpo::value<Files::EscapeStringVector>()->default_value(Files::EscapeStringVector(), "")
|
||||
->multitoken()->composing(), "file(s) with a list of global Lua scripts: omwscripts")
|
||||
|
||||
("no-sound", bpo::value<bool>()->implicit_value(true)
|
||||
->default_value(false), "disable all sounds")
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ if (GTEST_FOUND AND GMOCK_FOUND)
|
|||
lua/test_utilpackage.cpp
|
||||
lua/test_serialization.cpp
|
||||
lua/test_querypackage.cpp
|
||||
lua/test_omwscriptsparser.cpp
|
||||
lua/test_configuration.cpp
|
||||
|
||||
misc/test_stringops.cpp
|
||||
misc/test_endianness.cpp
|
||||
|
|
58
apps/openmw_test_suite/lua/test_configuration.cpp
Normal file
58
apps/openmw_test_suite/lua/test_configuration.cpp
Normal file
|
@ -0,0 +1,58 @@
|
|||
#include "gmock/gmock.h"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <components/lua/configuration.hpp>
|
||||
|
||||
#include "testing_util.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
TEST(LuaConfigurationTest, ValidConfiguration)
|
||||
{
|
||||
ESM::LuaScriptsCfg cfg;
|
||||
LuaUtil::parseOMWScripts(cfg, R"X(
|
||||
# Lines starting with '#' are comments
|
||||
GLOBAL: my_mod/#some_global_script.lua
|
||||
|
||||
# Script that will be automatically attached to the player
|
||||
PLAYER :my_mod/player.lua
|
||||
CUSTOM : my_mod/some_other_script.lua
|
||||
NPC , CREATURE PLAYER : my_mod/some_other_script.lua)X");
|
||||
LuaUtil::parseOMWScripts(cfg, ":my_mod/player.LUA \r\nCONTAINER,CUSTOM: my_mod/container.lua\r\n");
|
||||
|
||||
ASSERT_EQ(cfg.mScripts.size(), 6);
|
||||
EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[0]), "GLOBAL : my_mod/#some_global_script.lua");
|
||||
EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[1]), "PLAYER : my_mod/player.lua");
|
||||
EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[2]), "CUSTOM : my_mod/some_other_script.lua");
|
||||
EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[3]), "CREATURE NPC PLAYER : my_mod/some_other_script.lua");
|
||||
EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[4]), ": my_mod/player.LUA");
|
||||
EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[5]), "CONTAINER CUSTOM : my_mod/container.lua");
|
||||
|
||||
LuaUtil::ScriptsConfiguration conf;
|
||||
conf.init(std::move(cfg));
|
||||
ASSERT_EQ(conf.size(), 3);
|
||||
EXPECT_EQ(LuaUtil::scriptCfgToString(conf[0]), "GLOBAL : my_mod/#some_global_script.lua");
|
||||
// cfg.mScripts[1] is overridden by cfg.mScripts[4]
|
||||
// cfg.mScripts[2] is overridden by cfg.mScripts[3]
|
||||
EXPECT_EQ(LuaUtil::scriptCfgToString(conf[1]), "CREATURE NPC PLAYER : my_mod/some_other_script.lua");
|
||||
// cfg.mScripts[4] is removed because there are no flags
|
||||
EXPECT_EQ(LuaUtil::scriptCfgToString(conf[2]), "CONTAINER CUSTOM : my_mod/container.lua");
|
||||
|
||||
cfg = ESM::LuaScriptsCfg();
|
||||
conf.init(std::move(cfg));
|
||||
ASSERT_EQ(conf.size(), 0);
|
||||
}
|
||||
|
||||
TEST(LuaConfigurationTest, Errors)
|
||||
{
|
||||
ESM::LuaScriptsCfg cfg;
|
||||
EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "GLOBAL: something"),
|
||||
"Lua script should have suffix '.lua', got: GLOBAL: something");
|
||||
EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "something.lua"),
|
||||
"No flags found in: something.lua");
|
||||
EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "GLOBAL, PLAYER: something.lua"),
|
||||
"Global script can not have local flags");
|
||||
}
|
||||
|
||||
}
|
|
@ -58,7 +58,8 @@ return {
|
|||
{"invalid.lua", &invalidScriptFile}
|
||||
});
|
||||
|
||||
LuaUtil::LuaState mLua{mVFS.get()};
|
||||
LuaUtil::ScriptsConfiguration mCfg;
|
||||
LuaUtil::LuaState mLua{mVFS.get(), &mCfg};
|
||||
};
|
||||
|
||||
TEST_F(LuaStateTest, Sandbox)
|
||||
|
@ -148,7 +149,7 @@ return {
|
|||
|
||||
TEST_F(LuaStateTest, ProvideAPI)
|
||||
{
|
||||
LuaUtil::LuaState lua(mVFS.get());
|
||||
LuaUtil::LuaState lua(mVFS.get(), &mCfg);
|
||||
|
||||
sol::table api1 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api1"));
|
||||
sol::table api2 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api2"));
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
#include "gmock/gmock.h"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <components/lua/omwscriptsparser.hpp>
|
||||
|
||||
#include "testing_util.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace testing;
|
||||
|
||||
TestFile file1(
|
||||
"#comment.lua\n"
|
||||
"\n"
|
||||
"script1.lua\n"
|
||||
"some mod/Some Script.lua"
|
||||
);
|
||||
TestFile file2(
|
||||
"#comment.lua\r\n"
|
||||
"\r\n"
|
||||
"script2.lua\r\n"
|
||||
"some other mod/Some Script.lua\r"
|
||||
);
|
||||
TestFile emptyFile("");
|
||||
TestFile invalidFile("Invalid file");
|
||||
|
||||
struct OMWScriptsParserTest : Test
|
||||
{
|
||||
std::unique_ptr<VFS::Manager> mVFS = createTestVFS({
|
||||
{"file1.omwscripts", &file1},
|
||||
{"file2.omwscripts", &file2},
|
||||
{"empty.omwscripts", &emptyFile},
|
||||
{"invalid.lua", &file1},
|
||||
{"invalid.omwscripts", &invalidFile},
|
||||
});
|
||||
};
|
||||
|
||||
TEST_F(OMWScriptsParserTest, Basic)
|
||||
{
|
||||
internal::CaptureStdout();
|
||||
std::vector<std::string> res = LuaUtil::parseOMWScriptsFiles(
|
||||
mVFS.get(), {"file2.omwscripts", "empty.omwscripts", "file1.omwscripts"});
|
||||
EXPECT_EQ(internal::GetCapturedStdout(), "");
|
||||
EXPECT_THAT(res, ElementsAre("script2.lua", "some other mod/Some Script.lua",
|
||||
"script1.lua", "some mod/Some Script.lua"));
|
||||
}
|
||||
|
||||
TEST_F(OMWScriptsParserTest, InvalidFiles)
|
||||
{
|
||||
internal::CaptureStdout();
|
||||
std::vector<std::string> res = LuaUtil::parseOMWScriptsFiles(
|
||||
mVFS.get(), {"invalid.lua", "invalid.omwscripts"});
|
||||
EXPECT_EQ(internal::GetCapturedStdout(),
|
||||
"Script list should have suffix '.omwscripts', got: 'invalid.lua'\n"
|
||||
"Lua script should have suffix '.lua', got: 'Invalid file'\n");
|
||||
EXPECT_THAT(res, ElementsAre());
|
||||
}
|
||||
|
||||
}
|
|
@ -18,7 +18,10 @@ namespace
|
|||
|
||||
TestFile testScript(R"X(
|
||||
return {
|
||||
engineHandlers = { onUpdate = function(dt) print(' update ' .. tostring(dt)) end },
|
||||
engineHandlers = {
|
||||
onUpdate = function(dt) print(' update ' .. tostring(dt)) end,
|
||||
onLoad = function() print('load') end,
|
||||
},
|
||||
eventHandlers = {
|
||||
Event1 = function(eventData) print(' event1 ' .. tostring(eventData.x)) end,
|
||||
Event2 = function(eventData) print(' event2 ' .. tostring(eventData.x)) end,
|
||||
|
@ -75,15 +78,25 @@ return {
|
|||
)X");
|
||||
|
||||
TestFile overrideInterfaceScript(R"X(
|
||||
local old = require('openmw.interfaces').TestInterface
|
||||
local old = nil
|
||||
local interface = {
|
||||
fn = function(x)
|
||||
print('NEW FN', x)
|
||||
old.fn(x)
|
||||
end,
|
||||
value,
|
||||
}
|
||||
return {
|
||||
interfaceName = "TestInterface",
|
||||
interface = {
|
||||
fn = function(x)
|
||||
print('NEW FN', x)
|
||||
old.fn(x)
|
||||
end,
|
||||
value = old.value + 1
|
||||
interface = interface,
|
||||
engineHandlers = {
|
||||
onInit = function() print('init') end,
|
||||
onLoad = function() print('load') end,
|
||||
onInterfaceOverride = function(oldInterface)
|
||||
print('override')
|
||||
old = oldInterface
|
||||
interface.value = oldInterface.value + 1
|
||||
end
|
||||
},
|
||||
}
|
||||
)X");
|
||||
|
@ -115,7 +128,25 @@ return {
|
|||
{"useInterface.lua", &useInterfaceScript},
|
||||
});
|
||||
|
||||
LuaUtil::LuaState mLua{mVFS.get()};
|
||||
LuaUtil::ScriptsConfiguration mCfg;
|
||||
LuaUtil::LuaState mLua{mVFS.get(), &mCfg};
|
||||
|
||||
LuaScriptsContainerTest()
|
||||
{
|
||||
ESM::LuaScriptsCfg cfg;
|
||||
cfg.mScripts.push_back({"invalid.lua", "", ESM::LuaScriptCfg::sCustom});
|
||||
cfg.mScripts.push_back({"incorrect.lua", "", ESM::LuaScriptCfg::sCustom});
|
||||
cfg.mScripts.push_back({"empty.lua", "", ESM::LuaScriptCfg::sCustom});
|
||||
cfg.mScripts.push_back({"test1.lua", "", ESM::LuaScriptCfg::sCustom});
|
||||
cfg.mScripts.push_back({"stopEvent.lua", "", ESM::LuaScriptCfg::sCustom});
|
||||
cfg.mScripts.push_back({"test2.lua", "", ESM::LuaScriptCfg::sCustom});
|
||||
cfg.mScripts.push_back({"loadSave1.lua", "", ESM::LuaScriptCfg::sNPC});
|
||||
cfg.mScripts.push_back({"loadSave2.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sNPC});
|
||||
cfg.mScripts.push_back({"testInterface.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sPlayer});
|
||||
cfg.mScripts.push_back({"overrideInterface.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sPlayer});
|
||||
cfg.mScripts.push_back({"useInterface.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sPlayer});
|
||||
mCfg.init(std::move(cfg));
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(LuaScriptsContainerTest, VerifyStructure)
|
||||
|
@ -123,21 +154,21 @@ return {
|
|||
LuaUtil::ScriptsContainer scripts(&mLua, "Test");
|
||||
{
|
||||
testing::internal::CaptureStdout();
|
||||
EXPECT_FALSE(scripts.addNewScript("invalid.lua"));
|
||||
EXPECT_FALSE(scripts.addCustomScript(*mCfg.findId("invalid.lua")));
|
||||
std::string output = testing::internal::GetCapturedStdout();
|
||||
EXPECT_THAT(output, HasSubstr("Can't start Test[invalid.lua]"));
|
||||
}
|
||||
{
|
||||
testing::internal::CaptureStdout();
|
||||
EXPECT_TRUE(scripts.addNewScript("incorrect.lua"));
|
||||
EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("incorrect.lua")));
|
||||
std::string output = testing::internal::GetCapturedStdout();
|
||||
EXPECT_THAT(output, HasSubstr("Not supported handler 'incorrectHandler' in Test[incorrect.lua]"));
|
||||
EXPECT_THAT(output, HasSubstr("Not supported section 'incorrectSection' in Test[incorrect.lua]"));
|
||||
}
|
||||
{
|
||||
testing::internal::CaptureStdout();
|
||||
EXPECT_TRUE(scripts.addNewScript("empty.lua"));
|
||||
EXPECT_FALSE(scripts.addNewScript("empty.lua")); // already present
|
||||
EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("empty.lua")));
|
||||
EXPECT_FALSE(scripts.addCustomScript(*mCfg.findId("empty.lua"))); // already present
|
||||
EXPECT_EQ(internal::GetCapturedStdout(), "");
|
||||
}
|
||||
}
|
||||
|
@ -146,9 +177,9 @@ return {
|
|||
{
|
||||
LuaUtil::ScriptsContainer scripts(&mLua, "Test");
|
||||
testing::internal::CaptureStdout();
|
||||
EXPECT_TRUE(scripts.addNewScript("test1.lua"));
|
||||
EXPECT_TRUE(scripts.addNewScript("stopEvent.lua"));
|
||||
EXPECT_TRUE(scripts.addNewScript("test2.lua"));
|
||||
EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test1.lua")));
|
||||
EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("stopEvent.lua")));
|
||||
EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test2.lua")));
|
||||
scripts.update(1.5f);
|
||||
EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua]:\t update 1.5\n"
|
||||
"Test[test2.lua]:\t update 1.5\n");
|
||||
|
@ -157,9 +188,9 @@ return {
|
|||
TEST_F(LuaScriptsContainerTest, CallEvent)
|
||||
{
|
||||
LuaUtil::ScriptsContainer scripts(&mLua, "Test");
|
||||
EXPECT_TRUE(scripts.addNewScript("test1.lua"));
|
||||
EXPECT_TRUE(scripts.addNewScript("stopEvent.lua"));
|
||||
EXPECT_TRUE(scripts.addNewScript("test2.lua"));
|
||||
EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test1.lua")));
|
||||
EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("stopEvent.lua")));
|
||||
EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test2.lua")));
|
||||
|
||||
std::string X0 = LuaUtil::serialize(mLua.sol().create_table_with("x", 0.5));
|
||||
std::string X1 = LuaUtil::serialize(mLua.sol().create_table_with("x", 1.5));
|
||||
|
@ -204,9 +235,9 @@ return {
|
|||
TEST_F(LuaScriptsContainerTest, RemoveScript)
|
||||
{
|
||||
LuaUtil::ScriptsContainer scripts(&mLua, "Test");
|
||||
EXPECT_TRUE(scripts.addNewScript("test1.lua"));
|
||||
EXPECT_TRUE(scripts.addNewScript("stopEvent.lua"));
|
||||
EXPECT_TRUE(scripts.addNewScript("test2.lua"));
|
||||
EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test1.lua")));
|
||||
EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("stopEvent.lua")));
|
||||
EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test2.lua")));
|
||||
std::string X = LuaUtil::serialize(mLua.sol().create_table_with("x", 0.5));
|
||||
|
||||
{
|
||||
|
@ -221,8 +252,10 @@ return {
|
|||
}
|
||||
{
|
||||
testing::internal::CaptureStdout();
|
||||
EXPECT_TRUE(scripts.removeScript("stopEvent.lua"));
|
||||
EXPECT_FALSE(scripts.removeScript("stopEvent.lua")); // already removed
|
||||
int stopEventScriptId = *mCfg.findId("stopEvent.lua");
|
||||
EXPECT_TRUE(scripts.hasScript(stopEventScriptId));
|
||||
scripts.removeScript(stopEventScriptId);
|
||||
EXPECT_FALSE(scripts.hasScript(stopEventScriptId));
|
||||
scripts.update(1.5f);
|
||||
scripts.receiveEvent("Event1", X);
|
||||
EXPECT_EQ(internal::GetCapturedStdout(),
|
||||
|
@ -233,7 +266,7 @@ return {
|
|||
}
|
||||
{
|
||||
testing::internal::CaptureStdout();
|
||||
EXPECT_TRUE(scripts.removeScript("test1.lua"));
|
||||
scripts.removeScript(*mCfg.findId("test1.lua"));
|
||||
scripts.update(1.5f);
|
||||
scripts.receiveEvent("Event1", X);
|
||||
EXPECT_EQ(internal::GetCapturedStdout(),
|
||||
|
@ -242,17 +275,41 @@ return {
|
|||
}
|
||||
}
|
||||
|
||||
TEST_F(LuaScriptsContainerTest, Interface)
|
||||
TEST_F(LuaScriptsContainerTest, AutoStart)
|
||||
{
|
||||
LuaUtil::ScriptsContainer scripts(&mLua, "Test");
|
||||
LuaUtil::ScriptsContainer scripts(&mLua, "Test", ESM::LuaScriptCfg::sPlayer);
|
||||
testing::internal::CaptureStdout();
|
||||
EXPECT_TRUE(scripts.addNewScript("testInterface.lua"));
|
||||
EXPECT_TRUE(scripts.addNewScript("overrideInterface.lua"));
|
||||
EXPECT_TRUE(scripts.addNewScript("useInterface.lua"));
|
||||
scripts.update(1.5f);
|
||||
EXPECT_TRUE(scripts.removeScript("overrideInterface.lua"));
|
||||
scripts.addAutoStartedScripts();
|
||||
scripts.update(1.5f);
|
||||
EXPECT_EQ(internal::GetCapturedStdout(),
|
||||
"Test[overrideInterface.lua]:\toverride\n"
|
||||
"Test[overrideInterface.lua]:\tinit\n"
|
||||
"Test[overrideInterface.lua]:\tNEW FN\t4.5\n"
|
||||
"Test[testInterface.lua]:\tFN\t4.5\n");
|
||||
}
|
||||
|
||||
TEST_F(LuaScriptsContainerTest, Interface)
|
||||
{
|
||||
LuaUtil::ScriptsContainer scripts(&mLua, "Test", ESM::LuaScriptCfg::sCreature);
|
||||
int addIfaceId = *mCfg.findId("testInterface.lua");
|
||||
int overrideIfaceId = *mCfg.findId("overrideInterface.lua");
|
||||
int useIfaceId = *mCfg.findId("useInterface.lua");
|
||||
|
||||
testing::internal::CaptureStdout();
|
||||
scripts.addAutoStartedScripts();
|
||||
scripts.update(1.5f);
|
||||
EXPECT_EQ(internal::GetCapturedStdout(), "");
|
||||
|
||||
testing::internal::CaptureStdout();
|
||||
EXPECT_TRUE(scripts.addCustomScript(addIfaceId));
|
||||
EXPECT_TRUE(scripts.addCustomScript(overrideIfaceId));
|
||||
EXPECT_TRUE(scripts.addCustomScript(useIfaceId));
|
||||
scripts.update(1.5f);
|
||||
scripts.removeScript(overrideIfaceId);
|
||||
scripts.update(1.5f);
|
||||
EXPECT_EQ(internal::GetCapturedStdout(),
|
||||
"Test[overrideInterface.lua]:\toverride\n"
|
||||
"Test[overrideInterface.lua]:\tinit\n"
|
||||
"Test[overrideInterface.lua]:\tNEW FN\t4.5\n"
|
||||
"Test[testInterface.lua]:\tFN\t4.5\n"
|
||||
"Test[testInterface.lua]:\tFN\t3.5\n");
|
||||
|
@ -260,16 +317,12 @@ return {
|
|||
|
||||
TEST_F(LuaScriptsContainerTest, LoadSave)
|
||||
{
|
||||
LuaUtil::ScriptsContainer scripts1(&mLua, "Test");
|
||||
LuaUtil::ScriptsContainer scripts2(&mLua, "Test");
|
||||
LuaUtil::ScriptsContainer scripts3(&mLua, "Test");
|
||||
LuaUtil::ScriptsContainer scripts1(&mLua, "Test", ESM::LuaScriptCfg::sNPC);
|
||||
LuaUtil::ScriptsContainer scripts2(&mLua, "Test", ESM::LuaScriptCfg::sNPC);
|
||||
LuaUtil::ScriptsContainer scripts3(&mLua, "Test", ESM::LuaScriptCfg::sPlayer);
|
||||
|
||||
EXPECT_TRUE(scripts1.addNewScript("loadSave1.lua"));
|
||||
EXPECT_TRUE(scripts1.addNewScript("test1.lua"));
|
||||
EXPECT_TRUE(scripts1.addNewScript("loadSave2.lua"));
|
||||
|
||||
EXPECT_TRUE(scripts3.addNewScript("test2.lua"));
|
||||
EXPECT_TRUE(scripts3.addNewScript("loadSave2.lua"));
|
||||
scripts1.addAutoStartedScripts();
|
||||
EXPECT_TRUE(scripts1.addCustomScript(*mCfg.findId("test1.lua")));
|
||||
|
||||
scripts1.receiveEvent("Set", LuaUtil::serialize(mLua.sol().create_table_with(
|
||||
"n", 1,
|
||||
|
@ -282,23 +335,30 @@ return {
|
|||
|
||||
ESM::LuaScripts data;
|
||||
scripts1.save(data);
|
||||
scripts2.load(data, true);
|
||||
scripts3.load(data, false);
|
||||
|
||||
{
|
||||
testing::internal::CaptureStdout();
|
||||
scripts2.load(data);
|
||||
scripts2.receiveEvent("Print", "");
|
||||
EXPECT_EQ(internal::GetCapturedStdout(),
|
||||
"Test[test1.lua]:\tload\n"
|
||||
"Test[loadSave2.lua]:\t0.5\t3.5\n"
|
||||
"Test[test1.lua]:\tprint\n"
|
||||
"Test[loadSave1.lua]:\t2.5\t1.5\n");
|
||||
"Test[loadSave1.lua]:\t2.5\t1.5\n"
|
||||
"Test[test1.lua]:\tprint\n");
|
||||
EXPECT_FALSE(scripts2.hasScript(*mCfg.findId("testInterface.lua")));
|
||||
}
|
||||
{
|
||||
testing::internal::CaptureStdout();
|
||||
scripts3.load(data);
|
||||
scripts3.receiveEvent("Print", "");
|
||||
EXPECT_EQ(internal::GetCapturedStdout(),
|
||||
"Ignoring Test[loadSave1.lua]; this script is not allowed here\n"
|
||||
"Test[test1.lua]:\tload\n"
|
||||
"Test[overrideInterface.lua]:\toverride\n"
|
||||
"Test[overrideInterface.lua]:\tinit\n"
|
||||
"Test[loadSave2.lua]:\t0.5\t3.5\n"
|
||||
"Test[test2.lua]:\tprint\n");
|
||||
"Test[test1.lua]:\tprint\n");
|
||||
EXPECT_TRUE(scripts3.hasScript(*mCfg.findId("testInterface.lua")));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -306,8 +366,13 @@ return {
|
|||
{
|
||||
using TimeUnit = LuaUtil::ScriptsContainer::TimeUnit;
|
||||
LuaUtil::ScriptsContainer scripts(&mLua, "Test");
|
||||
EXPECT_TRUE(scripts.addNewScript("test1.lua"));
|
||||
EXPECT_TRUE(scripts.addNewScript("test2.lua"));
|
||||
int test1Id = *mCfg.findId("test1.lua");
|
||||
int test2Id = *mCfg.findId("test2.lua");
|
||||
|
||||
testing::internal::CaptureStdout();
|
||||
EXPECT_TRUE(scripts.addCustomScript(test1Id));
|
||||
EXPECT_TRUE(scripts.addCustomScript(test2Id));
|
||||
EXPECT_EQ(internal::GetCapturedStdout(), "");
|
||||
|
||||
int counter1 = 0, counter2 = 0, counter3 = 0, counter4 = 0;
|
||||
sol::function fn1 = sol::make_object(mLua.sol(), [&]() { counter1++; });
|
||||
|
@ -315,25 +380,25 @@ return {
|
|||
sol::function fn3 = sol::make_object(mLua.sol(), [&](int d) { counter3 += d; });
|
||||
sol::function fn4 = sol::make_object(mLua.sol(), [&](int d) { counter4 += d; });
|
||||
|
||||
scripts.registerTimerCallback("test1.lua", "A", fn3);
|
||||
scripts.registerTimerCallback("test1.lua", "B", fn4);
|
||||
scripts.registerTimerCallback("test2.lua", "B", fn3);
|
||||
scripts.registerTimerCallback("test2.lua", "A", fn4);
|
||||
scripts.registerTimerCallback(test1Id, "A", fn3);
|
||||
scripts.registerTimerCallback(test1Id, "B", fn4);
|
||||
scripts.registerTimerCallback(test2Id, "B", fn3);
|
||||
scripts.registerTimerCallback(test2Id, "A", fn4);
|
||||
|
||||
scripts.processTimers(1, 2);
|
||||
|
||||
scripts.setupSerializableTimer(TimeUnit::SECONDS, 10, "test1.lua", "B", sol::make_object(mLua.sol(), 3));
|
||||
scripts.setupSerializableTimer(TimeUnit::HOURS, 10, "test2.lua", "B", sol::make_object(mLua.sol(), 4));
|
||||
scripts.setupSerializableTimer(TimeUnit::SECONDS, 5, "test1.lua", "A", sol::make_object(mLua.sol(), 1));
|
||||
scripts.setupSerializableTimer(TimeUnit::HOURS, 5, "test2.lua", "A", sol::make_object(mLua.sol(), 2));
|
||||
scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, "test1.lua", "A", sol::make_object(mLua.sol(), 10));
|
||||
scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, "test1.lua", "B", sol::make_object(mLua.sol(), 20));
|
||||
scripts.setupSerializableTimer(TimeUnit::SECONDS, 10, test1Id, "B", sol::make_object(mLua.sol(), 3));
|
||||
scripts.setupSerializableTimer(TimeUnit::HOURS, 10, test2Id, "B", sol::make_object(mLua.sol(), 4));
|
||||
scripts.setupSerializableTimer(TimeUnit::SECONDS, 5, test1Id, "A", sol::make_object(mLua.sol(), 1));
|
||||
scripts.setupSerializableTimer(TimeUnit::HOURS, 5, test2Id, "A", sol::make_object(mLua.sol(), 2));
|
||||
scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, test1Id, "A", sol::make_object(mLua.sol(), 10));
|
||||
scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, test1Id, "B", sol::make_object(mLua.sol(), 20));
|
||||
|
||||
scripts.setupUnsavableTimer(TimeUnit::SECONDS, 10, "test2.lua", fn2);
|
||||
scripts.setupUnsavableTimer(TimeUnit::HOURS, 10, "test1.lua", fn2);
|
||||
scripts.setupUnsavableTimer(TimeUnit::SECONDS, 5, "test2.lua", fn1);
|
||||
scripts.setupUnsavableTimer(TimeUnit::HOURS, 5, "test1.lua", fn1);
|
||||
scripts.setupUnsavableTimer(TimeUnit::SECONDS, 15, "test2.lua", fn1);
|
||||
scripts.setupUnsavableTimer(TimeUnit::SECONDS, 10, test2Id, fn2);
|
||||
scripts.setupUnsavableTimer(TimeUnit::HOURS, 10, test1Id, fn2);
|
||||
scripts.setupUnsavableTimer(TimeUnit::SECONDS, 5, test2Id, fn1);
|
||||
scripts.setupUnsavableTimer(TimeUnit::HOURS, 5, test1Id, fn1);
|
||||
scripts.setupUnsavableTimer(TimeUnit::SECONDS, 15, test2Id, fn1);
|
||||
|
||||
EXPECT_EQ(counter1, 0);
|
||||
EXPECT_EQ(counter3, 0);
|
||||
|
@ -358,10 +423,12 @@ return {
|
|||
EXPECT_EQ(counter3, 5);
|
||||
EXPECT_EQ(counter4, 5);
|
||||
|
||||
testing::internal::CaptureStdout();
|
||||
ESM::LuaScripts data;
|
||||
scripts.save(data);
|
||||
scripts.load(data, true);
|
||||
scripts.registerTimerCallback("test1.lua", "B", fn4);
|
||||
scripts.load(data);
|
||||
scripts.registerTimerCallback(test1Id, "B", fn4);
|
||||
EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua]:\tload\nTest[test2.lua]:\tload\n");
|
||||
|
||||
testing::internal::CaptureStdout();
|
||||
scripts.processTimers(20, 20);
|
||||
|
|
|
@ -52,7 +52,7 @@ namespace
|
|||
}
|
||||
|
||||
#define EXPECT_ERROR(X, ERR_SUBSTR) try { X; FAIL() << "Expected error"; } \
|
||||
catch (std::exception& e) { EXPECT_THAT(e.what(), HasSubstr(ERR_SUBSTR)); }
|
||||
catch (std::exception& e) { EXPECT_THAT(e.what(), ::testing::HasSubstr(ERR_SUBSTR)); }
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#include <components/esm/esmreader.hpp>
|
||||
#include <components/esm/esmwriter.hpp>
|
||||
#include <components/loadinglistener/loadinglistener.hpp>
|
||||
#include <components/misc/stringops.hpp>
|
||||
|
||||
#include "apps/openmw/mwworld/esmstore.hpp"
|
||||
#include "apps/openmw/mwmechanics/spelllist.hpp"
|
||||
|
@ -88,7 +89,10 @@ struct ContentFileTest : public ::testing::Test
|
|||
|
||||
std::vector<std::string> contentFiles = variables["content"].as<Files::EscapeStringVector>().toStdStringVector();
|
||||
for (auto & contentFile : contentFiles)
|
||||
mContentFiles.push_back(collections.getPath(contentFile));
|
||||
{
|
||||
if (!Misc::StringUtils::ciEndsWith(contentFile, ".omwscripts"))
|
||||
mContentFiles.push_back(collections.getPath(contentFile));
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
|
|
|
@ -29,7 +29,7 @@ endif (GIT_CHECKOUT)
|
|||
# source files
|
||||
|
||||
add_component_dir (lua
|
||||
luastate scriptscontainer utilpackage serialization omwscriptsparser
|
||||
luastate scriptscontainer utilpackage serialization configuration
|
||||
)
|
||||
|
||||
add_component_dir (settings
|
||||
|
|
|
@ -165,6 +165,7 @@ enum RecNameInts
|
|||
// format 1
|
||||
REC_FILT = FourCC<'F','I','L','T'>::value,
|
||||
REC_DBGP = FourCC<'D','B','G','P'>::value, ///< only used in project files
|
||||
REC_LUAL = FourCC<'L','U','A','L'>::value, // LuaScriptsCfg
|
||||
|
||||
// format 16 - Lua scripts in saved games
|
||||
REC_LUAM = FourCC<'L','U','A','M'>::value, // LuaManager data
|
||||
|
|
|
@ -5,13 +5,15 @@
|
|||
|
||||
// List of all records, that are related to Lua.
|
||||
//
|
||||
// Record:
|
||||
// LUAM - MWLua::LuaManager
|
||||
// Records:
|
||||
// LUAL - LuaScriptsCfg - list of all scripts (in content files)
|
||||
// LUAM - MWLua::LuaManager (in saves)
|
||||
//
|
||||
// Subrecords:
|
||||
// LUAF - LuaScriptCfg::mFlags
|
||||
// LUAW - Start of MWLua::WorldView data
|
||||
// LUAE - Start of MWLua::LocalEvent or MWLua::GlobalEvent (eventName)
|
||||
// LUAS - Start LuaUtil::ScriptsContainer data (scriptName)
|
||||
// LUAS - VFS path to a Lua script
|
||||
// LUAD - Serialized Lua variable
|
||||
// LUAT - MWLua::ScriptsContainer::Timer
|
||||
// LUAC - Name of a timer callback (string)
|
||||
|
@ -37,6 +39,28 @@ std::string ESM::loadLuaBinaryData(ESMReader& esm)
|
|||
return data;
|
||||
}
|
||||
|
||||
void ESM::LuaScriptsCfg::load(ESMReader& esm)
|
||||
{
|
||||
while (esm.isNextSub("LUAS"))
|
||||
{
|
||||
std::string name = esm.getHString();
|
||||
uint64_t flags;
|
||||
esm.getHNT(flags, "LUAF");
|
||||
std::string data = loadLuaBinaryData(esm);
|
||||
mScripts.push_back({std::move(name), std::move(data), flags});
|
||||
}
|
||||
}
|
||||
|
||||
void ESM::LuaScriptsCfg::save(ESMWriter& esm) const
|
||||
{
|
||||
for (const LuaScriptCfg& script : mScripts)
|
||||
{
|
||||
esm.writeHNString("LUAS", script.mScriptPath);
|
||||
esm.writeHNT("LUAF", script.mFlags);
|
||||
saveLuaBinaryData(esm, script.mInitializationData);
|
||||
}
|
||||
}
|
||||
|
||||
void ESM::LuaScripts::load(ESMReader& esm)
|
||||
{
|
||||
while (esm.isNextSub("LUAS"))
|
||||
|
@ -63,8 +87,7 @@ void ESM::LuaScripts::save(ESMWriter& esm) const
|
|||
for (const LuaScript& script : mScripts)
|
||||
{
|
||||
esm.writeHNString("LUAS", script.mScriptPath);
|
||||
if (!script.mData.empty())
|
||||
saveLuaBinaryData(esm, script.mData);
|
||||
saveLuaBinaryData(esm, script.mData);
|
||||
for (const LuaTimer& timer : script.mTimers)
|
||||
{
|
||||
esm.startSubRecord("LUAT");
|
||||
|
|
|
@ -9,7 +9,44 @@ namespace ESM
|
|||
class ESMReader;
|
||||
class ESMWriter;
|
||||
|
||||
// Storage structure for LuaUtil::ScriptsContainer. This is not a top-level record.
|
||||
// LuaScriptCfg, LuaScriptsCfg are used in content files.
|
||||
|
||||
struct LuaScriptCfg
|
||||
{
|
||||
using Flags = uint64_t;
|
||||
static constexpr Flags sGlobal = 1ull << 0;
|
||||
static constexpr Flags sCustom = 1ull << 1; // local; can be attached/detached by a global script
|
||||
static constexpr Flags sPlayer = 1ull << 2; // auto attach to players
|
||||
// auto attach for other classes:
|
||||
static constexpr Flags sActivator = 1ull << 3;
|
||||
static constexpr Flags sArmor = 1ull << 4;
|
||||
static constexpr Flags sBook = 1ull << 5;
|
||||
static constexpr Flags sClothing = 1ull << 6;
|
||||
static constexpr Flags sContainer = 1ull << 7;
|
||||
static constexpr Flags sCreature = 1ull << 8;
|
||||
static constexpr Flags sDoor = 1ull << 9;
|
||||
static constexpr Flags sIngredient = 1ull << 10;
|
||||
static constexpr Flags sLight = 1ull << 11;
|
||||
static constexpr Flags sMiscItem = 1ull << 12;
|
||||
static constexpr Flags sNPC = 1ull << 13;
|
||||
static constexpr Flags sPotion = 1ull << 14;
|
||||
static constexpr Flags sWeapon = 1ull << 15;
|
||||
|
||||
std::string mScriptPath;
|
||||
std::string mInitializationData; // Serialized Lua table. It is a binary data. Can contain '\0'.
|
||||
Flags mFlags; // bitwise OR of Flags.
|
||||
};
|
||||
|
||||
struct LuaScriptsCfg
|
||||
{
|
||||
std::vector<LuaScriptCfg> mScripts;
|
||||
|
||||
void load(ESMReader &esm);
|
||||
void save(ESMWriter &esm) const;
|
||||
};
|
||||
|
||||
// LuaTimer, LuaScript, LuaScripts are used in saved game files.
|
||||
// Storage structure for LuaUtil::ScriptsContainer. These are not top-level records.
|
||||
// Used either for global scripts or for local scripts on a specific object.
|
||||
|
||||
struct LuaTimer
|
||||
|
@ -37,11 +74,11 @@ namespace ESM
|
|||
{
|
||||
std::vector<LuaScript> mScripts;
|
||||
|
||||
void load (ESMReader &esm);
|
||||
void save (ESMWriter &esm) const;
|
||||
void load(ESMReader &esm);
|
||||
void save(ESMWriter &esm) const;
|
||||
};
|
||||
|
||||
// Saves binary string `data` (can contain '\0') as record LUAD.
|
||||
// Saves binary string `data` (can contain '\0') as LUAD record.
|
||||
void saveLuaBinaryData(ESM::ESMWriter& esm, const std::string& data);
|
||||
|
||||
// Loads LUAD as binary string. If next subrecord is not LUAD, then returns an empty string.
|
||||
|
|
165
components/lua/configuration.cpp
Normal file
165
components/lua/configuration.cpp
Normal file
|
@ -0,0 +1,165 @@
|
|||
#include "configuration.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <bitset>
|
||||
#include <cassert>
|
||||
#include <sstream>
|
||||
|
||||
#include <components/misc/stringops.hpp>
|
||||
|
||||
namespace LuaUtil
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
const std::map<std::string, ESM::LuaScriptCfg::Flags, std::less<>> flagsByName{
|
||||
{"GLOBAL", ESM::LuaScriptCfg::sGlobal},
|
||||
{"CUSTOM", ESM::LuaScriptCfg::sCustom},
|
||||
{"PLAYER", ESM::LuaScriptCfg::sPlayer},
|
||||
{"ACTIVATOR", ESM::LuaScriptCfg::sActivator},
|
||||
{"ARMOR", ESM::LuaScriptCfg::sArmor},
|
||||
{"BOOK", ESM::LuaScriptCfg::sBook},
|
||||
{"CLOTHING", ESM::LuaScriptCfg::sClothing},
|
||||
{"CONTAINER", ESM::LuaScriptCfg::sContainer},
|
||||
{"CREATURE", ESM::LuaScriptCfg::sCreature},
|
||||
{"DOOR", ESM::LuaScriptCfg::sDoor},
|
||||
{"INGREDIENT", ESM::LuaScriptCfg::sIngredient},
|
||||
{"LIGHT", ESM::LuaScriptCfg::sLight},
|
||||
{"MISC_ITEM", ESM::LuaScriptCfg::sMiscItem},
|
||||
{"NPC", ESM::LuaScriptCfg::sNPC},
|
||||
{"POTION", ESM::LuaScriptCfg::sPotion},
|
||||
{"WEAPON", ESM::LuaScriptCfg::sWeapon},
|
||||
};
|
||||
}
|
||||
|
||||
const std::vector<int> ScriptsConfiguration::sEmpty;
|
||||
|
||||
void ScriptsConfiguration::init(ESM::LuaScriptsCfg cfg)
|
||||
{
|
||||
mScripts.clear();
|
||||
mScriptsByFlag.clear();
|
||||
mPathToIndex.clear();
|
||||
|
||||
// Find duplicates; only the last occurrence will be used.
|
||||
// Search for duplicates is case insensitive.
|
||||
std::vector<bool> skip(cfg.mScripts.size(), false);
|
||||
for (int i = cfg.mScripts.size() - 1; i >= 0; --i)
|
||||
{
|
||||
auto [_, inserted] = mPathToIndex.insert_or_assign(
|
||||
Misc::StringUtils::lowerCase(cfg.mScripts[i].mScriptPath), -1);
|
||||
if (!inserted || cfg.mScripts[i].mFlags == 0)
|
||||
skip[i] = true;
|
||||
}
|
||||
mPathToIndex.clear();
|
||||
int index = 0;
|
||||
for (size_t i = 0; i < cfg.mScripts.size(); ++i)
|
||||
{
|
||||
if (skip[i])
|
||||
continue;
|
||||
ESM::LuaScriptCfg& s = cfg.mScripts[i];
|
||||
mPathToIndex[s.mScriptPath] = index; // Stored paths are case sensitive.
|
||||
ESM::LuaScriptCfg::Flags flags = s.mFlags;
|
||||
ESM::LuaScriptCfg::Flags flag = 1;
|
||||
while (flags != 0)
|
||||
{
|
||||
if (flags & flag)
|
||||
mScriptsByFlag[flag].push_back(index);
|
||||
flags &= ~flag;
|
||||
flag = flag << 1;
|
||||
}
|
||||
mScripts.push_back(std::move(s));
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<int> ScriptsConfiguration::findId(std::string_view path) const
|
||||
{
|
||||
auto it = mPathToIndex.find(path);
|
||||
if (it != mPathToIndex.end())
|
||||
return it->second;
|
||||
else
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const std::vector<int>& ScriptsConfiguration::getListByFlag(ESM::LuaScriptCfg::Flags type) const
|
||||
{
|
||||
assert(std::bitset<64>(type).count() <= 1);
|
||||
auto it = mScriptsByFlag.find(type);
|
||||
if (it != mScriptsByFlag.end())
|
||||
return it->second;
|
||||
else
|
||||
return sEmpty;
|
||||
}
|
||||
|
||||
void parseOMWScripts(ESM::LuaScriptsCfg& cfg, std::string_view data)
|
||||
{
|
||||
while (!data.empty())
|
||||
{
|
||||
// Get next line
|
||||
std::string_view line = data.substr(0, data.find('\n'));
|
||||
data = data.substr(std::min(line.size() + 1, data.size()));
|
||||
if (!line.empty() && line.back() == '\r')
|
||||
line = line.substr(0, line.size() - 1);
|
||||
|
||||
while (!line.empty() && std::isspace(line[0]))
|
||||
line = line.substr(1);
|
||||
if (line.empty() || line[0] == '#') // Skip empty lines and comments
|
||||
continue;
|
||||
while (!line.empty() && std::isspace(line.back()))
|
||||
line = line.substr(0, line.size() - 1);
|
||||
|
||||
if (!Misc::StringUtils::ciEndsWith(line, ".lua"))
|
||||
throw std::runtime_error(Misc::StringUtils::format(
|
||||
"Lua script should have suffix '.lua', got: %s", std::string(line.substr(0, 300))));
|
||||
|
||||
// Split flags and script path
|
||||
size_t semicolonPos = line.find(':');
|
||||
if (semicolonPos == std::string::npos)
|
||||
throw std::runtime_error(Misc::StringUtils::format("No flags found in: %s", std::string(line)));
|
||||
std::string_view flagsStr = line.substr(0, semicolonPos);
|
||||
std::string_view scriptPath = line.substr(semicolonPos + 1);
|
||||
while (std::isspace(scriptPath[0]))
|
||||
scriptPath = scriptPath.substr(1);
|
||||
|
||||
// Parse flags
|
||||
ESM::LuaScriptCfg::Flags flags = 0;
|
||||
size_t flagsPos = 0;
|
||||
while (true)
|
||||
{
|
||||
while (flagsPos < flagsStr.size() && (std::isspace(flagsStr[flagsPos]) || flagsStr[flagsPos] == ','))
|
||||
flagsPos++;
|
||||
size_t startPos = flagsPos;
|
||||
while (flagsPos < flagsStr.size() && !std::isspace(flagsStr[flagsPos]) && flagsStr[flagsPos] != ',')
|
||||
flagsPos++;
|
||||
if (startPos == flagsPos)
|
||||
break;
|
||||
std::string_view flagName = flagsStr.substr(startPos, flagsPos - startPos);
|
||||
auto it = flagsByName.find(flagName);
|
||||
if (it != flagsByName.end())
|
||||
flags |= it->second;
|
||||
else
|
||||
throw std::runtime_error(Misc::StringUtils::format("Unknown flag '%s' in: %s",
|
||||
std::string(flagName), std::string(line)));
|
||||
}
|
||||
if ((flags & ESM::LuaScriptCfg::sGlobal) && flags != ESM::LuaScriptCfg::sGlobal)
|
||||
throw std::runtime_error("Global script can not have local flags");
|
||||
|
||||
cfg.mScripts.push_back(ESM::LuaScriptCfg{std::string(scriptPath), "", flags});
|
||||
}
|
||||
}
|
||||
|
||||
std::string scriptCfgToString(const ESM::LuaScriptCfg& script)
|
||||
{
|
||||
std::stringstream ss;
|
||||
for (const auto& [flagName, flag] : flagsByName)
|
||||
{
|
||||
if (script.mFlags & flag)
|
||||
ss << flagName << " ";
|
||||
}
|
||||
ss << ": " << script.mScriptPath;
|
||||
if (!script.mInitializationData.empty())
|
||||
ss << " (with data, " << script.mInitializationData.size() << " bytes)";
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
}
|
37
components/lua/configuration.hpp
Normal file
37
components/lua/configuration.hpp
Normal file
|
@ -0,0 +1,37 @@
|
|||
#ifndef COMPONENTS_LUA_CONFIGURATION_H
|
||||
#define COMPONENTS_LUA_CONFIGURATION_H
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
|
||||
#include <components/esm/luascripts.hpp>
|
||||
|
||||
namespace LuaUtil
|
||||
{
|
||||
|
||||
class ScriptsConfiguration
|
||||
{
|
||||
public:
|
||||
void init(ESM::LuaScriptsCfg);
|
||||
|
||||
size_t size() const { return mScripts.size(); }
|
||||
const ESM::LuaScriptCfg& operator[](int id) const { return mScripts[id]; }
|
||||
|
||||
std::optional<int> findId(std::string_view path) const;
|
||||
const std::vector<int>& getListByFlag(ESM::LuaScriptCfg::Flags type) const;
|
||||
|
||||
private:
|
||||
std::vector<ESM::LuaScriptCfg> mScripts;
|
||||
std::map<std::string, int, std::less<>> mPathToIndex;
|
||||
std::map<ESM::LuaScriptCfg::Flags, std::vector<int>> mScriptsByFlag;
|
||||
static const std::vector<int> sEmpty;
|
||||
};
|
||||
|
||||
// Parse ESM::LuaScriptsCfg from text and add to `cfg`.
|
||||
void parseOMWScripts(ESM::LuaScriptsCfg& cfg, std::string_view data);
|
||||
|
||||
std::string scriptCfgToString(const ESM::LuaScriptCfg& script);
|
||||
|
||||
}
|
||||
|
||||
#endif // COMPONENTS_LUA_CONFIGURATION_H
|
|
@ -22,7 +22,7 @@ namespace LuaUtil
|
|||
"type", "unpack", "xpcall", "rawequal", "rawget", "rawset", "getmetatable", "setmetatable"};
|
||||
static const std::string safePackages[] = {"coroutine", "math", "string", "table"};
|
||||
|
||||
LuaState::LuaState(const VFS::Manager* vfs) : mVFS(vfs)
|
||||
LuaState::LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf) : mConf(conf), mVFS(vfs)
|
||||
{
|
||||
mLua.open_libraries(sol::lib::base, sol::lib::coroutine, sol::lib::math,
|
||||
sol::lib::string, sol::lib::table, sol::lib::debug);
|
||||
|
@ -95,12 +95,11 @@ namespace LuaUtil
|
|||
return res;
|
||||
}
|
||||
|
||||
void LuaState::addCommonPackage(const std::string& packageName, const sol::object& package)
|
||||
void LuaState::addCommonPackage(std::string packageName, sol::object package)
|
||||
{
|
||||
if (package.is<sol::function>())
|
||||
mCommonPackages[packageName] = package;
|
||||
else
|
||||
mCommonPackages[packageName] = makeReadOnly(package);
|
||||
if (!package.is<sol::function>())
|
||||
package = makeReadOnly(std::move(package));
|
||||
mCommonPackages.emplace(std::move(packageName), std::move(package));
|
||||
}
|
||||
|
||||
sol::protected_function_result LuaState::runInNewSandbox(
|
||||
|
@ -148,7 +147,7 @@ namespace LuaUtil
|
|||
return std::move(res);
|
||||
}
|
||||
|
||||
sol::protected_function LuaState::loadScript(const std::string& path)
|
||||
sol::function LuaState::loadScript(const std::string& path)
|
||||
{
|
||||
auto iter = mCompiledScripts.find(path);
|
||||
if (iter != mCompiledScripts.end())
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
#include <components/vfs/manager.hpp>
|
||||
|
||||
#include "configuration.hpp"
|
||||
|
||||
namespace LuaUtil
|
||||
{
|
||||
|
||||
|
@ -22,12 +24,12 @@ namespace LuaUtil
|
|||
// - Access to common read-only resources from different sandboxes;
|
||||
// - Replace standard `require` with a safe version that allows to search
|
||||
// Lua libraries (only source, no dll's) in the virtual filesystem;
|
||||
// - Make `print` to add the script name to the every message and
|
||||
// write to Log rather than directly to stdout;
|
||||
// - Make `print` to add the script name to every message and
|
||||
// write to the Log rather than directly to stdout;
|
||||
class LuaState
|
||||
{
|
||||
public:
|
||||
explicit LuaState(const VFS::Manager* vfs);
|
||||
explicit LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf);
|
||||
~LuaState();
|
||||
|
||||
// Returns underlying sol::state.
|
||||
|
@ -40,7 +42,7 @@ namespace LuaUtil
|
|||
// The package can be either a sol::table with an API or a sol::function. If it is a function,
|
||||
// it will be evaluated (once per sandbox) the first time when requested. If the package
|
||||
// is a table, then `makeReadOnly` is applied to it automatically (but not to other tables it contains).
|
||||
void addCommonPackage(const std::string& packageName, const sol::object& package);
|
||||
void addCommonPackage(std::string packageName, sol::object package);
|
||||
|
||||
// Creates a new sandbox, runs a script, and returns the result
|
||||
// (the result is expected to be an interface of the script).
|
||||
|
@ -58,14 +60,17 @@ namespace LuaUtil
|
|||
|
||||
void dropScriptCache() { mCompiledScripts.clear(); }
|
||||
|
||||
const ScriptsConfiguration& getConfiguration() const { return *mConf; }
|
||||
|
||||
private:
|
||||
static sol::protected_function_result throwIfError(sol::protected_function_result&&);
|
||||
template <typename... Args>
|
||||
friend sol::protected_function_result call(sol::protected_function fn, Args&&... args);
|
||||
friend sol::protected_function_result call(const sol::protected_function& fn, Args&&... args);
|
||||
|
||||
sol::protected_function loadScript(const std::string& path);
|
||||
sol::function loadScript(const std::string& path);
|
||||
|
||||
sol::state mLua;
|
||||
const ScriptsConfiguration* mConf;
|
||||
sol::table mSandboxEnv;
|
||||
std::map<std::string, sol::bytecode> mCompiledScripts;
|
||||
std::map<std::string, sol::object> mCommonPackages;
|
||||
|
@ -75,7 +80,7 @@ namespace LuaUtil
|
|||
// Should be used for every call of every Lua function.
|
||||
// It is a workaround for a bug in `sol`. See https://github.com/ThePhD/sol2/issues/1078
|
||||
template <typename... Args>
|
||||
sol::protected_function_result call(sol::protected_function fn, Args&&... args)
|
||||
sol::protected_function_result call(const sol::protected_function& fn, Args&&... args)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -101,7 +106,7 @@ namespace LuaUtil
|
|||
std::string toString(const sol::object&);
|
||||
|
||||
// Makes a table read only (when accessed from Lua) by wrapping it with an empty userdata.
|
||||
// Needed to forbid any changes in common resources that can accessed from different sandboxes.
|
||||
// Needed to forbid any changes in common resources that can be accessed from different sandboxes.
|
||||
sol::table makeReadOnly(sol::table);
|
||||
sol::table getMutableFromReadOnly(const sol::userdata&);
|
||||
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
#include "omwscriptsparser.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <components/debug/debuglog.hpp>
|
||||
|
||||
std::vector<std::string> LuaUtil::parseOMWScriptsFiles(const VFS::Manager* vfs, const std::vector<std::string>& scriptLists)
|
||||
{
|
||||
auto endsWith = [](std::string_view s, std::string_view suffix)
|
||||
{
|
||||
return s.size() >= suffix.size() && std::equal(suffix.rbegin(), suffix.rend(), s.rbegin());
|
||||
};
|
||||
std::vector<std::string> res;
|
||||
for (const std::string& scriptListFile : scriptLists)
|
||||
{
|
||||
if (!endsWith(scriptListFile, ".omwscripts"))
|
||||
{
|
||||
Log(Debug::Error) << "Script list should have suffix '.omwscripts', got: '" << scriptListFile << "'";
|
||||
continue;
|
||||
}
|
||||
std::string content(std::istreambuf_iterator<char>(*vfs->get(scriptListFile)), {});
|
||||
std::string_view view(content);
|
||||
while (!view.empty())
|
||||
{
|
||||
size_t pos = 0;
|
||||
while (pos < view.size() && view[pos] != '\n')
|
||||
pos++;
|
||||
std::string_view line = view.substr(0, pos);
|
||||
view = view.substr(std::min(pos + 1, view.size()));
|
||||
if (!line.empty() && line.back() == '\r')
|
||||
line = line.substr(0, pos - 1);
|
||||
// Lines starting with '#' are comments.
|
||||
// TODO: Maybe make the parser more robust. It is a bit inconsistent that 'path/#to/file.lua'
|
||||
// is a valid path, but '#path/to/file.lua' is considered as a comment and ignored.
|
||||
if (line.empty() || line[0] == '#')
|
||||
continue;
|
||||
if (endsWith(line, ".lua"))
|
||||
res.push_back(std::string(line));
|
||||
else
|
||||
Log(Debug::Error) << "Lua script should have suffix '.lua', got: '" << line.substr(0, 300) << "'";
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
#ifndef COMPONENTS_LUA_OMWSCRIPTSPARSER_H
|
||||
#define COMPONENTS_LUA_OMWSCRIPTSPARSER_H
|
||||
|
||||
#include <components/vfs/manager.hpp>
|
||||
|
||||
namespace LuaUtil
|
||||
{
|
||||
|
||||
// Parses list of `*.omwscripts` files.
|
||||
std::vector<std::string> parseOMWScriptsFiles(const VFS::Manager* vfs, const std::vector<std::string>& scriptLists);
|
||||
|
||||
}
|
||||
|
||||
#endif // COMPONENTS_LUA_OMWSCRIPTSPARSER_H
|
|
@ -10,11 +10,10 @@ namespace LuaUtil
|
|||
static constexpr std::string_view INTERFACE_NAME = "interfaceName";
|
||||
static constexpr std::string_view INTERFACE = "interface";
|
||||
|
||||
static constexpr std::string_view HANDLER_INIT = "onInit";
|
||||
static constexpr std::string_view HANDLER_SAVE = "onSave";
|
||||
static constexpr std::string_view HANDLER_LOAD = "onLoad";
|
||||
|
||||
static constexpr std::string_view REGISTERED_TIMER_CALLBACKS = "_timers";
|
||||
static constexpr std::string_view TEMPORARY_TIMER_CALLBACKS = "_temp_timers";
|
||||
static constexpr std::string_view HANDLER_INTERFACE_OVERRIDE = "onInterfaceOverride";
|
||||
|
||||
std::string ScriptsContainer::ScriptId::toString() const
|
||||
{
|
||||
|
@ -25,147 +24,236 @@ namespace LuaUtil
|
|||
return res;
|
||||
}
|
||||
|
||||
ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix) : mNamePrefix(namePrefix), mLua(*lua)
|
||||
ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix, ESM::LuaScriptCfg::Flags autoStartMode)
|
||||
: mNamePrefix(namePrefix), mLua(*lua), mAutoStartMode(autoStartMode)
|
||||
{
|
||||
registerEngineHandlers({&mUpdateHandlers});
|
||||
mPublicInterfaces = sol::table(lua->sol(), sol::create);
|
||||
addPackage("openmw.interfaces", mPublicInterfaces);
|
||||
}
|
||||
|
||||
void ScriptsContainer::addPackage(const std::string& packageName, sol::object package)
|
||||
void ScriptsContainer::printError(int scriptId, std::string_view msg, const std::exception& e)
|
||||
{
|
||||
API[packageName] = makeReadOnly(std::move(package));
|
||||
Log(Debug::Error) << mNamePrefix << "[" << scriptPath(scriptId) << "] " << msg << ": " << e.what();
|
||||
}
|
||||
|
||||
bool ScriptsContainer::addNewScript(const std::string& path)
|
||||
void ScriptsContainer::addPackage(std::string packageName, sol::object package)
|
||||
{
|
||||
if (mScripts.count(path) != 0)
|
||||
mAPI.emplace(std::move(packageName), makeReadOnly(std::move(package)));
|
||||
}
|
||||
|
||||
bool ScriptsContainer::addCustomScript(int scriptId)
|
||||
{
|
||||
assert(mLua.getConfiguration()[scriptId].mFlags & ESM::LuaScriptCfg::sCustom);
|
||||
std::optional<sol::function> onInit, onLoad;
|
||||
bool ok = addScript(scriptId, onInit, onLoad);
|
||||
if (ok && onInit)
|
||||
callOnInit(scriptId, *onInit);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void ScriptsContainer::addAutoStartedScripts()
|
||||
{
|
||||
for (int scriptId : mLua.getConfiguration().getListByFlag(mAutoStartMode))
|
||||
{
|
||||
std::optional<sol::function> onInit, onLoad;
|
||||
bool ok = addScript(scriptId, onInit, onLoad);
|
||||
if (ok && onInit)
|
||||
callOnInit(scriptId, *onInit);
|
||||
}
|
||||
}
|
||||
|
||||
bool ScriptsContainer::addScript(int scriptId, std::optional<sol::function>& onInit, std::optional<sol::function>& onLoad)
|
||||
{
|
||||
assert(scriptId >= 0 && scriptId < static_cast<int>(mLua.getConfiguration().size()));
|
||||
if (mScripts.count(scriptId) != 0)
|
||||
return false; // already present
|
||||
|
||||
const std::string& path = scriptPath(scriptId);
|
||||
try
|
||||
{
|
||||
sol::table hiddenData(mLua.sol(), sol::create);
|
||||
hiddenData[ScriptId::KEY] = ScriptId{this, path};
|
||||
hiddenData[REGISTERED_TIMER_CALLBACKS] = mLua.newTable();
|
||||
hiddenData[TEMPORARY_TIMER_CALLBACKS] = mLua.newTable();
|
||||
mScripts[path].mHiddenData = hiddenData;
|
||||
sol::object script = mLua.runInNewSandbox(path, mNamePrefix, API, hiddenData);
|
||||
std::string interfaceName = "";
|
||||
sol::object publicInterface = sol::nil;
|
||||
if (script != sol::nil)
|
||||
Script& script = mScripts[scriptId];
|
||||
script.mHiddenData = mLua.newTable();
|
||||
script.mHiddenData[ScriptId::KEY] = ScriptId{this, scriptId, path};
|
||||
sol::object scriptOutput = mLua.runInNewSandbox(path, mNamePrefix, mAPI, script.mHiddenData);
|
||||
if (scriptOutput == sol::nil)
|
||||
return true;
|
||||
sol::object engineHandlers = sol::nil, eventHandlers = sol::nil;
|
||||
for (const auto& [key, value] : sol::table(scriptOutput))
|
||||
{
|
||||
for (auto& [key, value] : sol::table(script))
|
||||
std::string_view sectionName = key.as<std::string_view>();
|
||||
if (sectionName == ENGINE_HANDLERS)
|
||||
engineHandlers = value;
|
||||
else if (sectionName == EVENT_HANDLERS)
|
||||
eventHandlers = value;
|
||||
else if (sectionName == INTERFACE_NAME)
|
||||
script.mInterfaceName = value.as<std::string>();
|
||||
else if (sectionName == INTERFACE)
|
||||
script.mInterface = value.as<sol::table>();
|
||||
else
|
||||
Log(Debug::Error) << "Not supported section '" << sectionName << "' in " << mNamePrefix << "[" << path << "]";
|
||||
}
|
||||
if (engineHandlers != sol::nil)
|
||||
{
|
||||
for (const auto& [key, fn] : sol::table(engineHandlers))
|
||||
{
|
||||
std::string_view sectionName = key.as<std::string_view>();
|
||||
if (sectionName == ENGINE_HANDLERS)
|
||||
parseEngineHandlers(value, path);
|
||||
else if (sectionName == EVENT_HANDLERS)
|
||||
parseEventHandlers(value, path);
|
||||
else if (sectionName == INTERFACE_NAME)
|
||||
interfaceName = value.as<std::string>();
|
||||
else if (sectionName == INTERFACE)
|
||||
publicInterface = value.as<sol::table>();
|
||||
std::string_view handlerName = key.as<std::string_view>();
|
||||
if (handlerName == HANDLER_INIT)
|
||||
onInit = sol::function(fn);
|
||||
else if (handlerName == HANDLER_LOAD)
|
||||
onLoad = sol::function(fn);
|
||||
else if (handlerName == HANDLER_SAVE)
|
||||
script.mOnSave = sol::function(fn);
|
||||
else if (handlerName == HANDLER_INTERFACE_OVERRIDE)
|
||||
script.mOnOverride = sol::function(fn);
|
||||
else
|
||||
Log(Debug::Error) << "Not supported section '" << sectionName << "' in " << mNamePrefix << "[" << path << "]";
|
||||
{
|
||||
auto it = mEngineHandlers.find(handlerName);
|
||||
if (it == mEngineHandlers.end())
|
||||
Log(Debug::Error) << "Not supported handler '" << handlerName
|
||||
<< "' in " << mNamePrefix << "[" << path << "]";
|
||||
else
|
||||
insertHandler(it->second->mList, scriptId, fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (interfaceName.empty() != (publicInterface == sol::nil))
|
||||
if (eventHandlers != sol::nil)
|
||||
{
|
||||
for (const auto& [key, fn] : sol::table(eventHandlers))
|
||||
{
|
||||
std::string_view eventName = key.as<std::string_view>();
|
||||
auto it = mEventHandlers.find(eventName);
|
||||
if (it == mEventHandlers.end())
|
||||
it = mEventHandlers.emplace(std::string(eventName), EventHandlerList()).first;
|
||||
insertHandler(it->second, scriptId, fn);
|
||||
}
|
||||
}
|
||||
|
||||
if (script.mInterfaceName.empty() == script.mInterface.has_value())
|
||||
{
|
||||
Log(Debug::Error) << mNamePrefix << "[" << path << "]: 'interfaceName' should always be used together with 'interface'";
|
||||
else if (!interfaceName.empty())
|
||||
script.as<sol::table>()[INTERFACE] = mPublicInterfaces[interfaceName] = makeReadOnly(publicInterface);
|
||||
mScriptOrder.push_back(path);
|
||||
mScripts[path].mInterface = std::move(script);
|
||||
script.mInterfaceName.clear();
|
||||
script.mInterface = sol::nil;
|
||||
}
|
||||
else if (script.mInterface)
|
||||
{
|
||||
script.mInterface = makeReadOnly(*script.mInterface);
|
||||
insertInterface(scriptId, script);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
mScripts.erase(path);
|
||||
mScripts.erase(scriptId);
|
||||
Log(Debug::Error) << "Can't start " << mNamePrefix << "[" << path << "]; " << e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ScriptsContainer::removeScript(const std::string& path)
|
||||
void ScriptsContainer::removeScript(int scriptId)
|
||||
{
|
||||
auto scriptIter = mScripts.find(path);
|
||||
auto scriptIter = mScripts.find(scriptId);
|
||||
if (scriptIter == mScripts.end())
|
||||
return false; // no such script
|
||||
scriptIter->second.mHiddenData[ScriptId::KEY] = sol::nil;
|
||||
sol::object& script = scriptIter->second.mInterface;
|
||||
if (getFieldOrNil(script, INTERFACE_NAME) != sol::nil)
|
||||
{
|
||||
std::string_view interfaceName = getFieldOrNil(script, INTERFACE_NAME).as<std::string_view>();
|
||||
if (mPublicInterfaces[interfaceName] == getFieldOrNil(script, INTERFACE))
|
||||
{
|
||||
mPublicInterfaces[interfaceName] = sol::nil;
|
||||
auto prevIt = mScriptOrder.rbegin();
|
||||
while (*prevIt != path)
|
||||
prevIt++;
|
||||
prevIt++;
|
||||
while (prevIt != mScriptOrder.rend())
|
||||
{
|
||||
sol::object& prevScript = mScripts[*(prevIt++)].mInterface;
|
||||
sol::object prevInterfaceName = getFieldOrNil(prevScript, INTERFACE_NAME);
|
||||
if (prevInterfaceName != sol::nil && prevInterfaceName.as<std::string_view>() == interfaceName)
|
||||
{
|
||||
mPublicInterfaces[interfaceName] = getFieldOrNil(prevScript, INTERFACE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sol::object engineHandlers = getFieldOrNil(script, ENGINE_HANDLERS);
|
||||
if (engineHandlers != sol::nil)
|
||||
{
|
||||
for (auto& [key, value] : sol::table(engineHandlers))
|
||||
{
|
||||
std::string_view handlerName = key.as<std::string_view>();
|
||||
auto handlerIter = mEngineHandlers.find(handlerName);
|
||||
if (handlerIter == mEngineHandlers.end())
|
||||
continue;
|
||||
std::vector<sol::protected_function>& list = handlerIter->second->mList;
|
||||
list.erase(std::find(list.begin(), list.end(), value.as<sol::protected_function>()));
|
||||
}
|
||||
}
|
||||
sol::object eventHandlers = getFieldOrNil(script, EVENT_HANDLERS);
|
||||
if (eventHandlers != sol::nil)
|
||||
{
|
||||
for (auto& [key, value] : sol::table(eventHandlers))
|
||||
{
|
||||
EventHandlerList& list = mEventHandlers.find(key.as<std::string_view>())->second;
|
||||
list.erase(std::find(list.begin(), list.end(), value.as<sol::protected_function>()));
|
||||
}
|
||||
}
|
||||
return; // no such script
|
||||
Script& script = scriptIter->second;
|
||||
if (script.mInterface)
|
||||
removeInterface(scriptId, script);
|
||||
script.mHiddenData[ScriptId::KEY] = sol::nil;
|
||||
mScripts.erase(scriptIter);
|
||||
mScriptOrder.erase(std::find(mScriptOrder.begin(), mScriptOrder.end(), path));
|
||||
return true;
|
||||
for (auto& [_, handlers] : mEngineHandlers)
|
||||
removeHandler(handlers->mList, scriptId);
|
||||
for (auto& [_, handlers] : mEventHandlers)
|
||||
removeHandler(handlers, scriptId);
|
||||
}
|
||||
|
||||
void ScriptsContainer::parseEventHandlers(sol::table handlers, std::string_view scriptPath)
|
||||
void ScriptsContainer::insertInterface(int scriptId, const Script& script)
|
||||
{
|
||||
for (auto& [key, value] : handlers)
|
||||
assert(script.mInterface);
|
||||
const Script* prev = nullptr;
|
||||
const Script* next = nullptr;
|
||||
int nextId = 0;
|
||||
for (const auto& [otherId, otherScript] : mScripts)
|
||||
{
|
||||
std::string_view eventName = key.as<std::string_view>();
|
||||
auto it = mEventHandlers.find(eventName);
|
||||
if (it == mEventHandlers.end())
|
||||
it = mEventHandlers.insert({std::string(eventName), EventHandlerList()}).first;
|
||||
it->second.push_back(value);
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptsContainer::parseEngineHandlers(sol::table handlers, std::string_view scriptPath)
|
||||
{
|
||||
for (auto& [key, value] : handlers)
|
||||
{
|
||||
std::string_view handlerName = key.as<std::string_view>();
|
||||
if (handlerName == HANDLER_LOAD || handlerName == HANDLER_SAVE)
|
||||
continue; // save and load are handled separately
|
||||
auto it = mEngineHandlers.find(handlerName);
|
||||
if (it == mEngineHandlers.end())
|
||||
Log(Debug::Error) << "Not supported handler '" << handlerName << "' in " << mNamePrefix << "[" << scriptPath << "]";
|
||||
if (scriptId == otherId || script.mInterfaceName != otherScript.mInterfaceName)
|
||||
continue;
|
||||
if (otherId < scriptId)
|
||||
prev = &otherScript;
|
||||
else
|
||||
it->second->mList.push_back(value);
|
||||
{
|
||||
next = &otherScript;
|
||||
nextId = otherId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (prev && script.mOnOverride)
|
||||
{
|
||||
try { LuaUtil::call(*script.mOnOverride, *prev->mInterface); }
|
||||
catch (std::exception& e) { printError(scriptId, "onInterfaceOverride failed", e); }
|
||||
}
|
||||
if (next && next->mOnOverride)
|
||||
{
|
||||
try { LuaUtil::call(*next->mOnOverride, *script.mInterface); }
|
||||
catch (std::exception& e) { printError(nextId, "onInterfaceOverride failed", e); }
|
||||
}
|
||||
if (next == nullptr)
|
||||
mPublicInterfaces[script.mInterfaceName] = *script.mInterface;
|
||||
}
|
||||
|
||||
void ScriptsContainer::removeInterface(int scriptId, const Script& script)
|
||||
{
|
||||
assert(script.mInterface);
|
||||
const Script* prev = nullptr;
|
||||
const Script* next = nullptr;
|
||||
int nextId = 0;
|
||||
for (const auto& [otherId, otherScript] : mScripts)
|
||||
{
|
||||
if (scriptId == otherId || script.mInterfaceName != otherScript.mInterfaceName)
|
||||
continue;
|
||||
if (otherId < scriptId)
|
||||
prev = &otherScript;
|
||||
else
|
||||
{
|
||||
next = &otherScript;
|
||||
nextId = otherId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (next)
|
||||
{
|
||||
if (next->mOnOverride)
|
||||
{
|
||||
sol::object prevInterface = sol::nil;
|
||||
if (prev)
|
||||
prevInterface = *prev->mInterface;
|
||||
try { LuaUtil::call(*next->mOnOverride, prevInterface); }
|
||||
catch (std::exception& e) { printError(nextId, "onInterfaceOverride failed", e); }
|
||||
}
|
||||
}
|
||||
else if (prev)
|
||||
mPublicInterfaces[script.mInterfaceName] = *prev->mInterface;
|
||||
else
|
||||
mPublicInterfaces[script.mInterfaceName] = sol::nil;
|
||||
}
|
||||
|
||||
void ScriptsContainer::insertHandler(std::vector<Handler>& list, int scriptId, sol::function fn)
|
||||
{
|
||||
list.emplace_back();
|
||||
int pos = list.size() - 1;
|
||||
while (pos > 0 && list[pos - 1].mScriptId > scriptId)
|
||||
{
|
||||
list[pos] = std::move(list[pos - 1]);
|
||||
pos--;
|
||||
}
|
||||
list[pos].mScriptId = scriptId;
|
||||
list[pos].mFn = std::move(fn);
|
||||
}
|
||||
|
||||
void ScriptsContainer::removeHandler(std::vector<Handler>& list, int scriptId)
|
||||
{
|
||||
list.erase(std::remove_if(list.begin(), list.end(),
|
||||
[scriptId](const Handler& h){ return h.mScriptId == scriptId; }),
|
||||
list.end());
|
||||
}
|
||||
|
||||
void ScriptsContainer::receiveEvent(std::string_view eventName, std::string_view eventData)
|
||||
|
@ -191,13 +279,14 @@ namespace LuaUtil
|
|||
{
|
||||
try
|
||||
{
|
||||
sol::object res = LuaUtil::call(list[i], data);
|
||||
sol::object res = LuaUtil::call(list[i].mFn, data);
|
||||
if (res != sol::nil && !res.as<bool>())
|
||||
break; // Skip other handlers if 'false' was returned.
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Log(Debug::Error) << mNamePrefix << " eventHandler[" << eventName << "] failed. " << e.what();
|
||||
Log(Debug::Error) << mNamePrefix << "[" << scriptPath(list[i].mScriptId)
|
||||
<< "] eventHandler[" << eventName << "] failed. " << e.what();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -208,9 +297,19 @@ namespace LuaUtil
|
|||
mEngineHandlers[h->mName] = h;
|
||||
}
|
||||
|
||||
void ScriptsContainer::callOnInit(int scriptId, const sol::function& onInit)
|
||||
{
|
||||
try
|
||||
{
|
||||
const std::string& data = mLua.getConfiguration()[scriptId].mInitializationData;
|
||||
LuaUtil::call(onInit, deserialize(mLua.sol(), data, mSerializer));
|
||||
}
|
||||
catch (std::exception& e) { printError(scriptId, "onInit failed", e); }
|
||||
}
|
||||
|
||||
void ScriptsContainer::save(ESM::LuaScripts& data)
|
||||
{
|
||||
std::map<std::string, std::vector<ESM::LuaTimer>> timers;
|
||||
std::map<int, std::vector<ESM::LuaTimer>> timers;
|
||||
auto saveTimerFn = [&](const Timer& timer, TimeUnit timeUnit)
|
||||
{
|
||||
if (!timer.mSerializable)
|
||||
|
@ -220,78 +319,85 @@ namespace LuaUtil
|
|||
savedTimer.mUnit = timeUnit;
|
||||
savedTimer.mCallbackName = std::get<std::string>(timer.mCallback);
|
||||
savedTimer.mCallbackArgument = timer.mSerializedArg;
|
||||
if (timers.count(timer.mScript) == 0)
|
||||
timers[timer.mScript] = {};
|
||||
timers[timer.mScript].push_back(std::move(savedTimer));
|
||||
timers[timer.mScriptId].push_back(std::move(savedTimer));
|
||||
};
|
||||
for (const Timer& timer : mSecondsTimersQueue)
|
||||
saveTimerFn(timer, TimeUnit::SECONDS);
|
||||
for (const Timer& timer : mHoursTimersQueue)
|
||||
saveTimerFn(timer, TimeUnit::HOURS);
|
||||
data.mScripts.clear();
|
||||
for (const std::string& path : mScriptOrder)
|
||||
for (auto& [scriptId, script] : mScripts)
|
||||
{
|
||||
ESM::LuaScript savedScript;
|
||||
savedScript.mScriptPath = path;
|
||||
sol::object handler = getFieldOrNil(mScripts[path].mInterface, ENGINE_HANDLERS, HANDLER_SAVE);
|
||||
if (handler != sol::nil)
|
||||
savedScript.mScriptPath = script.mHiddenData.get<ScriptId>(ScriptId::KEY).mPath;
|
||||
if (script.mOnSave)
|
||||
{
|
||||
try
|
||||
{
|
||||
sol::object state = LuaUtil::call(handler);
|
||||
sol::object state = LuaUtil::call(*script.mOnSave);
|
||||
savedScript.mData = serialize(state, mSerializer);
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Log(Debug::Error) << mNamePrefix << "[" << path << "] onSave failed: " << e.what();
|
||||
}
|
||||
catch (std::exception& e) { printError(scriptId, "onSave failed", e); }
|
||||
}
|
||||
auto timersIt = timers.find(path);
|
||||
auto timersIt = timers.find(scriptId);
|
||||
if (timersIt != timers.end())
|
||||
savedScript.mTimers = std::move(timersIt->second);
|
||||
data.mScripts.push_back(std::move(savedScript));
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptsContainer::load(const ESM::LuaScripts& data, bool resetScriptList)
|
||||
void ScriptsContainer::load(const ESM::LuaScripts& data)
|
||||
{
|
||||
std::map<std::string, Script> scriptsWithoutSavedData;
|
||||
if (resetScriptList)
|
||||
removeAllScripts();
|
||||
const ScriptsConfiguration& cfg = mLua.getConfiguration();
|
||||
|
||||
std::map<int, const ESM::LuaScript*> scripts;
|
||||
for (int scriptId : mLua.getConfiguration().getListByFlag(mAutoStartMode))
|
||||
scripts[scriptId] = nullptr;
|
||||
for (const ESM::LuaScript& s : data.mScripts)
|
||||
{
|
||||
removeAllScripts();
|
||||
for (const ESM::LuaScript& script : data.mScripts)
|
||||
addNewScript(script.mScriptPath);
|
||||
}
|
||||
else
|
||||
scriptsWithoutSavedData = mScripts;
|
||||
mSecondsTimersQueue.clear();
|
||||
mHoursTimersQueue.clear();
|
||||
for (const ESM::LuaScript& script : data.mScripts)
|
||||
{
|
||||
auto iter = mScripts.find(script.mScriptPath);
|
||||
if (iter == mScripts.end())
|
||||
std::optional<int> scriptId = cfg.findId(s.mScriptPath);
|
||||
if (!scriptId)
|
||||
{
|
||||
Log(Debug::Verbose) << "Ignoring " << mNamePrefix << "[" << s.mScriptPath << "]; script not registered";
|
||||
continue;
|
||||
scriptsWithoutSavedData.erase(iter->first);
|
||||
iter->second.mHiddenData.get<sol::table>(TEMPORARY_TIMER_CALLBACKS).clear();
|
||||
try
|
||||
}
|
||||
if (!(cfg[*scriptId].mFlags & (ESM::LuaScriptCfg::sCustom | mAutoStartMode)))
|
||||
{
|
||||
sol::object handler = getFieldOrNil(iter->second.mInterface, ENGINE_HANDLERS, HANDLER_LOAD);
|
||||
if (handler != sol::nil)
|
||||
Log(Debug::Verbose) << "Ignoring " << mNamePrefix << "[" << s.mScriptPath << "]; this script is not allowed here";
|
||||
continue;
|
||||
}
|
||||
scripts[*scriptId] = &s;
|
||||
}
|
||||
|
||||
for (const auto& [scriptId, savedScript] : scripts)
|
||||
{
|
||||
std::optional<sol::function> onInit, onLoad;
|
||||
if (!addScript(scriptId, onInit, onLoad))
|
||||
continue;
|
||||
if (savedScript == nullptr)
|
||||
{
|
||||
if (onInit)
|
||||
callOnInit(scriptId, *onInit);
|
||||
continue;
|
||||
}
|
||||
if (onLoad)
|
||||
{
|
||||
try
|
||||
{
|
||||
sol::object state = deserialize(mLua.sol(), script.mData, mSerializer);
|
||||
LuaUtil::call(handler, state);
|
||||
sol::object state = deserialize(mLua.sol(), savedScript->mData, mSerializer);
|
||||
sol::object initializationData =
|
||||
deserialize(mLua.sol(), mLua.getConfiguration()[scriptId].mInitializationData, mSerializer);
|
||||
LuaUtil::call(*onLoad, state, initializationData);
|
||||
}
|
||||
catch (std::exception& e) { printError(scriptId, "onLoad failed", e); }
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Log(Debug::Error) << mNamePrefix << "[" << script.mScriptPath << "] onLoad failed: " << e.what();
|
||||
}
|
||||
for (const ESM::LuaTimer& savedTimer : script.mTimers)
|
||||
for (const ESM::LuaTimer& savedTimer : savedScript->mTimers)
|
||||
{
|
||||
Timer timer;
|
||||
timer.mCallback = savedTimer.mCallbackName;
|
||||
timer.mSerializable = true;
|
||||
timer.mScript = script.mScriptPath;
|
||||
timer.mScriptId = scriptId;
|
||||
timer.mTime = savedTimer.mTime;
|
||||
|
||||
try
|
||||
|
@ -306,24 +412,10 @@ namespace LuaUtil
|
|||
else
|
||||
mSecondsTimersQueue.push_back(std::move(timer));
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Log(Debug::Error) << mNamePrefix << "[" << script.mScriptPath << "] can not load timer: " << e.what();
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto& [path, script] : scriptsWithoutSavedData)
|
||||
{
|
||||
script.mHiddenData.get<sol::table>(TEMPORARY_TIMER_CALLBACKS).clear();
|
||||
sol::object handler = getFieldOrNil(script.mInterface, ENGINE_HANDLERS, HANDLER_LOAD);
|
||||
if (handler == sol::nil)
|
||||
continue;
|
||||
try { LuaUtil::call(handler); }
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Log(Debug::Error) << mNamePrefix << "[" << path << "] onLoad failed: " << e.what();
|
||||
catch (std::exception& e) { printError(scriptId, "can not load timer", e); }
|
||||
}
|
||||
}
|
||||
|
||||
std::make_heap(mSecondsTimersQueue.begin(), mSecondsTimersQueue.end());
|
||||
std::make_heap(mHoursTimersQueue.begin(), mHoursTimersQueue.end());
|
||||
}
|
||||
|
@ -334,12 +426,13 @@ namespace LuaUtil
|
|||
script.mHiddenData[ScriptId::KEY] = sol::nil;
|
||||
}
|
||||
|
||||
// Note: shouldn't be called from destructor because mEngineHandlers has pointers on
|
||||
// external objects that are already removed during child class destruction.
|
||||
void ScriptsContainer::removeAllScripts()
|
||||
{
|
||||
for (auto& [_, script] : mScripts)
|
||||
script.mHiddenData[ScriptId::KEY] = sol::nil;
|
||||
mScripts.clear();
|
||||
mScriptOrder.clear();
|
||||
for (auto& [_, handlers] : mEngineHandlers)
|
||||
handlers->mList.clear();
|
||||
mEventHandlers.clear();
|
||||
|
@ -351,17 +444,17 @@ namespace LuaUtil
|
|||
mPublicInterfaces[sol::meta_function::index] = mPublicInterfaces;
|
||||
}
|
||||
|
||||
sol::table ScriptsContainer::getHiddenData(const std::string& scriptPath)
|
||||
ScriptsContainer::Script& ScriptsContainer::getScript(int scriptId)
|
||||
{
|
||||
auto it = mScripts.find(scriptPath);
|
||||
auto it = mScripts.find(scriptId);
|
||||
if (it == mScripts.end())
|
||||
throw std::logic_error("ScriptsContainer::getHiddenData: script doesn't exist");
|
||||
return it->second.mHiddenData;
|
||||
throw std::logic_error("Script doesn't exist");
|
||||
return it->second;
|
||||
}
|
||||
|
||||
void ScriptsContainer::registerTimerCallback(const std::string& scriptPath, std::string_view callbackName, sol::function callback)
|
||||
void ScriptsContainer::registerTimerCallback(int scriptId, std::string_view callbackName, sol::function callback)
|
||||
{
|
||||
getHiddenData(scriptPath)[REGISTERED_TIMER_CALLBACKS][callbackName] = std::move(callback);
|
||||
getScript(scriptId).mRegisteredCallbacks.emplace(std::string(callbackName), std::move(callback));
|
||||
}
|
||||
|
||||
void ScriptsContainer::insertTimer(std::vector<Timer>& timerQueue, Timer&& t)
|
||||
|
@ -370,12 +463,12 @@ namespace LuaUtil
|
|||
std::push_heap(timerQueue.begin(), timerQueue.end());
|
||||
}
|
||||
|
||||
void ScriptsContainer::setupSerializableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath,
|
||||
void ScriptsContainer::setupSerializableTimer(TimeUnit timeUnit, double time, int scriptId,
|
||||
std::string_view callbackName, sol::object callbackArg)
|
||||
{
|
||||
Timer t;
|
||||
t.mCallback = std::string(callbackName);
|
||||
t.mScript = scriptPath;
|
||||
t.mScriptId = scriptId;
|
||||
t.mSerializable = true;
|
||||
t.mTime = time;
|
||||
t.mArg = callbackArg;
|
||||
|
@ -383,15 +476,15 @@ namespace LuaUtil
|
|||
insertTimer(timeUnit == TimeUnit::HOURS ? mHoursTimersQueue : mSecondsTimersQueue, std::move(t));
|
||||
}
|
||||
|
||||
void ScriptsContainer::setupUnsavableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, sol::function callback)
|
||||
void ScriptsContainer::setupUnsavableTimer(TimeUnit timeUnit, double time, int scriptId, sol::function callback)
|
||||
{
|
||||
Timer t;
|
||||
t.mScript = scriptPath;
|
||||
t.mScriptId = scriptId;
|
||||
t.mSerializable = false;
|
||||
t.mTime = time;
|
||||
|
||||
t.mCallback = mTemporaryCallbackCounter;
|
||||
getHiddenData(scriptPath)[TEMPORARY_TIMER_CALLBACKS][mTemporaryCallbackCounter] = std::move(callback);
|
||||
getScript(t.mScriptId).mTemporaryCallbacks.emplace(mTemporaryCallbackCounter, std::move(callback));
|
||||
mTemporaryCallbackCounter++;
|
||||
|
||||
insertTimer(timeUnit == TimeUnit::HOURS ? mHoursTimersQueue : mSecondsTimersQueue, std::move(t));
|
||||
|
@ -401,30 +494,23 @@ namespace LuaUtil
|
|||
{
|
||||
try
|
||||
{
|
||||
sol::table data = getHiddenData(t.mScript);
|
||||
Script& script = getScript(t.mScriptId);
|
||||
if (t.mSerializable)
|
||||
{
|
||||
const std::string& callbackName = std::get<std::string>(t.mCallback);
|
||||
sol::object callback = data[REGISTERED_TIMER_CALLBACKS][callbackName];
|
||||
if (!callback.is<sol::function>())
|
||||
auto it = script.mRegisteredCallbacks.find(callbackName);
|
||||
if (it == script.mRegisteredCallbacks.end())
|
||||
throw std::logic_error("Callback '" + callbackName + "' doesn't exist");
|
||||
LuaUtil::call(callback, t.mArg);
|
||||
LuaUtil::call(it->second, t.mArg);
|
||||
}
|
||||
else
|
||||
{
|
||||
int64_t id = std::get<int64_t>(t.mCallback);
|
||||
sol::table callbacks = data[TEMPORARY_TIMER_CALLBACKS];
|
||||
sol::object callback = callbacks[id];
|
||||
if (!callback.is<sol::function>())
|
||||
throw std::logic_error("Temporary timer callback doesn't exist");
|
||||
LuaUtil::call(callback);
|
||||
callbacks[id] = sol::nil;
|
||||
LuaUtil::call(script.mTemporaryCallbacks.at(id));
|
||||
script.mTemporaryCallbacks.erase(id);
|
||||
}
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Log(Debug::Error) << mNamePrefix << "[" << t.mScript << "] callTimer failed: " << e.what();
|
||||
}
|
||||
catch (std::exception& e) { printError(t.mScriptId, "callTimer failed", e); }
|
||||
}
|
||||
|
||||
void ScriptsContainer::updateTimerQueue(std::vector<Timer>& timerQueue, double time)
|
||||
|
|
|
@ -17,7 +17,7 @@ namespace LuaUtil
|
|||
// 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 (not implemented yet).
|
||||
// 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
|
||||
|
@ -42,11 +42,12 @@ namespace LuaUtil
|
|||
// -- 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) ... end, -- "state" is the data that was earlier returned by onSave
|
||||
// onLoad = function(state, initData) ... end, -- "state" is the data that was earlier returned by onSave
|
||||
//
|
||||
// -- Works only if ScriptsContainer::registerEngineHandler is overloaded in a child class
|
||||
// -- and explicitly supports 'onSomethingElse'
|
||||
// -- Works only if a child class has passed a EngineHandlerList
|
||||
// -- for 'onSomethingElse' to ScriptsContainer::registerEngineHandlers.
|
||||
// onSomethingElse = function() print("something else") end
|
||||
// },
|
||||
//
|
||||
|
@ -65,30 +66,36 @@ namespace LuaUtil
|
|||
constexpr static std::string_view KEY = "_id";
|
||||
|
||||
ScriptsContainer* mContainer;
|
||||
std::string mPath;
|
||||
int mIndex; // index in LuaUtil::ScriptsConfiguration
|
||||
std::string mPath; // path to the script source in VFS
|
||||
|
||||
std::string toString() const;
|
||||
};
|
||||
using TimeUnit = ESM::LuaTimer::TimeUnit;
|
||||
|
||||
// `namePrefix` is a common prefix for all scripts in the container. Used in logs for error messages and `print` output.
|
||||
ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix);
|
||||
// `autoStartMode` specifies the list of scripts that should be autostarted in this container; the list itself is
|
||||
// stored in ScriptsConfiguration: lua->getConfiguration().getListByFlag(autoStartMode).
|
||||
ScriptsContainer(LuaState* lua, std::string_view namePrefix, ESM::LuaScriptCfg::Flags autoStartMode = 0);
|
||||
|
||||
ScriptsContainer(const ScriptsContainer&) = delete;
|
||||
ScriptsContainer(ScriptsContainer&&) = delete;
|
||||
virtual ~ScriptsContainer();
|
||||
|
||||
ESM::LuaScriptCfg::Flags getAutoStartMode() const { return mAutoStartMode; }
|
||||
|
||||
// Adds package that will be available (via `require`) for all scripts in the container.
|
||||
// Automatically applies LuaUtil::makeReadOnly to the package.
|
||||
void addPackage(const std::string& packageName, sol::object package);
|
||||
void addPackage(std::string packageName, sol::object package);
|
||||
|
||||
// Finds a file with given path in the virtual file system, starts as a new script, and adds it to the container.
|
||||
// Returns `true` if the script was successfully added. Otherwise prints an error message and returns `false`.
|
||||
// `false` can be returned if either file not found or has syntax errors or such script already exists in the container.
|
||||
bool addNewScript(const std::string& path);
|
||||
// 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);
|
||||
|
||||
// Removes script. Returns `true` if it was successfully removed.
|
||||
bool removeScript(const std::string& path);
|
||||
void removeAllScripts();
|
||||
bool hasScript(int scriptId) const { return mScripts.count(scriptId) != 0; }
|
||||
void removeScript(int scriptId);
|
||||
|
||||
// Processes timers. gameSeconds and gameHours are time (in seconds and in game hours) passed from the game start.
|
||||
void processTimers(double gameSeconds, double gameHours);
|
||||
|
@ -107,22 +114,22 @@ namespace LuaUtil
|
|||
// only built-in types and types from util package can be serialized.
|
||||
void setSerializer(const UserdataSerializer* serializer) { mSerializer = 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&);
|
||||
|
||||
// Calls engineHandler "onLoad" for every script with given data.
|
||||
// If resetScriptList=true, then removes all currently active scripts and runs the scripts that were saved in ESM::LuaScripts.
|
||||
// If resetScriptList=false, then list of running scripts is not changed, only engineHandlers "onLoad" are called.
|
||||
void load(const ESM::LuaScripts&, bool resetScriptList);
|
||||
|
||||
// Returns the hidden data of a script.
|
||||
// Each script has a corresponding "hidden data" - a lua table that is not accessible from the script itself,
|
||||
// but can be used by built-in packages. It contains ScriptId and can contain any arbitrary data.
|
||||
sol::table getHiddenData(const std::string& scriptPath);
|
||||
// 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(const std::string& scriptPath, std::string_view callbackName, sol::function callback);
|
||||
void registerTimerCallback(int scriptId, std::string_view callbackName, sol::function callback);
|
||||
|
||||
// Sets up a timer, that can be automatically saved and loaded.
|
||||
// timeUnit - game seconds (TimeUnit::Seconds) or game hours (TimeUnit::Hours).
|
||||
|
@ -130,18 +137,24 @@ namespace LuaUtil
|
|||
// 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(TimeUnit timeUnit, double time, const std::string& scriptPath,
|
||||
void setupSerializableTimer(TimeUnit timeUnit, double time, int scriptId,
|
||||
std::string_view callbackName, sol::object callbackArg);
|
||||
|
||||
// Creates a timer. `callback` is an arbitrary Lua function. This type of timers is called "unsavable"
|
||||
// because it can not be stored in saves. I.e. loading a saved game will not fully restore the state.
|
||||
void setupUnsavableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, sol::function callback);
|
||||
void setupUnsavableTimer(TimeUnit timeUnit, double time, int scriptId, sol::function callback);
|
||||
|
||||
protected:
|
||||
struct Handler
|
||||
{
|
||||
int mScriptId;
|
||||
sol::function mFn;
|
||||
};
|
||||
|
||||
struct EngineHandlerList
|
||||
{
|
||||
std::string_view mName;
|
||||
std::vector<sol::protected_function> mList;
|
||||
std::vector<Handler> mList;
|
||||
|
||||
// "name" must be string literal
|
||||
explicit EngineHandlerList(std::string_view name) : mName(name) {}
|
||||
|
@ -151,12 +164,13 @@ namespace LuaUtil
|
|||
template <typename... Args>
|
||||
void callEngineHandlers(EngineHandlerList& handlers, const Args&... args)
|
||||
{
|
||||
for (sol::protected_function& handler : handlers.mList)
|
||||
for (Handler& handler : handlers.mList)
|
||||
{
|
||||
try { LuaUtil::call(handler, args...); }
|
||||
try { LuaUtil::call(handler.mFn, args...); }
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Log(Debug::Error) << mNamePrefix << " " << handlers.mName << " failed. " << e.what();
|
||||
Log(Debug::Error) << mNamePrefix << "[" << scriptPath(handler.mScriptId) << "] "
|
||||
<< handlers.mName << " failed. " << e.what();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -171,34 +185,49 @@ namespace LuaUtil
|
|||
private:
|
||||
struct Script
|
||||
{
|
||||
sol::object mInterface; // returned value of the script (sol::table or nil)
|
||||
std::optional<sol::function> mOnSave;
|
||||
std::optional<sol::function> mOnOverride;
|
||||
std::optional<sol::table> mInterface;
|
||||
std::string mInterfaceName;
|
||||
sol::table mHiddenData;
|
||||
std::map<std::string, sol::function> mRegisteredCallbacks;
|
||||
std::map<int64_t, sol::function> mTemporaryCallbacks;
|
||||
};
|
||||
struct Timer
|
||||
{
|
||||
double mTime;
|
||||
bool mSerializable;
|
||||
std::string mScript;
|
||||
int mScriptId;
|
||||
std::variant<std::string, int64_t> mCallback; // string if serializable, integer otherwise
|
||||
sol::object mArg;
|
||||
std::string mSerializedArg;
|
||||
|
||||
bool operator<(const Timer& t) const { return mTime > t.mTime; }
|
||||
};
|
||||
using EventHandlerList = std::vector<sol::protected_function>;
|
||||
using EventHandlerList = std::vector<Handler>;
|
||||
|
||||
void parseEngineHandlers(sol::table handlers, std::string_view scriptPath);
|
||||
void parseEventHandlers(sol::table handlers, std::string_view scriptPath);
|
||||
// Add to container without calling onInit/onLoad.
|
||||
bool addScript(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 std::string& scriptPath(int scriptId) const { return mLua.getConfiguration()[scriptId].mScriptPath; }
|
||||
void callOnInit(int scriptId, const sol::function& onInit);
|
||||
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);
|
||||
|
||||
ESM::LuaScriptCfg::Flags mAutoStartMode;
|
||||
const UserdataSerializer* mSerializer = nullptr;
|
||||
std::map<std::string, sol::object> API;
|
||||
std::map<std::string, sol::object> mAPI;
|
||||
|
||||
std::vector<std::string> mScriptOrder;
|
||||
std::map<std::string, Script> mScripts;
|
||||
std::map<int, Script> mScripts;
|
||||
sol::table mPublicInterfaces;
|
||||
|
||||
EngineHandlerList mUpdateHandlers{"onUpdate"};
|
||||
|
|
|
@ -25,6 +25,7 @@ class StringUtils
|
|||
template <typename T>
|
||||
static T argument(T value) noexcept
|
||||
{
|
||||
static_assert(!std::is_same_v<T, std::string_view>, "std::string_view is not supported");
|
||||
return value;
|
||||
}
|
||||
|
||||
|
@ -324,14 +325,20 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
static inline void replaceLast(std::string& str, const std::string& substr, const std::string& with)
|
||||
{
|
||||
size_t pos = str.rfind(substr);
|
||||
if (pos == std::string::npos)
|
||||
return;
|
||||
static inline void replaceLast(std::string& str, const std::string& substr, const std::string& with)
|
||||
{
|
||||
size_t pos = str.rfind(substr);
|
||||
if (pos == std::string::npos)
|
||||
return;
|
||||
|
||||
str.replace(pos, substr.size(), with);
|
||||
}
|
||||
str.replace(pos, substr.size(), with);
|
||||
}
|
||||
|
||||
static inline bool ciEndsWith(std::string_view s, std::string_view suffix)
|
||||
{
|
||||
return s.size() >= suffix.size() && std::equal(suffix.rbegin(), suffix.rend(), s.rbegin(),
|
||||
[](char l, char r) { return toLower(l) == toLower(r); });
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -6,14 +6,22 @@ Engine handler is a function defined by a script, that can be called by the engi
|
|||
+---------------------------------------------------------------------------------------------------------+
|
||||
| **Can be defined by any script** |
|
||||
+----------------------------------+----------------------------------------------------------------------+
|
||||
| onInit(initData) | | Called once when the script is created (not loaded). `InitData can`|
|
||||
| | | `be assigned to a script in openmw-cs (not yet implemented)`. |
|
||||
| | | ``onInterfaceOverride`` can be called before ``onInit``. |
|
||||
+----------------------------------+----------------------------------------------------------------------+
|
||||
| onUpdate(dt) | | Called every frame if game not paused. `dt` is the time |
|
||||
| | | from the last update in seconds. |
|
||||
+----------------------------------+----------------------------------------------------------------------+
|
||||
| onSave() -> data | | Called when the game is saving. May be called in inactive |
|
||||
| onSave() -> savedData | | Called when the game is saving. May be called in inactive |
|
||||
| | | state, so it shouldn't use `openmw.nearby`. |
|
||||
+----------------------------------+----------------------------------------------------------------------+
|
||||
| onLoad(data) | | Called on loading with the data previosly returned by |
|
||||
| | | onSave. During loading the object is always inactive. |
|
||||
| onLoad(savedData, initData) | | Called on loading with the data previosly returned by |
|
||||
| | | onSave. During loading the object is always inactive. initData is |
|
||||
| | | the same as in onInit. |
|
||||
+----------------------------------+----------------------------------------------------------------------+
|
||||
| onInterfaceOverride(base) | | Called if the current script has an interface and overrides an |
|
||||
| | | interface (``base``) of another script. |
|
||||
+----------------------------------+----------------------------------------------------------------------+
|
||||
| **Only for global scripts** |
|
||||
+----------------------------------+----------------------------------------------------------------------+
|
||||
|
|
|
@ -73,7 +73,7 @@ Let's write a simple example of a `Player script`:
|
|||
|
||||
.. code-block:: Lua
|
||||
|
||||
-- Saved to my_lua_mod/example/player.lua
|
||||
-- Save to my_lua_mod/example/player.lua
|
||||
|
||||
local ui = require('openmw.ui')
|
||||
|
||||
|
@ -87,42 +87,82 @@ Let's write a simple example of a `Player script`:
|
|||
}
|
||||
}
|
||||
|
||||
In order to attach it to the player we also need a global script:
|
||||
The script will be used only if it is specified in one of content files.
|
||||
OpenMW Lua is an inclusive OpenMW feature, so it can not be controlled by ESP/ESM.
|
||||
The options are:
|
||||
|
||||
.. code-block:: Lua
|
||||
|
||||
-- Saved to my_lua_mod/example/global.lua
|
||||
|
||||
return {
|
||||
engineHandlers = {
|
||||
onPlayerAdded = function(player) player:addScript('example/player.lua') end
|
||||
}
|
||||
}
|
||||
|
||||
And one more file -- to start the global script:
|
||||
|
||||
1. Create text file "my_lua_mod.omwscripts" with the following line:
|
||||
::
|
||||
|
||||
# Saved to my_lua_mod/my_lua_mod.omwscripts
|
||||
PLAYER: example/player.lua
|
||||
|
||||
# It is just a list of global scripts to run. Each file is on a separate line.
|
||||
example/global.lua
|
||||
2. (not implemented yet) Add the script in OpenMW CS on "Lua scripts" view and save as "my_lua_mod.omwaddon".
|
||||
|
||||
Finally :ref:`register <Lua scripting>` it in ``openmw.cfg``:
|
||||
|
||||
Enable it in ``openmw.cfg`` the same way as any other mod:
|
||||
|
||||
::
|
||||
|
||||
data=path/to/my_lua_mod
|
||||
lua-scripts=my_lua_mod.omwscripts
|
||||
content=my_lua_mod.omwscripts # or content=my_lua_mod.omwaddon
|
||||
|
||||
Now every time the player presses "X" on a keyboard, a message is shown.
|
||||
|
||||
|
||||
Format of ``.omwscripts``
|
||||
=========================
|
||||
|
||||
::
|
||||
|
||||
# Lines starting with '#' are comments
|
||||
|
||||
GLOBAL: my_mod/some_global_script.lua
|
||||
|
||||
# Script that will be automatically attached to the player
|
||||
PLAYER: my_mod/player.lua
|
||||
|
||||
# Local script that will be automatically attached to every NPC and every creature in the game
|
||||
NPC, CREATURE: my_mod/some_other_script.lua
|
||||
|
||||
# Local script that can be attached to any object by a global script
|
||||
CUSTOM: my_mod/something.lua
|
||||
|
||||
# Local script that will be automatically attached to any Container AND can be
|
||||
# attached to any other object by a global script.
|
||||
CONTAINER, CUSTOM: my_mod/container.lua
|
||||
|
||||
Each script is described by one line:
|
||||
``<flags>: <path to .lua file in virtual file system>``.
|
||||
The order of lines determines the script load order (i.e. script priorities).
|
||||
|
||||
Possible flags are:
|
||||
|
||||
- ``GLOBAL`` - a global script; always active, can not by stopped;
|
||||
- ``CUSTOM`` - dynamic local script that can be started or stopped by a global script;
|
||||
- ``PLAYER`` - an auto started player script;
|
||||
- ``ACTIVATOR`` - a local script that will be automatically attached to any activator;
|
||||
- ``ARMOR`` - a local script that will be automatically attached to any armor;
|
||||
- ``BOOK`` - a local script that will be automatically attached to any book;
|
||||
- ``CLOTHING`` - a local script that will be automatically attached to any clothing;
|
||||
- ``CONTAINER`` - a local script that will be automatically attached to any container;
|
||||
- ``CREATURE`` - a local script that will be automatically attached to any creature;
|
||||
- ``DOOR`` - a local script that will be automatically attached to any door;
|
||||
- ``INGREDIENT`` - a local script that will be automatically attached to any ingredient;
|
||||
- ``LIGHT`` - a local script that will be automatically attached to any light;
|
||||
- ``MISC_ITEM`` - a local script that will be automatically attached to any miscellaneous item;
|
||||
- ``NPC`` - a local script that will be automatically attached to any NPC;
|
||||
- ``POTION`` - a local script that will be automatically attached to any potion;
|
||||
- ``WEAPON`` - a local script that will be automatically attached to any weapon.
|
||||
|
||||
Several flags (except ``GLOBAL``) can be used with a single script. Use space or comma as a separator.
|
||||
|
||||
Hot reloading
|
||||
=============
|
||||
|
||||
It is possible to modify a script without restarting OpenMW. To apply changes, open the in-game console and run the command: ``reloadlua``.
|
||||
This will restart all Lua scripts using the `onSave and onLoad`_ handlers the same way as if the game was saved or loaded.
|
||||
It works only with existing ``*.lua`` files that are not packed to any archives. Adding new scripts or modifying ``*.omwscripts`` files always requires restarting the game.
|
||||
It reloads all ``.omwscripts`` files and ``.lua`` files that are not packed to any archives. ``.omwaddon`` files and scripts packed to BSA can not be changed without restarting the game.
|
||||
|
||||
Script structure
|
||||
================
|
||||
|
@ -196,7 +236,7 @@ Engine handlers
|
|||
|
||||
An engine handler is a function defined by a script, that can be called by the engine. I.e. it is an engine-to-script interaction.
|
||||
Not visible to other scripts. If several scripts register an engine handler with the same name,
|
||||
the engine calls all of them in the same order as the scripts were started.
|
||||
the engine calls all of them according to the load order (i.e. the order of ``content=`` entries in ``openmw.cfg``) and the order of scripts in ``omwaddon/omwscripts``.
|
||||
|
||||
Some engine handlers are allowed only for global, or only for local/player scripts. Some are universal.
|
||||
See :ref:`Engine handlers reference`.
|
||||
|
@ -210,12 +250,6 @@ The value that `onSave` returns will be passed to `onLoad` when the game is load
|
|||
It is the only way to save the internal state of a script. All other script variables will be lost after closing the game.
|
||||
The saved state must be :ref:`serializable <Serializable data>`.
|
||||
|
||||
The list of active global scripts is controlled by ``*.omwscripts`` files. Loading a save doesn't synchronize
|
||||
the list of global scripts with those that were active previously, it only calls `onLoad` for those currently active.
|
||||
|
||||
For local scripts the situation is different. When a save is loading, it tries to run all local scripts that were saved.
|
||||
So if ``lua-scripts=`` entries of some mod are removed, but ``data=`` entries are still enabled, then local scripts from the mod may still run.
|
||||
|
||||
`onSave` and `onLoad` can be called even for objects in inactive state, so it shouldn't use `openmw.nearby`.
|
||||
|
||||
An example:
|
||||
|
@ -366,26 +400,28 @@ Overriding the interface and adding a debug output:
|
|||
|
||||
.. code-block:: Lua
|
||||
|
||||
local interfaces = require('openmw.interfaces')
|
||||
local baseInterface = nil -- will be assigned by `onInterfaceOverride`
|
||||
interface = {
|
||||
version = 1,
|
||||
doSomething = function(x, y)
|
||||
print(string.format('SomeUtils.doSomething(%d, %d)', x, y))
|
||||
baseInterface.doSomething(x, y) -- calls the original `doSomething`
|
||||
|
||||
-- it is important to save it before returning the new interface
|
||||
local orig = interfaces.SomeUtils
|
||||
|
||||
return {
|
||||
interfaceName = "SomeUtils"
|
||||
interface = {
|
||||
version = orig.version,
|
||||
doSomething = function(x, y)
|
||||
print(string.format('SomeUtils.doSomething(%d, %d)', x, y))
|
||||
orig.doSomething(x, y) -- calls the original `doSomething`
|
||||
|
||||
-- WRONG! Would lead to an infinite recursion.
|
||||
-- interfaces.SomeUtils.doSomething(x, y)
|
||||
end,
|
||||
}
|
||||
-- WRONG! Would lead to an infinite recursion.
|
||||
-- local interfaces = require('openmw.interfaces')
|
||||
-- interfaces.SomeUtils.doSomething(x, y)
|
||||
end,
|
||||
}
|
||||
|
||||
A general recomendation about overriding is that the new interface should be fully compatible with the old one.
|
||||
return {
|
||||
interfaceName = "SomeUtils",
|
||||
interface = interface,
|
||||
engineHandlers = {
|
||||
onInterfaceOverride = function(base) baseInterface = base end,
|
||||
},
|
||||
}
|
||||
|
||||
A general recommendation about overriding is that the new interface should be fully compatible with the old one.
|
||||
So it is fine to change the behaviour of `SomeUtils.doSomething`, but if you want to add a completely new function, it would be
|
||||
better to create a new interface for it. For example `SomeUtilsExtended` with an additional function `doSomethingElse`.
|
||||
|
||||
|
@ -418,7 +454,7 @@ Events are the main way of interacting between local and global scripts.
|
|||
They are not recommended for interactions between two global scripts, because in this case interfaces are more convenient.
|
||||
|
||||
If several scripts register handlers for the same event, the handlers will be called in reverse order (opposite to engine handlers).
|
||||
I.e. the handler from the last attached script will be called first.
|
||||
I.e. the handler from the last script in the load order will be called first.
|
||||
Return value 'false' means "skip all other handlers for this event".
|
||||
Any other return value (including nil) means nothing.
|
||||
|
||||
|
@ -471,7 +507,7 @@ The protection mod attaches an additional local script to every actor. The scrip
|
|||
eventHandlers = { DamagedByDarkPower = reduceDarkDamage },
|
||||
}
|
||||
|
||||
In order to be able to intercept the event, the protection script should be attached after the original script (i.e. below in the load order).
|
||||
In order to be able to intercept the event, the protection script should be placed in the load order below the original script.
|
||||
|
||||
|
||||
Timers
|
||||
|
|
|
@ -333,17 +333,9 @@ Lua scripting
|
|||
OpenMW supports Lua scripts. See :ref:`Lua scripting documentation <OpenMW Lua scripting>`.
|
||||
It is not compatible with MWSE. A mod with Lua scripts will work only if it was developed specifically for OpenMW.
|
||||
|
||||
Mods can contain ``*.omwscripts`` files. They should be registered in the ``openmw.cfg`` via "lua-scripts" entries. The order of the "lua-scripts" entries can be important. If "some_lua_mod" uses API provided by "another_lua_mod", then omwscripts from "another_lua_mod" should be registered first. For example:
|
||||
|
||||
::
|
||||
|
||||
data="path/to/another_lua_mod"
|
||||
content=another_lua_mod.omwaddon
|
||||
lua-scripts=another_lua_mod.omwscripts
|
||||
|
||||
data="path/to/some_lua_mod"
|
||||
content=some_lua_mod.omwaddon
|
||||
lua-scripts=some_lua_mod.omwscripts
|
||||
Installation of a Lua mod is the same as of any other mod: add ``data=`` and ``content=`` entries to ``openmw.cfg``.
|
||||
Files with suffix ``.omwscripts`` are special type of content files and should also be enabled using ``content=`` entries.
|
||||
Note that for some mods load order can be important.
|
||||
|
||||
.. _`Graphic Herbalism`: https://www.nexusmods.com/morrowind/mods/46599
|
||||
.. _`OpenMW Containers Animated`: https://www.nexusmods.com/morrowind/mods/46232
|
||||
|
|
|
@ -192,10 +192,26 @@
|
|||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Add new local script to the object.
|
||||
-- Can be called only from a global script.
|
||||
-- Can be called only from a global script. Script should be specified in a content
|
||||
-- file (omwgame/omwaddon/omwscripts) with a CUSTOM flag.
|
||||
-- @function [parent=#GameObject] addScript
|
||||
-- @param self
|
||||
-- @param #string scriptPath Path to the script in OpenMW virtual filesystem
|
||||
-- @param #string scriptPath Path to the script in OpenMW virtual filesystem.
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Whether a script with given path is attached to this object.
|
||||
-- Can be called only from a global script.
|
||||
-- @function [parent=#GameObject] hasScript
|
||||
-- @param self
|
||||
-- @param #string scriptPath Path to the script in OpenMW virtual filesystem.
|
||||
-- @return #boolean
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Removes script that was attached by `addScript`
|
||||
-- Can be called only from a global script.
|
||||
-- @function [parent=#GameObject] removeScript
|
||||
-- @param self
|
||||
-- @param #string scriptPath Path to the script in OpenMW virtual filesystem.
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Moves object to given cell and position.
|
||||
|
|
Loading…
Reference in a new issue