From 24ce242941a13316fde09eb0d5ffc818a76ddb0c Mon Sep 17 00:00:00 2001
From: Andrei Kortunov <andrei.kortunov@yandex.ru>
Date: Sun, 24 Nov 2019 17:40:19 +0400
Subject: [PATCH] Implement TestCells (feature #5219)

---
 CHANGELOG.md                              |   1 +
 apps/openmw/mwbase/world.hpp              |   3 +
 apps/openmw/mwrender/renderingmanager.cpp |   6 +-
 apps/openmw/mwrender/renderingmanager.hpp |   4 +
 apps/openmw/mwscript/cellextensions.cpp   |  58 ++++++-
 apps/openmw/mwscript/docs/vmformat.txt    |   4 +-
 apps/openmw/mwworld/scene.cpp             | 178 +++++++++++++++++-----
 apps/openmw/mwworld/scene.hpp             |   9 +-
 apps/openmw/mwworld/store.cpp             |   8 +
 apps/openmw/mwworld/store.hpp             |   2 +
 apps/openmw/mwworld/worldimp.cpp          |  10 ++
 apps/openmw/mwworld/worldimp.hpp          |   3 +
 components/compiler/extensions0.cpp       |   2 +
 components/compiler/opcodes.hpp           |   2 +
 14 files changed, 247 insertions(+), 43 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 216547668f..9b50fdff6b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -235,6 +235,7 @@
     Feature #5147: Show spell magicka cost in spell buying window
     Feature #5170: Editor: Land shape editing, land selection
     Feature #5193: Weapon sheathing
+    Feature #5219: Impelement TestCells console command
     Feature #5224: Handle NiKeyframeController for NiTriShape
     Task #4686: Upgrade media decoder to a more current FFmpeg API
     Task #4695: Optimize Distant Terrain memory consumption
diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp
index a2e36d3d0e..0529140f46 100644
--- a/apps/openmw/mwbase/world.hpp
+++ b/apps/openmw/mwbase/world.hpp
@@ -118,6 +118,9 @@ namespace MWBase
 
             virtual MWWorld::CellStore *getCell (const ESM::CellId& id) = 0;
 
+            virtual void testExteriorCells() = 0;
+            virtual void testInteriorCells() = 0;
+
             virtual void useDeathCamera() = 0;
 
             virtual void setWaterHeight(const float height) = 0;
diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp
index ac4fb3a169..8f7b339b89 100644
--- a/apps/openmw/mwrender/renderingmanager.cpp
+++ b/apps/openmw/mwrender/renderingmanager.cpp
@@ -15,7 +15,6 @@
 #include <osg/TextureCubeMap>
 
 #include <osgUtil/LineSegmentIntersector>
-#include <osgUtil/IncrementalCompileOperation>
 
 #include <osg/ImageUtils>
 
@@ -391,6 +390,11 @@ namespace MWRender
         mWorkQueue = nullptr;
     }
 
+    osgUtil::IncrementalCompileOperation* RenderingManager::getIncrementalCompileOperation()
+    {
+        return mViewer->getIncrementalCompileOperation();
+    }
+
     MWRender::Objects& RenderingManager::getObjects()
     {
         return *mObjects.get();
diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp
index fb94caec88..22d2cf28c5 100644
--- a/apps/openmw/mwrender/renderingmanager.hpp
+++ b/apps/openmw/mwrender/renderingmanager.hpp
@@ -7,6 +7,8 @@
 
 #include <components/settings/settings.hpp>
 
+#include <osgUtil/IncrementalCompileOperation>
+
 #include "objects.hpp"
 
 #include "renderinginterface.hpp"
@@ -89,6 +91,8 @@ namespace MWRender
                          const std::string& resourcePath, DetourNavigator::Navigator& navigator);
         ~RenderingManager();
 
+        osgUtil::IncrementalCompileOperation* getIncrementalCompileOperation();
+
         MWRender::Objects& getObjects();
 
         Resource::ResourceSystem* getResourceSystem();
diff --git a/apps/openmw/mwscript/cellextensions.cpp b/apps/openmw/mwscript/cellextensions.cpp
index 80d4f7eb86..13ef98c634 100644
--- a/apps/openmw/mwscript/cellextensions.cpp
+++ b/apps/openmw/mwscript/cellextensions.cpp
@@ -10,11 +10,13 @@
 #include <components/interpreter/runtime.hpp>
 #include <components/interpreter/opcodes.hpp>
 
