mirror of https://github.com/OpenMW/openmw.git
Merge branch 'lua' into 'master'
Lua Closes #5990 See merge request OpenMW/openmw!430dont-compose-content
commit
61f3c528d2
@ -1,4 +1,4 @@
|
||||
#!/bin/sh -ex
|
||||
|
||||
curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/android/openmw-android-deps-20201129.zip -o ~/openmw-android-deps.zip
|
||||
curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/android/openmw-android-deps-20201230.zip -o ~/openmw-android-deps.zip
|
||||
unzip -o ~/openmw-android-deps -d /usr/lib/android-sdk/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr > /dev/null
|
||||
|
@ -0,0 +1,78 @@
|
||||
#ifndef GAME_MWBASE_LUAMANAGER_H
|
||||
#define GAME_MWBASE_LUAMANAGER_H
|
||||
|
||||
#include <SDL_events.h>
|
||||
|
||||
namespace MWWorld
|
||||
{
|
||||
class Ptr;
|
||||
}
|
||||
|
||||
namespace Loading
|
||||
{
|
||||
class Listener;
|
||||
}
|
||||
|
||||
namespace ESM
|
||||
{
|
||||
class ESMReader;
|
||||
class ESMWriter;
|
||||
struct LuaScripts;
|
||||
}
|
||||
|
||||
namespace MWBase
|
||||
{
|
||||
|
||||
class LuaManager
|
||||
{
|
||||
public:
|
||||
virtual ~LuaManager() = default;
|
||||
|
||||
virtual void newGameStarted() = 0;
|
||||
virtual void keyPressed(const SDL_KeyboardEvent &arg) = 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;
|
||||
virtual void objectRemovedFromScene(const MWWorld::Ptr& ptr) = 0;
|
||||
virtual void appliedToObject(const MWWorld::Ptr& toPtr, std::string_view recordId, const MWWorld::Ptr& fromPtr) = 0;
|
||||
// TODO: notify LuaManager about other events
|
||||
// virtual void objectOnHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object,
|
||||
// const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) = 0;
|
||||
|
||||
struct ActorControls
|
||||
{
|
||||
bool mDisableAI;
|
||||
bool mControlledFromLua;
|
||||
|
||||
bool mJump;
|
||||
bool mRun;
|
||||
float mMovement;
|
||||
float mSideMovement;
|
||||
float mTurn;
|
||||
};
|
||||
|
||||
virtual ActorControls* getActorControls(const MWWorld::Ptr&) const = 0;
|
||||
|
||||
virtual void clear() = 0;
|
||||
virtual void setupPlayer(const MWWorld::Ptr&) = 0;
|
||||
|
||||
// Saving
|
||||
int countSavedGameRecords() const { return 1; };
|
||||
virtual void write(ESM::ESMWriter& writer, Loading::Listener& progress) = 0;
|
||||
virtual void saveLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScripts& data) = 0;
|
||||
|
||||
// Loading from a save
|
||||
virtual void readRecord(ESM::ESMReader& reader, uint32_t type) = 0;
|
||||
virtual void loadLocalScripts(const MWWorld::Ptr& ptr, const ESM::LuaScripts& data) = 0;
|
||||
|
||||
// Should be called before loading. The map is used to fix refnums if the order of content files was changed.
|
||||
virtual void setContentFileMapping(const std::map<int, int>&) = 0;
|
||||
|
||||
// Drops script cache and reloads all scripts. Calls `onSave` and `onLoad` for every script.
|
||||
virtual void reloadAllScripts() = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // GAME_MWBASE_LUAMANAGER_H
|
@ -0,0 +1,124 @@
|
||||
#include "actions.hpp"
|
||||
|
||||
#include <components/debug/debuglog.hpp>
|
||||
|
||||
#include "../mwworld/cellstore.hpp"
|
||||
#include "../mwworld/class.hpp"
|
||||
#include "../mwworld/inventorystore.hpp"
|
||||
#include "../mwworld/player.hpp"
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
||||
void TeleportAction::apply(WorldView& worldView) const
|
||||
{
|
||||
MWWorld::CellStore* cell = worldView.findCell(mCell, mPos);
|
||||
if (!cell)
|
||||
{
|
||||
Log(Debug::Error) << "LuaManager::applyTeleport -> cell not found: '" << mCell << "'";
|
||||
return;
|
||||
}
|
||||
|
||||
MWBase::World* world = MWBase::Environment::get().getWorld();
|
||||
MWWorld::Ptr obj = worldView.getObjectRegistry()->getPtr(mObject, false);
|
||||
const MWWorld::Class& cls = obj.getClass();
|
||||
bool isPlayer = obj == world->getPlayerPtr();
|
||||
if (cls.isActor())
|
||||
cls.getCreatureStats(obj).land(isPlayer);
|
||||
if (isPlayer)
|
||||
{
|
||||
ESM::Position esmPos;
|
||||
static_assert(sizeof(esmPos) == sizeof(osg::Vec3f) * 2);
|
||||
std::memcpy(esmPos.pos, &mPos, sizeof(osg::Vec3f));
|
||||
std::memcpy(esmPos.rot, &mRot, sizeof(osg::Vec3f));
|
||||
world->getPlayer().setTeleported(true);
|
||||
if (cell->isExterior())
|
||||
world->changeToExteriorCell(esmPos, true);
|
||||
else
|
||||
world->changeToInteriorCell(mCell, esmPos, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
MWWorld::Ptr newObj = world->moveObject(obj, cell, mPos.x(), mPos.y(), mPos.z());
|
||||
world->rotateObject(newObj, mRot.x(), mRot.y(), mRot.z());
|
||||
}
|
||||
}
|
||||
|
||||
void SetEquipmentAction::apply(WorldView& worldView) const
|
||||
{
|
||||
MWWorld::Ptr actor = worldView.getObjectRegistry()->getPtr(mActor, false);
|
||||
MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor);
|
||||
std::array<bool, MWWorld::InventoryStore::Slots> usedSlots;
|
||||
std::fill(usedSlots.begin(), usedSlots.end(), false);
|
||||
|
||||
constexpr int anySlot = -1;
|
||||
auto tryEquipToSlot = [&actor, &store, &usedSlots, &worldView, anySlot](int slot, const Item& item) -> bool
|
||||
{
|
||||
auto old_it = slot != anySlot ? store.getSlot(slot) : store.end();
|
||||
MWWorld::Ptr itemPtr;
|
||||
if (std::holds_alternative<ObjectId>(item))
|
||||
{
|
||||
itemPtr = worldView.getObjectRegistry()->getPtr(std::get<ObjectId>(item), false);
|
||||
if (old_it != store.end() && *old_it == itemPtr)
|
||||
return true; // already equipped
|
||||
if (itemPtr.isEmpty() || itemPtr.getRefData().getCount() == 0 ||
|
||||
itemPtr.getContainerStore() != static_cast<const MWWorld::ContainerStore*>(&store))
|
||||
{
|
||||
Log(Debug::Warning) << "Object" << idToString(std::get<ObjectId>(item)) << " is not in inventory";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::string& recordId = std::get<std::string>(item);
|
||||
if (old_it != store.end() && *old_it->getCellRef().getRefIdPtr() == recordId)
|
||||
return true; // already equipped
|
||||
itemPtr = store.search(recordId);
|
||||
if (itemPtr.isEmpty() || itemPtr.getRefData().getCount() == 0)
|
||||
{
|
||||
Log(Debug::Warning) << "There is no object with recordId='" << recordId << "' in inventory";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto [allowedSlots, _] = itemPtr.getClass().getEquipmentSlots(itemPtr);
|
||||
bool requestedSlotIsAllowed = std::find(allowedSlots.begin(), allowedSlots.end(), slot) != allowedSlots.end();
|
||||
if (!requestedSlotIsAllowed)
|
||||
{
|
||||
auto firstAllowed = std::find_if(allowedSlots.begin(), allowedSlots.end(), [&](int s) { return !usedSlots[s]; });
|
||||
if (firstAllowed == allowedSlots.end())
|
||||
{
|
||||
Log(Debug::Warning) << "No suitable slot for " << ptrToString(itemPtr);
|
||||
return false;
|
||||
}
|
||||
slot = *firstAllowed;
|
||||
}
|
||||
|
||||
// TODO: Refactor InventoryStore to accept Ptr and get rid of this linear search.
|
||||
MWWorld::ContainerStoreIterator it = std::find(store.begin(), store.end(), itemPtr);
|
||||
if (it == store.end()) // should never happen
|
||||
throw std::logic_error("Item not found in container");
|
||||
|
||||
store.equip(slot, it, actor);
|
||||
return requestedSlotIsAllowed; // return true if equipped to requested slot and false if slot was changed
|
||||
};
|
||||
|
||||
for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot)
|
||||
{
|
||||
auto old_it = store.getSlot(slot);
|
||||
auto new_it = mEquipment.find(slot);
|
||||
if (new_it == mEquipment.end())
|
||||
{
|
||||
if (old_it != store.end())
|
||||
store.unequipSlot(slot, actor);
|
||||
continue;
|
||||
}
|
||||
if (tryEquipToSlot(slot, new_it->second))
|
||||
usedSlots[slot] = true;
|
||||
}
|
||||
for (const auto& [slot, item] : mEquipment)
|
||||
if (slot >= MWWorld::InventoryStore::Slots)
|
||||
tryEquipToSlot(anySlot, item);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
#ifndef MWLUA_ACTIONS_H
|
||||
#define MWLUA_ACTIONS_H
|
||||
|
||||
#include <variant>
|
||||
|
||||
#include "object.hpp"
|
||||
#include "worldview.hpp"
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
||||
// Some changes to the game world can not be done from the scripting thread (because it runs in parallel with OSG Cull),
|
||||
// so we need to queue it and apply from the main thread. All such changes should be implemented as classes inherited
|
||||
// from MWLua::Action.
|
||||
|
||||
class Action
|
||||
{
|
||||
public:
|
||||
virtual ~Action() {}
|
||||
virtual void apply(WorldView&) const = 0;
|
||||
};
|
||||
|
||||
class TeleportAction final : public Action
|
||||
{
|
||||
public:
|
||||
TeleportAction(ObjectId object, std::string cell, const osg::Vec3f& pos, const osg::Vec3f& rot)
|
||||
: mObject(object), mCell(std::move(cell)), mPos(pos), mRot(rot) {}
|
||||
|
||||
void apply(WorldView&) const override;
|
||||
|
||||
private:
|
||||
ObjectId mObject;
|
||||
std::string mCell;
|
||||
osg::Vec3f mPos;
|
||||
osg::Vec3f mRot;
|
||||
};
|
||||
|
||||
class SetEquipmentAction final : public Action
|
||||
{
|
||||
public:
|
||||
using Item = std::variant<std::string, ObjectId>; // recordId or ObjectId
|
||||
using Equipment = std::map<int, Item>; // slot to item
|
||||
|
||||
SetEquipmentAction(ObjectId actor, Equipment equipment) : mActor(actor), mEquipment(std::move(equipment)) {}
|
||||
|
||||
void apply(WorldView&) const override;
|
||||
|
||||
private:
|
||||
ObjectId mActor;
|
||||
Equipment mEquipment;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // MWLUA_ACTIONS_H
|
@ -0,0 +1,60 @@
|
||||
#include "luabindings.hpp"
|
||||
|
||||
namespace sol
|
||||
{
|
||||
template <>
|
||||
struct is_automagical<MWLua::AsyncPackageId> : std::false_type {};
|
||||
}
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
||||
struct TimerCallback
|
||||
{
|
||||
AsyncPackageId mAsyncId;
|
||||
std::string mName;
|
||||
};
|
||||
|
||||
sol::function getAsyncPackageInitializer(const Context& context)
|
||||
{
|
||||
using TimeUnit = LuaUtil::ScriptsContainer::TimeUnit;
|
||||
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));
|
||||
return TimerCallback{asyncId, std::string(name)};
|
||||
};
|
||||
api["newTimerInSeconds"] = [world=context.mWorldView](const AsyncPackageId&, double delay,
|
||||
const TimerCallback& callback, sol::object callbackArg)
|
||||
{
|
||||
callback.mAsyncId.mContainer->setupSerializableTimer(
|
||||
TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay,
|
||||
callback.mAsyncId.mScript, 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));
|
||||
};
|
||||
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));
|
||||
};
|
||||
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));
|
||||
};
|
||||
|
||||
auto initializer = [](sol::table hiddenData)
|
||||
{
|
||||
LuaUtil::ScriptsContainer::ScriptId id = hiddenData[LuaUtil::ScriptsContainer::ScriptId::KEY];
|
||||
return AsyncPackageId{id.mContainer, id.mPath};
|
||||
};
|
||||
return sol::make_object(context.mLua->sol(), initializer);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
#include "luabindings.hpp"
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
||||
sol::table initCameraPackage(const Context& context)
|
||||
{
|
||||
sol::table api(context.mLua->sol(), sol::create);
|
||||
// TODO
|
||||
return context.mLua->makeReadOnly(api);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
#include "luabindings.hpp"
|
||||
|
||||
#include <components/esm/loadcell.hpp>
|
||||
|
||||
#include "../mwworld/cellstore.hpp"
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
||||
template <class CellT, class ObjectT>
|
||||
static void initCellBindings(const std::string& prefix, const Context& context)
|
||||
{
|
||||
sol::usertype<CellT> cellT = context.mLua->sol().new_usertype<CellT>(prefix + "Cell");
|
||||
|
||||
cellT[sol::meta_function::equal_to] = [](const CellT& a, const CellT& b) { return a.mStore == b.mStore; };
|
||||
cellT[sol::meta_function::to_string] = [](const CellT& c)
|
||||
{
|
||||
const ESM::Cell* cell = c.mStore->getCell();
|
||||
std::stringstream res;
|
||||
if (cell->isExterior())
|
||||
res << "exterior(" << cell->getGridX() << ", " << cell->getGridY() << ")";
|
||||
else
|
||||
res << "interior(" << cell->mName << ")";
|
||||
return res.str();
|
||||
};
|
||||
|
||||
cellT["name"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->mName; });
|
||||
cellT["region"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->mRegion; });
|
||||
cellT["gridX"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->getGridX(); });
|
||||
cellT["gridY"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->getGridY(); });
|
||||
cellT["isExterior"] = sol::readonly_property([](const CellT& c) { return c.mStore->isExterior(); });
|
||||
cellT["hasWater"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->hasWater(); });
|
||||
|
||||
cellT["isInSameSpace"] = [](const CellT& c, const ObjectT& obj)
|
||||
{
|
||||
const MWWorld::Ptr& ptr = obj.ptr();
|
||||
if (!ptr.isInCell())
|
||||
return false;
|
||||
MWWorld::CellStore* cell = ptr.getCell();
|
||||
return cell == c.mStore || (cell->isExterior() && c.mStore->isExterior());
|
||||
};
|
||||
|
||||
if constexpr (std::is_same_v<CellT, GCell>)
|
||||
{ // only for global scripts
|
||||
cellT["selectObjects"] = [context](const CellT& cell, const Queries::Query& query)
|
||||
{
|
||||
return GObjectList{selectObjectsFromCellStore(query, cell.mStore, context)};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void initCellBindingsForLocalScripts(const Context& context)
|
||||
{
|
||||
initCellBindings<LCell, LObject>("L", context);
|
||||
}
|
||||
|
||||
void initCellBindingsForGlobalScripts(const Context& context)
|
||||
{
|
||||
initCellBindings<GCell, GObject>("G", context);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
#ifndef MWLUA_CONTEXT_H
|
||||
#define MWLUA_CONTEXT_H
|
||||
|
||||
#include "eventqueue.hpp"
|
||||
|
||||
namespace LuaUtil
|
||||
{
|
||||
class LuaState;
|
||||
class UserdataSerializer;
|
||||
}
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
class LuaManager;
|
||||
class WorldView;
|
||||
|
||||
struct Context
|
||||
{
|
||||
bool mIsGlobal;
|
||||
LuaManager* mLuaManager;
|
||||
LuaUtil::LuaState* mLua;
|
||||
LuaUtil::UserdataSerializer* mSerializer;
|
||||
WorldView* mWorldView;
|
||||
LocalEventQueue* mLocalEventQueue;
|
||||
GlobalEventQueue* mGlobalEventQueue;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // MWLUA_CONTEXT_H
|
@ -0,0 +1,63 @@
|
||||
#include "eventqueue.hpp"
|
||||
|
||||
#include <components/debug/debuglog.hpp>
|
||||
|
||||
#include <components/esm/esmreader.hpp>
|
||||
#include <components/esm/esmwriter.hpp>
|
||||
#include <components/esm/luascripts.hpp>
|
||||
|
||||
#include <components/lua/serialization.hpp>
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
||||
template <typename Event>
|
||||
void saveEvent(ESM::ESMWriter& esm, const ObjectId& dest, const Event& event)
|
||||
{
|
||||
esm.writeHNString("LUAE", event.mEventName);
|
||||
dest.save(esm, true);
|
||||
if (!event.mEventData.empty())
|
||||
saveLuaBinaryData(esm, event.mEventData);
|
||||
}
|
||||
|
||||
void loadEvents(sol::state& lua, ESM::ESMReader& esm, GlobalEventQueue& globalEvents, LocalEventQueue& localEvents,
|
||||
const std::map<int, int>& contentFileMapping, const LuaUtil::UserdataSerializer* serializer)
|
||||
{
|
||||
while (esm.isNextSub("LUAE"))
|
||||
{
|
||||
std::string name = esm.getHString();
|
||||
ObjectId dest;
|
||||
dest.load(esm, true);
|
||||
std::string data = loadLuaBinaryData(esm);
|
||||
try
|
||||
{
|
||||
data = LuaUtil::serialize(LuaUtil::deserialize(lua, data, serializer), serializer);
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Log(Debug::Error) << "loadEvent: invalid event data: " << e.what();
|
||||
}
|
||||
if (dest.isSet())
|
||||
{
|
||||
auto it = contentFileMapping.find(dest.mContentFile);
|
||||
if (it != contentFileMapping.end())
|
||||
dest.mContentFile = it->second;
|
||||
localEvents.push_back({dest, std::move(name), std::move(data)});
|
||||
}
|
||||
else
|
||||
globalEvents.push_back({std::move(name), std::move(data)});
|
||||
}
|
||||
}
|
||||
|
||||
void saveEvents(ESM::ESMWriter& esm, const GlobalEventQueue& globalEvents, const LocalEventQueue& localEvents)
|
||||
{
|
||||
ObjectId globalId;
|
||||
globalId.unset(); // Used as a marker of a global event.
|
||||
|
||||
for (const GlobalEvent& e : globalEvents)
|
||||
saveEvent(esm, globalId, e);
|
||||
for (const LocalEvent& e : localEvents)
|
||||
saveEvent(esm, e.mDest, e);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
#ifndef MWLUA_EVENTQUEUE_H
|
||||
#define MWLUA_EVENTQUEUE_H
|
||||
|
||||
#include "object.hpp"
|
||||
|
||||
namespace ESM
|
||||
{
|
||||
class ESMReader;
|
||||
class ESMWriter;
|
||||
}
|
||||
|
||||
namespace LuaUtil
|
||||
{
|
||||
class UserdataSerializer;
|
||||
}
|
||||
|
||||
namespace sol
|
||||
{
|
||||
class state;
|
||||
}
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
struct GlobalEvent
|
||||
{
|
||||
std::string mEventName;
|
||||
std::string mEventData;
|
||||
};
|
||||
struct LocalEvent
|
||||
{
|
||||
ObjectId mDest;
|
||||
std::string mEventName;
|
||||
std::string mEventData;
|
||||
};
|
||||
using GlobalEventQueue = std::vector<GlobalEvent>;
|
||||
using LocalEventQueue = std::vector<LocalEvent>;
|
||||
|
||||
void loadEvents(sol::state& lua, ESM::ESMReader& esm, GlobalEventQueue&, LocalEventQueue&,
|
||||
const std::map<int, int>& contentFileMapping, const LuaUtil::UserdataSerializer* serializer);
|
||||
void saveEvents(ESM::ESMWriter& esm, const GlobalEventQueue&, const LocalEventQueue&);
|
||||
}
|
||||
|
||||
#endif // MWLUA_EVENTQUEUE_H
|
@ -0,0 +1,36 @@
|
||||
#ifndef MWLUA_GLOBALSCRIPTS_H
|
||||
#define MWLUA_GLOBALSCRIPTS_H
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
#include <components/lua/luastate.hpp>
|
||||
#include <components/lua/scriptscontainer.hpp>
|
||||
|
||||
#include "object.hpp"
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
||||
class GlobalScripts : public LuaUtil::ScriptsContainer
|
||||
{
|
||||
public:
|
||||
GlobalScripts(LuaUtil::LuaState* lua) : LuaUtil::ScriptsContainer(lua, "Global")
|
||||
{
|
||||
registerEngineHandlers({&mActorActiveHandlers, &mNewGameHandlers, &mPlayerAddedHandlers});
|
||||
}
|
||||
|
||||
void newGameStarted() { callEngineHandlers(mNewGameHandlers); }
|
||||
void actorActive(const GObject& obj) { callEngineHandlers(mActorActiveHandlers, obj); }
|
||||
void playerAdded(const GObject& obj) { callEngineHandlers(mPlayerAddedHandlers, obj); }
|
||||
|
||||
private:
|
||||
EngineHandlerList mActorActiveHandlers{"onActorActive"};
|
||||
EngineHandlerList mNewGameHandlers{"onNewGame"};
|
||||
EngineHandlerList mPlayerAddedHandlers{"onPlayerAdded"};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // MWLUA_GLOBALSCRIPTS_H
|
@ -0,0 +1,114 @@
|
||||
#include "localscripts.hpp"
|
||||
|
||||
#include "../mwworld/ptr.hpp"
|
||||
#include "../mwworld/class.hpp"
|
||||
#include "../mwmechanics/aisequence.hpp"
|
||||
#include "../mwmechanics/aicombat.hpp"
|
||||
|
||||
#include "luamanagerimp.hpp"
|
||||
|
||||
namespace sol
|
||||
{
|
||||
template <>
|
||||
struct is_automagical<MWBase::LuaManager::ActorControls> : std::false_type {};
|
||||
template <>
|
||||
struct is_automagical<MWLua::LocalScripts::SelfObject> : std::false_type {};
|
||||
}
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
||||
void LocalScripts::initializeSelfPackage(const Context& context)
|
||||
{
|
||||
using ActorControls = MWBase::LuaManager::ActorControls;
|
||||
sol::usertype<ActorControls> controls = context.mLua->sol().new_usertype<ActorControls>("ActorControls");
|
||||
controls["movement"] = &ActorControls::mMovement;
|
||||
controls["sideMovement"] = &ActorControls::mSideMovement;
|
||||
controls["turn"] = &ActorControls::mTurn;
|
||||
controls["run"] = &ActorControls::mRun;
|
||||
controls["jump"] = &ActorControls::mJump;
|
||||
|
||||
sol::usertype<SelfObject> selfAPI =
|
||||
context.mLua->sol().new_usertype<SelfObject>("SelfObject", sol::base_classes, sol::bases<LObject>());
|
||||
selfAPI[sol::meta_function::to_string] = [](SelfObject& self) { return "openmw.self[" + self.toString() + "]"; };
|
||||
selfAPI["object"] = sol::readonly_property([](SelfObject& self) -> LObject { return LObject(self); });
|
||||
selfAPI["controls"] = sol::readonly_property([](SelfObject& self) { return &self.mControls; });
|
||||
selfAPI["isActive"] = [](SelfObject& self) { return &self.mIsActive; };
|
||||
selfAPI["setDirectControl"] = [](SelfObject& self, bool v) { self.mControls.mControlledFromLua = v; };
|
||||
selfAPI["enableAI"] = [](SelfObject& self, bool v) { self.mControls.mDisableAI = !v; };
|
||||
selfAPI["setEquipment"] = [manager=context.mLuaManager](const SelfObject& obj, sol::table equipment)
|
||||
{
|
||||
if (!obj.ptr().getClass().hasInventoryStore(obj.ptr()))
|
||||
{
|
||||
if (!equipment.empty())
|
||||
throw std::runtime_error(ptrToString(obj.ptr()) + " has no equipment slots");
|
||||
return;
|
||||
}
|
||||
SetEquipmentAction::Equipment eqp;
|
||||
for (auto& [key, value] : equipment)
|
||||
{
|
||||
int slot = key.as<int>();
|
||||
if (value.is<LObject>())
|
||||
eqp[slot] = value.as<LObject>().id();
|
||||
else
|
||||
eqp[slot] = value.as<std::string>();
|
||||
}
|
||||
manager->addAction(std::make_unique<SetEquipmentAction>(obj.id(), std::move(eqp)));
|
||||
};
|
||||
selfAPI["getCombatTarget"] = [worldView=context.mWorldView](SelfObject& self) -> sol::optional<LObject>
|
||||
{
|
||||
const MWWorld::Ptr& ptr = self.ptr();
|
||||
MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence();
|
||||
MWWorld::Ptr target;
|
||||
if (ai.getCombatTarget(target))
|
||||
return LObject(getId(target), worldView->getObjectRegistry());
|
||||
else
|
||||
return {};
|
||||
};
|
||||
selfAPI["stopCombat"] = [](SelfObject& self)
|
||||
{
|
||||
const MWWorld::Ptr& ptr = self.ptr();
|
||||
MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence();
|
||||
ai.stopCombat();
|
||||
};
|
||||
selfAPI["startCombat"] = [](SelfObject& self, const LObject& target)
|
||||
{
|
||||
const MWWorld::Ptr& ptr = self.ptr();
|
||||
MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence();
|
||||
ai.stack(MWMechanics::AiCombat(target.ptr()), ptr);
|
||||
};
|
||||
}
|
||||
|
||||
LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj)
|
||||
: LuaUtil::ScriptsContainer(lua, "L" + idToString(obj.id())), mData(obj)
|
||||
{
|
||||
mData.mControls.mControlledFromLua = false;
|
||||
mData.mControls.mDisableAI = false;
|
||||
this->addPackage("openmw.self", sol::make_object(lua->sol(), &mData));
|
||||
registerEngineHandlers({&mOnActiveHandlers, &mOnInactiveHandlers, &mOnConsumeHandlers});
|
||||
}
|
||||
|
||||
void LocalScripts::receiveEngineEvent(const EngineEvent& event, ObjectRegistry*)
|
||||
{
|
||||
std::visit([this](auto&& arg)
|
||||
{
|
||||
using EventT = std::decay_t<decltype(arg)>;
|
||||
if constexpr (std::is_same_v<EventT, OnActive>)
|
||||
{
|
||||
mData.mIsActive = true;
|
||||
callEngineHandlers(mOnActiveHandlers);
|
||||
}
|
||||
else if constexpr (std::is_same_v<EventT, OnInactive>)
|
||||
{
|
||||
mData.mIsActive = false;
|
||||
callEngineHandlers(mOnInactiveHandlers);
|
||||
}
|
||||
else
|
||||
{
|
||||
static_assert(std::is_same_v<EventT, OnConsume>);
|
||||
callEngineHandlers(mOnConsumeHandlers, arg.mRecordId);
|
||||
}
|
||||
}, event);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
#ifndef MWLUA_LOCALSCRIPTS_H
|
||||
#define MWLUA_LOCALSCRIPTS_H
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
#include <components/lua/luastate.hpp>
|
||||
#include <components/lua/scriptscontainer.hpp>
|
||||
|
||||
#include "../mwbase/luamanager.hpp"
|
||||
|
||||
#include "object.hpp"
|
||||
#include "luabindings.hpp"
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
||||
class LocalScripts : public LuaUtil::ScriptsContainer
|
||||
{
|
||||
public:
|
||||
static void initializeSelfPackage(const Context&);
|
||||
LocalScripts(LuaUtil::LuaState* lua, const LObject& obj);
|
||||
|
||||
MWBase::LuaManager::ActorControls* getActorControls() { return &mData.mControls; }
|
||||
|
||||
struct SelfObject : public LObject
|
||||
{
|
||||
SelfObject(const LObject& obj) : LObject(obj), mIsActive(false) {}
|
||||
MWBase::LuaManager::ActorControls mControls;
|
||||
bool mIsActive;
|
||||
};
|
||||
|
||||
struct OnActive {};
|
||||
struct OnInactive {};
|
||||
struct OnConsume
|
||||
{
|
||||
std::string mRecordId;
|
||||
};
|
||||
using EngineEvent = std::variant<OnActive, OnInactive, OnConsume>;
|
||||
|
||||
void receiveEngineEvent(const EngineEvent&, ObjectRegistry*);
|
||||
|
||||
protected:
|
||||
SelfObject mData;
|
||||
|
||||
private:
|
||||
EngineHandlerList mOnActiveHandlers{"onActive"};
|
||||
EngineHandlerList mOnInactiveHandlers{"onInactive"};
|
||||
EngineHandlerList mOnConsumeHandlers{"onConsume"};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // MWLUA_LOCALSCRIPTS_H
|
@ -0,0 +1,187 @@
|
||||
#include "luabindings.hpp"
|
||||
|
||||
#include <SDL_events.h>
|
||||
|
||||
#include <components/lua/luastate.hpp>
|
||||
#include <components/queries/luabindings.hpp>
|
||||
|
||||
#include "../mwworld/inventorystore.hpp"
|
||||
|
||||
#include "eventqueue.hpp"
|
||||
#include "worldview.hpp"
|
||||
|
||||
namespace sol
|
||||
{
|
||||
template <>
|
||||
struct is_automagical<SDL_Keysym> : std::false_type {};
|
||||
}
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
||||
static sol::table definitionList(LuaUtil::LuaState& lua, std::initializer_list<std::string> values)
|
||||
{
|
||||
sol::table res(lua.sol(), sol::create);
|
||||
for (const std::string& v : values)
|
||||
res[v] = v;
|
||||
return lua.makeReadOnly(res);
|
||||
}
|
||||
|
||||
sol::table initCorePackage(const Context& context)
|
||||
{
|
||||
auto* lua = context.mLua;
|
||||
sol::table api(lua->sol(), sol::create);
|
||||
api["API_VERSION"] = 0;
|
||||
api["sendGlobalEvent"] = [context](std::string eventName, const sol::object& eventData)
|
||||
{
|
||||
context.mGlobalEventQueue->push_back({std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer)});
|
||||
};
|
||||
api["getGameTimeInSeconds"] = [world=context.mWorldView]() { return world->getGameTimeInSeconds(); };
|
||||
api["getGameTimeInHours"] = [world=context.mWorldView]() { return world->getGameTimeInHours(); };
|
||||
api["OBJECT_TYPE"] = definitionList(*lua,
|
||||
{
|
||||
"Activator", "Armor", "Book", "Clothing", "Creature", "Door", "Ingredient",
|
||||
"Light", "Miscellaneous", "NPC", "Player", "Potion", "Static", "Weapon"
|
||||
});
|
||||
api["EQUIPMENT_SLOT"] = lua->makeReadOnly(lua->sol().create_table_with(
|
||||
"Helmet", MWWorld::InventoryStore::Slot_Helmet,
|
||||
"Cuirass", MWWorld::InventoryStore::Slot_Cuirass,
|
||||
"Greaves", MWWorld::InventoryStore::Slot_Greaves,
|
||||
"LeftPauldron", MWWorld::InventoryStore::Slot_LeftPauldron,
|
||||
"RightPauldron", MWWorld::InventoryStore::Slot_RightPauldron,
|
||||
"LeftGauntlet", MWWorld::InventoryStore::Slot_LeftGauntlet,
|
||||
"RightGauntlet", MWWorld::InventoryStore::Slot_RightGauntlet,
|
||||
"Boots", MWWorld::InventoryStore::Slot_Boots,
|
||||
"Shirt", MWWorld::InventoryStore::Slot_Shirt,
|
||||
"Pants", MWWorld::InventoryStore::Slot_Pants,
|
||||
"Skirt", MWWorld::InventoryStore::Slot_Skirt,
|
||||
"Robe", MWWorld::InventoryStore::Slot_Robe,
|
||||
"LeftRing", MWWorld::InventoryStore::Slot_LeftRing,
|
||||
"RightRing", MWWorld::InventoryStore::Slot_RightRing,
|
||||
"Amulet", MWWorld::InventoryStore::Slot_Amulet,
|
||||
"Belt", MWWorld::InventoryStore::Slot_Belt,
|
||||
"CarriedRight", MWWorld::InventoryStore::Slot_CarriedRight,
|
||||
"CarriedLeft", MWWorld::InventoryStore::Slot_CarriedLeft,
|
||||
"Ammunition", MWWorld::InventoryStore::Slot_Ammunition
|
||||
));
|
||||
return lua->makeReadOnly(api);
|
||||
}
|
||||
|
||||
sol::table initWorldPackage(const Context& context)
|
||||
{
|
||||
sol::table api(context.mLua->sol(), sol::create);
|
||||
WorldView* worldView = context.mWorldView;
|
||||
api["getCellByName"] = [worldView=context.mWorldView](const std::string& name) -> sol::optional<GCell>
|
||||
{
|
||||
MWWorld::CellStore* cell = worldView->findNamedCell(name);
|
||||
if (cell)
|
||||
return GCell{cell};
|
||||
else
|
||||
return sol::nullopt;
|
||||
};
|
||||
api["getExteriorCell"] = [worldView=context.mWorldView](int x, int y) -> sol::optional<GCell>
|
||||
{
|
||||
MWWorld::CellStore* cell = worldView->findExteriorCell(x, y);
|
||||
if (cell)
|
||||
return GCell{cell};
|
||||
else
|
||||
return sol::nullopt;
|
||||
};
|
||||
api["activeActors"] = GObjectList{worldView->getActorsInScene()};
|
||||
api["selectObjects"] = [context](const Queries::Query& query)
|
||||
{
|
||||
ObjectIdList list;
|
||||
WorldView* worldView = context.mWorldView;
|
||||
if (query.mQueryType == "activators")
|
||||
list = worldView->getActivatorsInScene();
|
||||
else if (query.mQueryType == "actors")
|
||||
list = worldView->getActorsInScene();
|
||||
else if (query.mQueryType == "containers")
|
||||
list = worldView->getContainersInScene();
|
||||
else if (query.mQueryType == "doors")
|
||||
list = worldView->getDoorsInScene();
|
||||
else if (query.mQueryType == "items")
|
||||
list = worldView->getItemsInScene();
|
||||
return GObjectList{selectObjectsFromList(query, list, context)};
|
||||
// TODO: Use sqlite to search objects that are not in the scene
|
||||
// return GObjectList{worldView->selectObjects(query, false)};
|
||||
};
|
||||
// TODO: add world.placeNewObject(recordId, cell, pos, [rot])
|
||||
return context.mLua->makeReadOnly(api);
|
||||
}
|
||||
|
||||
sol::table initNearbyPackage(const Context& context)
|
||||
{
|
||||
sol::table api(context.mLua->sol(), sol::create);
|
||||
WorldView* worldView = context.mWorldView;
|
||||
api["activators"] = LObjectList{worldView->getActivatorsInScene()};
|
||||
api["actors"] = LObjectList{worldView->getActorsInScene()};
|
||||
api["containers"] = LObjectList{worldView->getContainersInScene()};
|
||||
api["doors"] = LObjectList{worldView->getDoorsInScene()};
|
||||
api["items"] = LObjectList{worldView->getItemsInScene()};
|
||||
api["selectObjects"] = [context](const Queries::Query& query)
|
||||
{
|
||||
ObjectIdList list;
|
||||
WorldView* worldView = context.mWorldView;
|
||||
if (query.mQueryType == "activators")
|
||||
list = worldView->getActivatorsInScene();
|
||||
else if (query.mQueryType == "actors")
|
||||
list = worldView->getActorsInScene();
|
||||
else if (query.mQueryType == "containers")
|
||||
list = worldView->getContainersInScene();
|
||||
else if (query.mQueryType == "doors")
|
||||
list = worldView->getDoorsInScene();
|
||||
else if (query.mQueryType == "items")
|
||||
list = worldView->getItemsInScene();
|
||||
return LObjectList{selectObjectsFromList(query, list, context)};
|
||||
// TODO: Maybe use sqlite
|
||||
// return LObjectList{worldView->selectObjects(query, true)};
|
||||
};
|
||||
return context.mLua->makeReadOnly(api);
|
||||
}
|
||||
|
||||
sol::table initQueryPackage(const Context& context)
|
||||
{
|
||||
Queries::registerQueryBindings(context.mLua->sol());
|
||||
sol::table query(context.mLua->sol(), sol::create);
|
||||
for (std::string_view t : ObjectQueryTypes::types)
|
||||
query[t] = Queries::Query(std::string(t));
|
||||
for (const QueryFieldGroup& group : getBasicQueryFieldGroups())
|
||||
query[group.mName] = initFieldGroup(context, group);
|
||||
return query; // makeReadonly is applied by LuaState::addCommonPackage
|
||||
}
|
||||
|
||||
sol::table initFieldGroup(const Context& context, const QueryFieldGroup& group)
|
||||
{
|
||||
sol::table res(context.mLua->sol(), sol::create);
|
||||
for (const Queries::Field* field : group.mFields)
|
||||
{
|
||||
sol::table subgroup = res;
|
||||
if (field->path().empty())
|
||||
throw std::logic_error("Empty path in Queries::Field");
|
||||
for (size_t i = 0; i < field->path().size() - 1; ++i)
|
||||
{
|
||||
const std::string& name = field->path()[i];
|
||||
if (subgroup[name] == sol::nil)
|
||||
subgroup[name] = context.mLua->makeReadOnly(context.mLua->newTable());
|
||||
subgroup = context.mLua->getMutableFromReadOnly(subgroup[name]);
|
||||
}
|
||||
subgroup[field->path().back()] = field;
|
||||
}
|
||||
return context.mLua->makeReadOnly(res);
|
||||
}
|
||||
|
||||
void initInputBindings(const Context& context)
|
||||
{
|
||||
sol::usertype<SDL_Keysym> keyEvent = context.mLua->sol().new_usertype<SDL_Keysym>("KeyEvent");
|
||||
keyEvent["symbol"] = sol::readonly_property([](const SDL_Keysym& e) { return std::string(1, static_cast<char>(e.sym)); });
|
||||
keyEvent["code"] = sol::readonly_property([](const SDL_Keysym& e) -> int { return e.sym; });
|
||||
keyEvent["modifiers"] = sol::readonly_property([](const SDL_Keysym& e) -> int { return e.mod; });
|
||||
keyEvent["withShift"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_SHIFT; });
|
||||
keyEvent["withCtrl"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_CTRL; });
|
||||
keyEvent["withAlt"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_ALT; });
|
||||
keyEvent["withSuper"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_GUI; });
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,65 @@
|
||||
#ifndef MWLUA_LUABINDINGS_H
|
||||
#define MWLUA_LUABINDINGS_H
|
||||
|
||||
#include <components/lua/luastate.hpp>
|
||||
#include <components/lua/serialization.hpp>
|
||||
#include <components/lua/scriptscontainer.hpp>
|
||||
|
||||
#include "context.hpp"
|
||||
#include "eventqueue.hpp"
|
||||
#include "object.hpp"
|
||||
#include "query.hpp"
|
||||
#include "worldview.hpp"
|
||||
|
||||
namespace MWWorld
|
||||
{
|
||||
class CellStore;
|
||||
}
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
||||
sol::table initCorePackage(const Context&);
|
||||
sol::table initWorldPackage(const Context&);
|
||||
sol::table initNearbyPackage(const Context&);
|
||||
sol::table initQueryPackage(const Context&);
|
||||
|
||||
sol::table initFieldGroup(const Context&, const QueryFieldGroup&);
|
||||
|
||||
void initInputBindings(const Context&);
|
||||
|
||||
// Implemented in objectbindings.cpp
|
||||
void initObjectBindingsForLocalScripts(const Context&);
|
||||
void initObjectBindingsForGlobalScripts(const Context&);
|
||||
|
||||
// Implemented in cellbindings.cpp
|
||||
struct LCell // for local scripts
|
||||
{
|
||||
MWWorld::CellStore* mStore;
|
||||
};
|
||||
struct GCell // for global scripts
|
||||
{
|
||||
MWWorld::CellStore* mStore;
|
||||
};
|
||||
void initCellBindingsForLocalScripts(const Context&);
|
||||
void initCellBindingsForGlobalScripts(const Context&);
|
||||
|
||||
// Implemented in asyncbindings.cpp
|
||||
struct AsyncPackageId
|
||||
{
|
||||
// TODO: add ObjectId mLocalObject;
|
||||
LuaUtil::ScriptsContainer* mContainer;
|
||||
std::string mScript;
|
||||
};
|
||||
sol::function getAsyncPackageInitializer(const Context&);
|
||||
|
||||
// Implemented in camerabindings.cpp
|
||||
sol::table initCameraPackage(const Context&);
|
||||
|
||||
// Implemented in uibindings.cpp
|
||||
sol::table initUserInterfacePackage(const Context&);
|
||||
|
||||
// openmw.self package is implemented in localscripts.cpp
|
||||
}
|
||||
|
||||
#endif // MWLUA_LUABINDINGS_H
|
@ -0,0 +1,383 @@
|
||||
#include "luamanagerimp.hpp"
|
||||
|
||||
#include <components/debug/debuglog.hpp>
|
||||
|
||||
#include <components/esm/esmreader.hpp>
|
||||
#include <components/esm/esmwriter.hpp>
|
||||
#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/ptr.hpp"
|
||||
|
||||
#include "luabindings.hpp"
|
||||
#include "userdataserializer.hpp"
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
||||
LuaManager::LuaManager(const VFS::Manager* vfs, const std::vector<std::string>& scriptLists) : mLua(vfs)
|
||||
{
|
||||
Log(Debug::Info) << "Lua version: " << LuaUtil::getLuaVersion();
|
||||
mGlobalScriptList = LuaUtil::parseOMWScriptsFiles(vfs, scriptLists);
|
||||
|
||||
mGlobalSerializer = createUserdataSerializer(false, mWorldView.getObjectRegistry());
|
||||
mLocalSerializer = createUserdataSerializer(true, mWorldView.getObjectRegistry());
|
||||
mGlobalLoader = createUserdataSerializer(false, mWorldView.getObjectRegistry(), &mContentFileMapping);
|
||||
mLocalLoader = createUserdataSerializer(true, mWorldView.getObjectRegistry(), &mContentFileMapping);
|
||||
|
||||
mGlobalScripts.setSerializer(mGlobalSerializer.get());
|
||||
|
||||
Context context;
|
||||
context.mIsGlobal = true;
|
||||
context.mLuaManager = this;
|
||||
context.mLua = &mLua;
|
||||
context.mWorldView = &mWorldView;
|
||||
context.mLocalEventQueue = &mLocalEvents;
|
||||
context.mGlobalEventQueue = &mGlobalEvents;
|
||||
context.mSerializer = mGlobalSerializer.get();
|
||||
|
||||
Context localContext = context;
|
||||
localContext.mIsGlobal = false;
|
||||
localContext.mSerializer = mLocalSerializer.get();
|
||||
|
||||
initObjectBindingsForGlobalScripts(context);
|
||||
initCellBindingsForGlobalScripts(context);
|
||||
initObjectBindingsForLocalScripts(localContext);
|
||||
initCellBindingsForLocalScripts(localContext);
|
||||
LocalScripts::initializeSelfPackage(localContext);
|
||||
initInputBindings(localContext);
|
||||
|
||||
mLua.addCommonPackage("openmw.async", getAsyncPackageInitializer(context));
|
||||
mLua.addCommonPackage("openmw.util", LuaUtil::initUtilPackage(mLua.sol()));
|
||||
mLua.addCommonPackage("openmw.core", initCorePackage(context));
|
||||
mLua.addCommonPackage("openmw.query", initQueryPackage(context));
|
||||
mGlobalScripts.addPackage("openmw.world", initWorldPackage(context));
|
||||
mCameraPackage = initCameraPackage(localContext);
|
||||
mUserInterfacePackage = initUserInterfacePackage(localContext);
|
||||
mNearbyPackage = initNearbyPackage(localContext);
|
||||
}
|
||||
|
||||
void LuaManager::init()
|
||||
{
|
||||
mKeyPressEvents.clear();
|
||||
for (const std::string& path : mGlobalScriptList)
|
||||
if (mGlobalScripts.addNewScript(path))
|
||||
Log(Debug::Info) << "Global script started: " << path;
|
||||
}
|
||||
|
||||
void LuaManager::update(bool paused, float dt)
|
||||
{
|
||||
ObjectRegistry* objectRegistry = mWorldView.getObjectRegistry();
|
||||
|
||||
if (!mPlayer.isEmpty())
|
||||
{
|
||||
MWWorld::Ptr newPlayerPtr = MWBase::Environment::get().getWorld()->getPlayerPtr();
|
||||
if (!(getId(mPlayer) == getId(newPlayerPtr)))
|
||||
throw std::logic_error("Player Refnum was changed unexpectedly");
|
||||
if (!mPlayer.isInCell() || !newPlayerPtr.isInCell() || mPlayer.getCell() != newPlayerPtr.getCell())
|
||||
{
|
||||
mPlayer = newPlayerPtr; // player was moved to another cell, update ptr in registry
|
||||
objectRegistry->registerPtr(mPlayer);
|
||||
}
|
||||
}
|
||||
mWorldView.update();
|
||||
|
||||
if (paused)
|
||||
{
|
||||
mKeyPressEvents.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<GlobalEvent> globalEvents = std::move(mGlobalEvents);
|
||||
std::vector<LocalEvent> localEvents = std::move(mLocalEvents);
|
||||
mGlobalEvents = std::vector<GlobalEvent>();
|
||||
mLocalEvents = std::vector<LocalEvent>();
|
||||
|
||||
{ // Update time and process timers
|
||||
double seconds = mWorldView.getGameTimeInSeconds() + dt;
|
||||
mWorldView.setGameTimeInSeconds(seconds);
|
||||
double hours = mWorldView.getGameTimeInHours();
|
||||
|
||||
mGlobalScripts.processTimers(seconds, hours);
|
||||
for (LocalScripts* scripts : mActiveLocalScripts)
|
||||
scripts->processTimers(seconds, hours);
|
||||
}
|
||||
|
||||
// Receive events
|
||||
for (GlobalEvent& e : globalEvents)
|
||||
mGlobalScripts.receiveEvent(e.mEventName, e.mEventData);
|
||||
for (LocalEvent& e : localEvents)
|
||||
{
|
||||
LObject obj(e.mDest, objectRegistry);
|
||||
LocalScripts* scripts = obj.isValid() ? obj.ptr().getRefData().getLuaScripts() : nullptr;
|
||||
if (scripts)
|
||||
scripts->receiveEvent(e.mEventName, e.mEventData);
|
||||
else
|
||||
Log(Debug::Debug) << "Ignored event " << e.mEventName << " to L" << idToString(e.mDest)
|
||||
<< ". Object not found or has no attached scripts";
|
||||
}
|
||||
|
||||
// Engine handlers in local scripts
|
||||
PlayerScripts* playerScripts = dynamic_cast<PlayerScripts*>(mPlayer.getRefData().getLuaScripts());
|
||||
if (playerScripts)
|
||||
{
|
||||
for (const SDL_Keysym& key : mKeyPressEvents)
|
||||
playerScripts->keyPress(key);
|
||||
}
|
||||
mKeyPressEvents.clear();
|
||||
|
||||
for (const LocalEngineEvent& e : mLocalEngineEvents)
|
||||
{
|
||||
LObject obj(e.mDest, objectRegistry);
|
||||
if (!obj.isValid())
|
||||
{
|
||||
Log(Debug::Verbose) << "Can not call engine handlers: object" << idToString(e.mDest) << " is not found";
|
||||
continue;
|
||||
}
|
||||
LocalScripts* scripts = obj.ptr().getRefData().getLuaScripts();
|
||||
if (scripts)
|
||||
scripts->receiveEngineEvent(e.mEvent, objectRegistry);
|
||||
}
|
||||
mLocalEngineEvents.clear();
|
||||
|
||||
for (LocalScripts* scripts : mActiveLocalScripts)
|
||||
scripts->update(dt);
|
||||
|
||||
// Engine handlers in global scripts
|
||||
if (mPlayerChanged)
|
||||
{
|
||||
mPlayerChanged = false;
|
||||
mGlobalScripts.playerAdded(GObject(getId(mPlayer), objectRegistry));
|
||||
}
|
||||
|
||||
for (ObjectId id : mActorAddedEvents)
|
||||
mGlobalScripts.actorActive(GObject(id, objectRegistry));
|
||||
mActorAddedEvents.clear();
|
||||
|
||||
mGlobalScripts.update(dt);
|
||||
}
|
||||
|
||||
void LuaManager::applyQueuedChanges()
|
||||
{
|
||||
MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager();
|
||||
for (const std::string& message : mUIMessages)
|
||||
windowManager->messageBox(message);
|
||||
mUIMessages.clear();
|
||||
|
||||
for (std::unique_ptr<Action>& action : mActionQueue)
|
||||
action->apply(mWorldView);
|
||||
mActionQueue.clear();
|
||||
|
||||
if (mTeleportPlayerAction)
|
||||
mTeleportPlayerAction->apply(mWorldView);
|
||||
mTeleportPlayerAction.reset();
|
||||
}
|
||||
|
||||
void LuaManager::clear()
|
||||
{
|
||||
mActiveLocalScripts.clear();
|
||||
mLocalEvents.clear();
|
||||
mGlobalEvents.clear();
|
||||
mKeyPressEvents.clear();
|
||||
mActorAddedEvents.clear();
|
||||
mLocalEngineEvents.clear();
|
||||
mPlayerChanged = false;
|
||||
mWorldView.clear();
|
||||
if (!mPlayer.isEmpty())
|
||||
{
|
||||
mPlayer.getCellRef().unsetRefNum();
|
||||
mPlayer.getRefData().setLuaScripts(nullptr);
|
||||
mPlayer = MWWorld::Ptr();
|
||||
}
|
||||
}
|
||||
|
||||
void LuaManager::setupPlayer(const MWWorld::Ptr& ptr)
|
||||
{
|
||||
if (!mPlayer.isEmpty())
|
||||
throw std::logic_error("Player is initialized twice");
|
||||
mWorldView.objectAddedToScene(ptr);
|
||||
mPlayer = ptr;
|
||||
LocalScripts* localScripts = ptr.getRefData().getLuaScripts();
|
||||
if (!localScripts)
|
||||
localScripts = createLocalScripts(ptr);
|
||||
mActiveLocalScripts.insert(localScripts);
|
||||
mLocalEngineEvents.push_back({getId(ptr), LocalScripts::OnActive{}});
|
||||
mPlayerChanged = 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)
|
||||
{
|
||||
mActiveLocalScripts.insert(localScripts);
|
||||
mLocalEngineEvents.push_back({getId(ptr), LocalScripts::OnActive{}});
|
||||
}
|
||||
|
||||
if (ptr.getClass().isActor() && ptr != mPlayer)
|
||||
mActorAddedEvents.push_back(getId(ptr));
|
||||
}
|
||||
|
||||
void LuaManager::objectRemovedFromScene(const MWWorld::Ptr& ptr)
|
||||
{
|
||||
mWorldView.objectRemovedFromScene(ptr);
|
||||
LocalScripts* localScripts = ptr.getRefData().getLuaScripts();
|
||||
if (localScripts)
|
||||
{
|
||||
mActiveLocalScripts.erase(localScripts);
|
||||
if (!mWorldView.getObjectRegistry()->getPtr(getId(ptr), true).isEmpty())
|
||||
mLocalEngineEvents.push_back({getId(ptr), LocalScripts::OnInactive{}});
|
||||
}
|
||||
}
|
||||
|
||||
void LuaManager::registerObject(const MWWorld::Ptr& ptr)
|
||||
{
|
||||
mWorldView.getObjectRegistry()->registerPtr(ptr);
|
||||
}
|
||||
|
||||
void LuaManager::deregisterObject(const MWWorld::Ptr& ptr)
|
||||
{
|
||||
mWorldView.getObjectRegistry()->deregisterPtr(ptr);
|
||||
}
|
||||
|
||||
void LuaManager::keyPressed(const SDL_KeyboardEvent& arg)
|
||||
{
|
||||
mKeyPressEvents.push_back(arg.keysym);
|
||||
}
|
||||
|
||||
void LuaManager::appliedToObject(const MWWorld::Ptr& toPtr, std::string_view recordId, const MWWorld::Ptr& fromPtr)
|
||||
{
|
||||
mLocalEngineEvents.push_back({getId(toPtr), LocalScripts::OnConsume{std::string(recordId)}});
|
||||
}
|
||||
|
||||
MWBase::LuaManager::ActorControls* LuaManager::getActorControls(const MWWorld::Ptr& ptr) const
|
||||
{
|
||||
LocalScripts* localScripts = ptr.getRefData().getLuaScripts();
|
||||
if (!localScripts)
|
||||
return nullptr;
|
||||
return localScripts->getActorControls();
|
||||
}
|
||||
|
||||
void LuaManager::addLocalScript(const MWWorld::Ptr& ptr, const std::string& scriptPath)
|
||||
{
|
||||
LocalScripts* localScripts = ptr.getRefData().getLuaScripts();
|
||||
if (!localScripts)
|
||||
{
|
||||
localScripts = createLocalScripts(ptr);
|
||||
if (ptr.isInCell() && MWBase::Environment::get().getWorld()->isCellActive(ptr.getCell()))
|
||||
mActiveLocalScripts.insert(localScripts);
|
||||
}
|
||||
localScripts->addNewScript(scriptPath);
|
||||
}
|
||||
|
||||
LocalScripts* LuaManager::createLocalScripts(const MWWorld::Ptr& ptr)
|
||||
{
|
||||
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().getRefIdPtr() == "player")
|
||||
{
|
||||
scripts = std::make_shared<PlayerScripts>(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry()));
|
||||
scripts->addPackage("openmw.ui", mUserInterfacePackage);
|
||||
scripts->addPackage("openmw.camera", mCameraPackage);
|
||||
}
|
||||
else
|
||||
scripts = std::make_shared<LocalScripts>(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry()));
|
||||
scripts->addPackage("openmw.nearby", mNearbyPackage);
|
||||
scripts->setSerializer(mLocalSerializer.get());
|
||||
|
||||
MWWorld::RefData& refData = ptr.getRefData();
|
||||
refData.setLuaScripts(std::move(scripts));
|
||||
return refData.getLuaScripts();
|
||||
}
|
||||
|
||||
void LuaManager::write(ESM::ESMWriter& writer, Loading::Listener& progress)
|
||||
{
|
||||
writer.startRecord(ESM::REC_LUAM);
|
||||
|
||||
mWorldView.save(writer);
|
||||
ESM::LuaScripts globalScripts;
|
||||
mGlobalScripts.save(globalScripts);
|
||||
globalScripts.save(writer);
|
||||
saveEvents(writer, mGlobalEvents, mLocalEvents);
|
||||
|
||||
writer.endRecord(ESM::REC_LUAM);
|
||||
}
|
||||
|
||||
void LuaManager::readRecord(ESM::ESMReader& reader, uint32_t type)
|
||||
{
|
||||
if (type != ESM::REC_LUAM)
|
||||
throw std::runtime_error("ESM::REC_LUAM is expected");
|
||||
|
||||
mWorldView.load(reader);
|
||||
ESM::LuaScripts globalScripts;
|
||||
globalScripts.load(reader);
|
||||
loadEvents(mLua.sol(), reader, mGlobalEvents, mLocalEvents, mContentFileMapping, mGlobalLoader.get());
|
||||
|
||||
mGlobalScripts.setSerializer(mGlobalLoader.get());
|
||||
mGlobalScripts.load(globalScripts, false);
|
||||
mGlobalScripts.setSerializer(mGlobalSerializer.get());
|
||||
}
|
||||
|
||||
void LuaManager::saveLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScripts& data)
|
||||
{
|
||||
if (ptr.getRefData().getLuaScripts())
|
||||
ptr.getRefData().getLuaScripts()->save(data);
|
||||
else
|
||||
data.mScripts.clear();
|
||||
}
|
||||
|
||||
void LuaManager::loadLocalScripts(const MWWorld::Ptr& ptr, const ESM::LuaScripts& data)
|
||||
{
|
||||
if (data.mScripts.empty())
|
||||
{
|
||||
if (ptr.getRefData().getLuaScripts())
|
||||
ptr.getRefData().setLuaScripts(nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
mWorldView.getObjectRegistry()->registerPtr(ptr);
|
||||
LocalScripts* scripts = createLocalScripts(ptr);
|
||||
|
||||
scripts->setSerializer(mLocalLoader.get());
|
||||
scripts->load(data, true);
|
||||
scripts->setSerializer(mLocalSerializer.get());
|
||||
|
||||
// LiveCellRef is usually copied after loading, so this Ptr will become invalid and should be deregistered.
|
||||
mWorldView.getObjectRegistry()->deregisterPtr(ptr);
|
||||
}
|
||||
|
||||
void LuaManager::reloadAllScripts()
|
||||
{
|
||||
Log(Debug::Info) << "Reload Lua";
|
||||
mLua.dropScriptCache();
|
||||
|
||||
{ // 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);
|
||||
}
|
||||
|
||||
for (const auto& [id, ptr] : mWorldView.getObjectRegistry()->mObjectMapping)
|
||||
{ // Reload local scripts
|
||||
LocalScripts* scripts = ptr.getRefData().getLuaScripts();
|
||||
if (scripts == nullptr)
|
||||
continue;
|
||||
ESM::LuaScripts data;
|
||||
scripts->save(data);
|
||||
scripts->load(data, true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
#ifndef MWLUA_LUAMANAGERIMP_H
|
||||
#define MWLUA_LUAMANAGERIMP_H
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
#include <components/lua/luastate.hpp>
|
||||
|
||||
#include "../mwbase/luamanager.hpp"
|
||||
|
||||
#include "actions.hpp"
|
||||
#include "object.hpp"
|
||||
#include "eventqueue.hpp"
|
||||
#include "globalscripts.hpp"
|
||||
#include "localscripts.hpp"
|
||||
#include "playerscripts.hpp"
|
||||
#include "worldview.hpp"
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
||||
class LuaManager : public MWBase::LuaManager
|
||||
{
|
||||
public:
|
||||
LuaManager(const VFS::Manager* vfs, const std::vector<std::string>& globalScriptLists);
|
||||
|
||||
// Called by engine.cpp when environment is fully initialized.
|
||||
void init();
|
||||
|
||||
// Called by engine.cpp every frame. For performance reasons it works in a separate
|
||||
// thread (in parallel with osg Cull). Can not use scene graph.
|
||||
void update(bool paused, float dt);
|
||||
|
||||
// Called by engine.cpp from the main thread. Can use scene graph.
|
||||
void applyQueuedChanges();
|
||||
|
||||
// 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 objectAddedToScene(const MWWorld::Ptr& ptr) override;
|
||||
void objectRemovedFromScene(const MWWorld::Ptr& ptr) override;
|
||||
void registerObject(const MWWorld::Ptr& ptr) override;
|
||||
void deregisterObject(const MWWorld::Ptr& ptr) override;
|
||||
void keyPressed(const SDL_KeyboardEvent &arg) override;
|
||||
void appliedToObject(const MWWorld::Ptr& toPtr, std::string_view recordId, const MWWorld::Ptr& fromPtr) override;
|
||||
|
||||
MWBase::LuaManager::ActorControls* getActorControls(const MWWorld::Ptr&) const override;
|
||||
|
||||
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);
|
||||
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); }
|
||||
|
||||
// Saving
|
||||
void write(ESM::ESMWriter& writer, Loading::Listener& progress) override;
|
||||
void saveLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScripts& data) override;
|
||||
|
||||
// Loading from a save
|
||||
void readRecord(ESM::ESMReader& reader, uint32_t type) override;
|
||||
void loadLocalScripts(const MWWorld::Ptr& ptr, const ESM::LuaScripts& data) override;
|
||||
void setContentFileMapping(const std::map<int, int>& mapping) override { mContentFileMapping = mapping; }
|
||||
|
||||
// Drops script cache and reloads all scripts. Calls `onSave` and `onLoad` for every script.
|
||||
void reloadAllScripts() override;
|
||||
|
||||
private:
|
||||
LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr);
|
||||
|
||||
LuaUtil::LuaState mLua;
|
||||
sol::table mNearbyPackage;
|
||||
sol::table mUserInterfacePackage;
|
||||
sol::table mCameraPackage;
|
||||
|
||||
std::vector<std::string> mGlobalScriptList;
|
||||
GlobalScripts mGlobalScripts{&mLua};
|
||||
std::set<LocalScripts*> mActiveLocalScripts;
|
||||
WorldView mWorldView;
|
||||
|
||||
bool mPlayerChanged = false;
|
||||
MWWorld::Ptr mPlayer;
|
||||
|
||||
GlobalEventQueue mGlobalEvents;
|
||||
LocalEventQueue mLocalEvents;
|
||||
|
||||
std::unique_ptr<LuaUtil::UserdataSerializer> mGlobalSerializer;
|
||||
std::unique_ptr<LuaUtil::UserdataSerializer> mLocalSerializer;
|
||||
|
||||
std::map<int, int> mContentFileMapping;
|
||||
std::unique_ptr<LuaUtil::UserdataSerializer> mGlobalLoader;
|
||||
std::unique_ptr<LuaUtil::UserdataSerializer> mLocalLoader;
|
||||
|
||||
std::vector<SDL_Keysym> mKeyPressEvents;
|
||||
std::vector<ObjectId> mActorAddedEvents;
|
||||
|
||||
struct LocalEngineEvent
|
||||
{
|
||||
ObjectId mDest;
|
||||
LocalScripts::EngineEvent mEvent;
|
||||
};
|
||||
std::vector<LocalEngineEvent> mLocalEngineEvents;
|
||||
|
||||
// Queued actions that should be done in main thread. Processed by applyQueuedChanges().
|
||||
std::vector<std::unique_ptr<Action>> mActionQueue;
|
||||
std::unique_ptr<TeleportAction> mTeleportPlayerAction;
|
||||
std::vector<std::string> mUIMessages;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // MWLUA_LUAMANAGERIMP_H
|
@ -0,0 +1,155 @@
|
||||
#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"
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
||||
std::string idToString(const ObjectId& id)
|
||||
{
|
||||
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"},
|
||||
};
|
||||
|
||||
std::string_view getMWClassName(const std::type_index& cls_type, std::string_view fallback)
|
||||
{
|
||||
auto it = classNames.find(cls_type);
|
||||
if (it != classNames.end())
|
||||
return it->second;
|
||||
else
|
||||
return fallback;
|
||||
}
|
||||
|
||||
bool isMarker(const MWWorld::Ptr& ptr)
|
||||
{
|
||||
std::string_view id = *ptr.getCellRef().getRefIdPtr();
|
||||
return id == "prisonmarker" || id == "divinemarker" || id == "templemarker" || id == "northmarker";
|
||||
}
|
||||
|
||||
std::string_view getMWClassName(const MWWorld::Ptr& ptr)
|
||||
{
|
||||
if (*ptr.getCellRef().getRefIdPtr() == "player")
|
||||
return "Player";
|
||||
if (isMarker(ptr))
|
||||
return "Marker";
|
||||
return getMWClassName(typeid(ptr.getClass()), ptr.getTypeName());
|
||||
}
|
||||
|
||||
std::string ptrToString(const MWWorld::Ptr& ptr)
|
||||
{
|
||||
std::string res = "object";
|
||||
res.append(idToString(getId(ptr)));
|
||||
res.append(" (");
|
||||
res.append(getMWClassName(ptr));
|
||||
res.append(", ");
|
||||
res.append(*ptr.getCellRef().getRefIdPtr());
|
||||
res.append(")");
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string Object::toString() const
|
||||
{
|
||||
if (isValid())
|
||||
return ptrToString(ptr());
|
||||
else
|
||||
return "object" + idToString(mId) + " (not found)";
|
||||
}
|
||||
|
||||
bool Object::isValid() const
|
||||
{
|
||||
if (mLastUpdate < mObjectRegistry->mUpdateCounter)
|
||||
{
|
||||
updatePtr();
|
||||
mLastUpdate = mObjectRegistry->mUpdateCounter;
|
||||
}
|
||||
return !mPtr.isEmpty();
|
||||
}
|
||||
|
||||
const MWWorld::Ptr& Object::ptr() const
|
||||
{
|
||||
if (!isValid())
|
||||
throw std::runtime_error("Object is not available: " + idToString(mId));
|
||||
return mPtr;
|
||||
}
|
||||
|
||||
void ObjectRegistry::update()
|
||||
{
|
||||
if (mChanged)
|
||||
{
|
||||
mUpdateCounter++;
|
||||
mChanged = false;
|
||||
}
|
||||
}
|
||||
|
||||
void ObjectRegistry::clear()
|
||||
{
|
||||
mObjectMapping.clear();
|
||||
mChanged = false;
|
||||
mUpdateCounter = 0;
|
||||
mLastAssignedId.unset();
|
||||
}
|
||||
|
||||
MWWorld::Ptr ObjectRegistry::getPtr(ObjectId id, bool local)
|
||||
{
|
||||
MWWorld::Ptr ptr;
|
||||
auto it = mObjectMapping.find(id);
|
||||
if (it != mObjectMapping.end())
|
||||
ptr = it->second;
|
||||
if (local)
|
||||
{
|
||||
// TODO: Return ptr only if it is active or was active in the previous frame, otherwise return empty.
|
||||
// Needed because in multiplayer inactive objects will not be synchronized, so an be out of date.
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: If Ptr is empty then try to load the object from esp/esm.
|
||||
}
|
||||
return ptr;
|
||||
}
|
||||
|
||||
ObjectId ObjectRegistry::registerPtr(const MWWorld::Ptr& ptr)
|
||||
{
|
||||
ObjectId id = ptr.getCellRef().getOrAssignRefNum(mLastAssignedId);
|
||||
mChanged = true;
|
||||
mObjectMapping[id] = ptr;
|
||||
return id;
|
||||
}
|
||||
|
||||
ObjectId ObjectRegistry::deregisterPtr(const MWWorld::Ptr& ptr)
|
||||
{
|
||||
ObjectId id = getId(ptr);
|
||||
mChanged = true;
|
||||
mObjectMapping.erase(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
#ifndef MWLUA_OBJECT_H
|
||||
#define MWLUA_OBJECT_H
|
||||
|
||||
#include <typeindex>
|
||||
|
||||
#include <components/esm/cellref.hpp>
|
||||
|
||||
#include "../mwbase/environment.hpp"
|
||||
#include "../mwbase/world.hpp"
|
||||
|
||||
#include "../mwworld/ptr.hpp"
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
// ObjectId is a unique identifier of a game object.
|
||||
// It can change only if the order of content files was change.
|
||||
using ObjectId = ESM::RefNum;
|
||||
inline const ObjectId& getId(const MWWorld::Ptr& ptr) { return ptr.getCellRef().getRefNum(); }
|
||||
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);
|
||||
|
||||
// Holds a mapping ObjectId -> MWWord::Ptr.
|
||||
class ObjectRegistry
|
||||
{
|
||||
public:
|
||||
ObjectRegistry() { mLastAssignedId.unset(); }
|
||||
|
||||
void update(); // Should be called every frame.
|
||||
void clear(); // Should be called before starting or loading a new game.
|
||||
|
||||
ObjectId registerPtr(const MWWorld::Ptr& ptr);
|
||||
ObjectId deregisterPtr(const MWWorld::Ptr& ptr);
|
||||
|
||||
// Returns Ptr by id. If object is not found, returns empty Ptr.
|
||||
// If local = true, returns non-empty ptr only if it can be used in local scripts
|
||||
// (i.e. is active or was active in the previous frame).
|
||||
MWWorld::Ptr getPtr(ObjectId id, bool local);
|
||||
|
||||
// Needed only for saving/loading.
|
||||
const ObjectId& getLastAssignedId() const { return mLastAssignedId; }
|
||||
void setLastAssignedId(ObjectId id) { mLastAssignedId = id; }
|
||||
|
||||
private:
|
||||
friend class Object;
|
||||
friend class LuaManager;
|
||||
|
||||
bool mChanged = false;
|
||||
int64_t mUpdateCounter = 0;
|
||||
std::map<ObjectId, MWWorld::Ptr> mObjectMapping;
|
||||
ObjectId mLastAssignedId;
|
||||
};
|
||||
|
||||
// Lua scripts can't use MWWorld::Ptr directly, because lifetime of a script can be longer than lifetime of Ptr.
|
||||
// `GObject` and `LObject` are intended to be passed to Lua as a userdata.
|
||||
// It automatically updates the underlying Ptr when needed.
|
||||
class Object
|
||||
{
|
||||
public:
|
||||
Object(ObjectId id, ObjectRegistry* reg) : mId(id), mObjectRegistry(reg) {}
|
||||
virtual ~Object() {}
|
||||
ObjectId id() const { return mId; }
|
||||
|
||||
std::string toString() const;
|
||||
std::string_view type() const { return getMWClassName(ptr()); }
|
||||
|
||||
// Updates and returns the underlying Ptr. Throws an exception if object is not available.
|
||||
const MWWorld::Ptr& ptr() const;
|
||||
|
||||
// Returns `true` if calling `ptr()` is safe.
|
||||
bool isValid() const;
|
||||
|
||||
protected:
|
||||
virtual void updatePtr() const = 0;
|
||||
|
||||
const ObjectId mId;
|
||||
ObjectRegistry* mObjectRegistry;
|
||||
|
||||
mutable MWWorld::Ptr mPtr;
|
||||
mutable int64_t mLastUpdate = -1;
|
||||
};
|
||||
|
||||
// Used only in local scripts
|
||||
class LObject : public Object
|
||||
{
|
||||
using Object::Object;
|
||||
void updatePtr() const final { mPtr = mObjectRegistry->getPtr(mId, true); }
|
||||
};
|
||||
|
||||
// Used only in global scripts
|
||||
class GObject : public Object
|
||||
{
|
||||
using Object::Object;
|
||||
void updatePtr() const final { mPtr = mObjectRegistry->getPtr(mId, false); }
|
||||
};
|
||||
|
||||
using ObjectIdList = std::shared_ptr<std::vector<ObjectId>>;
|
||||
template <typename Obj>
|
||||
struct ObjectList { ObjectIdList mIds; };
|
||||
using GObjectList = ObjectList<GObject>;
|
||||
using LObjectList = ObjectList<LObject>;
|
||||
|
||||
}
|
||||
|
||||
#endif // MWLUA_OBJECT_H
|
@ -0,0 +1,340 @@
|
||||
#include "luabindings.hpp"
|
||||
|
||||
#include <components/lua/luastate.hpp>
|
||||
#include <components/queries/query.hpp>
|
||||
|
||||
#include "../mwclass/door.hpp"
|
||||
|
||||
#include "../mwworld/containerstore.hpp"
|
||||
#include "../mwworld/inventorystore.hpp"
|
||||
|
||||
#include "eventqueue.hpp"
|
||||
#include "luamanagerimp.hpp"
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
template <typename ObjectT>
|
||||
struct Inventory
|
||||
{
|
||||
ObjectT mObj;
|
||||
};
|
||||
}
|
||||
|
||||
namespace sol
|
||||
{
|
||||
template <>
|
||||
struct is_automagical<MWLua::LObject> : std::false_type {};
|
||||
template <>
|
||||
struct is_automagical<MWLua::GObject> : std::false_type {};
|
||||
template <>
|
||||
struct is_automagical<MWLua::LObjectList> : std::false_type {};
|
||||
template <>
|
||||
struct is_automagical<MWLua::GObjectList> : std::false_type {};
|
||||
template <>
|
||||
struct is_automagical<MWLua::Inventory<MWLua::LObject>> : std::false_type {};
|
||||
template <>
|
||||
struct is_automagical<MWLua::Inventory<MWLua::GObject>> : std::false_type {};
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (typeid(Class) != typeid(ptr.getClass()))
|
||||
{
|
||||
std::string msg = "Requires type '";
|
||||
msg.append(getMWClassName(typeid(Class)));
|
||||
msg.append("', but applied to ");
|
||||
msg.append(ptrToString(ptr));
|
||||
throw std::runtime_error(msg);
|
||||
}
|
||||
return ptr;
|
||||
}
|
||||
|
||||
template <class ObjectT>
|
||||
static void registerObjectList(const std::string& prefix, const Context& context)
|
||||
{
|
||||
using ListT = ObjectList<ObjectT>;
|
||||
sol::state& lua = context.mLua->sol();
|
||||
ObjectRegistry* registry = context.mWorldView->getObjectRegistry();
|
||||
sol::usertype<ListT> listT = lua.new_usertype<ListT>(prefix + "ObjectList");
|
||||
listT[sol::meta_function::to_string] =
|
||||
[](const ListT& list) { return "{" + std::to_string(list.mIds->size()) + " objects}"; };
|
||||
listT[sol::meta_function::length] = [](const ListT& list) { return list.mIds->size(); };
|
||||
listT[sol::meta_function::index] = [registry](const ListT& list, size_t index)
|
||||
{
|
||||
if (index > 0 && index <= list.mIds->size())
|
||||
return ObjectT((*list.mIds)[index - 1], registry);
|
||||
else
|
||||
throw std::runtime_error("Index out of range");
|
||||
};
|
||||
listT["ipairs"] = [registry](const ListT& list)
|
||||
{
|
||||
auto iter = [registry](const ListT& l, int64_t i) -> sol::optional<std::tuple<int64_t, ObjectT>>
|
||||
{
|
||||
if (i >= 0 && i < static_cast<int64_t>(l.mIds->size()))
|
||||
return std::make_tuple(i + 1, ObjectT((*l.mIds)[i], registry));
|
||||
else
|
||||
return sol::nullopt;
|
||||
};
|
||||
return std::make_tuple(iter, list, 0);
|
||||
};
|
||||
listT["select"] = [context](const ListT& list, const Queries::Query& query)
|
||||
{
|
||||
return ListT{selectObjectsFromList(query, list.mIds, context)};
|
||||
};
|
||||
}
|
||||
|
||||
template <class ObjectT>
|
||||
static void addBasicBindings(sol::usertype<ObjectT>& objectT, const Context& context)
|
||||
{
|
||||
objectT["isValid"] = [](const ObjectT& o) { return o.isValid(); };
|
||||
objectT["recordId"] = sol::readonly_property([](const ObjectT& o) -> std::string
|
||||
{
|
||||
return o.ptr().getCellRef().getRefId();
|
||||
});
|
||||
objectT["cell"] = sol::readonly_property([](const ObjectT& o) -> sol::optional<Cell<ObjectT>>
|
||||
{
|
||||
const MWWorld::Ptr& ptr = o.ptr();
|
||||
if (ptr.isInCell())
|
||||
return Cell<ObjectT>{ptr.getCell()};
|
||||
else
|
||||
return sol::nullopt;
|
||||
});
|
||||
objectT["position"] = sol::readonly_property([](const ObjectT& o) -> osg::Vec3f
|
||||
{
|
||||
return o.ptr().getRefData().getPosition().asVec3();
|
||||
});
|
||||
objectT["rotation"] = sol::readonly_property([](const ObjectT& o) -> osg::Vec3f
|
||||
{
|
||||
return o.ptr().getRefData().getPosition().asRotationVec3();
|
||||
});
|
||||
objectT["type"] = sol::readonly_property(&ObjectT::type);
|
||||
objectT["count"] = sol::readonly_property([](const ObjectT& o) { return o.ptr().getRefData().getCount(); });
|
||||
objectT[sol::meta_function::equal_to] = [](const ObjectT& a, const ObjectT& b) { return a.id() == b.id(); };
|
||||
objectT[sol::meta_function::to_string] = &ObjectT::toString;
|
||||
objectT["sendEvent"] = [context](const ObjectT& dest, std::string eventName, const sol::object& eventData)
|
||||
{
|
||||
context.mLocalEventQueue->push_back({dest.id(), std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer)});
|
||||
};
|
||||
|
||||
objectT["canMove"] = [](const ObjectT& o)
|
||||
{
|
||||
const MWWorld::Class& cls = o.ptr().getClass();
|
||||
return cls.getMaxSpeed(o.ptr()) > 0;
|
||||
};
|
||||
objectT["getRunSpeed"] = [](const ObjectT& o)
|
||||
{
|
||||
const MWWorld::Class& cls = o.ptr().getClass();
|
||||
return cls.getRunSpeed(o.ptr());
|
||||
};
|
||||
objectT["getWalkSpeed"] = [](const ObjectT& o)
|
||||
{
|
||||
const MWWorld::Class& cls = o.ptr().getClass();
|
||||
return cls.getWalkSpeed(o.ptr());
|
||||
};
|
||||
|
||||
if constexpr (std::is_same_v<ObjectT, GObject>)
|
||||
{ // Only for global scripts
|
||||
objectT["addScript"] = [luaManager=context.mLuaManager](const GObject& object, const std::string& path)
|
||||
{
|
||||
luaManager->addLocalScript(object.ptr(), path);
|
||||
};
|
||||
|
||||
objectT["teleport"] = [luaManager=context.mLuaManager](const GObject& object, std::string_view cell,
|
||||
const osg::Vec3f& pos, const sol::optional<osg::Vec3f>& optRot)
|
||||
{
|
||||
MWWorld::Ptr ptr = object.ptr();
|
||||
osg::Vec3f rot = optRot ? *optRot : ptr.getRefData().getPosition().asRotationVec3();
|
||||
auto action = std::make_unique<TeleportAction>(object.id(), std::string(cell), pos, rot);
|
||||
if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr())
|
||||
luaManager->addTeleportPlayerAction(std::move(action));
|
||||
else
|
||||
luaManager->addAction(std::move(action));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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()); };
|
||||
|
||||
objectT["isTeleport"] = sol::readonly_property([ptr](const ObjectT& o)
|
||||
{
|
||||
return ptr(o).getCellRef().getTeleport();
|
||||
});
|
||||
objectT["destPosition"] = sol::readonly_property([ptr](const ObjectT& o) -> osg::Vec3f
|
||||
{
|
||||
return ptr(o).getCellRef().getDoorDest().asVec3();
|
||||
});
|
||||
objectT["destRotation"] = sol::readonly_property([ptr](const ObjectT& o) -> osg::Vec3f
|
||||
{
|
||||
return ptr(o).getCellRef().getDoorDest().asRotationVec3();
|
||||
});
|
||||
objectT["destCell"] = sol::readonly_property(
|
||||
[ptr, worldView=context.mWorldView](const ObjectT& o) -> sol::optional<Cell<ObjectT>>
|
||||
{
|
||||
const MWWorld::CellRef& cellRef = ptr(o).getCellRef();
|
||||
if (!cellRef.getTeleport())
|
||||
return sol::nullopt;
|
||||
MWWorld::CellStore* cell = worldView->findCell(cellRef.getDestCell(), cellRef.getDoorDest().asVec3());
|
||||
if (cell)
|
||||
return Cell<ObjectT>{cell};
|
||||
else
|
||||
return sol::nullopt;
|
||||
});
|
||||
}
|
||||
|
||||
static SetEquipmentAction::Equipment parseEquipmentTable(sol::table equipment)
|
||||
{
|
||||
SetEquipmentAction::Equipment eqp;
|
||||
for (auto& [key, value] : equipment)
|
||||
{
|
||||
int slot = key.as<int>();
|
||||
if (value.is<GObject>())
|
||||
eqp[slot] = value.as<GObject>().id();
|
||||
else
|
||||
eqp[slot] = value.as<std::string>();
|
||||
}
|
||||
return eqp;
|
||||
}
|
||||
|
||||
template <class ObjectT>
|
||||
static void addInventoryBindings(sol::usertype<ObjectT>& objectT, const std::string& prefix, const Context& context)
|
||||
{
|
||||
using InventoryT = Inventory<ObjectT>;
|
||||
sol::usertype<InventoryT> inventoryT = context.mLua->sol().new_usertype<InventoryT>(prefix + "Inventory");
|
||||
|
||||
objectT["getEquipment"] = [context](const ObjectT& o)
|
||||
{
|
||||
const MWWorld::Ptr& ptr = o.ptr();
|
||||
sol::table equipment(context.mLua->sol(), sol::create);
|
||||
if (!ptr.getClass().hasInventoryStore(ptr))
|
||||
return equipment;
|
||||
|
||||
MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr);
|
||||
for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot)
|
||||
{
|
||||
auto it = store.getSlot(slot);
|
||||
if (it == store.end())
|
||||
continue;
|
||||
context.mWorldView->getObjectRegistry()->registerPtr(*it);
|
||||
equipment[slot] = ObjectT(getId(*it), context.mWorldView->getObjectRegistry());
|
||||
}
|
||||
return equipment;
|
||||
};
|
||||
objectT["isEquipped"] = [](const ObjectT& actor, const ObjectT& item)
|
||||
{
|
||||
const MWWorld::Ptr& ptr = actor.ptr();
|
||||
if (!ptr.getClass().hasInventoryStore(ptr))
|
||||
return false;
|
||||
MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr);
|
||||
return store.isEquipped(item.ptr());
|
||||
};
|
||||
|
||||
objectT["inventory"] = sol::readonly_property([](const ObjectT& o) { return InventoryT{o}; });
|
||||
inventoryT[sol::meta_function::to_string] =
|
||||
[](const InventoryT& inv) { return "Inventory[" + inv.mObj.toString() + "]"; };
|
||||
|
||||
auto getWithMask = [context](const InventoryT& inventory, int mask)
|
||||
{
|
||||
const MWWorld::Ptr& ptr = inventory.mObj.ptr();
|
||||
MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr);
|
||||
ObjectIdList list = std::make_shared<std::vector<ObjectId>>();
|
||||
auto it = store.begin(mask);
|
||||
while (it.getType() != -1)
|
||||
{
|
||||
const MWWorld::Ptr& item = *(it++);
|
||||
context.mWorldView->getObjectRegistry()->registerPtr(item);
|
||||
list->push_back(getId(item));
|
||||
}
|
||||
return ObjectList<ObjectT>{list};
|
||||
};
|
||||
|
||||
inventoryT["getAll"] =
|
||||
[getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_All); };
|
||||
inventoryT["getPotions"] =
|
||||
[getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Potion); };
|
||||
inventoryT["getApparatuses"] =
|
||||
[getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Apparatus); };
|
||||
inventoryT["getArmor"] =
|
||||
[getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Armor); };
|
||||
inventoryT["getBooks"] =
|
||||
[getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Book); };
|
||||
inventoryT["getClothing"] =
|
||||
[getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Clothing); };
|
||||
inventoryT["getIngredients"] =
|
||||
[getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Ingredient); };
|
||||
inventoryT["getLights"] =
|
||||
[getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Light); };
|
||||
inventoryT["getLockpicks"] =
|
||||
[getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Lockpick); };
|
||||
inventoryT["getMiscellaneous"] =
|
||||
[getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Miscellaneous); };
|
||||
inventoryT["getProbes"] =
|
||||
[getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Probe); };
|
||||
inventoryT["getRepairKits"] =
|
||||
[getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Repair); };
|
||||
inventoryT["getWeapons"] =
|
||||
[getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Weapon); };
|
||||
|
||||
inventoryT["countOf"] = [](const InventoryT& inventory, const std::string& recordId)
|
||||
{
|
||||
const MWWorld::Ptr& ptr = inventory.mObj.ptr();
|
||||
MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr);
|
||||
return store.count(recordId);
|
||||
};
|
||||
|
||||
if constexpr (std::is_same_v<ObjectT, GObject>)
|
||||
{ // Only for global scripts
|
||||
objectT["setEquipment"] = [manager=context.mLuaManager](const GObject& obj, sol::table equipment)
|
||||
{
|
||||
if (!obj.ptr().getClass().hasInventoryStore(obj.ptr()))
|
||||
{
|
||||
if (!equipment.empty())
|
||||
throw std::runtime_error(ptrToString(obj.ptr()) + " has no equipment slots");
|
||||
return;
|
||||
}
|
||||
manager->addAction(std::make_unique<SetEquipmentAction>(obj.id(), parseEquipmentTable(equipment)));
|
||||
};
|
||||
|
||||
// TODO
|
||||
// obj.inventory:drop(obj2, [count])
|
||||
// obj.inventory:drop(recordId, [count])
|
||||
// obj.inventory:addNew(recordId, [count])
|
||||
// obj.inventory:remove(obj/recordId, [count])
|
||||
/*objectT["moveInto"] = [](const GObject& obj, const InventoryT& inventory) {};
|
||||
inventoryT["drop"] = [](const InventoryT& inventory) {};
|
||||
inventoryT["addNew"] = [](const InventoryT& inventory) {};
|
||||
inventoryT["remove"] = [](const InventoryT& inventory) {};*/
|
||||
}
|
||||
}
|
||||
|
||||
template <class ObjectT>
|
||||
static void initObjectBindings(const std::string& prefix, const Context& context)
|
||||
{
|
||||
sol::usertype<ObjectT> objectT = context.mLua->sol().new_usertype<ObjectT>(prefix + "Object");
|
||||
addBasicBindings<ObjectT>(objectT, context);
|
||||
addDoorBindings<ObjectT>(objectT, context);
|
||||
addInventoryBindings<ObjectT>(objectT, prefix, context);
|
||||
|
||||
registerObjectList<ObjectT>(prefix, context);
|
||||
}
|
||||
|
||||
void initObjectBindingsForLocalScripts(const Context& context)
|
||||
{
|
||||
initObjectBindings<LObject>("L", context);
|
||||
}
|
||||
|
||||
void initObjectBindingsForGlobalScripts(const Context& context)
|
||||
{
|
||||
initObjectBindings<GObject>("G", context);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
#ifndef MWLUA_PLAYERSCRIPTS_H
|
||||
#define MWLUA_PLAYERSCRIPTS_H
|
||||
|
||||
#include <SDL_events.h>
|
||||
|
||||
#include "localscripts.hpp"
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
||||
class PlayerScripts : public LocalScripts
|
||||
{
|
||||
public:
|
||||
PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj)
|
||||
{
|
||||
registerEngineHandlers({&mKeyPressHandlers});
|
||||
}
|
||||
|
||||
void keyPress(const SDL_Keysym& key) { callEngineHandlers(mKeyPressHandlers, key); }
|
||||
|
||||
private:
|
||||
EngineHandlerList mKeyPressHandlers{"onKeyPress"};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // MWLUA_PLAYERSCRIPTS_H
|
@ -0,0 +1,191 @@
|
||||
#include "query.hpp"
|
||||
|
||||
#include <sol/sol.hpp>
|
||||
|
||||
#include <components/lua/luastate.hpp>
|
||||
|
||||
#include "../mwclass/container.hpp"
|
||||
#include "../mwworld/cellstore.hpp"
|
||||
|
||||
#include "worldview.hpp"
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
||||
static std::vector<QueryFieldGroup> initBasicFieldGroups()
|
||||
{
|
||||
auto createGroup = [](std::string name, const auto& arr) -> QueryFieldGroup
|
||||
{
|
||||
std::vector<const Queries::Field*> fieldPtrs;
|
||||
fieldPtrs.reserve(arr.size());
|
||||
for (const Queries::Field& field : arr)
|
||||
fieldPtrs.push_back(&field);
|
||||
return {std::move(name), std::move(fieldPtrs)};
|
||||
};
|
||||
static std::array objectFields = {
|
||||
Queries::Field({"type"}, typeid(std::string)),
|
||||
Queries::Field({"recordId"}, typeid(std::string)),
|
||||
Queries::Field({"cell", "name"}, typeid(std::string)),
|
||||
Queries::Field({"cell", "region"}, typeid(std::string)),
|
||||
Queries::Field({"cell", "isExterior"}, typeid(bool)),
|
||||
Queries::Field({"count"}, typeid(int32_t)),
|
||||
};
|
||||
static std::array doorFields = {
|
||||
Queries::Field({"isTeleport"}, typeid(bool)),
|
||||
Queries::Field({"destCell", "name"}, typeid(std::string)),
|
||||
Queries::Field({"destCell", "region"}, typeid(std::string)),
|
||||
Queries::Field({"destCell", "isExterior"}, typeid(bool)),
|
||||
};
|
||||
return std::vector<QueryFieldGroup>{
|
||||
createGroup("OBJECT", objectFields),
|
||||
createGroup("DOOR", doorFields),
|
||||
};
|
||||
}
|
||||
|
||||
const std::vector<QueryFieldGroup>& getBasicQueryFieldGroups()
|
||||
{
|
||||
static std::vector<QueryFieldGroup> fieldGroups = initBasicFieldGroups();
|
||||
return fieldGroups;
|
||||
}
|
||||
|
||||
bool checkQueryConditions(const Queries::Query& query, const ObjectId& id, const Context& context)
|
||||
{
|
||||
auto compareFn = [](auto&& a, auto&& b, Queries::Condition::Type t)
|
||||
{
|
||||
switch (t)
|
||||
{
|
||||
case Queries::Condition::EQUAL: return a == b;
|
||||
case Queries::Condition::NOT_EQUAL: return a != b;
|
||||
case Queries::Condition::GREATER: return a > b;
|
||||
case Queries::Condition::GREATER_OR_EQUAL: return a >= b;
|
||||
case Queries::Condition::LESSER: return a < b;
|
||||
case Queries::Condition::LESSER_OR_EQUAL: return a <= b;
|
||||
default:
|
||||
throw std::runtime_error("Unsupported condition type");
|
||||
}
|
||||
};
|
||||
sol::object obj;
|
||||
MWWorld::Ptr ptr;
|
||||
if (context.mIsGlobal)
|
||||
{
|
||||
GObject g(id, context.mWorldView->getObjectRegistry());
|
||||
if (!g.isValid())
|
||||
return false;
|
||||
ptr = g.ptr();
|
||||
obj = sol::make_object(context.mLua->sol(), g);
|
||||
}
|
||||
else
|
||||
{
|
||||
LObject l(id, context.mWorldView->getObjectRegistry());
|
||||
if (!l.isValid())
|
||||
return false;
|
||||
ptr = l.ptr();
|
||||
obj = sol::make_object(context.mLua->sol(), l);
|
||||
}
|
||||
if (ptr.getRefData().getCount() == 0)
|
||||
return false;
|
||||
|
||||
// It is important to exclude all markers before checking what class it is.
|
||||
// For example "prisonmarker" has class "Door" despite that it is only an invisible marker.
|
||||
if (isMarker(ptr))
|
||||
return false;
|
||||
|
||||
const MWWorld::Class& cls = ptr.getClass();
|
||||
if (cls.isActivator() != (query.mQueryType == ObjectQueryTypes::ACTIVATORS))
|
||||
return false;
|
||||
if (cls.isActor() != (query.mQueryType == ObjectQueryTypes::ACTORS))
|
||||
return false;
|
||||
if (cls.isDoor() != (query.mQueryType == ObjectQueryTypes::DOORS))
|
||||
return false;
|
||||
if ((typeid(cls) == typeid(MWClass::Container)) != (query.mQueryType == ObjectQueryTypes::CONTAINERS))
|
||||
return false;
|
||||
|
||||
std::vector<char> condStack;
|
||||
for (const Queries::Operation& op : query.mFilter.mOperations)
|
||||
{
|
||||
switch(op.mType)
|
||||
{
|
||||
case Queries::Operation::PUSH:
|
||||
{
|
||||
const Queries::Condition& cond = query.mFilter.mConditions[op.mConditionIndex];
|
||||
sol::object fieldObj = obj;
|
||||
for (const std::string& field : cond.mField->path())
|
||||
fieldObj = LuaUtil::getFieldOrNil(fieldObj, field);
|
||||
bool c;
|
||||
if (fieldObj == sol::nil)
|
||||
c = false;
|
||||
else if (cond.mField->type() == typeid(std::string))
|
||||
c = compareFn(fieldObj.as<std::string_view>(), std::get<std::string>(cond.mValue), cond.mType);
|
||||
else if (cond.mField->type() == typeid(float))
|
||||
c = compareFn(fieldObj.as<float>(), std::get<float>(cond.mValue), cond.mType);
|
||||
else if (cond.mField->type() == typeid(double))
|
||||
c = compareFn(fieldObj.as<double>(), std::get<double>(cond.mValue), cond.mType);
|
||||
else if (cond.mField->type() == typeid(bool))
|
||||
c = compareFn(fieldObj.as<bool>(), std::get<bool>(cond.mValue), cond.mType);
|
||||
else if (cond.mField->type() == typeid(int32_t))
|
||||
c = compareFn(fieldObj.as<int32_t>(), std::get<int32_t>(cond.mValue), cond.mType);
|
||||
else if (cond.mField->type() == typeid(int64_t))
|
||||
c = compareFn(fieldObj.as<int64_t>(), std::get<int64_t>(cond.mValue), cond.mType);
|
||||
else
|
||||
throw std::runtime_error("Unknown field type");
|
||||
condStack.push_back(c);
|
||||
break;
|
||||
}
|
||||
case Queries::Operation::NOT:
|
||||
condStack.back() = !condStack.back();
|
||||
break;
|
||||
case Queries::Operation::AND:
|
||||
{
|
||||
bool v = condStack.back();
|
||||
condStack.pop_back();
|
||||
condStack.back() = condStack.back() && v;
|
||||
break;
|
||||
}
|
||||
case Queries::Operation::OR:
|
||||
{
|
||||
bool v = condStack.back();
|
||||
condStack.pop_back();
|
||||
condStack.back() = condStack.back() || v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return condStack.empty() || condStack.back() != 0;
|
||||
}
|
||||
|
||||
ObjectIdList selectObjectsFromList(const Queries::Query& query, const ObjectIdList& list, const Context& context)
|
||||
{
|
||||
if (!query.mOrderBy.empty() || !query.mGroupBy.empty() || query.mOffset > 0)
|
||||
throw std::runtime_error("OrderBy, GroupBy, and Offset are not supported");
|
||||
|
||||
ObjectIdList res = std::make_shared<std::vector<ObjectId>>();
|
||||
for (const ObjectId& id : *list)
|
||||
{
|
||||
if (static_cast<int64_t>(res->size()) == query.mLimit)
|
||||
break;
|
||||
if (checkQueryConditions(query, id, context))
|
||||
res->push_back(id);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
ObjectIdList selectObjectsFromCellStore(const Queries::Query& query, MWWorld::CellStore* store, const Context& context)
|
||||
{
|
||||
if (!query.mOrderBy.empty() || !query.mGroupBy.empty() || query.mOffset > 0)
|
||||
throw std::runtime_error("OrderBy, GroupBy, and Offset are not supported");
|
||||
|
||||
ObjectIdList res = std::make_shared<std::vector<ObjectId>>();
|
||||
auto visitor = [&](const MWWorld::Ptr& ptr)
|
||||
{
|
||||
if (static_cast<int64_t>(res->size()) == query.mLimit)
|
||||
return false;
|
||||
context.mWorldView->getObjectRegistry()->registerPtr(ptr);
|
||||
if (checkQueryConditions(query, getId(ptr), context))
|
||||
res->push_back(getId(ptr));
|
||||
return static_cast<int64_t>(res->size()) != query.mLimit;
|
||||
};
|
||||
store->forEach(std::move(visitor)); // TODO: maybe use store->forEachType<TYPE> depending on query.mType
|
||||
return res;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
#ifndef MWLUA_QUERY_H
|
||||
#define MWLUA_QUERY_H
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <components/queries/query.hpp>
|
||||
|
||||
#include "context.hpp"
|
||||
#include "object.hpp"
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
||||
struct ObjectQueryTypes
|
||||
{
|
||||
static constexpr std::string_view ACTIVATORS = "activators";
|
||||
static constexpr std::string_view ACTORS = "actors";
|
||||
static constexpr std::string_view CONTAINERS = "containers";
|
||||
static constexpr std::string_view DOORS = "doors";
|
||||
static constexpr std::string_view ITEMS = "items";
|
||||
|
||||
static constexpr std::string_view types[] = {ACTIVATORS, ACTORS, CONTAINERS, DOORS, ITEMS};
|
||||
};
|
||||
|
||||
struct QueryFieldGroup
|
||||
{
|
||||
std::string mName;
|
||||
std::vector<const Queries::Field*> mFields;
|
||||
};
|
||||
const std::vector<QueryFieldGroup>& getBasicQueryFieldGroups();
|
||||
|
||||
// TODO: Implement custom fields. QueryFieldGroup registerCustomFields(...);
|
||||
|
||||
ObjectIdList selectObjectsFromList(const Queries::Query& query, const ObjectIdList& list, const Context&);
|
||||
ObjectIdList selectObjectsFromCellStore(const Queries::Query& query, MWWorld::CellStore* store, const Context&);
|
||||
|
||||
}
|
||||
|
||||
#endif // MWLUA_QUERY_H
|
@ -0,0 +1,18 @@
|
||||
#include "luabindings.hpp"
|
||||
|
||||
#include "luamanagerimp.hpp"
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
||||
sol::table initUserInterfacePackage(const Context& context)
|
||||
{
|
||||
sol::table api(context.mLua->sol(), sol::create);
|
||||
api["showMessage"] = [luaManager=context.mLuaManager](std::string_view message)
|
||||
{
|
||||
luaManager->addUIMessage(message);
|
||||
};
|
||||
return context.mLua->makeReadOnly(api);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
#include "userdataserializer.hpp"
|
||||
|
||||
#include <components/lua/serialization.hpp>
|
||||
#include <components/misc/endianness.hpp>
|
||||
|
||||
#include "object.hpp"
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
||||
class Serializer final : public LuaUtil::UserdataSerializer
|
||||
{
|
||||
public:
|
||||
explicit Serializer(bool localSerializer, ObjectRegistry* registry, std::map<int, int>* contentFileMapping)
|
||||
: mLocalSerializer(localSerializer), mObjectRegistry(registry), mContentFileMapping(contentFileMapping) {}
|
||||
|
||||
private:
|
||||
// Appends serialized sol::userdata to the end of BinaryData.
|
||||
// Returns false if this type of userdata is not supported by this serializer.
|
||||
bool serialize(LuaUtil::BinaryData& out, const sol::userdata& data) const override
|
||||
{
|
||||
if (data.is<GObject>() || data.is<LObject>())
|
||||
{
|
||||
ObjectId id = data.as<Object>().id();
|
||||
static_assert(sizeof(ObjectId) == 8);
|
||||
id.mIndex = Misc::toLittleEndian(id.mIndex);
|
||||
id.mContentFile = Misc::toLittleEndian(id.mContentFile);
|
||||
append(out, "o", &id, sizeof(ObjectId));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Deserializes userdata of type "typeName" from binaryData. Should push the result on stack using sol::stack::push.
|
||||
// Returns false if this type is not supported by this serializer.
|
||||
bool deserialize(std::string_view typeName, std::string_view binaryData, sol::state& lua) const override
|
||||
{
|
||||
if (typeName == "o")
|
||||
{
|
||||
if (binaryData.size() != sizeof(ObjectId))
|
||||
throw std::runtime_error("Incorrect serialization format. Size of ObjectId doesn't match.");
|
||||
ObjectId id;
|
||||
std::memcpy(&id, binaryData.data(), sizeof(ObjectId));
|
||||
id.mIndex = Misc::fromLittleEndian(id.mIndex);
|
||||
id.mContentFile = Misc::fromLittleEndian(id.mContentFile);
|
||||
if (id.hasContentFile() && mContentFileMapping)
|
||||
{
|
||||
auto iter = mContentFileMapping->find(id.mContentFile);
|
||||
if (iter != mContentFileMapping->end())
|
||||
id.mContentFile = iter->second;
|
||||
}
|
||||
if (mLocalSerializer)
|
||||
sol::stack::push<LObject>(lua, LObject(id, mObjectRegistry));
|
||||
else
|
||||
sol::stack::push<GObject>(lua, GObject(id, mObjectRegistry));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool mLocalSerializer;
|
||||
ObjectRegistry* mObjectRegistry;
|
||||
std::map<int, int>* mContentFileMapping;
|
||||
};
|
||||
|
||||
std::unique_ptr<LuaUtil::UserdataSerializer> createUserdataSerializer(
|
||||
bool local, ObjectRegistry* registry, std::map<int, int>* contentFileMapping)
|
||||
{
|
||||
return std::make_unique<Serializer>(local, registry, contentFileMapping);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
#ifndef MWLUA_USERDATASERIALIZER_H
|
||||
#define MWLUA_USERDATASERIALIZER_H
|
||||
|
||||
#include "object.hpp"
|
||||
|
||||
namespace LuaUtil
|
||||
{
|
||||
class UserdataSerializer;
|
||||
}
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
// UserdataSerializer is an extension for components/lua/serialization.hpp
|
||||
// Needed to serialize references to objects.
|
||||
// If local=true, then during deserialization creates LObject, otherwise creates GObject.
|
||||
// contentFileMapping is used only for deserialization. Needed to fix references if the order
|
||||
// of content files was changed.
|
||||
std::unique_ptr<LuaUtil::UserdataSerializer> createUserdataSerializer(
|
||||
bool local, ObjectRegistry* registry, std::map<int, int>* contentFileMapping = nullptr);
|
||||
}
|
||||
|
||||
#endif // MWLUA_USERDATASERIALIZER_H
|
@ -0,0 +1,152 @@
|
||||
#include "worldview.hpp"
|
||||
|
||||
#include <components/esm/esmreader.hpp>
|
||||
#include <components/esm/esmwriter.hpp>
|
||||
#include <components/esm/loadcell.hpp>
|
||||
|
||||
#include "../mwclass/container.hpp"
|
||||
|
||||
#include "../mwworld/class.hpp"
|
||||
#include "../mwworld/timestamp.hpp"
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
||||
void WorldView::update()
|
||||
{
|
||||
mObjectRegistry.update();
|
||||
mActivatorsInScene.updateList();
|
||||
mActorsInScene.updateList();
|
||||
mContainersInScene.updateList();
|
||||
mDoorsInScene.updateList();
|
||||
mItemsInScene.updateList();
|
||||
}
|
||||
|
||||
void WorldView::clear()
|
||||
{
|
||||
mObjectRegistry.clear();
|
||||
mActivatorsInScene.clear();
|
||||
mActorsInScene.clear();
|
||||
mContainersInScene.clear();
|
||||
mDoorsInScene.clear();
|
||||
mItemsInScene.clear();
|
||||
}
|
||||
|
||||
WorldView::ObjectGroup* WorldView::chooseGroup(const MWWorld::Ptr& ptr)
|
||||
{
|
||||
// It is important to check `isMarker` first.
|
||||
// For example "prisonmarker" has class "Door" despite that it is only an invisible marker.
|
||||
if (isMarker(ptr))
|
||||
return nullptr;
|
||||
const MWWorld::Class& cls = ptr.getClass();
|
||||
if (cls.isActivator())
|
||||
return &mActivatorsInScene;
|
||||
if (cls.isActor())
|
||||
return &mActorsInScene;
|
||||
if (cls.isDoor())
|
||||
return &mDoorsInScene;
|
||||
if (typeid(cls) == typeid(MWClass::Container))
|
||||
return &mContainersInScene;
|
||||
if (cls.hasToolTip(ptr))
|
||||
return &mItemsInScene;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void WorldView::objectAddedToScene(const MWWorld::Ptr& ptr)
|
||||
{
|
||||
mObjectRegistry.registerPtr(ptr);
|
||||
ObjectGroup* group = chooseGroup(ptr);
|
||||
if (group)
|
||||
addToGroup(*group, ptr);
|
||||
}
|
||||
|
||||
void WorldView::objectRemovedFromScene(const MWWorld::Ptr& ptr)
|
||||
{
|
||||
ObjectGroup* group = chooseGroup(ptr);
|
||||
if (group)
|
||||
removeFromGroup(*group, ptr);
|
||||
}
|
||||
|
||||
double WorldView::getGameTimeInHours() const
|
||||
{
|
||||
MWBase::World* world = MWBase::Environment::get().getWorld();
|
||||
MWWorld::TimeStamp timeStamp = world->getTimeStamp();
|
||||
return static_cast<double>(timeStamp.getDay()) * 24 + timeStamp.getHour();
|
||||
}
|
||||
|
||||
void WorldView::load(ESM::ESMReader& esm)
|
||||
{
|
||||
esm.getHNT(mGameSeconds, "LUAW");
|
||||
ObjectId lastAssignedId;
|
||||
lastAssignedId.load(esm, true);
|
||||
mObjectRegistry.setLastAssignedId(lastAssignedId);
|
||||
}
|
||||
|
||||
void WorldView::save(ESM::ESMWriter& esm) const
|
||||
{
|
||||
esm.writeHNT("LUAW", mGameSeconds);
|
||||
mObjectRegistry.getLastAssignedId().save(esm, true);
|
||||
}
|
||||
|
||||
void WorldView::ObjectGroup::updateList()
|
||||
{
|
||||
if (mChanged)
|
||||
{
|
||||
mList->clear();
|
||||
for (const ObjectId& id : mSet)
|
||||
mList->push_back(id);
|
||||
mChanged = false;
|
||||
}
|
||||
}
|
||||
|
||||
void WorldView::ObjectGroup::clear()
|
||||
{
|
||||
mChanged = false;
|
||||
mList->clear();
|
||||
mSet.clear();
|
||||
}
|
||||
|
||||
void WorldView::addToGroup(ObjectGroup& group, const MWWorld::Ptr& ptr)
|
||||
{
|
||||
group.mSet.insert(getId(ptr));
|
||||
group.mChanged = true;
|
||||
}
|
||||
|
||||
void WorldView::removeFromGroup(ObjectGroup& group, const MWWorld::Ptr& ptr)
|
||||
{
|
||||
group.mSet.erase(getId(ptr));
|
||||
group.mChanged = true;
|
||||
}
|
||||
|
||||
// TODO: If Lua scripts will use several threads at the same time, then `find*Cell` functions should have critical sections.
|
||||
MWWorld::CellStore* WorldView::findCell(const std::string& name, osg::Vec3f position)
|
||||
{
|
||||
MWBase::World* world = MWBase::Environment::get().getWorld();
|
||||
bool exterior = name.empty() || world->getExterior(name);
|
||||
if (exterior)
|
||||
{
|
||||
int cellX, cellY;
|
||||
world->positionToIndex(position.x(), position.y(), cellX, cellY);
|
||||
return world->getExterior(cellX, cellY);
|
||||
}
|
||||
else
|
||||
return world->getInterior(name);
|
||||
}
|
||||
|
||||
MWWorld::CellStore* WorldView::findNamedCell(const std::string& name)
|
||||
{
|
||||
MWBase::World* world = MWBase::Environment::get().getWorld();
|
||||
const ESM::Cell* esmCell = world->getExterior(name);
|
||||
if (esmCell)
|
||||
return world->getExterior(esmCell->getGridX(), esmCell->getGridY());
|
||||
else
|
||||
return world->getInterior(name);
|
||||
}
|
||||
|
||||
MWWorld::CellStore* WorldView::findExteriorCell(int x, int y)
|
||||
{
|
||||
MWBase::World* world = MWBase::Environment::get().getWorld();
|
||||
return world->getExterior(x, y);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
#ifndef MWLUA_WORLDVIEW_H
|
||||
#define MWLUA_WORLDVIEW_H
|
||||
|
||||
#include "object.hpp"
|
||||
|
||||
namespace ESM
|
||||
{
|
||||
class ESMWriter;
|
||||
class ESMReader;
|
||||
}
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
|
||||
// Tracks all used game objects.
|
||||
class WorldView
|
||||
{
|
||||
public:
|
||||
void update(); // Should be called every frame.
|
||||
void clear(); // Should be called every time before starting or loading a new game.
|
||||
|
||||
// Returns the number of seconds passed from the beginning of the game.
|
||||
double getGameTimeInSeconds() const { return mGameSeconds; }
|
||||
void setGameTimeInSeconds(double t) { mGameSeconds = t; }
|
||||
|
||||
// Returns the number of game hours passed from the beginning of the game.
|
||||
// Note that the number of seconds in a game hour is not fixed.
|
||||
double getGameTimeInHours() const;
|
||||
|
||||
ObjectIdList getActivatorsInScene() const { return mActivatorsInScene.mList; }
|
||||
ObjectIdList getActorsInScene() const { return mActorsInScene.mList; }
|
||||
ObjectIdList getContainersInScene() const { return mContainersInScene.mList; }
|
||||
ObjectIdList getDoorsInScene() const { return mDoorsInScene.mList; }
|
||||
ObjectIdList getItemsInScene() const { return mItemsInScene.mList; }
|
||||
|
||||
ObjectRegistry* getObjectRegistry() { return &mObjectRegistry; }
|
||||
|
||||
void objectUnloaded(const MWWorld::Ptr& ptr) { mObjectRegistry.deregisterPtr(ptr); }
|
||||
|
||||
void objectAddedToScene(const MWWorld::Ptr& ptr);
|
||||
void objectRemovedFromScene(const MWWorld::Ptr& ptr);
|
||||
|
||||
// Returns list of objects that meets the `query` criteria.
|
||||
// If onlyActive = true, then search only among the objects that are currently in the scene.
|
||||
// TODO: ObjectIdList selectObjects(const Queries::Query& query, bool onlyActive);
|
||||
|
||||
MWWorld::CellStore* findCell(const std::string& name, osg::Vec3f position);
|
||||
MWWorld::CellStore* findNamedCell(const std::string& name);
|
||||
MWWorld::CellStore* findExteriorCell(int x, int y);
|
||||
|
||||
void load(ESM::ESMReader& esm);
|
||||
void save(ESM::ESMWriter& esm) const;
|
||||
|
||||
private:
|
||||
struct ObjectGroup
|
||||
{
|
||||
void updateList();
|
||||
void clear();
|
||||
|
||||
bool mChanged = false;
|
||||
ObjectIdList mList = std::make_shared<std::vector<ObjectId>>();
|
||||
std::set<ObjectId> mSet;
|
||||
};
|
||||
|
||||
ObjectGroup* chooseGroup(const MWWorld::Ptr& ptr);
|
||||
void addToGroup(ObjectGroup& group, const MWWorld::Ptr& ptr);
|
||||
void removeFromGroup(ObjectGroup& group, const MWWorld::Ptr& ptr);
|
||||
|
||||
ObjectRegistry mObjectRegistry;
|
||||
ObjectGroup mActivatorsInScene;
|
||||
ObjectGroup mActorsInScene;
|
||||
ObjectGroup mContainersInScene;
|
||||
ObjectGroup mDoorsInScene;
|
||||
ObjectGroup mItemsInScene;
|
||||
|
||||
double mGameSeconds = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // MWLUA_WORLDVIEW_H
|
@ -0,0 +1,167 @@
|
||||
#include "gmock/gmock.h"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <components/lua/luastate.hpp>
|
||||
|
||||
#include "testing_util.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace testing;
|
||||
|
||||
TestFile counterFile(R"X(
|
||||
x = 42
|
||||
return {
|
||||
get = function() return x end,
|
||||
inc = function(v) x = x + v end
|
||||
}
|
||||
)X");
|
||||
|
||||
TestFile invalidScriptFile("Invalid script");
|
||||
|
||||
TestFile testsFile(R"X(
|
||||
return {
|
||||
-- should work
|
||||
sin = function(x) return math.sin(x) end,
|
||||
requireMathSin = function(x) return require('math').sin(x) end,
|
||||
useCounter = function()
|
||||
local counter = require('aaa.counter')
|
||||
counter.inc(1)
|
||||
return counter.get()
|
||||
end,
|
||||
callRawset = function()
|
||||
t = {a = 1, b = 2}
|
||||
rawset(t, 'b', 3)
|
||||
return t.b
|
||||
end,
|
||||
print = print,
|
||||
|
||||
-- should throw an error
|
||||
incorrectRequire = function() require('counter') end,
|
||||
modifySystemLib = function() math.sin = 5 end,
|
||||
rawsetSystemLib = function() rawset(math, 'sin', 5) end,
|
||||
callLoadstring = function() loadstring('print(1)') end,
|
||||
setSqr = function() require('sqrlib').sqr = math.sin end,
|
||||
setOmwName = function() require('openmw').name = 'abc' end,
|
||||
|
||||
-- should work if API is registered
|
||||
sqr = function(x) return require('sqrlib').sqr(x) end,
|
||||
apiName = function() return require('test.api').name end
|
||||
}
|
||||
)X");
|
||||
|
||||
struct LuaStateTest : Test
|
||||
{
|
||||
std::unique_ptr<VFS::Manager> mVFS = createTestVFS({
|
||||
{"aaa/counter.lua", &counterFile},
|
||||
{"bbb/tests.lua", &testsFile},
|
||||
{"invalid.lua", &invalidScriptFile}
|
||||
});
|
||||
|
||||
LuaUtil::LuaState mLua{mVFS.get()};
|
||||
};
|
||||
|
||||
TEST_F(LuaStateTest, Sandbox)
|
||||
{
|
||||
sol::table script1 = mLua.runInNewSandbox("aaa/counter.lua");
|
||||
|
||||
EXPECT_EQ(LuaUtil::call(script1["get"]).get<int>(), 42);
|
||||
LuaUtil::call(script1["inc"], 3);
|
||||
EXPECT_EQ(LuaUtil::call(script1["get"]).get<int>(), 45);
|
||||
|
||||
sol::table script2 = mLua.runInNewSandbox("aaa/counter.lua");
|
||||
EXPECT_EQ(LuaUtil::call(script2["get"]).get<int>(), 42);
|
||||
LuaUtil::call(script2["inc"], 1);
|
||||
EXPECT_EQ(LuaUtil::call(script2["get"]).get<int>(), 43);
|
||||
|
||||
EXPECT_EQ(LuaUtil::call(script1["get"]).get<int>(), 45);
|
||||
}
|
||||
|
||||
TEST_F(LuaStateTest, ErrorHandling)
|
||||
{
|
||||
EXPECT_ERROR(mLua.runInNewSandbox("invalid.lua"), "[string \"invalid.lua\"]:1:");
|
||||
}
|
||||
|
||||
TEST_F(LuaStateTest, CustomRequire)
|
||||
{
|
||||
sol::table script = mLua.runInNewSandbox("bbb/tests.lua");
|
||||
|
||||
EXPECT_FLOAT_EQ(LuaUtil::call(script["sin"], 1).get<float>(),
|
||||
-LuaUtil::call(script["requireMathSin"], -1).get<float>());
|
||||
|
||||
EXPECT_EQ(LuaUtil::call(script["useCounter"]).get<int>(), 43);
|
||||
EXPECT_EQ(LuaUtil::call(script["useCounter"]).get<int>(), 44);
|
||||
{
|
||||
sol::table script2 = mLua.runInNewSandbox("bbb/tests.lua");
|
||||
EXPECT_EQ(LuaUtil::call(script2["useCounter"]).get<int>(), 43);
|
||||
}
|
||||
EXPECT_EQ(LuaUtil::call(script["useCounter"]).get<int>(), 45);
|
||||
|
||||
EXPECT_ERROR(LuaUtil::call(script["incorrectRequire"]), "Resource 'counter.lua' not found");
|
||||
}
|
||||
|
||||
TEST_F(LuaStateTest, ReadOnly)
|
||||
{
|
||||
sol::table script = mLua.runInNewSandbox("bbb/tests.lua");
|
||||
|
||||
// rawset itself is allowed
|
||||
EXPECT_EQ(LuaUtil::call(script["callRawset"]).get<int>(), 3);
|
||||
|
||||
// but read-only object can not be modified even with rawset
|
||||
EXPECT_ERROR(LuaUtil::call(script["rawsetSystemLib"]), "bad argument #1 to 'rawset' (table expected, got userdata)");
|
||||
EXPECT_ERROR(LuaUtil::call(script["modifySystemLib"]), "a userdata value");
|
||||
|
||||
EXPECT_EQ(mLua.getMutableFromReadOnly(mLua.makeReadOnly(script)), script);
|
||||
}
|
||||
|
||||
TEST_F(LuaStateTest, Print)
|
||||
{
|
||||
{
|
||||
sol::table script = mLua.runInNewSandbox("bbb/tests.lua");
|
||||
testing::internal::CaptureStdout();
|
||||
LuaUtil::call(script["print"], 1, 2, 3);
|
||||
std::string output = testing::internal::GetCapturedStdout();
|
||||
EXPECT_EQ(output, "[bbb/tests.lua]:\t1\t2\t3\n");
|
||||
}
|
||||
{
|
||||
sol::table script = mLua.runInNewSandbox("bbb/tests.lua", "prefix");
|
||||
testing::internal::CaptureStdout();
|
||||
LuaUtil::call(script["print"]); // print with no arguments
|
||||
std::string output = testing::internal::GetCapturedStdout();
|
||||
EXPECT_EQ(output, "prefix[bbb/tests.lua]:\n");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LuaStateTest, UnsafeFunction)
|
||||
{
|
||||
sol::table script = mLua.runInNewSandbox("bbb/tests.lua");
|
||||
EXPECT_ERROR(LuaUtil::call(script["callLoadstring"]), "a nil value");
|
||||
}
|
||||
|
||||
TEST_F(LuaStateTest, ProvideAPI)
|
||||
{
|
||||
LuaUtil::LuaState lua(mVFS.get());
|
||||
|
||||
sol::table api1 = lua.makeReadOnly(lua.sol().create_table_with("name", "api1"));
|
||||
sol::table api2 = lua.makeReadOnly(lua.sol().create_table_with("name", "api2"));
|
||||
|
||||
sol::table script1 = lua.runInNewSandbox("bbb/tests.lua", "", {{"test.api", api1}});
|
||||
|
||||
lua.addCommonPackage(
|
||||
"sqrlib", lua.sol().create_table_with("sqr", [](int x) { return x * x; }));
|
||||
|
||||
sol::table script2 = lua.runInNewSandbox("bbb/tests.lua", "", {{"test.api", api2}});
|
||||
|
||||
EXPECT_ERROR(LuaUtil::call(script1["sqr"], 3), "Resource 'sqrlib.lua' not found");
|
||||
EXPECT_EQ(LuaUtil::call(script2["sqr"], 3).get<int>(), 9);
|
||||
|
||||
EXPECT_EQ(LuaUtil::call(script1["apiName"]).get<std::string>(), "api1");
|
||||
EXPECT_EQ(LuaUtil::call(script2["apiName"]).get<std::string>(), "api2");
|
||||
}
|
||||
|
||||
TEST_F(LuaStateTest, GetLuaVersion)
|
||||
{
|
||||
EXPECT_THAT(LuaUtil::getLuaVersion(), HasSubstr("Lua"));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
#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());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
#include "gmock/gmock.h"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <components/queries/luabindings.hpp>
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace testing;
|
||||
|
||||
TEST(LuaQueryPackageTest, basic)
|
||||
{
|
||||
sol::state lua;
|
||||
lua.open_libraries(sol::lib::base, sol::lib::string);
|
||||
Queries::registerQueryBindings(lua);
|
||||
lua["query"] = Queries::Query("test");
|
||||
lua["fieldX"] = Queries::Field({ "x" }, typeid(std::string));
|
||||
lua["fieldY"] = Queries::Field({ "y" }, typeid(int));
|
||||
lua.safe_script("t = query:where(fieldX:eq('abc') + fieldX:like('%abcd%'))");
|
||||
lua.safe_script("t = t:where(fieldY:gt(5))");
|
||||
lua.safe_script("t = t:orderBy(fieldX)");
|
||||
lua.safe_script("t = t:orderByDesc(fieldY)");
|
||||
lua.safe_script("t = t:groupBy(fieldY)");
|
||||
lua.safe_script("t = t:limit(10):offset(5)");
|
||||
EXPECT_EQ(
|
||||
lua.safe_script("return tostring(t)").get<std::string>(),
|
||||
"SELECT test WHERE ((x == \"abc\") OR (x LIKE \"%abcd%\")) AND (y > 5) ORDER BY x, y DESC GROUP BY y LIMIT 10 OFFSET 5");
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,376 @@
|
||||
#include "gmock/gmock.h"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <components/esm/luascripts.hpp>
|
||||
|
||||
#include <components/lua/luastate.hpp>
|
||||
#include <components/lua/scriptscontainer.hpp>
|
||||
|
||||
#include "testing_util.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace testing;
|
||||
|
||||
TestFile invalidScript("not a script");
|
||||
TestFile incorrectScript("return { incorrectSection = {}, engineHandlers = { incorrectHandler = function() end } }");
|
||||
TestFile emptyScript("");
|
||||
|
||||
TestFile testScript(R"X(
|
||||
return {
|
||||
engineHandlers = { onUpdate = function(dt) print(' update ' .. tostring(dt)) end },
|
||||
eventHandlers = {
|
||||
Event1 = function(eventData) print(' event1 ' .. tostring(eventData.x)) end,
|
||||
Event2 = function(eventData) print(' event2 ' .. tostring(eventData.x)) end,
|
||||
Print = function() print('print') end
|
||||
}
|
||||
}
|
||||
)X");
|
||||
|
||||
TestFile stopEventScript(R"X(
|
||||
return {
|
||||
eventHandlers = {
|
||||
Event1 = function(eventData)
|
||||
print(' event1 ' .. tostring(eventData.x))
|
||||
return eventData.x >= 1
|
||||
end
|
||||
}
|
||||
}
|
||||
)X");
|
||||
|
||||
TestFile loadSaveScript(R"X(
|
||||
x = 0
|
||||
y = 0
|
||||
return {
|
||||
engineHandlers = {
|
||||
onSave = function(state)
|
||||
return {x = x, y = y}
|
||||
end,
|
||||
onLoad = function(state)
|
||||
x, y = state.x, state.y
|
||||
end
|
||||
},
|
||||
eventHandlers = {
|
||||
Set = function(eventData)
|
||||
eventData.n = eventData.n - 1
|
||||
if eventData.n == 0 then
|
||||
x, y = eventData.x, eventData.y
|
||||
end
|
||||
end,
|
||||
Print = function()
|
||||
print(x, y)
|
||||
end
|
||||
}
|
||||
}
|
||||
)X");
|
||||
|
||||
TestFile interfaceScript(R"X(
|
||||
return {
|
||||
interfaceName = "TestInterface",
|
||||
interface = {
|
||||
fn = function(x) print('FN', x) end,
|
||||
value = 3.5
|
||||
},
|
||||
}
|
||||
)X");
|
||||
|
||||
TestFile overrideInterfaceScript(R"X(
|
||||
local old = require('openmw.interfaces').TestInterface
|
||||
return {
|
||||
interfaceName = "TestInterface",
|
||||
interface = {
|
||||
fn = function(x)
|
||||
print('NEW FN', x)
|
||||
old.fn(x)
|
||||
end,
|
||||
value = old.value + 1
|
||||
},
|
||||
}
|
||||
)X");
|
||||
|
||||
TestFile useInterfaceScript(R"X(
|
||||
local interfaces = require('openmw.interfaces')
|
||||
return {
|
||||
engineHandlers = {
|
||||
onUpdate = function()
|
||||
interfaces.TestInterface.fn(interfaces.TestInterface.value)
|
||||
end,
|
||||
},
|
||||
}
|
||||
)X");
|
||||
|
||||
struct LuaScriptsContainerTest : Test
|
||||
{
|
||||
std::unique_ptr<VFS::Manager> mVFS = createTestVFS({
|
||||
{"invalid.lua", &invalidScript},
|
||||
{"incorrect.lua", &incorrectScript},
|
||||
{"empty.lua", &emptyScript},
|
||||
{"test1.lua", &testScript},
|
||||
{"test2.lua", &testScript},
|
||||
{"stopEvent.lua", &stopEventScript},
|
||||
{"loadSave1.lua", &loadSaveScript},
|
||||
{"loadSave2.lua", &loadSaveScript},
|
||||
{"testInterface.lua", &interfaceScript},
|
||||
{"overrideInterface.lua", &overrideInterfaceScript},
|
||||
{"useInterface.lua", &useInterfaceScript},
|
||||
});
|
||||
|
||||
LuaUtil::LuaState mLua{mVFS.get()};
|
||||
};
|
||||
|
||||
TEST_F(LuaScriptsContainerTest, VerifyStructure)
|
||||
{
|
||||
LuaUtil::ScriptsContainer scripts(&mLua, "Test");
|
||||
{
|
||||
testing::internal::CaptureStdout();
|
||||
EXPECT_FALSE(scripts.addNewScript("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"));
|
||||
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_EQ(internal::GetCapturedStdout(), "");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LuaScriptsContainerTest, CallHandler)
|
||||
{
|
||||
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"));
|
||||
scripts.update(1.5f);
|
||||
EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua]:\t update 1.5\n"
|
||||
"Test[test2.lua]:\t update 1.5\n");
|
||||
}
|
||||
|
||||
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"));
|
||||
|
||||
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));
|
||||
|
||||
{
|
||||
testing::internal::CaptureStdout();
|
||||
scripts.receiveEvent("SomeEvent", X1);
|
||||
EXPECT_EQ(internal::GetCapturedStdout(),
|
||||
"Test has received event 'SomeEvent', but there are no handlers for this event\n");
|
||||
}
|
||||
{
|
||||
testing::internal::CaptureStdout();
|
||||
scripts.receiveEvent("Event1", X1);
|
||||
EXPECT_EQ(internal::GetCapturedStdout(),
|
||||
"Test[test2.lua]:\t event1 1.5\n"
|
||||
"Test[stopEvent.lua]:\t event1 1.5\n"
|
||||
"Test[test1.lua]:\t event1 1.5\n");
|
||||
}
|
||||
{
|
||||
testing::internal::CaptureStdout();
|
||||
scripts.receiveEvent("Event2", X1);
|
||||
EXPECT_EQ(internal::GetCapturedStdout(),
|
||||
"Test[test2.lua]:\t event2 1.5\n"
|
||||
"Test[test1.lua]:\t event2 1.5\n");
|
||||
}
|
||||
{
|
||||
testing::internal::CaptureStdout();
|
||||
scripts.receiveEvent("Event1", X0);
|
||||
EXPECT_EQ(internal::GetCapturedStdout(),
|
||||
"Test[test2.lua]:\t event1 0.5\n"
|
||||
"Test[stopEvent.lua]:\t event1 0.5\n");
|
||||
}
|
||||
{
|
||||
testing::internal::CaptureStdout();
|
||||
scripts.receiveEvent("Event2", X0);
|
||||
EXPECT_EQ(internal::GetCapturedStdout(),
|
||||
"Test[test2.lua]:\t event2 0.5\n"
|
||||
"Test[test1.lua]:\t event2 0.5\n");
|
||||
}
|
||||
}
|
||||
|
||||
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"));
|
||||
std::string X = LuaUtil::serialize(mLua.sol().create_table_with("x", 0.5));
|
||||
|
||||
{
|
||||
testing::internal::CaptureStdout();
|
||||
scripts.update(1.5f);
|
||||
scripts.receiveEvent("Event1", X);
|
||||
EXPECT_EQ(internal::GetCapturedStdout(),
|
||||
"Test[test1.lua]:\t update 1.5\n"
|
||||
"Test[test2.lua]:\t update 1.5\n"
|
||||
"Test[test2.lua]:\t event1 0.5\n"
|
||||
"Test[stopEvent.lua]:\t event1 0.5\n");
|
||||
}
|
||||
{
|
||||
testing::internal::CaptureStdout();
|
||||
EXPECT_TRUE(scripts.removeScript("stopEvent.lua"));
|
||||
EXPECT_FALSE(scripts.removeScript("stopEvent.lua")); // already removed
|
||||
scripts.update(1.5f);
|
||||
scripts.receiveEvent("Event1", X);
|
||||
EXPECT_EQ(internal::GetCapturedStdout(),
|
||||
"Test[test1.lua]:\t update 1.5\n"
|
||||
"Test[test2.lua]:\t update 1.5\n"
|
||||
"Test[test2.lua]:\t event1 0.5\n"
|
||||
"Test[test1.lua]:\t event1 0.5\n");
|
||||
}
|
||||
{
|
||||
testing::internal::CaptureStdout();
|
||||
EXPECT_TRUE(scripts.removeScript("test1.lua"));
|
||||
scripts.update(1.5f);
|
||||
scripts.receiveEvent("Event1", X);
|
||||
EXPECT_EQ(internal::GetCapturedStdout(),
|
||||
"Test[test2.lua]:\t update 1.5\n"
|
||||
"Test[test2.lua]:\t event1 0.5\n");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LuaScriptsContainerTest, Interface)
|
||||
{
|
||||
LuaUtil::ScriptsContainer scripts(&mLua, "Test");
|
||||
testing::internal::CaptureStdout();
|
||||
EXPECT_TRUE(scripts.addNewScript("testInterface.lua"));
|
||||
EXPECT_TRUE(scripts.addNewScript("overrideInterface.lua"));
|
||||
EXPECT_TRUE(scripts.addNewScript("useInterface.lua"));
|
||||
scripts.update(1.5f);
|
||||
EXPECT_TRUE(scripts.removeScript("overrideInterface.lua"));
|
||||
scripts.update(1.5f);
|
||||
EXPECT_EQ(internal::GetCapturedStdout(),
|
||||
"Test[overrideInterface.lua]:\tNEW FN\t4.5\n"
|
||||
"Test[testInterface.lua]:\tFN\t4.5\n"
|
||||
"Test[testInterface.lua]:\tFN\t3.5\n");
|
||||
}
|
||||
|
||||
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"));
|
||||
|
||||
EXPECT_TRUE(scripts3.addNewScript("test2.lua"));
|
||||
EXPECT_TRUE(scripts3.addNewScript("loadSave2.lua"));
|
||||
|
||||
scripts1.receiveEvent("Set", LuaUtil::serialize(mLua.sol().create_table_with(
|
||||
"n", 1,
|
||||
"x", 0.5,
|
||||
"y", 3.5)));
|
||||
scripts1.receiveEvent("Set", LuaUtil::serialize(mLua.sol().create_table_with(
|
||||
"n", 2,
|
||||
"x", 2.5,
|
||||
"y", 1.5)));
|
||||
|
||||
ESM::LuaScripts data;
|
||||
scripts1.save(data);
|
||||
scripts2.load(data, true);
|
||||
scripts3.load(data, false);
|
||||
|
||||
{
|
||||
testing::internal::CaptureStdout();
|
||||
scripts2.receiveEvent("Print", "");
|
||||
EXPECT_EQ(internal::GetCapturedStdout(),
|
||||
"Test[loadSave2.lua]:\t0.5\t3.5\n"
|
||||
"Test[test1.lua]:\tprint\n"
|
||||
"Test[loadSave1.lua]:\t2.5\t1.5\n");
|
||||
}
|
||||
{
|
||||
testing::internal::CaptureStdout();
|
||||
scripts3.receiveEvent("Print", "");
|
||||
EXPECT_EQ(internal::GetCapturedStdout(),
|
||||
"Test[loadSave2.lua]:\t0.5\t3.5\n"
|
||||
"Test[test2.lua]:\tprint\n");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LuaScriptsContainerTest, Timers)
|
||||
{
|
||||
using TimeUnit = LuaUtil::ScriptsContainer::TimeUnit;
|
||||
LuaUtil::ScriptsContainer scripts(&mLua, "Test");
|
||||
EXPECT_TRUE(scripts.addNewScript("test1.lua"));
|
||||
EXPECT_TRUE(scripts.addNewScript("test2.lua"));
|
||||
|
||||
int counter1 = 0, counter2 = 0, counter3 = 0, counter4 = 0;
|
||||
sol::function fn1 = sol::make_object(mLua.sol(), [&]() { counter1++; });
|
||||
sol::function fn2 = sol::make_object(mLua.sol(), [&]() { counter2++; });
|
||||
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.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.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);
|
||||
|
||||
EXPECT_EQ(counter1, 0);
|
||||
EXPECT_EQ(counter3, 0);
|
||||
|
||||
scripts.processTimers(6, 4);
|
||||
|
||||
EXPECT_EQ(counter1, 1);
|
||||
EXPECT_EQ(counter3, 1);
|
||||
EXPECT_EQ(counter4, 0);
|
||||
|
||||
scripts.processTimers(6, 8);
|
||||
|
||||
EXPECT_EQ(counter1, 2);
|
||||
EXPECT_EQ(counter2, 0);
|
||||
EXPECT_EQ(counter3, 1);
|
||||
EXPECT_EQ(counter4, 2);
|
||||
|
||||
scripts.processTimers(11, 12);
|
||||
|
||||
EXPECT_EQ(counter1, 2);
|
||||
EXPECT_EQ(counter2, 2);
|
||||
EXPECT_EQ(counter3, 5);
|
||||
EXPECT_EQ(counter4, 5);
|
||||
|
||||
ESM::LuaScripts data;
|
||||
scripts.save(data);
|
||||
scripts.load(data, true);
|
||||
scripts.registerTimerCallback("test1.lua", "B", fn4);
|
||||
|
||||
testing::internal::CaptureStdout();
|
||||
scripts.processTimers(20, 20);
|
||||
EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua] callTimer failed: Callback 'A' doesn't exist\n");
|
||||
|
||||
EXPECT_EQ(counter1, 2);
|
||||
EXPECT_EQ(counter2, 2);
|
||||
EXPECT_EQ(counter3, 5);
|
||||
EXPECT_EQ(counter4, 25);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,207 @@
|
||||
#include "gmock/gmock.h"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <osg/Vec2f>
|
||||
#include <osg/Vec3f>
|
||||
|
||||
#include <components/lua/serialization.hpp>
|
||||
|
||||
#include <components/misc/endianness.hpp>
|
||||
|
||||
#include "testing_util.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace testing;
|
||||
|
||||
TEST(LuaSerializationTest, Nil)
|
||||
{
|
||||
sol::state lua;
|
||||
EXPECT_EQ(LuaUtil::serialize(sol::nil), "");
|
||||
EXPECT_EQ(LuaUtil::deserialize(lua, ""), sol::nil);
|
||||
}
|
||||
|
||||
TEST(LuaSerializationTest, Number)
|
||||
{
|
||||
sol::state lua;
|
||||
std::string serialized = LuaUtil::serialize(sol::make_object<double>(lua, 3.14));
|
||||
EXPECT_EQ(serialized.size(), 10); // version, type, 8 bytes value
|
||||
sol::object value = LuaUtil::deserialize(lua, serialized);
|
||||
ASSERT_TRUE(value.is<double>());
|
||||
EXPECT_FLOAT_EQ(value.as<double>(), 3.14);
|
||||
}
|
||||
|
||||
TEST(LuaSerializationTest, Boolean)
|
||||
{
|
||||
sol::state lua;
|
||||
{
|
||||
std::string serialized = LuaUtil::serialize(sol::make_object<bool>(lua, true));
|
||||
EXPECT_EQ(serialized.size(), 3); // version, type, 1 byte value
|
||||
sol::object value = LuaUtil::deserialize(lua, serialized);
|
||||
EXPECT_FALSE(value.is<double>());
|
||||
ASSERT_TRUE(value.is<bool>());
|
||||
EXPECT_TRUE(value.as<bool>());
|
||||
}
|
||||
{
|
||||
std::string serialized = LuaUtil::serialize(sol::make_object<bool>(lua, false));
|
||||
EXPECT_EQ(serialized.size(), 3); // version, type, 1 byte value
|
||||
sol::object value = LuaUtil::deserialize(lua, serialized);
|
||||
EXPECT_FALSE(value.is<double>());
|
||||
ASSERT_TRUE(value.is<bool>());
|
||||
EXPECT_FALSE(value.as<bool>());
|
||||
}
|
||||
}
|
||||
|
||||
TEST(LuaSerializationTest, String)
|
||||
{
|
||||
sol::state lua;
|
||||
std::string_view emptyString = "";
|
||||
std::string_view shortString = "abc";
|
||||
std::string_view longString = "It is a string with more than 32 characters...........................";
|
||||
|
||||
{
|
||||
std::string serialized = LuaUtil::serialize(sol::make_object<std::string_view>(lua, emptyString));
|
||||
EXPECT_EQ(serialized.size(), 2); // version, type
|
||||
sol::object value = LuaUtil::deserialize(lua, serialized);
|
||||
ASSERT_TRUE(value.is<std::string>());
|
||||
EXPECT_EQ(value.as<std::string>(), emptyString);
|
||||
}
|
||||
{
|
||||
std::string serialized = LuaUtil::serialize(sol::make_object<std::string_view>(lua, shortString));
|
||||
EXPECT_EQ(serialized.size(), 2 + shortString.size()); // version, type, str data
|
||||
sol::object value = LuaUtil::deserialize(lua, serialized);
|
||||
ASSERT_TRUE(value.is<std::string>());
|
||||
EXPECT_EQ(value.as<std::string>(), shortString);
|
||||
}
|
||||
{
|
||||
std::string serialized = LuaUtil::serialize(sol::make_object<std::string_view>(lua, longString));
|
||||
EXPECT_EQ(serialized.size(), 6 + longString.size()); // version, type, size, str data
|
||||
sol::object value = LuaUtil::deserialize(lua, serialized);
|
||||
ASSERT_TRUE(value.is<std::string>());
|
||||
EXPECT_EQ(value.as<std::string>(), longString);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(LuaSerializationTest, Vector)
|
||||
{
|
||||
sol::state lua;
|
||||
osg::Vec2f vec2(1, 2);
|
||||
osg::Vec3f vec3(1, 2, 3);
|
||||
|
||||
{
|
||||
std::string serialized = LuaUtil::serialize(sol::make_object(lua, vec2));
|
||||
EXPECT_EQ(serialized.size(), 10); // version, type, 2x float
|
||||
sol::object value = LuaUtil::deserialize(lua, serialized);
|
||||
ASSERT_TRUE(value.is<osg::Vec2f>());
|
||||
EXPECT_EQ(value.as<osg::Vec2f>(), vec2);
|
||||
}
|
||||
{
|
||||
std::string serialized = LuaUtil::serialize(sol::make_object(lua, vec3));
|
||||
EXPECT_EQ(serialized.size(), 14); // version, type, 3x float
|
||||
sol::object value = LuaUtil::deserialize(lua, serialized);
|
||||
ASSERT_TRUE(value.is<osg::Vec3f>());
|
||||
EXPECT_EQ(value.as<osg::Vec3f>(), vec3);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(LuaSerializationTest, Table)
|
||||
{
|
||||
sol::state lua;
|
||||
sol::table table(lua, sol::create);
|
||||
table["aa"] = 1;
|
||||
table["ab"] = true;
|
||||
table["nested"] = sol::table(lua, sol::create);
|
||||
table["nested"]["aa"] = 2;
|
||||
table["nested"]["bb"] = "something";
|
||||
table["nested"][5] = -0.5;
|
||||
table["nested_empty"] = sol::table(lua, sol::create);
|
||||
table[1] = osg::Vec2f(1, 2);
|
||||
table[2] = osg::Vec2f(2, 1);
|
||||
|
||||
std::string serialized = LuaUtil::serialize(table);
|
||||
EXPECT_EQ(serialized.size(), 123);
|
||||
sol::table res_table = LuaUtil::deserialize(lua, serialized);
|
||||
|
||||
EXPECT_EQ(res_table.get<int>("aa"), 1);
|
||||
EXPECT_EQ(res_table.get<bool>("ab"), true);
|
||||
EXPECT_EQ(res_table.get<sol::table>("nested").get<int>("aa"), 2);
|
||||
EXPECT_EQ(res_table.get<sol::table>("nested").get<std::string>("bb"), "something");
|
||||
EXPECT_FLOAT_EQ(res_table.get<sol::table>("nested").get<double>(5), -0.5);
|
||||
EXPECT_EQ(res_table.get<osg::Vec2f>(1), osg::Vec2f(1, 2));
|
||||
EXPECT_EQ(res_table.get<osg::Vec2f>(2), osg::Vec2f(2, 1));
|
||||
}
|
||||
|
||||
struct TestStruct1 { double a, b; };
|
||||
struct TestStruct2 { int a, b; };
|
||||
|
||||
class TestSerializer final : public LuaUtil::UserdataSerializer
|
||||
{
|
||||
bool serialize(LuaUtil::BinaryData& out, const sol::userdata& data) const override
|
||||
{
|
||||
if (data.is<TestStruct1>())
|
||||
{
|
||||
TestStruct1 t = data.as<TestStruct1>();
|
||||
t.a = Misc::toLittleEndian(t.a);
|
||||
t.b = Misc::toLittleEndian(t.b);
|
||||
append(out, "ts1", &t, sizeof(t));
|
||||
return true;
|
||||
}
|
||||
if (data.is<TestStruct2>())
|
||||
{
|
||||
TestStruct2 t = data.as<TestStruct2>();
|
||||
t.a = Misc::toLittleEndian(t.a);
|
||||
t.b = Misc::toLittleEndian(t.b);
|
||||
append(out, "test_struct2", &t, sizeof(t));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool deserialize(std::string_view typeName, std::string_view binaryData, sol::state& lua) const override
|
||||
{
|
||||
if (typeName == "ts1")
|
||||
{
|
||||
if (sizeof(TestStruct1) != binaryData.size())
|
||||
throw std::runtime_error("Incorrect binaryData.size() for TestStruct1: " + std::to_string(binaryData.size()));
|
||||
TestStruct1 t = *reinterpret_cast<const TestStruct1*>(binaryData.data());
|
||||
t.a = Misc::fromLittleEndian(t.a);
|
||||
t.b = Misc::fromLittleEndian(t.b);
|
||||
sol::stack::push<TestStruct1>(lua, t);
|
||||
return true;
|
||||
}
|
||||
if (typeName == "test_struct2")
|
||||
{
|
||||
if (sizeof(TestStruct2) != binaryData.size())
|
||||
throw std::runtime_error("Incorrect binaryData.size() for TestStruct2: " + std::to_string(binaryData.size()));
|
||||
TestStruct2 t = *reinterpret_cast<const TestStruct2*>(binaryData.data());
|
||||
t.a = Misc::fromLittleEndian(t.a);
|
||||
t.b = Misc::fromLittleEndian(t.b);
|
||||
sol::stack::push<TestStruct2>(lua, t);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
TEST(LuaSerializationTest, UserdataSerializer)
|
||||
{
|
||||
sol::state lua;
|
||||
sol::table table(lua, sol::create);
|
||||
table["x"] = TestStruct1{1.5, 2.5};
|
||||
table["y"] = TestStruct2{4, 3};
|
||||
TestSerializer serializer;
|
||||
|
||||
EXPECT_ERROR(LuaUtil::serialize(table), "Unknown userdata");
|
||||
std::string serialized = LuaUtil::serialize(table, &serializer);
|
||||
EXPECT_ERROR(LuaUtil::deserialize(lua, serialized), "Unknown type:");
|
||||
sol::table res = LuaUtil::deserialize(lua, serialized, &serializer);
|
||||
|
||||
TestStruct1 rx = res.get<TestStruct1>("x");
|
||||
TestStruct2 ry = res.get<TestStruct2>("y");
|
||||
EXPECT_EQ(rx.a, 1.5);
|
||||
EXPECT_EQ(rx.b, 2.5);
|
||||
EXPECT_EQ(ry.a, 4);
|
||||
EXPECT_EQ(ry.b, 3);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
#include "gmock/gmock.h"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <components/lua/utilpackage.hpp>
|
||||
|
||||
#include "testing_util.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace testing;
|
||||
|
||||
TEST(LuaUtilPackageTest, Vector2)
|
||||
{
|
||||
sol::state lua;
|
||||
lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string);
|
||||
lua["util"] = LuaUtil::initUtilPackage(lua);
|
||||
lua.safe_script("v = util.vector2(3, 4)");
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return v.x").get<float>(), 3);
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return v.y").get<float>(), 4);
|
||||
EXPECT_EQ(lua.safe_script("return tostring(v)").get<std::string>(), "(3, 4)");
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return v:length()").get<float>(), 5);
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return v:length2()").get<float>(), 25);
|
||||
EXPECT_FALSE(lua.safe_script("return util.vector2(1, 2) == util.vector2(1, 3)").get<bool>());
|
||||
EXPECT_TRUE(lua.safe_script("return util.vector2(1, 2) + util.vector2(2, 5) == util.vector2(3, 7)").get<bool>());
|
||||
EXPECT_TRUE(lua.safe_script("return util.vector2(1, 2) - util.vector2(2, 5) == -util.vector2(1, 3)").get<bool>());
|
||||
EXPECT_TRUE(lua.safe_script("return util.vector2(1, 2) == util.vector2(2, 4) / 2").get<bool>());
|
||||
EXPECT_TRUE(lua.safe_script("return util.vector2(1, 2) * 2 == util.vector2(2, 4)").get<bool>());
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return util.vector2(3, 2) * v").get<float>(), 17);
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return util.vector2(3, 2):dot(v)").get<float>(), 17);
|
||||
EXPECT_ERROR(lua.safe_script("v2, len = v.normalize()"), "value is not a valid userdata"); // checks that it doesn't segfault
|
||||
lua.safe_script("v2, len = v:normalize()");
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return len").get<float>(), 5);
|
||||
EXPECT_TRUE(lua.safe_script("return v2 == util.vector2(3/5, 4/5)").get<bool>());
|
||||
lua.safe_script("_, len = util.vector2(0, 0):normalize()");
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return len").get<float>(), 0);
|
||||
}
|
||||
|
||||
TEST(LuaUtilPackageTest, Vector3)
|
||||
{
|
||||
sol::state lua;
|
||||
lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string);
|
||||
lua["util"] = LuaUtil::initUtilPackage(lua);
|
||||
lua.safe_script("v = util.vector3(5, 12, 13)");
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return v.x").get<float>(), 5);
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return v.y").get<float>(), 12);
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return v.z").get<float>(), 13);
|
||||
EXPECT_EQ(lua.safe_script("return tostring(v)").get<std::string>(), "(5, 12, 13)");
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return util.vector3(4, 0, 3):length()").get<float>(), 5);
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return util.vector3(4, 0, 3):length2()").get<float>(), 25);
|
||||
EXPECT_FALSE(lua.safe_script("return util.vector3(1, 2, 3) == util.vector3(1, 3, 2)").get<bool>());
|
||||
EXPECT_TRUE(lua.safe_script("return util.vector3(1, 2, 3) + util.vector3(2, 5, 1) == util.vector3(3, 7, 4)").get<bool>());
|
||||
EXPECT_TRUE(lua.safe_script("return util.vector3(1, 2, 3) - util.vector3(2, 5, 1) == -util.vector3(1, 3, -2)").get<bool>());
|
||||
EXPECT_TRUE(lua.safe_script("return util.vector3(1, 2, 3) == util.vector3(2, 4, 6) / 2").get<bool>());
|
||||
EXPECT_TRUE(lua.safe_script("return util.vector3(1, 2, 3) * 2 == util.vector3(2, 4, 6)").get<bool>());
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return util.vector3(3, 2, 1) * v").get<float>(), 5*3 + 12*2 + 13*1);
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return util.vector3(3, 2, 1):dot(v)").get<float>(), 5*3 + 12*2 + 13*1);
|
||||
EXPECT_TRUE(lua.safe_script("return util.vector3(1, 0, 0) ^ util.vector3(0, 1, 0) == util.vector3(0, 0, 1)").get<bool>());
|
||||
EXPECT_ERROR(lua.safe_script("v2, len = util.vector3(3, 4, 0).normalize()"), "value is not a valid userdata");
|
||||
lua.safe_script("v2, len = util.vector3(3, 4, 0):normalize()");
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return len").get<float>(), 5);
|
||||
EXPECT_TRUE(lua.safe_script("return v2 == util.vector3(3/5, 4/5, 0)").get<bool>());
|
||||
lua.safe_script("_, len = util.vector3(0, 0, 0):normalize()");
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return len").get<float>(), 0);
|
||||
}
|
||||
|
||||
TEST(LuaUtilPackageTest, UtilityFunctions)
|
||||
{
|
||||
sol::state lua;
|
||||
lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string);
|
||||
lua["util"] = LuaUtil::initUtilPackage(lua);
|
||||
lua.safe_script("v = util.vector2(1, 0):rotate(math.rad(120))");
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return v.x").get<float>(), -0.5);
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return v.y").get<float>(), 0.86602539);
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return util.normalizeAngle(math.pi * 10 + 0.1)").get<float>(), 0.1);
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return util.clamp(0.1, 0, 1.5)").get<float>(), 0.1);
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return util.clamp(-0.1, 0, 1.5)").get<float>(), 0);
|
||||
EXPECT_FLOAT_EQ(lua.safe_script("return util.clamp(2.1, 0, 1.5)").get<float>(), 1.5);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
#ifndef LUA_TESTING_UTIL_H
|
||||
#define LUA_TESTING_UTIL_H
|
||||
|
||||
#include <sstream>
|
||||
|
||||
#include <components/vfs/archive.hpp>
|
||||
#include <components/vfs/manager.hpp>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
class TestFile : public VFS::File
|
||||
{
|
||||
public:
|
||||
explicit TestFile(std::string content) : mContent(std::move(content)) {}
|
||||
|
||||
Files::IStreamPtr open() override
|
||||
{
|
||||
return std::make_shared<std::stringstream>(mContent, std::ios_base::in);
|
||||
}
|
||||
|
||||
private:
|
||||
const std::string mContent;
|
||||
};
|
||||
|
||||
struct TestData : public VFS::Archive
|
||||
{
|
||||
std::map<std::string, VFS::File*> mFiles;
|
||||
|
||||
TestData(std::map<std::string, VFS::File*> files) : mFiles(std::move(files)) {}
|
||||
|
||||
void listResources(std::map<std::string, VFS::File*>& out, char (*normalize_function) (char)) override
|
||||
{
|
||||
out = mFiles;
|
||||
}
|
||||
|
||||
bool contains(const std::string& file, char (*normalize_function) (char)) const override
|
||||
{
|
||||
return mFiles.count(file) != 0;
|
||||
}
|
||||
|
||||
std::string getDescription() const override { return "TestData"; }
|
||||
|
||||
};
|
||||
|
||||
inline std::unique_ptr<VFS::Manager> createTestVFS(std::map<std::string, VFS::File*> files)
|
||||
{
|
||||
auto vfs = std::make_unique<VFS::Manager>(true);
|
||||
vfs->addArchive(new TestData(std::move(files)));
|
||||
vfs->buildIndex();
|
||||
return vfs;
|
||||
}
|
||||
|
||||
#define EXPECT_ERROR(X, ERR_SUBSTR) try { X; FAIL() << "Expected error"; } \
|
||||
catch (std::exception& e) { EXPECT_THAT(e.what(), HasSubstr(ERR_SUBSTR)); }
|
||||
|
||||
}
|
||||
|
||||
#endif // LUA_TESTING_UTIL_H
|
@ -0,0 +1,14 @@
|
||||
# Once found, defines:
|
||||
# LuaJit_FOUND
|
||||
# LuaJit_INCLUDE_DIR
|
||||
# LuaJit_LIBRARIES
|
||||
|
||||
include(LibFindMacros)
|
||||
|
||||
libfind_pkg_detect(LuaJit luajit
|
||||
FIND_PATH luajit.h PATH_SUFFIXES luajit luajit-2.1
|
||||
FIND_LIBRARY luajit-5.1 luajit
|
||||
)
|
||||
|
||||
libfind_process(LuaJit)
|
||||
|
@ -0,0 +1,80 @@
|
||||
#include "luascripts.hpp"
|
||||
|
||||
#include "esmreader.hpp"
|
||||
#include "esmwriter.hpp"
|
||||
|
||||
// List of all records, that are related to Lua.
|
||||
//
|
||||
// Record:
|
||||
// LUAM - MWLua::LuaManager
|
||||
//
|
||||
// Subrecords:
|
||||
// LUAW - Start of MWLua::WorldView data
|
||||
// LUAE - Start of MWLua::LocalEvent or MWLua::GlobalEvent (eventName)
|
||||
// LUAS - Start LuaUtil::ScriptsContainer data (scriptName)
|
||||
// LUAD - Serialized Lua variable
|
||||
// LUAT - MWLua::ScriptsContainer::Timer
|
||||
// LUAC - Name of a timer callback (string)
|
||||
|
||||
void ESM::saveLuaBinaryData(ESMWriter& esm, const std::string& data)
|
||||
{
|
||||
if (data.empty())
|
||||
return;
|
||||
esm.startSubRecord("LUAD");
|
||||
esm.write(data.data(), data.size());
|
||||
esm.endRecord("LUAD");
|
||||
}
|
||||
|
||||
std::string ESM::loadLuaBinaryData(ESMReader& esm)
|
||||
{
|
||||
std::string data;
|
||||
if (esm.isNextSub("LUAD"))
|
||||
{
|
||||
esm.getSubHeader();
|
||||
data.resize(esm.getSubSize());
|
||||
esm.getExact(data.data(), data.size());
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
void ESM::LuaScripts::load(ESMReader& esm)
|
||||
{
|
||||
while (esm.isNextSub("LUAS"))
|
||||
{
|
||||
std::string name = esm.getHString();
|
||||
std::string data = loadLuaBinaryData(esm);
|
||||
std::vector<LuaTimer> timers;
|
||||
while (esm.isNextSub("LUAT"))
|
||||
{
|
||||
esm.getSubHeader();
|
||||
LuaTimer timer;
|
||||
esm.getT(timer.mUnit);
|
||||
esm.getT(timer.mTime);
|
||||
timer.mCallbackName = esm.getHNString("LUAC");
|
||||
timer.mCallbackArgument = loadLuaBinaryData(esm);
|
||||
timers.push_back(std::move(timer));
|
||||
}
|
||||
mScripts.push_back({std::move(name), std::move(data), std::move(timers)});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
for (const LuaTimer& timer : script.mTimers)
|
||||
{
|
||||
esm.startSubRecord("LUAT");
|
||||
esm.writeT(timer.mUnit);
|
||||
esm.writeT(timer.mTime);
|
||||
esm.endRecord("LUAT");
|
||||
esm.writeHNString("LUAC", timer.mCallbackName);
|
||||
if (!timer.mCallbackArgument.empty())
|
||||
saveLuaBinaryData(esm, timer.mCallbackArgument);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
#ifndef OPENMW_ESM_LUASCRIPTS_H
|
||||
#define OPENMW_ESM_LUASCRIPTS_H
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace ESM
|
||||
{
|
||||
class ESMReader;
|
||||
class ESMWriter;
|
||||
|
||||
// Storage structure for LuaUtil::ScriptsContainer. This is not a top-level record.
|
||||
// Used either for global scripts or for local scripts on a specific object.
|
||||
|
||||
struct LuaTimer
|
||||
{
|
||||
enum class TimeUnit : bool
|
||||
{
|
||||
SECONDS = 0,
|
||||
HOURS = 1,
|
||||
};
|
||||
|
||||
TimeUnit mUnit;
|
||||
double mTime;
|
||||
std::string mCallbackName;
|
||||
std::string mCallbackArgument; // Serialized Lua table. It is a binary data. Can contain '\0'.
|
||||
};
|
||||
|
||||
struct LuaScript
|
||||
{
|
||||
std::string mScriptPath;
|
||||
std::string mData; // Serialized Lua table. It is a binary data. Can contain '\0'.
|
||||
std::vector<LuaTimer> mTimers;
|
||||
};
|
||||
|
||||
struct LuaScripts
|
||||
{
|
||||
std::vector<LuaScript> mScripts;
|
||||
|
||||
void load (ESMReader &esm);
|
||||
void save (ESMWriter &esm) const;
|
||||
};
|
||||
|
||||
// Saves binary string `data` (can contain '\0') as record LUAD.
|
||||
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.
|
||||
std::string loadLuaBinaryData(ESM::ESMReader& esm);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
@ -0,0 +1,167 @@
|
||||
#include "luastate.hpp"
|
||||
|
||||
#ifndef NO_LUAJIT
|
||||
#include <luajit.h>
|
||||
#endif // NO_LUAJIT
|
||||
|
||||
#include <components/debug/debuglog.hpp>
|
||||
|
||||
namespace LuaUtil
|
||||
{
|
||||
|
||||
static std::string packageNameToPath(std::string_view packageName)
|
||||
{
|
||||
std::string res(packageName);
|
||||
std::replace(res.begin(), res.end(), '.', '/');
|
||||
res.append(".lua");
|
||||
return res;
|
||||
}
|
||||
|
||||
static const std::string safeFunctions[] = {
|
||||
"assert", "error", "ipairs", "next", "pairs", "pcall", "select", "tonumber", "tostring",
|
||||
"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)
|
||||
{
|
||||
mLua.open_libraries(sol::lib::base, sol::lib::coroutine, sol::lib::math, sol::lib::string, sol::lib::table);
|
||||
|
||||
mLua["math"]["randomseed"](static_cast<unsigned>(std::time(nullptr)));
|
||||
mLua["math"]["randomseed"] = sol::nil;
|
||||
|
||||
mLua["writeToLog"] = [](std::string_view s) { Log(Debug::Level::Info) << s; };
|
||||
mLua.script(R"(printToLog = function(name, ...)
|
||||
local msg = name
|
||||
for _, v in ipairs({...}) do
|
||||
msg = msg .. '\t' .. tostring(v)
|
||||
end
|
||||
return writeToLog(msg)
|
||||
end)");
|
||||
mLua.script("printGen = function(name) return function(...) return printToLog(name, ...) end end");
|
||||
|
||||
// Some fixes for compatibility between different Lua versions
|
||||
if (mLua["unpack"] == sol::nil)
|
||||
mLua["unpack"] = mLua["table"]["unpack"];
|
||||
else if (mLua["table"]["unpack"] == sol::nil)
|
||||
mLua["table"]["unpack"] = mLua["unpack"];
|
||||
|
||||
mSandboxEnv = sol::table(mLua, sol::create);
|
||||
mSandboxEnv["_VERSION"] = mLua["_VERSION"];
|
||||
for (const std::string& s : safeFunctions)
|
||||
{
|
||||
if (mLua[s] == sol::nil) throw std::logic_error("Lua function not found: " + s);
|
||||
mSandboxEnv[s] = mLua[s];
|
||||
}
|
||||
for (const std::string& s : safePackages)
|
||||
{
|
||||
if (mLua[s] == sol::nil) throw std::logic_error("Lua package not found: " + s);
|
||||
mCommonPackages[s] = mSandboxEnv[s] = makeReadOnly(mLua[s]);
|
||||
}
|
||||
}
|
||||
|
||||
LuaState::~LuaState()
|
||||
{
|
||||
// Should be cleaned before destructing mLua.
|
||||
mCommonPackages.clear();
|
||||
mSandboxEnv = sol::nil;
|
||||
}
|
||||
|
||||
sol::table LuaState::makeReadOnly(sol::table table)
|
||||
{
|
||||
if (table.is<sol::userdata>())
|
||||
return table; // it is already userdata, no sense to wrap it again
|
||||
|
||||
table[sol::meta_function::index] = table;
|
||||
sol::stack::push(mLua, std::move(table));
|
||||
lua_newuserdata(mLua, 0);
|
||||
lua_pushvalue(mLua, -2);
|
||||
lua_setmetatable(mLua, -2);
|
||||
return sol::stack::pop<sol::table>(mLua);
|
||||
}
|
||||
|
||||
sol::table LuaState::getMutableFromReadOnly(const sol::userdata& ro)
|
||||
{
|
||||
sol::stack::push(mLua, ro);
|
||||
lua_getmetatable(mLua, -1);
|
||||
sol::table res = sol::stack::pop<sol::table>(mLua);
|
||||
lua_pop(mLua, 1);
|
||||
return res;
|
||||
}
|
||||
|
||||
void LuaState::addCommonPackage(const std::string& packageName, const sol::object& package)
|
||||
{
|
||||
if (package.is<sol::function>())
|
||||
mCommonPackages[packageName] = package;
|
||||
else
|
||||
mCommonPackages[packageName] = makeReadOnly(package);
|
||||
}
|
||||
|
||||
sol::protected_function_result LuaState::runInNewSandbox(
|
||||
const std::string& path, const std::string& namePrefix,
|
||||
const std::map<std::string, sol::object>& packages, const sol::object& hiddenData)
|
||||
{
|
||||
sol::protected_function script = loadScript(path);
|
||||
|
||||
sol::environment env(mLua, sol::create, mSandboxEnv);
|
||||
std::string envName = namePrefix + "[" + path + "]:";
|
||||
env["print"] = mLua["printGen"](envName);
|
||||
|
||||
sol::table loaded(mLua, sol::create);
|
||||
for (const auto& [key, value] : mCommonPackages)
|
||||
loaded[key] = value;
|
||||
for (const auto& [key, value] : packages)
|
||||
loaded[key] = value;
|
||||
env["require"] = [this, env, loaded, hiddenData](std::string_view packageName)
|
||||
{
|
||||
sol::table packages = loaded;
|
||||
sol::object package = packages[packageName];
|
||||
if (package == sol::nil)
|
||||
{
|
||||
sol::protected_function packageLoader = loadScript(packageNameToPath(packageName));
|
||||
sol::set_environment(env, packageLoader);
|
||||
package = throwIfError(packageLoader());
|
||||
if (!package.is<sol::table>())
|
||||
throw std::runtime_error("Lua package must return a table.");
|
||||
packages[packageName] = package;
|
||||
}
|
||||
else if (package.is<sol::function>())
|
||||
package = packages[packageName] = call(package.as<sol::protected_function>(), hiddenData);
|
||||
return package;
|
||||
};
|
||||
|
||||
sol::set_environment(env, script);
|
||||
return call(script);
|
||||
}
|
||||
|
||||
sol::protected_function_result LuaState::throwIfError(sol::protected_function_result&& res)
|
||||
{
|
||||
if (!res.valid() && static_cast<int>(res.get_type()) == LUA_TSTRING)
|
||||
throw std::runtime_error("Lua error: " + res.get<std::string>());
|
||||
else
|
||||
return std::move(res);
|
||||
}
|
||||
|
||||
sol::protected_function LuaState::loadScript(const std::string& path)
|
||||
{
|
||||
auto iter = mCompiledScripts.find(path);
|
||||
if (iter != mCompiledScripts.end())
|
||||
return mLua.load(iter->second.as_string_view(), path, sol::load_mode::binary);
|
||||
|
||||
std::string fileContent(std::istreambuf_iterator<char>(*mVFS->get(path)), {});
|
||||
sol::load_result res = mLua.load(fileContent, path, sol::load_mode::text);
|
||||
if (!res.valid())
|
||||
throw std::runtime_error("Lua error: " + res.get<std::string>());
|
||||
mCompiledScripts[path] = res.get<sol::function>().dump();
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string getLuaVersion()
|
||||
{
|
||||
#ifdef NO_LUAJIT
|
||||
return LUA_RELEASE;
|
||||
#else
|
||||
return LUA_RELEASE " (" LUAJIT_VERSION ")";
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
#ifndef COMPONENTS_LUA_LUASTATE_H
|
||||
#define COMPONENTS_LUA_LUASTATE_H
|
||||
|
||||
#include <map>
|
||||
|
||||
#include <sol/sol.hpp>
|
||||
|
||||
#include <components/vfs/manager.hpp>
|
||||
|
||||
namespace LuaUtil
|
||||
{
|
||||
|
||||
std::string getLuaVersion();
|
||||
|
||||
// Holds Lua state.
|
||||
// Provides additional features:
|
||||
// - Load scripts from the virtual filesystem;
|
||||
// - Caching of loaded scripts;
|
||||
// - Disable unsafe Lua functions;
|
||||
// - Run every instance of every script in a separate sandbox;
|
||||
// - Forbid any interactions between sandboxes except than via provided API;
|
||||
// - 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;
|
||||
class LuaState
|
||||
{
|
||||
public:
|
||||
explicit LuaState(const VFS::Manager* vfs);
|
||||
~LuaState();
|
||||
|
||||
// Returns underlying sol::state.
|
||||
sol::state& sol() { return mLua; }
|
||||
|
||||
// A shortcut to create a new Lua table.
|
||||
sol::table newTable() { return sol::table(mLua, sol::create); }
|
||||
|
||||
// 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.
|
||||
sol::table makeReadOnly(sol::table);
|
||||
sol::table getMutableFromReadOnly(const sol::userdata&);
|
||||
|
||||
// Registers a package that will be available from every sandbox via `require(name)`.
|
||||
// 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);
|
||||
|
||||
// Creates a new sandbox, runs a script, and returns the result
|
||||
// (the result is expected to be an interface of the script).
|
||||
// Args:
|
||||
// path: path to the script in the virtual filesystem;
|
||||
// namePrefix: sandbox name will be "<namePrefix>[<filePath>]". Sandbox name
|
||||
// will be added to every `print` output.
|
||||
// packages: additional packages that should be available from the sandbox via `require`. Each package
|
||||
// should be either a sol::table or a sol::function. If it is a function, it will be evaluated
|
||||
// (once per sandbox) with the argument 'hiddenData' the first time when requested.
|
||||
sol::protected_function_result runInNewSandbox(const std::string& path,
|
||||
const std::string& namePrefix = "",
|
||||
const std::map<std::string, sol::object>& packages = {},
|
||||
const sol::object& hiddenData = sol::nil);
|
||||
|
||||
void dropScriptCache() { mCompiledScripts.clear(); }
|
||||
|
||||
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);
|
||||
|
||||
sol::protected_function loadScript(const std::string& path);
|
||||
|
||||
sol::state mLua;
|
||||
sol::table mSandboxEnv;
|
||||
std::map<std::string, sol::bytecode> mCompiledScripts;
|
||||
std::map<std::string, sol::object> mCommonPackages;
|
||||
const VFS::Manager* mVFS;
|
||||
};
|
||||
|
||||
// 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)
|
||||
{
|
||||
try
|
||||
{
|
||||
return LuaState::throwIfError(fn(std::forward<Args>(args)...));
|
||||
}
|
||||
catch (std::exception&) { throw; }
|
||||
catch (...) { throw std::runtime_error("Unknown error"); }
|
||||
}
|
||||
|
||||
// getFieldOrNil(table, "a", "b", "c") returns table["a"]["b"]["c"] or nil if some of the fields doesn't exist.
|
||||
template <class... Str>
|
||||
sol::object getFieldOrNil(const sol::object& table, std::string_view first, const Str&... str)
|
||||
{
|
||||
if (!table.is<sol::table>())
|
||||
return sol::nil;
|
||||
if constexpr (sizeof...(str) == 0)
|
||||
return table.as<sol::table>()[first];
|
||||
else
|
||||
return getFieldOrNil(table.as<sol::table>()[first], str...);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif // COMPONENTS_LUA_LUASTATE_H
|
@ -0,0 +1,44 @@
|
||||
#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;
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
#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
|
@ -0,0 +1,428 @@
|
||||
#include "scriptscontainer.hpp"
|
||||
|
||||
#include <components/esm/luascripts.hpp>
|
||||
|
||||
namespace LuaUtil
|
||||
{
|
||||
static constexpr std::string_view ENGINE_HANDLERS = "engineHandlers";
|
||||
static constexpr std::string_view EVENT_HANDLERS = "eventHandlers";
|
||||
|
||||
static constexpr std::string_view INTERFACE_NAME = "interfaceName";
|
||||
static constexpr std::string_view INTERFACE = "interface";
|
||||
|
||||
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";
|
||||
|
||||
ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix) : mNamePrefix(namePrefix), mLua(*lua)
|
||||
{
|
||||
registerEngineHandlers({&mUpdateHandlers});
|
||||
mPublicInterfaces = sol::table(lua->sol(), sol::create);
|
||||
addPackage("openmw.interfaces", mPublicInterfaces);
|
||||
}
|
||||
|
||||
void ScriptsContainer::addPackage(const std::string& packageName, sol::object package)
|
||||
{
|
||||
API[packageName] = mLua.makeReadOnly(std::move(package));
|
||||
}
|
||||
|
||||
bool ScriptsContainer::addNewScript(const std::string& path)
|
||||
{
|
||||
if (mScripts.count(path) != 0)
|
||||
return false; // already present
|
||||
|
||||
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)
|
||||
{
|
||||
for (auto& [key, value] : sol::table(script))
|
||||
{
|
||||
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>();
|
||||
else
|
||||
Log(Debug::Error) << "Not supported section '" << sectionName << "' in " << mNamePrefix << "[" << path << "]";
|
||||
}
|
||||
}
|
||||
if (interfaceName.empty() != (publicInterface == sol::nil))
|
||||
Log(Debug::Error) << mNamePrefix << "[" << path << "]: 'interfaceName' should always be used together with 'interface'";
|
||||
else if (!interfaceName.empty())
|
||||
script.as<sol::table>()[INTERFACE] = mPublicInterfaces[interfaceName] = mLua.makeReadOnly(publicInterface);
|
||||
mScriptOrder.push_back(path);
|
||||
mScripts[path].mInterface = std::move(script);
|
||||
return true;
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
mScripts.erase(path);
|
||||
Log(Debug::Error) << "Can't start " << mNamePrefix << "[" << path << "]; " << e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ScriptsContainer::removeScript(const std::string& path)
|
||||
{
|
||||
auto scriptIter = mScripts.find(path);
|
||||
if (scriptIter == mScripts.end())
|
||||
return false; // no such script
|
||||
sol::object& script = scriptIter->second.mInterface;
|
||||
if (getFieldOrNil(script, INTERFACE_NAME) != sol::nil)
|
||||
{
|
||||
std::string_view interfaceName = getFieldOrNil(script, INTERFACE_NAME).as<std::string_view>();
|
||||
if (mPublicInterfaces[interfaceName] == getFieldOrNil(script, INTERFACE))
|
||||
{
|
||||
mPublicInterfaces[interfaceName] = sol::nil;
|
||||
auto prevIt = mScriptOrder.rbegin();
|
||||
while (*prevIt != path)
|
||||
prevIt++;
|
||||
prevIt++;
|
||||
while (prevIt != mScriptOrder.rend())
|
||||
{
|
||||
sol::object& prevScript = mScripts[*(prevIt++)].mInterface;
|
||||
sol::object prevInterfaceName = getFieldOrNil(prevScript, INTERFACE_NAME);
|
||||
if (prevInterfaceName != sol::nil && prevInterfaceName.as<std::string_view>() == interfaceName)
|
||||
{
|
||||
mPublicInterfaces[interfaceName] = getFieldOrNil(prevScript, INTERFACE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sol::object engineHandlers = getFieldOrNil(script, ENGINE_HANDLERS);
|
||||
if (engineHandlers != sol::nil)
|
||||
{
|
||||
for (auto& [key, value] : sol::table(engineHandlers))
|
||||
{
|
||||
std::string_view handlerName = key.as<std::string_view>();
|
||||
auto handlerIter = mEngineHandlers.find(handlerName);
|
||||
if (handlerIter == mEngineHandlers.end())
|
||||
continue;
|
||||
std::vector<sol::protected_function>& list = handlerIter->second->mList;
|
||||
list.erase(std::find(list.begin(), list.end(), value.as<sol::protected_function>()));
|
||||
}
|
||||
}
|
||||
sol::object eventHandlers = getFieldOrNil(script, EVENT_HANDLERS);
|
||||
if (eventHandlers != sol::nil)
|
||||
{
|
||||
for (auto& [key, value] : sol::table(eventHandlers))
|
||||
{
|
||||
EventHandlerList& list = mEventHandlers.find(key.as<std::string_view>())->second;
|
||||
list.erase(std::find(list.begin(), list.end(), value.as<sol::protected_function>()));
|
||||
}
|
||||
}
|
||||
mScripts.erase(scriptIter);
|
||||
mScriptOrder.erase(std::find(mScriptOrder.begin(), mScriptOrder.end(), path));
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScriptsContainer::parseEventHandlers(sol::table handlers, std::string_view scriptPath)
|
||||
{
|
||||
for (auto& [key, value] : handlers)
|
||||
{
|
||||
std::string_view eventName = key.as<std::string_view>();
|
||||
auto it = mEventHandlers.find(eventName);
|
||||
if (it == mEventHandlers.end())
|
||||
it = mEventHandlers.insert({std::string(eventName), EventHandlerList()}).first;
|
||||
it->second.push_back(value);
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptsContainer::parseEngineHandlers(sol::table handlers, std::string_view scriptPath)
|
||||
{
|
||||
for (auto& [key, value] : handlers)
|
||||
{
|
||||
std::string_view handlerName = key.as<std::string_view>();
|
||||
if (handlerName == HANDLER_LOAD || handlerName == HANDLER_SAVE)
|
||||
continue; // save and load are handled separately
|
||||
auto it = mEngineHandlers.find(handlerName);
|
||||
if (it == mEngineHandlers.end())
|
||||
Log(Debug::Error) << "Not supported handler '" << handlerName << "' in " << mNamePrefix << "[" << scriptPath << "]";
|
||||
else
|
||||
it->second->mList.push_back(value);
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptsContainer::receiveEvent(std::string_view eventName, std::string_view eventData)
|
||||
{
|
||||
auto it = mEventHandlers.find(eventName);
|
||||
if (it == mEventHandlers.end())
|
||||
{
|
||||
Log(Debug::Warning) << mNamePrefix << " has received event '" << eventName << "', but there are no handlers for this event";
|
||||
return;
|
||||
}
|
||||
sol::object data;
|
||||
try
|
||||
{
|
||||
data = LuaUtil::deserialize(mLua.sol(), eventData, mSerializer);
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Log(Debug::Error) << mNamePrefix << " can not parse eventData for '" << eventName << "': " << e.what();
|
||||
return;
|
||||
}
|
||||
EventHandlerList& list = it->second;
|
||||
for (int i = list.size() - 1; i >= 0; --i)
|
||||
{
|
||||
try
|
||||
{
|
||||
sol::object res = LuaUtil::call(list[i], 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptsContainer::registerEngineHandlers(std::initializer_list<EngineHandlerList*> handlers)
|
||||
{
|
||||
for (EngineHandlerList* h : handlers)
|
||||
mEngineHandlers[h->mName] = h;
|
||||
}
|
||||
|
||||
void ScriptsContainer::save(ESM::LuaScripts& data)
|
||||
{
|
||||
std::map<std::string, std::vector<ESM::LuaTimer>> timers;
|
||||
auto saveTimerFn = [&](const Timer& timer, TimeUnit timeUnit)
|
||||
{
|
||||
if (!timer.mSerializable)
|
||||
return;
|
||||
ESM::LuaTimer savedTimer;
|
||||
savedTimer.mTime = timer.mTime;
|
||||
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));
|
||||
};
|
||||
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)
|
||||
{
|
||||
ESM::LuaScript savedScript;
|
||||
savedScript.mScriptPath = path;
|
||||
sol::object handler = getFieldOrNil(mScripts[path].mInterface, ENGINE_HANDLERS, HANDLER_SAVE);
|
||||
if (handler != sol::nil)
|
||||
{
|
||||
try
|
||||
{
|
||||
sol::object state = LuaUtil::call(handler);
|
||||
savedScript.mData = serialize(state, mSerializer);
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Log(Debug::Error) << mNamePrefix << "[" << path << "] onSave failed: " << e.what();
|
||||
}
|
||||
}
|
||||
auto timersIt = timers.find(path);
|
||||
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)
|
||||
{
|
||||
std::map<std::string, Script> scriptsWithoutSavedData;
|
||||
if (resetScriptList)
|
||||
{
|
||||
removeAllScripts();
|
||||
for (const ESM::LuaScript& script : data.mScripts)
|
||||
addNewScript(script.mScriptPath);
|
||||
}
|
||||
else
|
||||
scriptsWithoutSavedData = mScripts;
|
||||
mSecondsTimersQueue.clear();
|
||||
mHoursTimersQueue.clear();
|
||||
for (const ESM::LuaScript& script : data.mScripts)
|
||||
{
|
||||
auto iter = mScripts.find(script.mScriptPath);
|
||||
if (iter == mScripts.end())
|
||||
continue;
|
||||
scriptsWithoutSavedData.erase(iter->first);
|
||||
iter->second.mHiddenData.get<sol::table>(TEMPORARY_TIMER_CALLBACKS).clear();
|
||||
try
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Log(Debug::Error) << mNamePrefix << "[" << script.mScriptPath << "] onLoad failed: " << e.what();
|
||||
}
|
||||
for (const ESM::LuaTimer& savedTimer : script.mTimers)
|
||||
{
|
||||
Timer timer;
|
||||
timer.mCallback = savedTimer.mCallbackName;
|
||||
timer.mSerializable = true;
|
||||
timer.mScript = script.mScriptPath;
|
||||
timer.mTime = savedTimer.mTime;
|
||||
|
||||
try
|
||||
{
|
||||
timer.mArg = deserialize(mLua.sol(), savedTimer.mCallbackArgument, mSerializer);
|
||||
// It is important if the order of content files was changed. The deserialize-serialize procedure
|
||||
// updates refnums, so timer.mSerializedArg may be not equal to savedTimer.mCallbackArgument.
|
||||
timer.mSerializedArg = serialize(timer.mArg, mSerializer);
|
||||
|
||||
if (savedTimer.mUnit == TimeUnit::HOURS)
|
||||
mHoursTimersQueue.push_back(std::move(timer));
|
||||
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();
|
||||
}
|
||||
}
|
||||
std::make_heap(mSecondsTimersQueue.begin(), mSecondsTimersQueue.end());
|
||||
std::make_heap(mHoursTimersQueue.begin(), mHoursTimersQueue.end());
|
||||
}
|
||||
|
||||
void ScriptsContainer::removeAllScripts()
|
||||
{
|
||||
mScripts.clear();
|
||||
mScriptOrder.clear();
|
||||
for (auto& [_, handlers] : mEngineHandlers)
|
||||
handlers->mList.clear();
|
||||
mEventHandlers.clear();
|
||||
mSecondsTimersQueue.clear();
|
||||
mHoursTimersQueue.clear();
|
||||
|
||||
mPublicInterfaces.clear();
|
||||
// Assigned by mLua.makeReadOnly, but `clear` removes it, so we need to assign it again.
|
||||
mPublicInterfaces[sol::meta_function::index] = mPublicInterfaces;
|
||||
}
|
||||
|
||||
sol::table ScriptsContainer::getHiddenData(const std::string& scriptPath)
|
||||
{
|
||||
auto it = mScripts.find(scriptPath);
|
||||
if (it == mScripts.end())
|
||||
throw std::logic_error("ScriptsContainer::getHiddenData: script doesn't exist");
|
||||
return it->second.mHiddenData;
|
||||
}
|
||||
|
||||
void ScriptsContainer::registerTimerCallback(const std::string& scriptPath, std::string_view callbackName, sol::function callback)
|
||||
{
|
||||
getHiddenData(scriptPath)[REGISTERED_TIMER_CALLBACKS][callbackName] = std::move(callback);
|
||||
}
|
||||
|
||||
void ScriptsContainer::insertTimer(std::vector<Timer>& timerQueue, Timer&& t)
|
||||
{
|
||||
timerQueue.push_back(std::move(t));
|
||||
std::push_heap(timerQueue.begin(), timerQueue.end());
|
||||
}
|
||||
|
||||
void ScriptsContainer::setupSerializableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath,
|
||||
std::string_view callbackName, sol::object callbackArg)
|
||||
{
|
||||
Timer t;
|
||||
t.mCallback = std::string(callbackName);
|
||||
t.mScript = scriptPath;
|
||||
t.mSerializable = true;
|
||||
t.mTime = time;
|
||||
t.mArg = callbackArg;
|
||||
t.mSerializedArg = serialize(t.mArg, mSerializer);
|
||||
insertTimer(timeUnit == TimeUnit::HOURS ? mHoursTimersQueue : mSecondsTimersQueue, std::move(t));
|
||||
}
|
||||
|
||||
void ScriptsContainer::setupUnsavableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, sol::function callback)
|
||||
{
|
||||
Timer t;
|
||||
t.mScript = scriptPath;
|
||||
t.mSerializable = false;
|
||||
t.mTime = time;
|
||||
|
||||
t.mCallback = mTemporaryCallbackCounter;
|
||||
getHiddenData(scriptPath)[TEMPORARY_TIMER_CALLBACKS][mTemporaryCallbackCounter] = std::move(callback);
|
||||
mTemporaryCallbackCounter++;
|
||||
|
||||
insertTimer(timeUnit == TimeUnit::HOURS ? mHoursTimersQueue : mSecondsTimersQueue, std::move(t));
|
||||
}
|
||||
|
||||
void ScriptsContainer::callTimer(const Timer& t)
|
||||
{
|
||||
try
|
||||
{
|
||||
sol::table data = getHiddenData(t.mScript);
|
||||
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>())
|
||||
throw std::logic_error("Callback '" + callbackName + "' doesn't exist");
|
||||
LuaUtil::call(callback, 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;
|
||||
}
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Log(Debug::Error) << mNamePrefix << "[" << t.mScript << "] callTimer failed: " << e.what();
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptsContainer::updateTimerQueue(std::vector<Timer>& timerQueue, double time)
|
||||
{
|
||||
while (!timerQueue.empty() && timerQueue.front().mTime <= time)
|
||||
{
|
||||
callTimer(timerQueue.front());
|
||||
std::pop_heap(timerQueue.begin(), timerQueue.end());
|
||||
timerQueue.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptsContainer::processTimers(double gameSeconds, double gameHours)
|
||||
{
|
||||
updateTimerQueue(mSecondsTimersQueue, gameSeconds);
|
||||
updateTimerQueue(mHoursTimersQueue, gameHours);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,213 @@
|
||||
#ifndef COMPONENTS_LUA_SCRIPTSCONTAINER_H
|
||||
#define COMPONENTS_LUA_SCRIPTSCONTAINER_H
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
#include <components/debug/debuglog.hpp>
|
||||
#include <components/esm/luascripts.hpp>
|
||||
|
||||
#include "luastate.hpp"
|
||||
#include "serialization.hpp"
|
||||
|
||||
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).
|
||||
// All scripts from one container have the same set of API packages available.
|
||||
//
|
||||
// Each script should return a table in a specific format that describes its
|
||||
// handlers and interfaces. Every section of the table is optional. Basic structure:
|
||||
//
|
||||
// local function update(dt)
|
||||
// print("Update")
|
||||
// end
|
||||
//
|
||||
// local function someEventHandler(eventData)
|
||||
// print("'SomeEvent' received")
|
||||
// end
|
||||
//
|
||||
// return {
|
||||
// -- Provides interface for other scripts in the same container
|
||||
// interfaceName = "InterfaceName",
|
||||
// interface = {
|
||||
// someFunction = function() print("someFunction was called from another script") end,
|
||||
// },
|
||||
//
|
||||
// -- Script interface for the engine. Not available for other script.
|
||||
// -- An error is printed if unknown handler is specified.
|
||||
// engineHandlers = {
|
||||
// onUpdate = update,
|
||||
// onSave = function() return ... end,
|
||||
// onLoad = function(state) ... 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'
|
||||
// onSomethingElse = function() print("something else") end
|
||||
// },
|
||||
//
|
||||
// -- Handlers for events, sent from other scripts. Engine itself never sent events. Any name can be used for an event.
|
||||
// eventHandlers = {
|
||||
// SomeEvent = someEventHandler
|
||||
// }
|
||||
// }
|
||||
|
||||
class ScriptsContainer
|
||||
{
|
||||
public:
|
||||
struct ScriptId
|
||||
{
|
||||
// ScriptId is stored in hidden data (see getHiddenData) with this key.
|
||||
constexpr static std::string_view KEY = "_id";
|
||||
|
||||
ScriptsContainer* mContainer;
|
||||
std::string mPath;
|
||||
};
|
||||
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);
|
||||
ScriptsContainer(const ScriptsContainer&) = delete;
|
||||
ScriptsContainer(ScriptsContainer&&) = delete;
|
||||
virtual ~ScriptsContainer() {}
|
||||
|
||||
// Adds package that will be available (via `require`) for all scripts in the container.
|
||||
// Automatically applies LuaState::makeReadOnly to the package.
|
||||
void addPackage(const 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);
|
||||
|
||||
// Removes script. Returns `true` if it was successfully removed.
|
||||
bool removeScript(const std::string& path);
|
||||
void removeAllScripts();
|
||||
|
||||
// Processes timers. gameSeconds and gameHours are time (in seconds and in game hours) passed from the game start.
|
||||
void processTimers(double gameSeconds, double gameHours);
|
||||
|
||||
// Calls `onUpdate` (if present) for every script in the container.
|
||||
// Handlers are called in the same order as scripts were added.
|
||||
void update(float dt) { callEngineHandlers(mUpdateHandlers, dt); }
|
||||
|
||||
// Calls event handlers `eventName` (if present) for every script.
|
||||
// If several scripts register handlers for `eventName`, they are called in reverse order.
|
||||
// If some handler returns `false`, all remaining handlers are ignored. Any other return value
|
||||
// (including `nil`) has no effect.
|
||||
void receiveEvent(std::string_view eventName, std::string_view eventData);
|
||||
|
||||
// Serializer defines how to serialize/deserialize userdata. If serializer is not provided,
|
||||
// only built-in types and types from util package can be serialized.
|
||||
void setSerializer(const UserdataSerializer* serializer) { mSerializer = serializer; }
|
||||
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
|
||||
// Sets up a timer, that can be automatically saved and loaded.
|
||||
// timeUnit - game seconds (TimeUnit::Seconds) or game hours (TimeUnit::Hours).
|
||||
// time - the absolute game time (in seconds or in hours) when the timer should be executed.
|
||||
// scriptPath - script path in VFS is used as script id. The script with the given path should already present in the container.
|
||||
// callbackName - callback (should be registered in advance) for this timer.
|
||||
// callbackArg - parameter for the callback (should be serializable).
|
||||
void setupSerializableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath,
|
||||
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);
|
||||
|
||||
protected:
|
||||
struct EngineHandlerList
|
||||
{
|
||||
std::string_view mName;
|
||||
std::vector<sol::protected_function> mList;
|
||||
|
||||
// "name" must be string literal
|
||||
explicit EngineHandlerList(std::string_view name) : mName(name) {}
|
||||
};
|
||||
|
||||
// Calls given handlers in direct order.
|
||||
template <typename... Args>
|
||||
void callEngineHandlers(EngineHandlerList& handlers, const Args&... args)
|
||||
{
|
||||
for (sol::protected_function& handler : handlers.mList)
|
||||
{
|
||||
try { LuaUtil::call(handler, args...); }
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Log(Debug::Error) << mNamePrefix << " " << handlers.mName << " failed. " << e.what();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// To add a new engine handler a derived class should register the corresponding EngineHandlerList and define
|
||||
// a public function (see how ScriptsContainer::update is implemented) that calls `callEngineHandlers`.
|
||||
void registerEngineHandlers(std::initializer_list<EngineHandlerList*> handlers);
|
||||
|
||||
const std::string mNamePrefix;
|
||||
LuaUtil::LuaState& mLua;
|
||||
|
||||
private:
|
||||
struct Script
|
||||
{
|
||||
sol::object mInterface; // returned value of the script (sol::table or nil)
|
||||
sol::table mHiddenData;
|
||||
};
|
||||
struct Timer
|
||||
{
|
||||
double mTime;
|
||||
bool mSerializable;
|
||||
std::string mScript;
|
||||
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>;
|
||||
|
||||
void parseEngineHandlers(sol::table handlers, std::string_view scriptPath);
|
||||
void parseEventHandlers(sol::table handlers, std::string_view scriptPath);
|
||||
|
||||
void callTimer(const Timer& t);
|
||||
void updateTimerQueue(std::vector<Timer>& timerQueue, double time);
|
||||
static void insertTimer(std::vector<Timer>& timerQueue, Timer&& t);
|
||||
|
||||
const UserdataSerializer* mSerializer = nullptr;
|
||||
std::map<std::string, sol::object> API;
|
||||
|
||||
std::vector<std::string> mScriptOrder;
|
||||
std::map<std::string, Script> mScripts;
|
||||
sol::table mPublicInterfaces;
|
||||
|
||||
EngineHandlerList mUpdateHandlers{"onUpdate"};
|
||||
std::map<std::string_view, EngineHandlerList*> mEngineHandlers;
|
||||
std::map<std::string, EventHandlerList, std::less<>> mEventHandlers;
|
||||
|
||||
std::vector<Timer> mSecondsTimersQueue;
|
||||
std::vector<Timer> mHoursTimersQueue;
|
||||
int64_t mTemporaryCallbackCounter = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // COMPONENTS_LUA_SCRIPTSCONTAINER_H
|
@ -0,0 +1,257 @@
|
||||
#include "serialization.hpp"
|
||||
|
||||
#include <osg/Vec2f>
|
||||
#include <osg/Vec3f>
|
||||
|
||||
#include <components/misc/endianness.hpp>
|
||||
|
||||
namespace LuaUtil
|
||||
{
|
||||
|
||||
constexpr unsigned char FORMAT_VERSION = 0;
|
||||
|
||||
enum class SerializedType : char
|
||||
{
|
||||
NUMBER = 0x0,
|
||||
LONG_STRING = 0x1,
|
||||
BOOLEAN = 0x2,
|
||||
TABLE_START = 0x3,
|
||||
TABLE_END = 0x4,
|
||||
|
||||
VEC2 = 0x10,
|
||||
VEC3 = 0x11,
|
||||
|
||||
// All values should be lesser than 0x20 (SHORT_STRING_FLAG).
|
||||
};
|
||||
constexpr unsigned char SHORT_STRING_FLAG = 0x20; // 0b001SSSSS. SSSSS = string length
|
||||
constexpr unsigned char CUSTOM_FULL_FLAG = 0x40; // 0b01TTTTTT + 32bit dataSize
|
||||
constexpr unsigned char CUSTOM_COMPACT_FLAG = 0x80; // 0b1SSSSTTT. SSSS = dataSize, TTT = (typeName size - 1)
|
||||
|
||||
static void appendType(BinaryData& out, SerializedType type)
|
||||
{
|
||||
out.push_back(static_cast<char>(type));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static void appendValue(BinaryData& out, T v)
|
||||
{
|
||||
v = Misc::toLittleEndian(v);
|
||||
out.append(reinterpret_cast<const char*>(&v), sizeof(v));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static T getValue(std::string_view& binaryData)
|
||||
{
|
||||
if (binaryData.size() < sizeof(T))
|
||||
throw std::runtime_error("Unexpected end");
|
||||
T v;
|
||||
std::memcpy(&v, binaryData.data(), sizeof(T));
|
||||
binaryData = binaryData.substr(sizeof(T));
|
||||
return Misc::fromLittleEndian(v);
|
||||
}
|
||||
|
||||
static void appendString(BinaryData& out, std::string_view str)
|
||||
{
|
||||
if (str.size() < 32)
|
||||
out.push_back(SHORT_STRING_FLAG | char(str.size()));
|
||||
else
|
||||
{
|
||||
appendType(out, SerializedType::LONG_STRING);
|
||||
appendValue<uint32_t>(out, str.size());
|
||||
}
|
||||
out.append(str.data(), str.size());
|
||||
}
|
||||
|
||||
static void appendData(BinaryData& out, const void* data, size_t dataSize)
|
||||
{
|
||||
out.append(reinterpret_cast<const char*>(data), dataSize);
|
||||
}
|
||||
|
||||
void UserdataSerializer::append(BinaryData& out, std::string_view typeName, const void* data, size_t dataSize)
|
||||
{
|
||||
assert(!typeName.empty() && typeName.size() <= 64);
|
||||
if (typeName.size() <= 8 && dataSize < 16)
|
||||
{ // Compact form: 0b1SSSSTTT. SSSS = dataSize, TTT = (typeName size - 1).
|
||||
unsigned char t = CUSTOM_COMPACT_FLAG | (dataSize << 3) | (typeName.size() - 1);
|
||||
out.push_back(t);
|
||||
}
|
||||
else
|
||||
{ // Full form: 0b01TTTTTT + 32bit dataSize.
|
||||
unsigned char t = CUSTOM_FULL_FLAG | (typeName.size() - 1);
|
||||
out.push_back(t);
|
||||
appendValue<uint32_t>(out, dataSize);
|
||||
}
|
||||
out.append(typeName.data(), typeName.size());
|
||||
appendData(out, data, dataSize);
|
||||
}
|
||||
|
||||
static void serializeUserdata(BinaryData& out, const sol::userdata& data, const UserdataSerializer* customSerializer)
|
||||
{
|
||||
if (data.is<osg::Vec2f>())
|
||||
{
|
||||
appendType(out, SerializedType::VEC2);
|
||||
osg::Vec2f v = data.as<osg::Vec2f>();
|
||||
appendValue<float>(out, v.x());
|
||||
appendValue<float>(out, v.y());
|
||||
return;
|
||||
}
|
||||
if (data.is<osg::Vec3f>())
|
||||
{
|
||||
appendType(out, SerializedType::VEC3);
|
||||
osg::Vec3f v = data.as<osg::Vec3f>();
|
||||
appendValue<float>(out, v.x());
|
||||
appendValue<float>(out, v.y());
|
||||
appendValue<float>(out, v.z());
|
||||
return;
|
||||
}
|
||||
if (customSerializer && customSerializer->serialize(out, data))
|
||||
return;
|
||||
else
|
||||
throw std::runtime_error("Unknown userdata");
|
||||
}
|
||||
|
||||
static void serialize(BinaryData& out, const sol::object& obj, const UserdataSerializer* customSerializer, int recursionCounter)
|
||||
{
|
||||
if (obj.get_type() == sol::type::lightuserdata)
|
||||
throw std::runtime_error("light userdata is not allowed to be serialized");
|
||||
if (obj.is<sol::function>())
|
||||
throw std::runtime_error("functions are not allowed to be serialized");
|
||||
else if (obj.is<sol::userdata>())
|
||||
serializeUserdata(out, obj, customSerializer);
|
||||
else if (obj.is<sol::lua_table>())
|
||||
{
|
||||
if (recursionCounter >= 32)
|
||||
throw std::runtime_error("Can not serialize more than 32 nested tables. Likely the table contains itself.");
|
||||
sol::table table = obj;
|
||||
appendType(out, SerializedType::TABLE_START);
|
||||
for (auto& [key, value] : table)
|
||||
{
|
||||
serialize(out, key, customSerializer, recursionCounter + 1);
|
||||
serialize(out, value, customSerializer, recursionCounter + 1);
|
||||
}
|
||||
appendType(out, SerializedType::TABLE_END);
|
||||
}
|
||||
else if (obj.is<double>())
|
||||
{
|
||||
appendType(out, SerializedType::NUMBER);
|
||||
appendValue<double>(out, obj.as<double>());
|
||||
}
|
||||
else if (obj.is<std::string_view>())
|
||||
appendString(out, obj.as<std::string_view>());
|
||||
else if (obj.is<bool>())
|
||||
{
|
||||
char v = obj.as<bool>() ? 1 : 0;
|
||||
appendType(out, SerializedType::BOOLEAN);
|
||||
out.push_back(v);
|
||||
} else
|
||||
throw std::runtime_error("Unknown lua type");
|
||||
}
|
||||
|
||||
static void deserializeImpl(sol::state& lua, std::string_view& binaryData, const UserdataSerializer* customSerializer)
|
||||
{
|
||||
if (binaryData.empty())
|
||||
throw std::runtime_error("Unexpected end");
|
||||
unsigned char type = binaryData[0];
|
||||
binaryData = binaryData.substr(1);
|
||||
if (type & (CUSTOM_COMPACT_FLAG | CUSTOM_FULL_FLAG))
|
||||
{
|
||||
size_t typeNameSize, dataSize;
|
||||
if (type & CUSTOM_COMPACT_FLAG)
|
||||
{ // Compact form: 0b1SSSSTTT. SSSS = dataSize, TTT = (typeName size - 1).
|
||||
typeNameSize = (type & 7) + 1;
|
||||
dataSize = (type >> 3) & 15;
|
||||
}
|
||||
else
|
||||
{ // Full form: 0b01TTTTTT + 32bit dataSize.
|
||||
typeNameSize = (type & 63) + 1;
|
||||
dataSize = getValue<uint32_t>(binaryData);
|
||||
}
|
||||
std::string_view typeName = binaryData.substr(0, typeNameSize);
|
||||
std::string_view data = binaryData.substr(typeNameSize, dataSize);
|
||||
binaryData = binaryData.substr(typeNameSize + dataSize);
|
||||
if (!customSerializer || !customSerializer->deserialize(typeName, data, lua))
|
||||
throw std::runtime_error("Unknown type: " + std::string(typeName));
|
||||
return;
|
||||
}
|
||||
if (type & SHORT_STRING_FLAG)
|
||||
{
|
||||
size_t size = type & 0x1f;
|
||||
sol::stack::push<std::string_view>(lua.lua_state(), binaryData.substr(0, size));
|
||||
binaryData = binaryData.substr(size);
|
||||
return;
|
||||
}
|
||||
switch (static_cast<SerializedType>(type))
|
||||
{
|
||||
case SerializedType::NUMBER:
|
||||
sol::stack::push<double>(lua.lua_state(), getValue<double>(binaryData));
|
||||
return;
|
||||
case SerializedType::BOOLEAN:
|
||||
sol::stack::push<bool>(lua.lua_state(), getValue<char>(binaryData) != 0);
|
||||
return;
|
||||
case SerializedType::LONG_STRING:
|
||||
{
|
||||
uint32_t size = getValue<uint32_t>(binaryData);
|
||||
sol::stack::push<std::string_view>(lua.lua_state(), binaryData.substr(0, size));
|
||||
binaryData = binaryData.substr(size);
|
||||
return;
|
||||
}
|
||||
case SerializedType::TABLE_START:
|
||||
{
|
||||
lua_createtable(lua, 0, 0);
|
||||
while (!binaryData.empty() && binaryData[0] != char(SerializedType::TABLE_END))
|
||||
{
|
||||
deserializeImpl(lua, binaryData, customSerializer);
|
||||
deserializeImpl(lua, binaryData, customSerializer);
|
||||
lua_settable(lua, -3);
|
||||
}
|
||||
if (binaryData.empty())
|
||||
throw std::runtime_error("Unexpected end");
|
||||
binaryData = binaryData.substr(1);
|
||||
return;
|
||||
}
|
||||
case SerializedType::TABLE_END:
|
||||
throw std::runtime_error("Unexpected table end");
|
||||
case SerializedType::VEC2:
|
||||
{
|
||||
float x = getValue<float>(binaryData);
|
||||
float y = getValue<float>(binaryData);
|
||||
sol::stack::push<osg::Vec2f>(lua.lua_state(), osg::Vec2f(x, y));
|
||||
return;
|
||||
}
|
||||
case SerializedType::VEC3:
|
||||
{
|
||||
float x = getValue<float>(binaryData);
|
||||
float y = getValue<float>(binaryData);
|
||||
float z = getValue<float>(binaryData);
|
||||
sol::stack::push<osg::Vec3f>(lua.lua_state(), osg::Vec3f(x, y, z));
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw std::runtime_error("Unknown type: " + std::to_string(type));
|
||||
}
|
||||
|
||||
BinaryData serialize(const sol::object& obj, const UserdataSerializer* customSerializer)
|
||||
{
|
||||
if (obj == sol::nil)
|
||||
return "";
|
||||
BinaryData res;
|
||||
res.push_back(FORMAT_VERSION);
|
||||
serialize(res, obj, customSerializer, 0);
|
||||
return res;
|
||||
}
|
||||
|
||||
sol::object deserialize(sol::state& lua, std::string_view binaryData, const UserdataSerializer* customSerializer)
|
||||
{
|
||||
if (binaryData.empty())
|
||||
return sol::nil;
|
||||
if (binaryData[0] != FORMAT_VERSION)
|
||||
throw std::runtime_error("Incorrect version of Lua serialization format: " +
|
||||
std::to_string(static_cast<unsigned>(binaryData[0])));
|
||||
binaryData = binaryData.substr(1);
|
||||
deserializeImpl(lua, binaryData, customSerializer);
|
||||
if (!binaryData.empty())
|
||||
throw std::runtime_error("Unexpected data after serialized object");
|
||||
return sol::stack::pop<sol::object>(lua.lua_state());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
#ifndef COMPONENTS_LUA_SERIALIZATION_H
|
||||
#define COMPONENTS_LUA_SERIALIZATION_H
|
||||
|
||||
#include <sol/sol.hpp>
|
||||
|
||||
namespace LuaUtil
|
||||
{
|
||||
|
||||
// Note: it can contain \0
|
||||
using BinaryData = std::string;
|
||||
|
||||
class UserdataSerializer
|
||||
{
|
||||
public:
|
||||
virtual ~UserdataSerializer() {}
|
||||
|
||||
// Appends serialized sol::userdata to the end of BinaryData.
|
||||
// Returns false if this type of userdata is not supported by this serializer.
|
||||
virtual bool serialize(BinaryData&, const sol::userdata&) const = 0;
|
||||
|
||||
// Deserializes userdata of type "typeName" from binaryData. Should push the result on stack using sol::stack::push.
|
||||
// Returns false if this type is not supported by this serializer.
|
||||
virtual bool deserialize(std::string_view typeName, std::string_view binaryData, sol::state&) const = 0;
|
||||
|
||||
protected:
|
||||
static void append(BinaryData&, std::string_view typeName, const void* data, size_t dataSize);
|
||||
};
|
||||
|
||||
BinaryData serialize(const sol::object&, const UserdataSerializer* customSerializer = nullptr);
|
||||
sol::object deserialize(sol::state& lua, std::string_view binaryData, const UserdataSerializer* customSerializer = nullptr);
|
||||
|
||||
}
|
||||
|
||||
#endif // COMPONENTS_LUA_SERIALIZATION_H
|
@ -0,0 +1,98 @@
|
||||
#include "utilpackage.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
|
||||
#include <osg/Vec3f>
|
||||
|
||||
#include <components/misc/mathutil.hpp>
|
||||
|
||||
namespace sol
|
||||
{
|
||||
template <>
|
||||
struct is_automagical<osg::Vec2f> : std::false_type {};
|
||||
|
||||
template <>
|
||||
struct is_automagical<osg::Vec3f> : std::false_type {};
|
||||
}
|
||||
|
||||
namespace LuaUtil
|
||||
{
|
||||
|
||||
sol::table initUtilPackage(sol::state& lua)
|
||||
{
|
||||
sol::table util(lua, sol::create);
|
||||
|
||||
// TODO: Add bindings for osg::Matrix
|
||||
|
||||
// Lua bindings for osg::Vec2f
|
||||
util["vector2"] = [](float x, float y) { return osg::Vec2f(x, y); };
|
||||
sol::usertype<osg::Vec2f> vec2Type = lua.new_usertype<osg::Vec2f>("Vec2");
|
||||
vec2Type["x"] = sol::readonly_property([](const osg::Vec2f& v) -> float { return v.x(); } );
|
||||
vec2Type["y"] = sol::readonly_property([](const osg::Vec2f& v) -> float { return v.y(); } );
|
||||
vec2Type[sol::meta_function::to_string] = [](const osg::Vec2f& v) {
|
||||
std::stringstream ss;
|
||||
ss << "(" << v.x() << ", " << v.y() << ")";
|
||||
return ss.str();
|
||||
};
|
||||
vec2Type[sol::meta_function::unary_minus] = [](const osg::Vec2f& a) { return -a; };
|
||||
vec2Type[sol::meta_function::addition] = [](const osg::Vec2f& a, const osg::Vec2f& b) { return a + b; };
|
||||
vec2Type[sol::meta_function::subtraction] = [](const osg::Vec2f& a, const osg::Vec2f& b) { return a - b; };
|
||||
vec2Type[sol::meta_function::equal_to] = [](const osg::Vec2f& a, const osg::Vec2f& b) { return a == b; };
|
||||
vec2Type[sol::meta_function::multiplication] = sol::overload(
|
||||
[](const osg::Vec2f& a, float c) { return a * c; },
|
||||
[](const osg::Vec2f& a, const osg::Vec2f& b) { return a * b; });
|
||||
vec2Type[sol::meta_function::division] = [](const osg::Vec2f& a, float c) { return a / c; };
|
||||
vec2Type["dot"] = [](const osg::Vec2f& a, const osg::Vec2f& b) { return a * b; };
|
||||
vec2Type["length"] = &osg::Vec2f::length;
|
||||
vec2Type["length2"] = &osg::Vec2f::length2;
|
||||
vec2Type["normalize"] = [](const osg::Vec2f& v) {
|
||||
float len = v.length();
|
||||
if (len == 0)
|
||||
return std::make_tuple(osg::Vec2f(), 0.f);
|
||||
else
|
||||
return std::make_tuple(v * (1.f / len), len);
|
||||
};
|
||||
vec2Type["rotate"] = &Misc::rotateVec2f;
|
||||
|
||||
// Lua bindings for osg::Vec3f
|
||||
util["vector3"] = [](float x, float y, float z) { return osg::Vec3f(x, y, z); };
|
||||
sol::usertype<osg::Vec3f> vec3Type = lua.new_usertype<osg::Vec3f>("Vec3");
|
||||
vec3Type["x"] = sol::readonly_property([](const osg::Vec3f& v) -> float { return v.x(); } );
|
||||
vec3Type["y"] = sol::readonly_property([](const osg::Vec3f& v) -> float { return v.y(); } );
|
||||
vec3Type["z"] = sol::readonly_property([](const osg::Vec3f& v) -> float { return v.z(); } );
|
||||
vec3Type[sol::meta_function::to_string] = [](const osg::Vec3f& v) {
|
||||
std::stringstream ss;
|
||||
ss << "(" << v.x() << ", " << v.y() << ", " << v.z() << ")";
|
||||
return ss.str();
|
||||
};
|
||||
vec3Type[sol::meta_function::unary_minus] = [](const osg::Vec3f& a) { return -a; };
|
||||
vec3Type[sol::meta_function::addition] = [](const osg::Vec3f& a, const osg::Vec3f& b) { return a + b; };
|
||||
vec3Type[sol::meta_function::subtraction] = [](const osg::Vec3f& a, const osg::Vec3f& b) { return a - b; };
|
||||
vec3Type[sol::meta_function::equal_to] = [](const osg::Vec3f& a, const osg::Vec3f& b) { return a == b; };
|
||||
vec3Type[sol::meta_function::multiplication] = sol::overload(
|
||||
[](const osg::Vec3f& a, float c) { return a * c; },
|
||||
[](const osg::Vec3f& a, const osg::Vec3f& b) { return a * b; });
|
||||
vec3Type[sol::meta_function::division] = [](const osg::Vec3f& a, float c) { return a / c; };
|
||||
vec3Type[sol::meta_function::involution] = [](const osg::Vec3f& a, const osg::Vec3f& b) { return a ^ b; };
|
||||
vec3Type["dot"] = [](const osg::Vec3f& a, const osg::Vec3f& b) { return a * b; };
|
||||
vec3Type["cross"] = [](const osg::Vec3f& a, const osg::Vec3f& b) { return a ^ b; };
|
||||
vec3Type["length"] = &osg::Vec3f::length;
|
||||
vec3Type["length2"] = &osg::Vec3f::length2;
|
||||
vec3Type["normalize"] = [](const osg::Vec3f& v) {
|
||||
float len = v.length();
|
||||
if (len == 0)
|
||||
return std::make_tuple(osg::Vec3f(), 0.f);
|
||||
else
|
||||
return std::make_tuple(v * (1.f / len), len);
|
||||
};
|
||||
|
||||
// Utility functions
|
||||
util["clamp"] = [](float value, float from, float to) { return std::clamp(value, from, to); };
|
||||
// NOTE: `util["clamp"] = std::clamp<float>` causes error 'AddressSanitizer: stack-use-after-scope'
|
||||
util["normalizeAngle"] = &Misc::normalizeAngle;
|
||||
|
||||
return util;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
#ifndef COMPONENTS_LUA_UTILPACKAGE_H
|
||||
#define COMPONENTS_LUA_UTILPACKAGE_H
|
||||
|
||||
#include <sol/sol.hpp>
|
||||
|
||||
namespace LuaUtil
|
||||
{
|
||||
|
||||
sol::table initUtilPackage(sol::state&);
|
||||
|
||||
}
|
||||
|
||||
#endif // COMPONENTS_LUA_UTILPACKAGE_H
|
@ -0,0 +1,118 @@
|
||||
#include "luabindings.hpp"
|
||||
|
||||
namespace sol
|
||||
{
|
||||
template <>
|
||||
struct is_automagical<Queries::Field> : std::false_type {};
|
||||
|
||||
template <>
|
||||
struct is_automagical<Queries::Filter> : std::false_type {};
|
||||
|
||||
template <>
|
||||
struct is_automagical<Queries::Query> : std::false_type {};
|
||||
}
|
||||
|
||||
namespace Queries
|
||||
{
|
||||
template <Condition::Type type>
|
||||
struct CondBuilder
|
||||
{
|
||||
Filter operator()(const Field& field, const sol::object& o)
|
||||
{
|
||||
FieldValue value;
|
||||
if (field.type() == typeid(bool) && o.is<bool>())
|
||||
value = o.as<bool>();
|
||||
else if (field.type() == typeid(int32_t) && o.is<int32_t>())
|
||||
value = o.as<int32_t>();
|
||||
else if (field.type() == typeid(int64_t) && o.is<int64_t>())
|
||||
value = o.as<int64_t>();
|
||||
else if (field.type() == typeid(float) && o.is<float>())
|
||||
value = o.as<float>();
|
||||
else if (field.type() == typeid(double) && o.is<double>())
|
||||
value = o.as<double>();
|
||||
else if (field.type() == typeid(std::string) && o.is<std::string>())
|
||||
value = o.as<std::string>();
|
||||
else
|
||||
throw std::logic_error("Invalid value for field " + field.toString());
|
||||
Filter filter;
|
||||
filter.add({&field, type, value});
|
||||
return filter;
|
||||
}
|
||||
};
|
||||
|
||||
void registerQueryBindings(sol::state& lua)
|
||||
{
|
||||
sol::usertype<Field> field = lua.new_usertype<Field>("QueryField");
|
||||
sol::usertype<Filter> filter = lua.new_usertype<Filter>("QueryFilter");
|
||||
sol::usertype<Query> query = lua.new_usertype<Query>("Query");
|
||||
|
||||
field[sol::meta_function::to_string] = [](const Field& f) { return f.toString(); };
|
||||
field["eq"] = CondBuilder<Condition::EQUAL>();
|
||||
field["neq"] = CondBuilder<Condition::NOT_EQUAL>();
|
||||
field["lt"] = CondBuilder<Condition::LESSER>();
|
||||
field["lte"] = CondBuilder<Condition::LESSER_OR_EQUAL>();
|
||||
field["gt"] = CondBuilder<Condition::GREATER>();
|
||||
field["gte"] = CondBuilder<Condition::GREATER_OR_EQUAL>();
|
||||
field["like"] = CondBuilder<Condition::LIKE>();
|
||||
|
||||
filter[sol::meta_function::to_string] = [](const Filter& filter) { return filter.toString(); };
|
||||
filter[sol::meta_function::multiplication] = [](const Filter& a, const Filter& b)
|
||||
{
|
||||
Filter res = a;
|
||||
res.add(b, Operation::AND);
|
||||
return res;
|
||||
};
|
||||
filter[sol::meta_function::addition] = [](const Filter& a, const Filter& b)
|
||||
{
|
||||
Filter res = a;
|
||||
res.add(b, Operation::OR);
|
||||
return res;
|
||||
};
|
||||
filter[sol::meta_function::unary_minus] = [](const Filter& a)
|
||||
{
|
||||
Filter res = a;
|
||||
if (!a.mConditions.empty())
|
||||
res.mOperations.push_back({Operation::NOT, 0});
|
||||
return res;
|
||||
};
|
||||
|
||||
query[sol::meta_function::to_string] = [](const Query& q) { return q.toString(); };
|
||||
query["where"] = [](const Query& q, const Filter& filter)
|
||||
{
|
||||
Query res = q;
|
||||
res.mFilter.add(filter, Operation::AND);
|
||||
return res;
|
||||
};
|
||||
query["orderBy"] = [](const Query& q, const Field& field)
|
||||
{
|
||||
Query res = q;
|
||||
res.mOrderBy.push_back({&field, false});
|
||||
return res;
|
||||
};
|
||||
query["orderByDesc"] = [](const Query& q, const Field& field)
|
||||
{
|
||||
Query res = q;
|
||||
res.mOrderBy.push_back({&field, true});
|
||||
return res;
|
||||
};
|
||||
query["groupBy"] = [](const Query& q, const Field& field)
|
||||
{
|
||||
Query res = q;
|
||||
res.mGroupBy.push_back(&field);
|
||||
return res;
|
||||
};
|
||||
query["offset"] = [](const Query& q, int64_t offset)
|
||||
{
|
||||
Query res = q;
|
||||
res.mOffset = offset;
|
||||
return res;
|
||||
};
|
||||
query["limit"] = [](const Query& q, int64_t limit)
|
||||
{
|
||||
Query res = q;
|
||||
res.mLimit = limit;
|
||||
return res;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
#include <sol/sol.hpp>
|
||||
|
||||
#include "query.hpp"
|
||||
|
||||
namespace Queries
|
||||
{
|
||||
void registerQueryBindings(sol::state& lua);
|
||||
}
|
@ -0,0 +1,185 @@
|
||||
#include "query.hpp"
|
||||
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
namespace Queries
|
||||
{
|
||||
Field::Field(std::vector<std::string> path, std::type_index type)
|
||||
: mPath(std::move(path))
|
||||
, mType(type) {}
|
||||
|
||||
std::string Field::toString() const
|
||||
{
|
||||
std::string result;
|
||||
for (const std::string& segment : mPath)
|
||||
{
|
||||
if (!result.empty())
|
||||
result += ".";
|
||||
result += segment;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string toString(const FieldValue& value)
|
||||
{
|
||||
return std::visit([](auto&& arg) -> std::string
|
||||
{
|
||||
using T = std::decay_t<decltype(arg)>;
|
||||
if constexpr (std::is_same_v<T, std::string>)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << std::quoted(arg);
|
||||
return oss.str();
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, bool>)
|
||||
return arg ? "true" : "false";
|
||||
else
|
||||
return std::to_string(arg);
|
||||
}, value);
|
||||
}
|
||||
|
||||
std::string Condition::toString() const
|
||||
{
|
||||
std::string res;
|
||||
res += mField->toString();
|
||||
switch (mType)
|
||||
{
|
||||
case Condition::EQUAL: res += " == "; break;
|
||||
case Condition::NOT_EQUAL: res += " != "; break;
|
||||
case Condition::LESSER: res += " < "; break;
|
||||
case Condition::LESSER_OR_EQUAL: res += " <= "; break;
|
||||
case Condition::GREATER: res += " > "; break;
|
||||
case Condition::GREATER_OR_EQUAL: res += " >= "; break;
|
||||
case Condition::LIKE: res += " LIKE "; break;
|
||||
}
|
||||
res += Queries::toString(mValue);
|
||||
return res;
|
||||
}
|
||||
|
||||
void Filter::add(const Condition& c, Operation::Type op)
|
||||
{
|
||||
mOperations.push_back({Operation::PUSH, mConditions.size()});
|
||||
mConditions.push_back(c);
|
||||
if (mConditions.size() > 1)
|
||||
mOperations.push_back({op, 0});
|
||||
}
|
||||
|
||||
void Filter::add(const Filter& f, Operation::Type op)
|
||||
{
|
||||
size_t conditionOffset = mConditions.size();
|
||||
size_t operationsBefore = mOperations.size();
|
||||
mConditions.insert(mConditions.end(), f.mConditions.begin(), f.mConditions.end());
|
||||
mOperations.insert(mOperations.end(), f.mOperations.begin(), f.mOperations.end());
|
||||
for (size_t i = operationsBefore; i < mOperations.size(); ++i)
|
||||
mOperations[i].mConditionIndex += conditionOffset;
|
||||
if (conditionOffset > 0 && !f.mConditions.empty())
|
||||
mOperations.push_back({op, 0});
|
||||
}
|
||||
|
||||
std::string Filter::toString() const
|
||||
{
|
||||
if(mOperations.empty())
|
||||
return "";
|
||||
std::vector<std::string> stack;
|
||||
auto pop = [&stack](){ auto v = stack.back(); stack.pop_back(); return v; };
|
||||
auto push = [&stack](const std::string& s) { stack.push_back(s); };
|
||||
for (const Operation& op : mOperations)
|
||||
{
|
||||
if(op.mType == Operation::PUSH)
|
||||
push(mConditions[op.mConditionIndex].toString());
|
||||
else if(op.mType == Operation::AND)
|
||||
{
|
||||
auto rhs = pop();
|
||||
auto lhs = pop();
|
||||
std::string res;
|
||||
res += "(";
|
||||
res += lhs;
|
||||
res += ") AND (";
|
||||
res += rhs;
|
||||
res += ")";
|
||||
push(res);
|
||||
}
|
||||
else if (op.mType == Operation::OR)
|
||||
{
|
||||
auto rhs = pop();
|
||||
auto lhs = pop();
|
||||
std::string res;
|
||||
res += "(";
|
||||
res += lhs;
|
||||
res += ") OR (";
|
||||
res += rhs;
|
||||
res += ")";
|
||||
push(res);
|
||||
}
|
||||
else if (op.mType == Operation::NOT)
|
||||
{
|
||||
std::string res;
|
||||
res += "NOT (";
|
||||
res += pop();
|
||||
res += ")";
|
||||
push(res);
|
||||
}
|
||||
else
|
||||
throw std::logic_error("Unknown operation type!");
|
||||
}
|
||||
return pop();
|
||||
}
|
||||
|
||||
std::string Query::toString() const
|
||||
{
|
||||
std::string res;
|
||||
res += "SELECT ";
|
||||
res += mQueryType;
|
||||
|
||||
std::string filter = mFilter.toString();
|
||||
if(!filter.empty())
|
||||
{
|
||||
res += " WHERE ";
|
||||
res += filter;
|
||||
}
|
||||
|
||||
std::string order;
|
||||
for(const OrderBy& ord : mOrderBy)
|
||||
{
|
||||
if(!order.empty())
|
||||
order += ", ";
|
||||
order += ord.mField->toString();
|
||||
if(ord.mDescending)
|
||||
order += " DESC";
|
||||
}
|
||||
if (!order.empty())
|
||||
{
|
||||
res += " ORDER BY ";
|
||||
res += order;
|
||||
}
|
||||
|
||||
std::string group;
|
||||
for (const Field* f: mGroupBy)
|
||||
{
|
||||
if (!group.empty())
|
||||
group += " ,";
|
||||
group += f->toString();
|
||||
}
|
||||
if (!group.empty())
|
||||
{
|
||||
res += " GROUP BY ";
|
||||
res += group;
|
||||
}
|
||||
|
||||
if (mLimit != sNoLimit)
|
||||
{
|
||||
res += " LIMIT ";
|
||||
res += std::to_string(mLimit);
|
||||
}
|
||||
|
||||
if (mOffset != 0)
|
||||
{
|
||||
res += " OFFSET ";
|
||||
res += std::to_string(mOffset);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,99 @@
|
||||
#ifndef COMPONENTS_QUERIES_QUERY
|
||||
#define COMPONENTS_QUERIES_QUERY
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <typeindex>
|
||||
#include <variant>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace Queries
|
||||
{
|
||||
class Field
|
||||
{
|
||||
public:
|
||||
Field(std::vector<std::string> path, std::type_index type);
|
||||
|
||||
const std::vector<std::string>& path() const { return mPath; }
|
||||
const std::type_index type() const { return mType; }
|
||||
|
||||
std::string toString() const;
|
||||
|
||||
private:
|
||||
std::vector<std::string> mPath;
|
||||
std::type_index mType;
|
||||
};
|
||||
|
||||
struct OrderBy
|
||||
{
|
||||
const Field* mField;
|
||||
bool mDescending;
|
||||
};
|
||||
|
||||
using FieldValue = std::variant<bool, int32_t, int64_t, float, double, std::string>;
|
||||
std::string toString(const FieldValue& value);
|
||||
|
||||
struct Condition
|
||||
{
|
||||
enum Type
|
||||
{
|
||||
EQUAL = 0,
|
||||
NOT_EQUAL = 1,
|
||||
GREATER = 2,
|
||||
GREATER_OR_EQUAL = 3,
|
||||
LESSER = 4,
|
||||
LESSER_OR_EQUAL = 5,
|
||||
LIKE = 6,
|
||||
};
|
||||
|
||||
std::string toString() const;
|
||||
|
||||
const Field* mField;
|
||||
Type mType;
|
||||
FieldValue mValue;
|
||||
};
|
||||
|
||||
struct Operation
|
||||
{
|
||||
enum Type
|
||||
{
|
||||
PUSH = 0, // push condition on stack
|
||||
NOT = 1, // invert top condition on stack
|
||||
AND = 2, // logic AND for two top conditions
|
||||
OR = 3, // logic OR for two top conditions
|
||||
};
|
||||
|
||||
Type mType;
|
||||
size_t mConditionIndex; // used only if mType == PUSH
|
||||
};
|
||||
|
||||
struct Filter
|
||||
{
|
||||
std::string toString() const;
|
||||
|
||||
// combines with given condition or filter using operation `AND` or `OR`.
|
||||
void add(const Condition& c, Operation::Type op = Operation::AND);
|
||||
void add(const Filter& f, Operation::Type op = Operation::AND);
|
||||
|
||||
std::vector<Condition> mConditions;
|
||||
std::vector<Operation> mOperations; // operations on conditions in reverse polish notation
|
||||
};
|
||||
|
||||
struct Query
|
||||
{
|
||||
static constexpr int64_t sNoLimit = -1;
|
||||
|
||||
Query(std::string type) : mQueryType(std::move(type)) {}
|
||||
std::string toString() const;
|
||||
|
||||
std::string mQueryType;
|
||||
Filter mFilter;
|
||||
std::vector<OrderBy> mOrderBy;
|
||||
std::vector<const Field*> mGroupBy;
|
||||
int64_t mOffset = 0;
|
||||
int64_t mLimit = sNoLimit;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // !COMPONENTS_QUERIES_QUERY
|
||||
|
@ -0,0 +1,113 @@
|
||||
#luadoc tt { font-family: monospace; }
|
||||
|
||||
#luadoc p,
|
||||
#luadoc td,
|
||||
#luadoc th { font-size: .95em; line-height: 1.2em;}
|
||||
|
||||
#luadoc p,
|
||||
#luadoc ul
|
||||
{ margin: 10px 0 0 10px;}
|
||||
|
||||
#luadoc strong { font-weight: bold;}
|
||||
|
||||
#luadoc em { font-style: italic;}
|
||||
|
||||
#luadoc h1 {
|
||||
font-size: 1.5em;
|
||||
margin: 25px 0 20px 0;
|
||||
}
|
||||
#luadoc h2,
|
||||
#luadoc h3,
|
||||
#luadoc h4 { margin: 15px 0 10px 0; }
|
||||
#luadoc h2 { font-size: 1.25em; }
|
||||
#luadoc h3 { font-size: 1.15em; }
|
||||
#luadoc h4 { font-size: 1.06em; }
|
||||
|
||||
#luadoc hr {
|
||||
color:#cccccc;
|
||||
background: #00007f;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
#luadoc blockquote { margin-left: 3em; }
|
||||
|
||||
#luadoc ul { list-style-type: disc; }
|
||||
|
||||
#luadoc p.name {
|
||||
font-family: "Andale Mono", monospace;
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
#luadoc p:first-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
#luadoc table.function_list {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #cccccc;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
#luadoc table.function_list td {
|
||||
border-width: 1px;
|
||||
padding: 3px;
|
||||
border-style: solid;
|
||||
border-color: #cccccc;
|
||||
}
|
||||
|
||||
#luadoc table.function_list td.name { background-color: #f0f0f0; }
|
||||
#luadoc table.function_list td.summary { width: 100%; }
|
||||
|
||||
#luadoc dl.table dt,
|
||||
#luadoc dl.function dt {border-top: 1px solid #ccc; padding-top: 1em;}
|
||||
#luadoc dl.table dd,
|
||||
#luadoc dl.function dd {padding-bottom: 1em; margin: 10px 0 0 20px;}
|
||||
#luadoc dl.table h3,
|
||||
#luadoc dl.function h3 {font-size: .95em;}
|
||||
|
||||
|
||||
|
||||
#luadoc pre.example {
|
||||
background-color: #eeffcc;
|
||||
border: 1px solid #e1e4e5;
|
||||
padding: 10px;
|
||||
margin: 10px 0 10px 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
#luadoc code {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
border: none;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#luadoc pre.example code {
|
||||
color: #404040;
|
||||
background-color: #eeffcc;
|
||||
border: none;
|
||||
white-space: pre;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
#luadoc dt {
|
||||
background: inherit;
|
||||
color: inherit;
|
||||
width: 100%;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
#luadoc a:not(:link) {
|
||||
font-weight: bold;
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
cursor: inherit;
|
||||
}
|
||||
#luadoc a:link { font-weight: bold; color: #004080; text-decoration: none; }
|
||||
#luadoc a:visited { font-weight: bold; color: #006699; text-decoration: none; }
|
||||
#luadoc a:link:hover { text-decoration: underline; }
|
||||
|
||||
#luadoc dl,
|
||||
#luadoc dd {margin: 0px; line-height: 1.2em;}
|
||||
#luadoc li {list-style: bullet;}
|
||||
|
@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
|
||||
# How to install openmwluadocumentor:
|
||||
|
||||
# sudo apt install luarocks
|
||||
# git clone https://gitlab.com/ptmikheev/openmw-luadocumentor.git
|
||||
# cd openmw-luadocumentor/luarocks
|
||||
# luarocks --local pack openmwluadocumentor-0.1.1-1.rockspec
|
||||
# luarocks --local install openmwluadocumentor-0.1.1-1.src.rock
|
||||
|
||||
if [ -f /.dockerenv ]; then
|
||||
# We are inside readthedocs pipeline
|
||||
echo "Install lua 5.1"
|
||||
cd ~
|
||||
curl -R -O https://www.lua.org/ftp/lua-5.1.5.tar.gz
|
||||
tar -zxf lua-5.1.5.tar.gz
|
||||
cd lua-5.1.5/
|
||||
make linux
|
||||
PATH=$PATH:~/lua-5.1.5/src
|
||||
|
||||
echo "Install luarocks"
|
||||
cd ~
|
||||
wget https://luarocks.org/releases/luarocks-2.4.2.tar.gz
|
||||
tar zxpf luarocks-2.4.2.tar.gz
|
||||
cd luarocks-2.4.2/
|
||||
./configure --with-lua-bin=$HOME/lua-5.1.5/src --with-lua-include=$HOME/lua-5.1.5/src --prefix=$HOME/luarocks
|
||||
make build
|
||||
make install
|
||||
PATH=$PATH:~/luarocks/bin
|
||||
|
||||
echo "Install openmwluadocumentor"
|
||||
cd ~
|
||||
git clone https://gitlab.com/ptmikheev/openmw-luadocumentor.git
|
||||
cd openmw-luadocumentor/luarocks
|
||||
luarocks --local install checks
|
||||
luarocks --local pack openmwluadocumentor-0.1.1-1.rockspec
|
||||
luarocks --local install openmwluadocumentor-0.1.1-1.src.rock
|
||||
fi
|
||||
|
||||
DOCS_SOURCE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
FILES_DIR=$DOCS_SOURCE_DIR/../../files
|
||||
OUTPUT_DIR=$DOCS_SOURCE_DIR/reference/lua-scripting/generated_html
|
||||
|
||||
rm -f $OUTPUT_DIR/*.html
|
||||
|
||||
cd $FILES_DIR/lua_api
|
||||
~/.luarocks/bin/openmwluadocumentor -f doc -d $OUTPUT_DIR openmw/*lua
|
||||
|
||||
cd $FILES_DIR/builtin_scripts
|
||||
~/.luarocks/bin/openmwluadocumentor -f doc -d $OUTPUT_DIR openmw_aux/*lua
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue