From 215b46503c4d6c830782b8ba5adf8f156c07064d Mon Sep 17 00:00:00 2001 From: elsid Date: Thu, 3 Feb 2022 02:05:43 +0100 Subject: [PATCH] Support rendering for navmesh update frequency as a heatmap Useful when need to find tiles with high number of updates. Add debug Lua package with new functions to toggle render mode and set navmesh render mode. --- apps/openmw/CMakeLists.txt | 4 +- apps/openmw/mwlua/debugbindings.cpp | 51 +++++++++++++++++++ apps/openmw/mwlua/debugbindings.hpp | 13 +++++ apps/openmw/mwlua/luabindings.cpp | 2 +- apps/openmw/mwlua/luamanagerimp.cpp | 3 ++ apps/openmw/mwlua/luamanagerimp.hpp | 1 + apps/openmw/mwrender/navmesh.cpp | 39 +++++++++++--- apps/openmw/mwrender/navmesh.hpp | 8 ++- apps/openmw/mwrender/navmeshmode.cpp | 16 ++++++ apps/openmw/mwrender/navmeshmode.hpp | 17 +++++++ apps/openmw/mwrender/renderingmanager.cpp | 8 ++- apps/openmw/mwrender/renderingmanager.hpp | 4 +- components/sceneutil/navmesh.cpp | 47 ++++++++++++++--- components/sceneutil/navmesh.hpp | 10 +++- docs/source/reference/lua-scripting/api.rst | 45 ++-------------- .../reference/lua-scripting/openmw_debug.rst | 5 ++ .../reference/lua-scripting/overview.rst | 42 +-------------- .../lua-scripting/tables/aux_packages.rst | 11 ++++ .../lua-scripting/tables/packages.rst | 33 ++++++++++++ .../reference/modding/settings/navigator.rst | 13 ++++- .../scripts/omw/console/player.lua | 1 + files/lua_api/CMakeLists.txt | 2 +- files/lua_api/openmw/debug.lua | 44 ++++++++++++++++ files/settings-default.cfg | 3 ++ 24 files changed, 319 insertions(+), 103 deletions(-) create mode 100644 apps/openmw/mwlua/debugbindings.cpp create mode 100644 apps/openmw/mwlua/debugbindings.hpp create mode 100644 apps/openmw/mwrender/navmeshmode.cpp create mode 100644 apps/openmw/mwrender/navmeshmode.hpp create mode 100644 docs/source/reference/lua-scripting/openmw_debug.rst create mode 100644 docs/source/reference/lua-scripting/tables/aux_packages.rst create mode 100644 docs/source/reference/lua-scripting/tables/packages.rst create mode 100644 files/lua_api/openmw/debug.lua diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 1756824164..f0ee1639a1 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -23,7 +23,7 @@ add_openmw_dir (mwrender creatureanimation effectmanager util renderinginterface pathgrid rendermode weaponanimation screenshotmanager bulletdebugdraw globalmap characterpreview camera localmap water terrainstorage ripplesimulation renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover - postprocessor pingpongcull hdr pingpongcanvas transparentpass + postprocessor pingpongcull hdr pingpongcanvas transparentpass navmeshmode ) add_openmw_dir (mwinput @@ -61,7 +61,7 @@ add_openmw_dir (mwscript add_openmw_dir (mwlua luamanagerimp object worldview userdataserializer eventqueue luabindings localscripts playerscripts objectbindings cellbindings asyncbindings settingsbindings - camerabindings uibindings inputbindings nearbybindings postprocessingbindings stats + camerabindings uibindings inputbindings nearbybindings postprocessingbindings stats debugbindings types/types types/door types/actor types/container types/weapon types/npc types/creature ) diff --git a/apps/openmw/mwlua/debugbindings.cpp b/apps/openmw/mwlua/debugbindings.cpp new file mode 100644 index 0000000000..fe00fa72b2 --- /dev/null +++ b/apps/openmw/mwlua/debugbindings.cpp @@ -0,0 +1,51 @@ +#include "debugbindings.hpp" +#include "context.hpp" +#include "luamanagerimp.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwrender/renderingmanager.hpp" + +#include + +namespace MWLua +{ + sol::table initDebugPackage(const Context& context) + { + sol::table api = context.mLua->newTable(); + + api["RENDER_MODE"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ + {"CollisionDebug", MWRender::Render_CollisionDebug}, + {"Wireframe", MWRender::Render_Wireframe}, + {"Pathgrid", MWRender::Render_Pathgrid}, + {"Water", MWRender::Render_Water}, + {"Scene", MWRender::Render_Scene}, + {"NavMesh", MWRender::Render_NavMesh}, + {"ActorsPaths", MWRender::Render_ActorsPaths}, + {"RecastMesh", MWRender::Render_RecastMesh}, + })); + + api["toggleRenderMode"] = [context] (MWRender::RenderMode value) + { + context.mLuaManager->addAction([value] + { + MWBase::Environment::get().getWorld()->toggleRenderMode(value); + }); + }; + + api["NAV_MESH_RENDER_MODE"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ + {"AreaType", MWRender::NavMeshMode::AreaType}, + {"UpdateFrequency", MWRender::NavMeshMode::UpdateFrequency}, + })); + + api["setNavMeshRenderMode"] = [context] (MWRender::NavMeshMode value) + { + context.mLuaManager->addAction([value] + { + MWBase::Environment::get().getWorld()->getRenderingManager()->setNavMeshMode(value); + }); + }; + + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/debugbindings.hpp b/apps/openmw/mwlua/debugbindings.hpp new file mode 100644 index 0000000000..c508b54496 --- /dev/null +++ b/apps/openmw/mwlua/debugbindings.hpp @@ -0,0 +1,13 @@ +#ifndef OPENMW_MWLUA_DEBUGBINDINGS_H +#define OPENMW_MWLUA_DEBUGBINDINGS_H + +#include + +namespace MWLua +{ + struct Context; + + sol::table initDebugPackage(const Context& context); +} + +#endif // OPENMW_MWLUA_DEBUGBINDINGS_H diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index dc50d093ca..4878e0423e 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -41,7 +41,7 @@ namespace MWLua { auto* lua = context.mLua; sol::table api(lua->sol(), sol::create); - api["API_REVISION"] = 22; + api["API_REVISION"] = 23; api["quit"] = [lua]() { Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback(); diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 96b4db4f20..02b986080b 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -23,6 +23,7 @@ #include "luabindings.hpp" #include "userdataserializer.hpp" #include "types/types.hpp" +#include "debugbindings.hpp" namespace MWLua { @@ -96,6 +97,7 @@ namespace MWLua mLocalStoragePackage = initLocalStoragePackage(localContext, &mGlobalStorage); mPlayerStoragePackage = initPlayerStoragePackage(localContext, &mGlobalStorage, &mPlayerStorage); mPostprocessingPackage = initPostprocessingPackage(localContext); + mDebugPackage = initDebugPackage(localContext); initConfiguration(); mInitialized = true; @@ -409,6 +411,7 @@ namespace MWLua scripts->addPackage("openmw.settings", mPlayerSettingsPackage); scripts->addPackage("openmw.storage", mPlayerStoragePackage); scripts->addPackage("openmw.postprocessing", mPostprocessingPackage); + scripts->addPackage("openmw.debug", mDebugPackage); } else { diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index 0700895497..0c08deb4fb 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -141,6 +141,7 @@ namespace MWLua sol::table mLocalStoragePackage; sol::table mPlayerStoragePackage; sol::table mPostprocessingPackage; + sol::table mDebugPackage; GlobalScripts mGlobalScripts{&mLua}; std::set mActiveLocalScripts; diff --git a/apps/openmw/mwrender/navmesh.cpp b/apps/openmw/mwrender/navmesh.cpp index 8345df9265..47c06abca1 100644 --- a/apps/openmw/mwrender/navmesh.cpp +++ b/apps/openmw/mwrender/navmesh.cpp @@ -12,6 +12,8 @@ #include #include +#include + #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" @@ -43,6 +45,7 @@ namespace MWRender const osg::ref_ptr mDebugDrawStateSet; const DetourNavigator::Settings mSettings; std::map mTiles; + NavMeshMode mMode; std::atomic_bool mAborted {false}; std::mutex mMutex; bool mStarted = false; @@ -52,7 +55,8 @@ namespace MWRender explicit CreateNavMeshTileGroups(std::size_t id, DetourNavigator::Version version, std::weak_ptr navMesh, const osg::ref_ptr& groupStateSet, const osg::ref_ptr& debugDrawStateSet, - const DetourNavigator::Settings& settings, const std::map& tiles) + const DetourNavigator::Settings& settings, const std::map& tiles, + NavMeshMode mode) : mId(id) , mVersion(version) , mNavMesh(navMesh) @@ -60,6 +64,7 @@ namespace MWRender , mDebugDrawStateSet(debugDrawStateSet) , mSettings(settings) , mTiles(tiles) + , mMode(mode) { } @@ -79,10 +84,14 @@ namespace MWRender return; std::vector> existingTiles; + unsigned minSalt = std::numeric_limits::max(); + unsigned maxSalt = 0; - navMeshPtr->lockConst()->forEachUsedTile([&] (const TilePosition& position, const Version& version, const dtMeshTile& /*meshTile*/) + navMeshPtr->lockConst()->forEachUsedTile([&] (const TilePosition& position, const Version& version, const dtMeshTile& meshTile) { existingTiles.emplace_back(position, version); + minSalt = std::min(minSalt, meshTile.salt); + maxSalt = std::max(maxSalt, meshTile.salt); }); if (mAborted.load(std::memory_order_acquire)) @@ -98,10 +107,15 @@ namespace MWRender std::vector> updatedTiles; + const unsigned char flags = SceneUtil::NavMeshTileDrawFlagsOffMeshConnections + | SceneUtil::NavMeshTileDrawFlagsClosedList + | (mMode == NavMeshMode::UpdateFrequency ? SceneUtil::NavMeshTileDrawFlagsHeat : 0); + for (const auto& [position, version] : existingTiles) { const auto it = mTiles.find(position); - if (it != mTiles.end() && it->second.mGroup != nullptr && it->second.mVersion == version) + if (it != mTiles.end() && it->second.mGroup != nullptr && it->second.mVersion == version + && mMode != NavMeshMode::UpdateFrequency) continue; osg::ref_ptr group; @@ -114,7 +128,8 @@ namespace MWRender if (mAborted.load(std::memory_order_acquire)) return; - group = SceneUtil::createNavMeshTileGroup(navMesh->getImpl(), *meshTile, mSettings, mGroupStateSet, mDebugDrawStateSet); + group = SceneUtil::createNavMeshTileGroup(navMesh->getImpl(), *meshTile, mSettings, mGroupStateSet, + mDebugDrawStateSet, flags, minSalt, maxSalt); } if (group == nullptr) { @@ -147,12 +162,14 @@ namespace MWRender : mWorkItem(std::move(workItem)) {} }; - NavMesh::NavMesh(const osg::ref_ptr& root, const osg::ref_ptr& workQueue, bool enabled) + NavMesh::NavMesh(const osg::ref_ptr& root, const osg::ref_ptr& workQueue, + bool enabled, NavMeshMode mode) : mRootNode(root) , mWorkQueue(workQueue) , mGroupStateSet(SceneUtil::makeNavMeshTileStateSet()) , mDebugDrawStateSet(SceneUtil::DebugDraw::makeStateSet()) , mEnabled(enabled) + , mMode(mode) , mId(std::numeric_limits::max()) { } @@ -261,11 +278,13 @@ namespace MWRender workItem->mId = id; workItem->mVersion = version; workItem->mTiles = mTiles; + workItem->mMode = mMode; return; } - osg::ref_ptr workItem = new CreateNavMeshTileGroups(id, version, navMesh, mGroupStateSet, mDebugDrawStateSet, settings, mTiles); + osg::ref_ptr workItem = new CreateNavMeshTileGroups(id, version, navMesh, + mGroupStateSet, mDebugDrawStateSet, settings, mTiles, mMode); mWorkQueue->addWorkItem(workItem); mWorkItems.push_back(std::move(workItem)); } @@ -290,4 +309,12 @@ namespace MWRender reset(); mEnabled = false; } + + void NavMesh::setMode(NavMeshMode value) + { + if (mMode == value) + return; + reset(); + mMode = value; + } } diff --git a/apps/openmw/mwrender/navmesh.hpp b/apps/openmw/mwrender/navmesh.hpp index 1c2cfbd323..f4d3f07e94 100644 --- a/apps/openmw/mwrender/navmesh.hpp +++ b/apps/openmw/mwrender/navmesh.hpp @@ -1,6 +1,8 @@ #ifndef OPENMW_MWRENDER_NAVMESH_H #define OPENMW_MWRENDER_NAVMESH_H +#include "navmeshmode.hpp" + #include #include #include @@ -11,6 +13,7 @@ #include #include #include +#include class dtNavMesh; @@ -38,7 +41,7 @@ namespace MWRender { public: explicit NavMesh(const osg::ref_ptr& root, const osg::ref_ptr& workQueue, - bool enabled); + bool enabled, NavMeshMode mode); ~NavMesh(); bool toggle(); @@ -57,6 +60,8 @@ namespace MWRender return mEnabled; } + void setMode(NavMeshMode value); + private: struct Tile { @@ -73,6 +78,7 @@ namespace MWRender osg::ref_ptr mGroupStateSet; osg::ref_ptr mDebugDrawStateSet; bool mEnabled; + NavMeshMode mMode; std::size_t mId; DetourNavigator::Version mVersion; std::map mTiles; diff --git a/apps/openmw/mwrender/navmeshmode.cpp b/apps/openmw/mwrender/navmeshmode.cpp new file mode 100644 index 0000000000..d08e7cf693 --- /dev/null +++ b/apps/openmw/mwrender/navmeshmode.cpp @@ -0,0 +1,16 @@ +#include "navmeshmode.hpp" + +#include +#include + +namespace MWRender +{ + NavMeshMode parseNavMeshMode(std::string_view value) + { + if (value == "area type") + return NavMeshMode::AreaType; + if (value == "update frequency") + return NavMeshMode::UpdateFrequency; + throw std::logic_error("Unsupported navigation mesh rendering mode: " + std::string(value)); + } +} diff --git a/apps/openmw/mwrender/navmeshmode.hpp b/apps/openmw/mwrender/navmeshmode.hpp new file mode 100644 index 0000000000..9401479e21 --- /dev/null +++ b/apps/openmw/mwrender/navmeshmode.hpp @@ -0,0 +1,17 @@ +#ifndef OPENMW_MWRENDER_NAVMESHMODE_H +#define OPENMW_MWRENDER_NAVMESHMODE_H + +#include + +namespace MWRender +{ + enum class NavMeshMode + { + AreaType, + UpdateFrequency, + }; + + NavMeshMode parseNavMeshMode(std::string_view value); +} + +#endif diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 3c032e2fd6..e0869a45dd 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -435,7 +435,8 @@ namespace MWRender // It is unnecessary to stop/start the viewer as no frames are being rendered yet. mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(globalDefines); - mNavMesh.reset(new NavMesh(mRootNode, mWorkQueue, Settings::Manager::getBool("enable nav mesh render", "Navigator"))); + mNavMesh.reset(new NavMesh(mRootNode, mWorkQueue, Settings::Manager::getBool("enable nav mesh render", "Navigator"), + parseNavMeshMode(Settings::Manager::getString("nav mesh render mode", "Navigator")))); mActorsPaths.reset(new ActorsPaths(mRootNode, Settings::Manager::getBool("enable agents paths render", "Navigator"))); mRecastMesh.reset(new RecastMesh(mRootNode, Settings::Manager::getBool("enable recast mesh render", "Navigator"))); mPathgrid.reset(new Pathgrid(mRootNode)); @@ -1573,4 +1574,9 @@ namespace MWRender if (mObjectPaging) mObjectPaging->getPagedRefnums(activeGrid, out); } + + void RenderingManager::setNavMeshMode(NavMeshMode value) + { + mNavMesh->setMode(value); + } } diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index cd4b6edca4..128ef43195 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -10,7 +10,7 @@ #include #include "objects.hpp" - +#include "navmeshmode.hpp" #include "renderinginterface.hpp" #include "rendermode.hpp" @@ -253,6 +253,8 @@ namespace MWRender void setScreenRes(int width, int height); + void setNavMeshMode(NavMeshMode value); + private: void updateTextureFiltering(); void updateAmbient(); diff --git a/components/sceneutil/navmesh.cpp b/components/sceneutil/navmesh.cpp index eac3d17156..a8a9fbac5f 100644 --- a/components/sceneutil/navmesh.cpp +++ b/components/sceneutil/navmesh.cpp @@ -10,6 +10,8 @@ #include #include +#include + namespace { // Copied from https://github.com/recastnavigation/recastnavigation/blob/c5cbd53024c8a9d8d097a4371215e3342d2fdc87/DebugUtils/Source/DetourDebugDraw.cpp#L26-L38 @@ -107,15 +109,46 @@ namespace dd->end(); } + float getHeat(unsigned salt, unsigned minSalt, unsigned maxSalt) + { + if (salt < minSalt) + return 0; + if (salt > maxSalt) + return 1; + if (maxSalt <= minSalt) + return 0.5; + return static_cast(salt - minSalt) / static_cast(maxSalt - minSalt); + } + + int getRgbaComponent(float v, int base) + { + return static_cast(std::round(v * base)); + } + + unsigned heatToColor(float heat, int alpha) + { + constexpr int min = 100; + constexpr int max = 200; + if (heat < 0.25f) + return duRGBA(min, min + getRgbaComponent(4 * heat, max - min), max, alpha); + if (heat < 0.5f) + return duRGBA(min, max, min + getRgbaComponent(1 - 4 * (heat - 0.5f), max - min), alpha); + if (heat < 0.75f) + return duRGBA(min + getRgbaComponent(4 * (heat - 0.5f), max - min), max, min, alpha); + return duRGBA(max, min + getRgbaComponent(1 - 4 * (heat - 0.75f), max - min), min, alpha); + } + // Based on https://github.com/recastnavigation/recastnavigation/blob/c5cbd53024c8a9d8d097a4371215e3342d2fdc87/DebugUtils/Source/DetourDebugDraw.cpp#L120-L235 void drawMeshTile(duDebugDraw* dd, const dtNavMesh& mesh, const dtNavMeshQuery* query, - const dtMeshTile* tile, unsigned char flags) + const dtMeshTile* tile, unsigned char flags, float heat) { + using namespace SceneUtil; + dtPolyRef base = mesh.getPolyRefBase(tile); int tileNum = mesh.decodePolyIdTile(base); - const unsigned int tileNumColor = duIntToCol(tileNum, 128); const unsigned alpha = tile->header->userId == 0 ? 64 : 128; + const unsigned int tileNumColor = duIntToCol(tileNum, alpha); dd->depthMask(false); @@ -133,8 +166,10 @@ namespace col = duRGBA(255, 196, 0, alpha); else { - if (flags & DU_DRAWNAVMESH_COLOR_TILES) + if (flags & NavMeshTileDrawFlagsColorTiles) col = duTransCol(tileNumColor, alpha); + else if (flags & NavMeshTileDrawFlagsHeat) + col = heatToColor(heat, alpha); else col = duTransCol(dd->areaToCol(p->getArea()), alpha); } @@ -159,7 +194,7 @@ namespace // Draw outer poly boundaries drawPolyBoundaries(dd, tile, duRGBA(0,48,64,220), 2.5f, false); - if (flags & DU_DRAWNAVMESH_OFFMESHCONS) + if (flags & NavMeshTileDrawFlagsOffMeshConnections) { dd->begin(DU_DRAW_LINES, 2.0f); for (int i = 0; i < tile->header->polyCount; ++i) @@ -246,7 +281,7 @@ namespace SceneUtil osg::ref_ptr createNavMeshTileGroup(const dtNavMesh& navMesh, const dtMeshTile& meshTile, const DetourNavigator::Settings& settings, const osg::ref_ptr& groupStateSet, - const osg::ref_ptr& debugDrawStateSet) + const osg::ref_ptr& debugDrawStateSet, unsigned char flags, unsigned minSalt, unsigned maxSalt) { if (meshTile.header == nullptr) return nullptr; @@ -257,7 +292,7 @@ namespace SceneUtil DebugDraw debugDraw(*group, debugDrawStateSet, osg::Vec3f(0, 0, shift), 1.0f / settings.mRecast.mRecastScaleFactor); dtNavMeshQuery navMeshQuery; navMeshQuery.init(&navMesh, settings.mDetour.mMaxNavMeshQueryNodes); - drawMeshTile(&debugDraw, navMesh, &navMeshQuery, &meshTile, DU_DRAWNAVMESH_OFFMESHCONS | DU_DRAWNAVMESH_CLOSEDLIST); + drawMeshTile(&debugDraw, navMesh, &navMeshQuery, &meshTile, flags, getHeat(meshTile.salt, minSalt, maxSalt)); return group; } diff --git a/components/sceneutil/navmesh.hpp b/components/sceneutil/navmesh.hpp index ca9b8bd570..95d79ea4de 100644 --- a/components/sceneutil/navmesh.hpp +++ b/components/sceneutil/navmesh.hpp @@ -19,11 +19,19 @@ namespace DetourNavigator namespace SceneUtil { + enum NavMeshTileDrawFlags : unsigned char + { + NavMeshTileDrawFlagsOffMeshConnections = 1, + NavMeshTileDrawFlagsClosedList = 1 << 1, + NavMeshTileDrawFlagsColorTiles = 1 << 2, + NavMeshTileDrawFlagsHeat = 1 << 3, + }; + osg::ref_ptr makeNavMeshTileStateSet(); osg::ref_ptr createNavMeshTileGroup(const dtNavMesh& navMesh, const dtMeshTile& meshTile, const DetourNavigator::Settings& settings, const osg::ref_ptr& groupStateSet, - const osg::ref_ptr& debugDrawStateSet); + const osg::ref_ptr& debugDrawStateSet, unsigned char flags, unsigned minSalt, unsigned maxSalt); } #endif diff --git a/docs/source/reference/lua-scripting/api.rst b/docs/source/reference/lua-scripting/api.rst index c3b77a15ee..3538eca9e5 100644 --- a/docs/source/reference/lua-scripting/api.rst +++ b/docs/source/reference/lua-scripting/api.rst @@ -21,6 +21,7 @@ Lua API reference openmw_ui openmw_camera openmw_postprocessing + openmw_debug openmw_aux_calendar openmw_aux_util openmw_aux_time @@ -47,54 +48,14 @@ It can not be overloaded even if there is a lua file with the same name. The list of available packages is different for global and for local scripts. Player scripts are local scripts that are attached to a player. -+------------------------------------------------------------+--------------------+---------------------------------------------------------------+ -| Package | Can be used | Description | -+============================================================+====================+===============================================================+ -|:ref:`openmw.interfaces