-#include "../mwbase/environment.hpp"
-#include "../mwbase/world.hpp"
-#include "../mwworld/player.hpp"
-#include "../mwworld/cellstore.hpp"
 #include "../mwworld/actionteleport.hpp"
+#include "../mwworld/cellstore.hpp"
+#include "../mwbase/environment.hpp"
+#include "../mwworld/player.hpp"
+#include "../mwbase/statemanager.hpp"
+#include "../mwbase/windowmanager.hpp"
+#include "../mwbase/world.hpp"
 
 #include "../mwmechanics/actorutil.hpp"
 
@@ -34,6 +36,52 @@ namespace MWScript
                 }
         };
 
+        class OpTestCells : public Interpreter::Opcode0
+        {
+            public:
+
+                virtual void execute (Interpreter::Runtime& runtime)
+                {
+                    if (MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_NoGame)
+                    {
+                        runtime.getContext().report("Use TestCells from the main menu, when there is no active game session.");
+                        return;
+                    }
+
+                    bool wasConsole = MWBase::Environment::get().getWindowManager()->isConsoleMode();
+                    if (wasConsole)
+                        MWBase::Environment::get().getWindowManager()->toggleConsole();
+
+                    MWBase::Environment::get().getWorld()->testExteriorCells();
+
+                    if (wasConsole)
+                        MWBase::Environment::get().getWindowManager()->toggleConsole();
+                }
+        };
+
+        class OpTestInteriorCells : public Interpreter::Opcode0
+        {
+            public:
+
+                virtual void execute (Interpreter::Runtime& runtime)
+                {
+                    if (MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_NoGame)
+                    {
+                        runtime.getContext().report("Use TestInteriorCells from the main menu, when there is no active game session.");
+                        return;
+                    }
+
+                    bool wasConsole = MWBase::Environment::get().getWindowManager()->isConsoleMode();
+                    if (wasConsole)
+                        MWBase::Environment::get().getWindowManager()->toggleConsole();
+
+                    MWBase::Environment::get().getWorld()->testInteriorCells();
+
+                    if (wasConsole)
+                        MWBase::Environment::get().getWindowManager()->toggleConsole();
+                }
+        };
+
         class OpCOC : public Interpreter::Opcode0
         {
             public:
@@ -204,6 +252,8 @@ namespace MWScript
         void installOpcodes (Interpreter::Interpreter& interpreter)
         {
             interpreter.installSegment5 (Compiler::Cell::opcodeCellChanged, new OpCellChanged);
+            interpreter.installSegment5 (Compiler::Cell::opcodeTestCells, new OpTestCells);
+            interpreter.installSegment5 (Compiler::Cell::opcodeTestInteriorCells, new OpTestInteriorCells);
             interpreter.installSegment5 (Compiler::Cell::opcodeCOC, new OpCOC);
             interpreter.installSegment5 (Compiler::Cell::opcodeCOE, new OpCOE);
             interpreter.installSegment5 (Compiler::Cell::opcodeGetInterior, new OpGetInterior);
diff --git a/apps/openmw/mwscript/docs/vmformat.txt b/apps/openmw/mwscript/docs/vmformat.txt
index b3029f4170..6795a058fc 100644
--- a/apps/openmw/mwscript/docs/vmformat.txt
+++ b/apps/openmw/mwscript/docs/vmformat.txt
@@ -461,5 +461,7 @@ op 0x200030a: SetNavMeshNumber
 op 0x200030b: Journal, explicit
 op 0x200030c: RepairedOnMe
 op 0x200030d: RepairedOnMe, explicit
+op 0x200030e: TestCells
+op 0x200030f: TestInteriorCells
 
-opcodes 0x200030c-0x3ffffff unused
+opcodes 0x2000310-0x3ffffff unused
diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp
index 0d1b98ebbb..4de04251b6 100644
--- a/apps/openmw/mwworld/scene.cpp
+++ b/apps/openmw/mwworld/scene.cpp
@@ -12,6 +12,7 @@
 #include <components/resource/resourcesystem.hpp>
 #include <components/resource/scenemanager.hpp>
 #include <components/resource/bulletshape.hpp>
+#include <components/sceneutil/unrefqueue.hpp>
 #include <components/detournavigator/navigator.hpp>
 #include <components/detournavigator/debug.hpp>
 #include <components/misc/convert.hpp>
@@ -22,6 +23,8 @@
 #include "../mwbase/mechanicsmanager.hpp"
 #include "../mwbase/windowmanager.hpp"
 
+#include "../mwmechanics/actorutil.hpp"
+
 #include "../mwrender/renderingmanager.hpp"
 #include "../mwrender/landmanager.hpp"
 
@@ -205,10 +208,11 @@ namespace
     {
         MWWorld::CellStore& mCell;
         Loading::Listener& mLoadingListener;
+        bool mTest;
 
         std::vector<MWWorld::Ptr> mToInsert;
 
-        InsertVisitor (MWWorld::CellStore& cell, Loading::Listener& loadingListener);
+        InsertVisitor (MWWorld::CellStore& cell, Loading::Listener& loadingListener, bool test);
 
         bool operator() (const MWWorld::Ptr& ptr);
 
@@ -216,8 +220,8 @@ namespace
         void insert(AddObject&& addObject);
     };
 
-    InsertVisitor::InsertVisitor (MWWorld::CellStore& cell, Loading::Listener& loadingListener)
-    : mCell (cell), mLoadingListener (loadingListener)
+    InsertVisitor::InsertVisitor (MWWorld::CellStore& cell, Loading::Listener& loadingListener, bool test)
+    : mCell (cell), mLoadingListener (loadingListener), mTest(test)
     {}
 
     bool InsertVisitor::operator() (const MWWorld::Ptr& ptr)
@@ -246,7 +250,8 @@ namespace
                 }
             }
 
