Implement TestCells (feature #5219)

pull/2622/head
Andrei Kortunov 5 years ago
parent 31c5c6d993
commit 24ce242941

@ -235,6 +235,7 @@
Feature #5147: Show spell magicka cost in spell buying window Feature #5147: Show spell magicka cost in spell buying window
Feature #5170: Editor: Land shape editing, land selection Feature #5170: Editor: Land shape editing, land selection
Feature #5193: Weapon sheathing Feature #5193: Weapon sheathing
Feature #5219: Impelement TestCells console command
Feature #5224: Handle NiKeyframeController for NiTriShape Feature #5224: Handle NiKeyframeController for NiTriShape
Task #4686: Upgrade media decoder to a more current FFmpeg API Task #4686: Upgrade media decoder to a more current FFmpeg API
Task #4695: Optimize Distant Terrain memory consumption Task #4695: Optimize Distant Terrain memory consumption

@ -118,6 +118,9 @@ namespace MWBase
virtual MWWorld::CellStore *getCell (const ESM::CellId& id) = 0; virtual MWWorld::CellStore *getCell (const ESM::CellId& id) = 0;
virtual void testExteriorCells() = 0;
virtual void testInteriorCells() = 0;
virtual void useDeathCamera() = 0; virtual void useDeathCamera() = 0;
virtual void setWaterHeight(const float height) = 0; virtual void setWaterHeight(const float height) = 0;

@ -15,7 +15,6 @@
#include <osg/TextureCubeMap> #include <osg/TextureCubeMap>
#include <osgUtil/LineSegmentIntersector> #include <osgUtil/LineSegmentIntersector>
#include <osgUtil/IncrementalCompileOperation>
#include <osg/ImageUtils> #include <osg/ImageUtils>
@ -391,6 +390,11 @@ namespace MWRender
mWorkQueue = nullptr; mWorkQueue = nullptr;
} }
osgUtil::IncrementalCompileOperation* RenderingManager::getIncrementalCompileOperation()
{
return mViewer->getIncrementalCompileOperation();
}
MWRender::Objects& RenderingManager::getObjects() MWRender::Objects& RenderingManager::getObjects()
{ {
return *mObjects.get(); return *mObjects.get();

@ -7,6 +7,8 @@
#include <components/settings/settings.hpp> #include <components/settings/settings.hpp>
#include <osgUtil/IncrementalCompileOperation>
#include "objects.hpp" #include "objects.hpp"
#include "renderinginterface.hpp" #include "renderinginterface.hpp"
@ -89,6 +91,8 @@ namespace MWRender
const std::string& resourcePath, DetourNavigator::Navigator& navigator); const std::string& resourcePath, DetourNavigator::Navigator& navigator);
~RenderingManager(); ~RenderingManager();
osgUtil::IncrementalCompileOperation* getIncrementalCompileOperation();
MWRender::Objects& getObjects(); MWRender::Objects& getObjects();
Resource::ResourceSystem* getResourceSystem(); Resource::ResourceSystem* getResourceSystem();

