Merge branch 'omwscripts' into 'master'

Change format of "*.omwscripts" files

See merge request OpenMW/openmw!1271
pull/3207/head
psi29a 3 years ago
commit 34b63bf142

@ -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;
};
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 getMWClassName(const std::type_index& cls_type, std::string_view fallback)
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

@ -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, AutoStart)
{
LuaUtil::ScriptsContainer scripts(&mLua, "Test", ESM::LuaScriptCfg::sPlayer);
testing::internal::CaptureStdout();
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");
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();
EXPECT_TRUE(scripts.addNewScript("testInterface.lua"));
EXPECT_TRUE(scripts.addNewScript("overrideInterface.lua"));
EXPECT_TRUE(scripts.addNewScript("useInterface.lua"));
scripts.addAutoStartedScripts();
scripts.update(1.5f);
EXPECT_TRUE(scripts.removeScript("overrideInterface.lua"));
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");
EXPECT_TRUE(scripts1.addNewScript("loadSave1.lua"));
EXPECT_TRUE(scripts1.addNewScript("test1.lua"));
EXPECT_TRUE(scripts1.addNewScript("loadSave2.lua"));
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(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.

@ -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();
}
}

@ -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)
{
Log(Debug::Error) << mNamePrefix << "[" << scriptPath(scriptId) << "] " << msg << ": " << e.what();
}
void ScriptsContainer::addPackage(std::string packageName, sol::object package)
{
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()
{
API[packageName] = makeReadOnly(std::move(package));
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::addNewScript(const std::string& path)
bool ScriptsContainer::addScript(int scriptId, std::optional<sol::function>& onInit, std::optional<sol::function>& onLoad)
{
if (mScripts.count(path) != 0)
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)
return; // no such script
Script& script = scriptIter->second;
if (script.mInterface)
removeInterface(scriptId, script);
script.mHiddenData[ScriptId::KEY] = sol::nil;
mScripts.erase(scriptIter);
for (auto& [_, handlers] : mEngineHandlers)
removeHandler(handlers->mList, scriptId);
for (auto& [_, handlers] : mEventHandlers)
removeHandler(handlers, scriptId);
}
void ScriptsContainer::insertInterface(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)
{
std::string_view interfaceName = getFieldOrNil(script, INTERFACE_NAME).as<std::string_view>();
if (mPublicInterfaces[interfaceName] == getFieldOrNil(script, INTERFACE))
if (scriptId == otherId || script.mInterfaceName != otherScript.mInterfaceName)
continue;
if (otherId < scriptId)
prev = &otherScript;
else
{
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;
}
}
next = &otherScript;
nextId = otherId;
break;
}
}
sol::object engineHandlers = getFieldOrNil(script, ENGINE_HANDLERS);
if (engineHandlers != sol::nil)
if (prev && script.mOnOverride)
{
for (auto& [key, value] : sol::table(engineHandlers))
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
{
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>()));
next = &otherScript;
nextId = otherId;
break;
}
}
sol::object eventHandlers = getFieldOrNil(script, EVENT_HANDLERS);
if (eventHandlers != sol::nil)
if (next)
{
for (auto& [key, value] : sol::table(eventHandlers))
if (next->mOnOverride)
{
EventHandlerList& list = mEventHandlers.find(key.as<std::string_view>())->second;
list.erase(std::find(list.begin(), list.end(), value.as<sol::protected_function>()));
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); }
}
}
mScripts.erase(scriptIter);
mScriptOrder.erase(std::find(mScriptOrder.begin(), mScriptOrder.end(), path));
return true;
else if (prev)
mPublicInterfaces[script.mInterfaceName] = *prev->mInterface;
else
mPublicInterfaces[script.mInterfaceName] = sol::nil;
}
void ScriptsContainer::parseEventHandlers(sol::table handlers, std::string_view scriptPath)
void ScriptsContainer::insertHandler(std::vector<Handler>& list, int scriptId, sol::function fn)
{
for (auto& [key, value] : handlers)
list.emplace_back();
int pos = list.size() - 1;
while (pos > 0 && list[pos - 1].mScriptId > scriptId)
{
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);
list[pos] = std::move(list[pos - 1]);
pos--;
}
list[pos].mScriptId = scriptId;
list[pos].mFn = std::move(fn);
}
void ScriptsContainer::parseEngineHandlers(sol::table handlers, std::string_view scriptPath)
void ScriptsContainer::removeHandler(std::vector<Handler>& list, int scriptId)
{
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 << "]";
else
it->second->mList.push_back(value);
}
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);
std::optional<int> scriptId = cfg.findId(s.mScriptPath);
if (!scriptId)
{
Log(Debug::Verbose) << "Ignoring " << mNamePrefix << "[" << s.mScriptPath << "]; script not registered";
continue;
}
if (!(cfg[*scriptId].mFlags & (ESM::LuaScriptCfg::sCustom | mAutoStartMode)))
{
Log(Debug::Verbose) << "Ignoring " << mNamePrefix << "[" << s.mScriptPath << "]; this script is not allowed here";
continue;
}
scripts[*scriptId] = &s;
}
else
scriptsWithoutSavedData = mScripts;
mSecondsTimersQueue.clear();
mHoursTimersQueue.clear();
for (const ESM::LuaScript& script : data.mScripts)
for (const auto& [scriptId, savedScript] : scripts)
{
auto iter = mScripts.find(script.mScriptPath);
if (iter == mScripts.end())
std::optional<sol::function> onInit, onLoad;
if (!addScript(scriptId, onInit, onLoad))
continue;
scriptsWithoutSavedData.erase(iter->first);
iter->second.mHiddenData.get<sol::table>(TEMPORARY_TIMER_CALLBACKS).clear();
try
if (savedScript == nullptr)
{
sol::object handler = getFieldOrNil(iter->second.mInterface, ENGINE_HANDLERS, HANDLER_LOAD);
if (handler != sol::nil)
{
sol::object state = deserialize(mLua.sol(), script.mData, mSerializer);
LuaUtil::call(handler, state);
}
if (onInit)
callOnInit(scriptId, *onInit);
continue;
}
catch (std::exception& e)
if (onLoad)
{
Log(Debug::Error) << mNamePrefix << "[" << script.mScriptPath << "] onLoad failed: " << e.what();
try
{
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); }
}
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>;
// Add to container without calling onInit/onLoad.
bool addScript(int scriptId, std::optional<sol::function>& onInit, std::optional<sol::function>& onLoad);
void parseEngineHandlers(sol::table handlers, std::string_view scriptPath);
void parseEventHandlers(sol::table handlers, std::string_view scriptPath);
// 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
1. Create text file "my_lua_mod.omwscripts" with the following line:
::
return {
engineHandlers = {
onPlayerAdded = function(player) player:addScript('example/player.lua') end
}
}
PLAYER: example/player.lua
2. (not implemented yet) Add the script in OpenMW CS on "Lua scripts" view and save as "my_lua_mod.omwaddon".
And one more file -- to start the global script:
Enable it in ``openmw.cfg`` the same way as any other mod:
::
# Saved to my_lua_mod/my_lua_mod.omwscripts
data=path/to/my_lua_mod
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.
# It is just a list of global scripts to run. Each file is on a separate line.
example/global.lua
Finally :ref:`register <Lua scripting>` it in ``openmw.cfg``:
Format of ``.omwscripts``
=========================
::
data=path/to/my_lua_mod
lua-scripts=my_lua_mod.omwscripts
# Lines starting with '#' are comments
Now every time the player presses "X" on a keyboard, a message is shown.
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')
-- it is important to save it before returning the new interface
local orig = interfaces.SomeUtils
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`
-- WRONG! Would lead to an infinite recursion.
-- local interfaces = require('openmw.interfaces')
-- interfaces.SomeUtils.doSomething(x, y)
end,
}
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,
}
interfaceName = "SomeUtils",
interface = interface,
engineHandlers = {
onInterfaceOverride = function(base) baseInterface = base end,
},
}
A general recomendation about overriding is that the new interface should be fully compatible with the old one.
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…
Cancel
Save