-            mLoadingListener.increaseProgress (1);
+            if (!mTest)
+                mLoadingListener.increaseProgress (1);
         }
     }
 
@@ -317,9 +322,10 @@ namespace MWWorld
         mPreloader->updateCache(mRendering.getReferenceTime());
     }
 
-    void Scene::unloadCell (CellStoreCollection::iterator iter)
+    void Scene::unloadCell (CellStoreCollection::iterator iter, bool test)
     {
-        Log(Debug::Info) << "Unloading cell " << (*iter)->getCell()->getDescription();
+        if (!test)
+            Log(Debug::Info) << "Unloading cell " << (*iter)->getCell()->getDescription();
 
         const auto navigator = MWBase::Environment::get().getWorld()->getNavigator();
         ListAndResetObjectsVisitor visitor;
@@ -373,13 +379,16 @@ namespace MWWorld
         mActiveCells.erase(*iter);
     }
 
-    void Scene::loadCell (CellStore *cell, Loading::Listener* loadingListener, bool respawn)
+    void Scene::loadCell (CellStore *cell, Loading::Listener* loadingListener, bool respawn, bool test)
     {
         std::pair<CellStoreCollection::iterator, bool> result = mActiveCells.insert(cell);
 
         if(result.second)
         {
-            Log(Debug::Info) << "Loading cell " << cell->getCell()->getDescription();
+            if (test)
+                Log(Debug::Info) << "Testing cell " << cell->getCell()->getDescription();
+            else
+                Log(Debug::Info) << "Loading cell " << cell->getCell()->getDescription();
 
             float verts = ESM::Land::LAND_SIZE;
             float worldsize = ESM::Land::REAL_SIZE;
@@ -390,7 +399,7 @@ namespace MWWorld
             const int cellY = cell->getCell()->getGridY();
 
             // Load terrain physics first...
-            if (cell->getCell()->isExterior())
+            if (!test && cell->getCell()->isExterior())
             {
                 osg::ref_ptr<const ESMTerrain::LandObject> land = mRendering.getLandManager()->getLand(cellX, cellY);
                 const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VHGT) : 0;
@@ -418,38 +427,44 @@ namespace MWWorld
                 cell->respawn();
 
             // ... then references. This is important for adjustPosition to work correctly.
-            insertCell (*cell, loadingListener);
+            insertCell (*cell, loadingListener, test);
 
             mRendering.addCell(cell);
-            MWBase::Environment::get().getWindowManager()->addCell(cell);
-            bool waterEnabled = cell->getCell()->hasWater() || cell->isExterior();
-            float waterLevel = cell->getWaterLevel();
-            mRendering.setWaterEnabled(waterEnabled);
-            if (waterEnabled)
+            if (!test)
             {
-                mPhysics->enableWater(waterLevel);
-                mRendering.setWaterHeight(waterLevel);
-
-                if (cell->getCell()->isExterior())
+                MWBase::Environment::get().getWindowManager()->addCell(cell);
+                bool waterEnabled = cell->getCell()->hasWater() || cell->isExterior();
+                float waterLevel = cell->getWaterLevel();
+                mRendering.setWaterEnabled(waterEnabled);
+                if (waterEnabled)
                 {
-                    if (const auto heightField = mPhysics->getHeightField(cellX, cellY))
-                        navigator->addWater(osg::Vec2i(cellX, cellY), ESM::Land::REAL_SIZE,
-                            cell->getWaterLevel(), heightField->getCollisionObject()->getWorldTransform());
+                    mPhysics->enableWater(waterLevel);
+                    mRendering.setWaterHeight(waterLevel);
+
+                    if (cell->getCell()->isExterior())
+                    {
+                        if (const auto heightField = mPhysics->getHeightField(cellX, cellY))
+                            navigator->addWater(osg::Vec2i(cellX, cellY), ESM::Land::REAL_SIZE,
+                                cell->getWaterLevel(), heightField->getCollisionObject()->getWorldTransform());
+                    }
+                    else
+                    {
+                        navigator->addWater(osg::Vec2i(cellX, cellY), std::numeric_limits<int>::max(),
+                            cell->getWaterLevel(), btTransform::getIdentity());
+                    }
                 }
                 else
+                    mPhysics->disableWater();
+
+                const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr();
+                navigator->update(player.getRefData().getPosition().asVec3());
+
+                if (!cell->isExterior() && !(cell->getCell()->mData.mFlags & ESM::Cell::QuasiEx))
                 {
-                    navigator->addWater(osg::Vec2i(cellX, cellY), std::numeric_limits<int>::max(),
-                        cell->getWaterLevel(), btTransform::getIdentity());
+
+                    mRendering.configureAmbient(cell->getCell());
                 }
             }
-            else
-                mPhysics->disableWater();
-
-            const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr();
-            navigator->update(player.getRefData().getPosition().asVec3());
-
-            if (!cell->isExterior() && !(cell->getCell()->mData.mFlags & ESM::Cell::QuasiEx))
-                mRendering.configureAmbient(cell->getCell());
         }
 
         mPreloader->notifyLoaded(cell);
@@ -594,6 +609,101 @@ namespace MWWorld
             mCellChanged = true;
     }
 