@ -10,11 +10,13 @@
#include <components/interpreter/runtime.hpp> #include <components/interpreter/runtime.hpp>
#include <components/interpreter/opcodes.hpp> #include <components/interpreter/opcodes.hpp>
#include "../mwworld/actionteleport.hpp"
#include "../mwworld/cellstore.hpp"
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
#include "../mwworld/player.hpp" #include "../mwworld/player.hpp"
#include "../mwworld/cellstore.hpp" #include "../mwbase/statemanager.hpp"
#include "../mwworld/actionteleport.hpp" #include "../mwbase/windowmanager.hpp"
#include "../mwbase/world.hpp"
#include "../mwmechanics/actorutil.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 class OpCOC : public Interpreter::Opcode0
{ {
public: public:
@ -204,6 +252,8 @@ namespace MWScript
void installOpcodes (Interpreter::Interpreter& interpreter) void installOpcodes (Interpreter::Interpreter& interpreter)
{ {
interpreter.installSegment5 (Compiler::Cell::opcodeCellChanged, new OpCellChanged); 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::opcodeCOC, new OpCOC);
interpreter.installSegment5 (Compiler::Cell::opcodeCOE, new OpCOE); interpreter.installSegment5 (Compiler::Cell::opcodeCOE, new OpCOE);
interpreter.installSegment5 (Compiler::Cell::opcodeGetInterior, new OpGetInterior); interpreter.installSegment5 (Compiler::Cell::opcodeGetInterior, new OpGetInterior);

@ -461,5 +461,7 @@ op 0x200030a: SetNavMeshNumber
op 0x200030b: Journal, explicit op 0x200030b: Journal, explicit
op 0x200030c: RepairedOnMe op 0x200030c: RepairedOnMe
op 0x200030d: RepairedOnMe, explicit op 0x200030d: RepairedOnMe, explicit
op 0x200030e: TestCells
op 0x200030f: TestInteriorCells
opcodes 0x200030c-0x3ffffff unused opcodes 0x2000310-0x3ffffff unused

@ -12,6 +12,7 @@
#include <components/resource/resourcesystem.hpp> #include <components/resource/resourcesystem.hpp>
#include <components/resource/scenemanager.hpp> #include <components/resource/scenemanager.hpp>
#include <components/resource/bulletshape.hpp> #include <components/resource/bulletshape.hpp>
#include <components/sceneutil/unrefqueue.hpp>
#include <components/detournavigator/navigator.hpp> #include <components/detournavigator/navigator.hpp>
#include <components/detournavigator/debug.hpp> #include <components/detournavigator/debug.hpp>
#include <components/misc/convert.hpp> #include <components/misc/convert.hpp>
@ -22,6 +23,8 @@
#include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
#include "../mwmechanics/actorutil.hpp"
#include "../mwrender/renderingmanager.hpp" #include "../mwrender/renderingmanager.hpp"
#include "../mwrender/landmanager.hpp" #include "../mwrender/landmanager.hpp"
@ -205,10 +208,11 @@ namespace
{ {
MWWorld::CellStore& mCell; MWWorld::CellStore& mCell;
Loading::Listener& mLoadingListener; Loading::Listener& mLoadingListener;
bool mTest;
std::vector<MWWorld::Ptr> mToInsert; 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); bool operator() (const MWWorld::Ptr& ptr);
@ -216,8 +220,8 @@ namespace
void insert(AddObject&& addObject); void insert(AddObject&& addObject);
}; };
InsertVisitor::InsertVisitor (MWWorld::CellStore& cell, Loading::Listener& loadingListener) InsertVisitor::InsertVisitor (MWWorld::CellStore& cell, Loading::Listener& loadingListener, bool test)
: mCell (cell), mLoadingListener (loadingListener) : mCell (cell), mLoadingListener (loadingListener), mTest(test)
{} {}
bool InsertVisitor::operator() (const MWWorld::Ptr& ptr) 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()); 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(); const auto navigator = MWBase::Environment::get().getWorld()->getNavigator();
ListAndResetObjectsVisitor visitor; ListAndResetObjectsVisitor visitor;
@ -373,13 +379,16 @@ namespace MWWorld
mActiveCells.erase(*iter); 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); std::pair<CellStoreCollection::iterator, bool> result = mActiveCells.insert(cell);
if(result.second) 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 verts = ESM::Land::LAND_SIZE;
float worldsize = ESM::Land::REAL_SIZE; float worldsize = ESM::Land::REAL_SIZE;
@ -390,7 +399,7 @@ namespace MWWorld
const int cellY = cell->getCell()->getGridY(); const int cellY = cell->getCell()->getGridY();
// Load terrain physics first... // 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); 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; const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VHGT) : 0;
@ -418,38 +427,44 @@ namespace MWWorld
cell->respawn(); cell->respawn();
// ... then references. This is important for adjustPosition to work correctly. // ... then references. This is important for adjustPosition to work correctly.
insertCell (*cell, loadingListener); insertCell (*cell, loadingListener, test);
mRendering.addCell(cell); mRendering.addCell(cell);
MWBase::Environment::get().getWindowManager()->addCell(cell); if (!test)
bool waterEnabled = cell->getCell()->hasWater() || cell->isExterior();
float waterLevel = cell->getWaterLevel();
mRendering.setWaterEnabled(waterEnabled);
if (waterEnabled)
{ {
mPhysics->enableWater(waterLevel); MWBase::Environment::get().getWindowManager()->addCell(cell);
mRendering.setWaterHeight(waterLevel); bool waterEnabled = cell->getCell()->hasWater() || cell->isExterior();
float waterLevel = cell->getWaterLevel();
if (cell->getCell()->isExterior()) mRendering.setWaterEnabled(waterEnabled);
if (waterEnabled)
{ {
if (const auto heightField = mPhysics->getHeightField(cellX, cellY)) mPhysics->enableWater(waterLevel);
navigator->addWater(osg::Vec2i(cellX, cellY), ESM::Land::REAL_SIZE, mRendering.setWaterHeight(waterLevel);
cell->getWaterLevel(), heightField->getCollisionObject()->getWorldTransform());
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 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); mPreloader->notifyLoaded(cell);
@ -594,6 +609,101 @@ namespace MWWorld
mCellChanged = true; 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) void Scene::changePlayerCell(CellStore *cell, const ESM::Position &pos, bool adjustPlayerPos)
{ {
mCurrentCell = cell; mCurrentCell = cell;
@ -759,9 +869,9 @@ namespace MWWorld
mCellChanged = false; 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); cell.forEach (insertVisitor);
insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mRendering); }); insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mRendering); });
insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mNavigator); }); insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mNavigator); });