+    void Scene::testExteriorCells()
+    {
+        // Note: temporary disable ICO to decrease memory usage
+        mRendering.getResourceSystem()->getSceneManager()->setIncrementalCompileOperation(nullptr);
+
+        mRendering.getResourceSystem()->setExpiryDelay(1.f);
+
+        const MWWorld::Store<ESM::Cell> &cells = MWBase::Environment::get().getWorld()->getStore().get<ESM::Cell>();
+
+        Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen();
+        Loading::ScopedLoad load(loadingListener);
+        loadingListener->setProgressRange(cells.getExtSize());
+
+        MWWorld::Store<ESM::Cell>::iterator it = cells.extBegin();
+        int i = 1;
+        for (; it != cells.extEnd(); ++it)
+        {
+            loadingListener->setLabel("Testing exterior cells ("+std::to_string(i)+"/"+std::to_string(cells.getExtSize())+")...");
+
+            CellStoreCollection::iterator iter = mActiveCells.begin();
+
+            CellStore *cell = MWBase::Environment::get().getWorld()->getExterior(it->mData.mX, it->mData.mY);
+            loadCell (cell, loadingListener, false, true);
+
+            iter = mActiveCells.begin();
+            while (iter != mActiveCells.end())
+            {
+                if (it->isExterior() && it->mData.mX == (*iter)->getCell()->getGridX() &&
+                    it->mData.mY == (*iter)->getCell()->getGridY())
+                {
+                    unloadCell(iter, true);
+                    break;
+                }
+
+                ++iter;
+            }
+
+            mRendering.getResourceSystem()->updateCache(mRendering.getReferenceTime());
+            mRendering.getUnrefQueue()->flush(mRendering.getWorkQueue());
+
+            loadingListener->increaseProgress (1);
+            i++;
+        }
+
+        mRendering.getResourceSystem()->getSceneManager()->setIncrementalCompileOperation(mRendering.getIncrementalCompileOperation());
+        mRendering.getResourceSystem()->setExpiryDelay(Settings::Manager::getFloat("cache expiry delay", "Cells"));
+    }
+
+    void Scene::testInteriorCells()
+    {
+        // Note: temporary disable ICO to decrease memory usage
+        mRendering.getResourceSystem()->getSceneManager()->setIncrementalCompileOperation(nullptr);
+
+        mRendering.getResourceSystem()->setExpiryDelay(1.f);
+
+        const MWWorld::Store<ESM::Cell> &cells = MWBase::Environment::get().getWorld()->getStore().get<ESM::Cell>();
+
+        Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen();
+        Loading::ScopedLoad load(loadingListener);
+        loadingListener->setProgressRange(cells.getIntSize());
+
+        int i = 1;
+        MWWorld::Store<ESM::Cell>::iterator it = cells.intBegin();
+        for (; it != cells.intEnd(); ++it)
+        {
+            loadingListener->setLabel("Testing interior cells ("+std::to_string(i)+"/"+std::to_string(cells.getIntSize())+")...");
+
+            CellStore *cell = MWBase::Environment::get().getWorld()->getInterior(it->mName);
+            loadCell (cell, loadingListener, false, true);
+
+            CellStoreCollection::iterator iter = mActiveCells.begin();
+            while (iter != mActiveCells.end())
+            {
+                assert (!(*iter)->getCell()->isExterior());
+
+                if (it->mName == (*iter)->getCell()->mName)
+                {
+                    unloadCell(iter, true);
+                    break;
+                }
+
+                ++iter;
+            }
+
+            mRendering.getResourceSystem()->updateCache(mRendering.getReferenceTime());
+            mRendering.getUnrefQueue()->flush(mRendering.getWorkQueue());
+
+            loadingListener->increaseProgress (1);
+            i++;
+        }
+
+        mRendering.getResourceSystem()->getSceneManager()->setIncrementalCompileOperation(mRendering.getIncrementalCompileOperation());
+        mRendering.getResourceSystem()->setExpiryDelay(Settings::Manager::getFloat("cache expiry delay", "Cells"));
+    }
+
     void Scene::changePlayerCell(CellStore *cell, const ESM::Position &pos, bool adjustPlayerPos)
     {
         mCurrentCell = cell;
@@ -759,9 +869,9 @@ namespace MWWorld
         mCellChanged = false;
     }
 
-    void Scene::insertCell (CellStore &cell, Loading::Listener* loadingListener)
+    void Scene::insertCell (CellStore &cell, Loading::Listener* loadingListener, bool test)
     {
-        InsertVisitor insertVisitor (cell, *loadingListener);
+        InsertVisitor insertVisitor (cell, *loadingListener, test);
         cell.forEach (insertVisitor);
         insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mRendering); });
         insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mNavigator); });
diff --git a/apps/openmw/mwworld/scene.hpp b/apps/openmw/mwworld/scene.hpp
index 57f994fab3..da795f84b7 100644
--- a/apps/openmw/mwworld/scene.hpp
+++ b/apps/openmw/mwworld/scene.hpp
@@ -85,7 +85,7 @@ namespace MWWorld
 
             osg::Vec3f mLastPlayerPos;
 
-            void insertCell (CellStore &cell, Loading::Listener* loadingListener);
+            void insertCell (CellStore &cell, Loading::Listener* loadingListener, bool test = false);
 
             // Load and unload cells as necessary to create a cell grid with "X" and "Y" in the center
             void changeCellGrid (int playerCellX, int playerCellY, bool changeEvent = true);
@@ -107,9 +107,9 @@ namespace MWWorld
             void preloadCell(MWWorld::CellStore* cell, bool preloadSurrounding=false);
             void preloadTerrain(const osg::Vec3f& pos);
 
-            void unloadCell (CellStoreCollection::iterator iter);
+            void unloadCell (CellStoreCollection::iterator iter, bool test = false);
 