@ -85,7 +85,7 @@ namespace MWWorld
osg::Vec3f mLastPlayerPos; 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 // 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); void changeCellGrid (int playerCellX, int playerCellY, bool changeEvent = true);
@ -107,9 +107,9 @@ namespace MWWorld
void preloadCell(MWWorld::CellStore* cell, bool preloadSurrounding=false); void preloadCell(MWWorld::CellStore* cell, bool preloadSurrounding=false);
void preloadTerrain(const osg::Vec3f& pos); 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); void playerMoved (const osg::Vec3f& pos);
@ -151,6 +151,9 @@ namespace MWWorld
Ptr searchPtrViaActorId (int actorId); Ptr searchPtrViaActorId (int actorId);
void preload(const std::string& mesh, bool useAnim=false); void preload(const std::string& mesh, bool useAnim=false);
void testExteriorCells();
void testInteriorCells();
}; };
} }

@ -792,6 +792,14 @@ namespace MWWorld
{ {
return mSharedInt.size() + mSharedExt.size(); 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 void Store<ESM::Cell>::listIdentifier(std::vector<std::string> &list) const
{ {
list.reserve(list.size() + mSharedInt.size()); list.reserve(list.size() + mSharedInt.size());

@ -314,6 +314,8 @@ namespace MWWorld
const ESM::Cell *searchExtByRegion(const std::string &id) const; const ESM::Cell *searchExtByRegion(const std::string &id) const;
size_t getSize() const; size_t getSize() const;
size_t getExtSize() const;
size_t getIntSize() const;
void listIdentifier(std::vector<std::string> &list) const; void listIdentifier(std::vector<std::string> &list) const;

@ -586,6 +586,16 @@ namespace MWWorld
return getInterior (id.mWorldspace); return getInterior (id.mWorldspace);
} }
void World::testExteriorCells()
{
mWorldScene->testExteriorCells();
}
void World::testInteriorCells()
{
mWorldScene->testInteriorCells();
}
void World::useDeathCamera() void World::useDeathCamera()
{ {
if(mRendering->getCamera()->isVanityOrPreviewModeEnabled() ) if(mRendering->getCamera()->isVanityOrPreviewModeEnabled() )

@ -223,6 +223,9 @@ namespace MWWorld
CellStore *getCell (const ESM::CellId& id) override; CellStore *getCell (const ESM::CellId& id) override;
void testExteriorCells() override;
void testInteriorCells() override;
//switch to POV before showing player's death animation //switch to POV before showing player's death animation
void useDeathCamera() override; void useDeathCamera() override;

@ -89,6 +89,8 @@ namespace Compiler
void registerExtensions (Extensions& extensions) void registerExtensions (Extensions& extensions)
{ {
extensions.registerFunction ("cellchanged", 'l', "", opcodeCellChanged); extensions.registerFunction ("cellchanged", 'l', "", opcodeCellChanged);
extensions.registerInstruction("testcells", "", opcodeTestCells);
extensions.registerInstruction("testinteriorcells", "", opcodeTestInteriorCells);
extensions.registerInstruction ("coc", "S", opcodeCOC); extensions.registerInstruction ("coc", "S", opcodeCOC);
extensions.registerInstruction ("centeroncell", "S", opcodeCOC); extensions.registerInstruction ("centeroncell", "S", opcodeCOC);
extensions.registerInstruction ("coe", "ll", opcodeCOE); extensions.registerInstruction ("coe", "ll", opcodeCOE);

@ -75,6 +75,8 @@ namespace Compiler
namespace Cell namespace Cell
{ {
const int opcodeCellChanged = 0x2000000; const int opcodeCellChanged = 0x2000000;
const int opcodeTestCells = 0x200030e;
const int opcodeTestInteriorCells = 0x200030f;
const int opcodeCOC = 0x2000026; const int opcodeCOC = 0x2000026;
const int opcodeCOE = 0x2000226; const int opcodeCOE = 0x2000226;
const int opcodeGetInterior = 0x2000131; const int opcodeGetInterior = 0x2000131;

Loading…
Cancel
Save