-            void loadCell (CellStore *cell, Loading::Listener* loadingListener, bool respawn);
+            void loadCell (CellStore *cell, Loading::Listener* loadingListener, bool respawn, bool test = false);
 
             void playerMoved (const osg::Vec3f& pos);
 
@@ -151,6 +151,9 @@ namespace MWWorld
             Ptr searchPtrViaActorId (int actorId);
 
             void preload(const std::string& mesh, bool useAnim=false);
+
+            void testExteriorCells();
+            void testInteriorCells();
     };
 }
 
diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp
index 8ceab26b64..a414585efe 100644
--- a/apps/openmw/mwworld/store.cpp
+++ b/apps/openmw/mwworld/store.cpp
@@ -792,6 +792,14 @@ namespace MWWorld
     {
         return mSharedInt.size() + mSharedExt.size();
     }
+    size_t Store<ESM::Cell>::getExtSize() const
+    {
+        return mSharedExt.size();
+    }
+    size_t Store<ESM::Cell>::getIntSize() const
+    {
+        return mSharedInt.size();
+    }
     void Store<ESM::Cell>::listIdentifier(std::vector<std::string> &list) const
     {
         list.reserve(list.size() + mSharedInt.size());
diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp
index 4162e84c59..e7f2d35db0 100644
--- a/apps/openmw/mwworld/store.hpp
+++ b/apps/openmw/mwworld/store.hpp
@@ -314,6 +314,8 @@ namespace MWWorld
         const ESM::Cell *searchExtByRegion(const std::string &id) const;
 
         size_t getSize() const;
+        size_t getExtSize() const;
+        size_t getIntSize() const;
 
         void listIdentifier(std::vector<std::string> &list) const;
 
diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp
index 2ac27f0b70..71948119a8 100644
--- a/apps/openmw/mwworld/worldimp.cpp
+++ b/apps/openmw/mwworld/worldimp.cpp
@@ -586,6 +586,16 @@ namespace MWWorld
             return getInterior (id.mWorldspace);
     }
 
+    void World::testExteriorCells()
+    {
+        mWorldScene->testExteriorCells();
+    }
+
+    void World::testInteriorCells()
+    {
+        mWorldScene->testInteriorCells();
+    }
+
     void World::useDeathCamera()
     {
         if(mRendering->getCamera()->isVanityOrPreviewModeEnabled() )
diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp
index 6090e2ce4c..ed622b5b82 100644
--- a/apps/openmw/mwworld/worldimp.hpp
+++ b/apps/openmw/mwworld/worldimp.hpp
@@ -223,6 +223,9 @@ namespace MWWorld
 
             CellStore *getCell (const ESM::CellId& id) override;
 
+            void testExteriorCells() override;
+            void testInteriorCells() override;
+
             //switch to POV before showing player's death animation
             void useDeathCamera() override;
 
diff --git a/components/compiler/extensions0.cpp b/components/compiler/extensions0.cpp
index 0c7b0e26b5..5bec1bca14 100644
--- a/components/compiler/extensions0.cpp
+++ b/components/compiler/extensions0.cpp
@@ -89,6 +89,8 @@ namespace Compiler
         void registerExtensions (Extensions& extensions)
         {
             extensions.registerFunction ("cellchanged", 'l', "", opcodeCellChanged);
+            extensions.registerInstruction("testcells", "", opcodeTestCells);
+            extensions.registerInstruction("testinteriorcells", "", opcodeTestInteriorCells);
             extensions.registerInstruction ("coc", "S", opcodeCOC);
             extensions.registerInstruction ("centeroncell", "S", opcodeCOC);
             extensions.registerInstruction ("coe", "ll", opcodeCOE);
diff --git a/components/compiler/opcodes.hpp b/components/compiler/opcodes.hpp
index 47686d45ee..5fa8cc170f 100644
--- a/components/compiler/opcodes.hpp
+++ b/components/compiler/opcodes.hpp
@@ -75,6 +75,8 @@ namespace Compiler
     namespace Cell
     {
         const int opcodeCellChanged = 0x2000000;
+        const int opcodeTestCells = 0x200030e;
+        const int opcodeTestInteriorCells = 0x200030f;
         const int opcodeCOC = 0x2000026;
         const int opcodeCOE = 0x2000226;
         const int opcodeGetInterior = 0x2000131;