mirror of
https://github.com/OpenMW/openmw.git
synced 2025-07-01 22:41:34 +00:00
Merge branch 'cleanup_preloading' into 'master'
Cleanup preloading code See merge request OpenMW/openmw!4104
This commit is contained in:
commit
03652104fe
7 changed files with 74 additions and 70 deletions
|
@ -84,6 +84,7 @@ add_openmw_dir (mwworld
|
||||||
store esmstore fallback actionrepair actionsoulgem livecellref actiondoor
|
store esmstore fallback actionrepair actionsoulgem livecellref actiondoor
|
||||||
contentloader esmloader actiontrap cellreflist cellref weather projectilemanager
|
contentloader esmloader actiontrap cellreflist cellref weather projectilemanager
|
||||||
cellpreloader datetimemanager groundcoverstore magiceffects cell ptrregistry
|
cellpreloader datetimemanager groundcoverstore magiceffects cell ptrregistry
|
||||||
|
positioncellgrid
|
||||||
)
|
)
|
||||||
|
|
||||||
add_openmw_dir (mwphysics
|
add_openmw_dir (mwphysics
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
#include "cellpreloader.hpp"
|
#include "cellpreloader.hpp"
|
||||||
|
|
||||||
#include <array>
|
#include <algorithm>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
#include <osg/Stats>
|
#include <osg/Stats>
|
||||||
|
|
||||||
|
@ -26,32 +27,27 @@
|
||||||
#include "cellstore.hpp"
|
#include "cellstore.hpp"
|
||||||
#include "class.hpp"
|
#include "class.hpp"
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
template <class Contained>
|
|
||||||
bool contains(const std::vector<MWWorld::CellPreloader::PositionCellGrid>& container, const Contained& contained,
|
|
||||||
float tolerance)
|
|
||||||
{
|
|
||||||
for (const auto& pos : contained)
|
|
||||||
{
|
|
||||||
bool found = false;
|
|
||||||
for (const auto& pos2 : container)
|
|
||||||
{
|
|
||||||
if ((pos.first - pos2.first).length2() < tolerance * tolerance && pos.second == pos2.second)
|
|
||||||
{
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace MWWorld
|
namespace MWWorld
|
||||||
{
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool contains(std::span<const PositionCellGrid> positions, const PositionCellGrid& contained, float tolerance)
|
||||||
|
{
|
||||||
|
const float squaredTolerance = tolerance * tolerance;
|
||||||
|
const auto predicate = [&](const PositionCellGrid& v) {
|
||||||
|
return (contained.mPosition - v.mPosition).length2() < squaredTolerance
|
||||||
|
&& contained.mCellBounds == v.mCellBounds;
|
||||||
|
};
|
||||||
|
return std::ranges::any_of(positions, predicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool contains(
|
||||||
|
std::span<const PositionCellGrid> container, std::span<const PositionCellGrid> contained, float tolerance)
|
||||||
|
{
|
||||||
|
const auto predicate = [&](const PositionCellGrid& v) { return contains(container, v, tolerance); };
|
||||||
|
return std::ranges::all_of(contained, predicate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ListModelsVisitor
|
struct ListModelsVisitor
|
||||||
{
|
{
|
||||||
|
@ -169,12 +165,12 @@ namespace MWWorld
|
||||||
class TerrainPreloadItem : public SceneUtil::WorkItem
|
class TerrainPreloadItem : public SceneUtil::WorkItem
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
TerrainPreloadItem(const std::vector<osg::ref_ptr<Terrain::View>>& views, Terrain::World* world,
|
explicit TerrainPreloadItem(const std::vector<osg::ref_ptr<Terrain::View>>& views, Terrain::World* world,
|
||||||
const std::vector<CellPreloader::PositionCellGrid>& preloadPositions)
|
std::span<const PositionCellGrid> preloadPositions)
|
||||||
: mAbort(false)
|
: mAbort(false)
|
||||||
, mTerrainViews(views)
|
, mTerrainViews(views)
|
||||||
, mWorld(world)
|
, mWorld(world)
|
||||||
, mPreloadPositions(preloadPositions)
|
, mPreloadPositions(preloadPositions.begin(), preloadPositions.end())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,8 +179,8 @@ namespace MWWorld
|
||||||
for (unsigned int i = 0; i < mTerrainViews.size() && i < mPreloadPositions.size() && !mAbort; ++i)
|
for (unsigned int i = 0; i < mTerrainViews.size() && i < mPreloadPositions.size() && !mAbort; ++i)
|
||||||
{
|
{
|
||||||
mTerrainViews[i]->reset();
|
mTerrainViews[i]->reset();
|
||||||
mWorld->preload(mTerrainViews[i], mPreloadPositions[i].first, mPreloadPositions[i].second, mAbort,
|
mWorld->preload(mTerrainViews[i], mPreloadPositions[i].mPosition, mPreloadPositions[i].mCellBounds,
|
||||||
mLoadingReporter);
|
mAbort, mLoadingReporter);
|
||||||
}
|
}
|
||||||
mLoadingReporter.complete();
|
mLoadingReporter.complete();
|
||||||
}
|
}
|
||||||
|
@ -197,7 +193,7 @@ namespace MWWorld
|
||||||
std::atomic<bool> mAbort;
|
std::atomic<bool> mAbort;
|
||||||
std::vector<osg::ref_ptr<Terrain::View>> mTerrainViews;
|
std::vector<osg::ref_ptr<Terrain::View>> mTerrainViews;
|
||||||
Terrain::World* mWorld;
|
Terrain::World* mWorld;
|
||||||
std::vector<CellPreloader::PositionCellGrid> mPreloadPositions;
|
std::vector<PositionCellGrid> mPreloadPositions;
|
||||||
Loading::Reporter mLoadingReporter;
|
Loading::Reporter mLoadingReporter;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -375,19 +371,19 @@ namespace MWWorld
|
||||||
mTerrainPreloadItem->wait(listener);
|
mTerrainPreloadItem->wait(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CellPreloader::abortTerrainPreloadExcept(const CellPreloader::PositionCellGrid* exceptPos)
|
void CellPreloader::abortTerrainPreloadExcept(const PositionCellGrid* exceptPos)
|
||||||
{
|
{
|
||||||
if (exceptPos && contains(mTerrainPreloadPositions, std::array{ *exceptPos }, Constants::CellSizeInUnits))
|
if (exceptPos != nullptr && contains(mTerrainPreloadPositions, *exceptPos, Constants::CellSizeInUnits))
|
||||||
return;
|
return;
|
||||||
if (mTerrainPreloadItem && !mTerrainPreloadItem->isDone())
|
if (mTerrainPreloadItem && !mTerrainPreloadItem->isDone())
|
||||||
{
|
{
|
||||||
mTerrainPreloadItem->abort();
|
mTerrainPreloadItem->abort();
|
||||||
mTerrainPreloadItem->waitTillDone();
|
mTerrainPreloadItem->waitTillDone();
|
||||||
}
|
}
|
||||||
setTerrainPreloadPositions(std::vector<CellPreloader::PositionCellGrid>());
|
setTerrainPreloadPositions({});
|
||||||
}
|
}
|
||||||
|
|
||||||
void CellPreloader::setTerrainPreloadPositions(const std::vector<CellPreloader::PositionCellGrid>& positions)
|
void CellPreloader::setTerrainPreloadPositions(std::span<const PositionCellGrid> positions)
|
||||||
{
|
{
|
||||||
if (positions.empty())
|
if (positions.empty())
|
||||||
{
|
{
|
||||||
|
@ -408,7 +404,7 @@ namespace MWWorld
|
||||||
mTerrainViews.emplace_back(mTerrain->createView());
|
mTerrainViews.emplace_back(mTerrain->createView());
|
||||||
}
|
}
|
||||||
|
|
||||||
mTerrainPreloadPositions = positions;
|
mTerrainPreloadPositions.assign(positions.begin(), positions.end());
|
||||||
if (!positions.empty())
|
if (!positions.empty())
|
||||||
{
|
{
|
||||||
mTerrainPreloadItem = new TerrainPreloadItem(mTerrainViews, mTerrain, positions);
|
mTerrainPreloadItem = new TerrainPreloadItem(mTerrainViews, mTerrain, positions);
|
||||||
|
@ -417,10 +413,10 @@ namespace MWWorld
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CellPreloader::isTerrainLoaded(const CellPreloader::PositionCellGrid& position, double referenceTime) const
|
bool CellPreloader::isTerrainLoaded(const PositionCellGrid& position, double referenceTime) const
|
||||||
{
|
{
|
||||||
return mLoadedTerrainTimestamp + mResourceSystem->getSceneManager()->getExpiryDelay() > referenceTime
|
return mLoadedTerrainTimestamp + mResourceSystem->getSceneManager()->getExpiryDelay() > referenceTime
|
||||||
&& contains(mLoadedTerrainPositions, std::array{ position }, Constants::CellSizeInUnits);
|
&& contains(mLoadedTerrainPositions, position, Constants::CellSizeInUnits);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CellPreloader::setTerrain(Terrain::World* terrain)
|
void CellPreloader::setTerrain(Terrain::World* terrain)
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
#ifndef OPENMW_MWWORLD_CELLPRELOADER_H
|
#ifndef OPENMW_MWWORLD_CELLPRELOADER_H
|
||||||
#define OPENMW_MWWORLD_CELLPRELOADER_H
|
#define OPENMW_MWWORLD_CELLPRELOADER_H
|
||||||
|
|
||||||
|
#include "positioncellgrid.hpp"
|
||||||
|
|
||||||
#include <components/sceneutil/workqueue.hpp>
|
#include <components/sceneutil/workqueue.hpp>
|
||||||
|
|
||||||
#include <map>
|
|
||||||
|
|
||||||
#include <osg/Vec3f>
|
|
||||||
#include <osg/Vec4i>
|
|
||||||
#include <osg/ref_ptr>
|
#include <osg/ref_ptr>
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
namespace osg
|
namespace osg
|
||||||
{
|
{
|
||||||
|
@ -79,12 +78,11 @@ namespace MWWorld
|
||||||
|
|
||||||
void setWorkQueue(osg::ref_ptr<SceneUtil::WorkQueue> workQueue);
|
void setWorkQueue(osg::ref_ptr<SceneUtil::WorkQueue> workQueue);
|
||||||
|
|
||||||
typedef std::pair<osg::Vec3f, osg::Vec4i> PositionCellGrid;
|
void setTerrainPreloadPositions(std::span<const PositionCellGrid> positions);
|
||||||
void setTerrainPreloadPositions(const std::vector<PositionCellGrid>& positions);
|
|
||||||
|
|
||||||
void syncTerrainLoad(Loading::Listener& listener);
|
void syncTerrainLoad(Loading::Listener& listener);
|
||||||
void abortTerrainPreloadExcept(const PositionCellGrid* exceptPos);
|
void abortTerrainPreloadExcept(const PositionCellGrid* exceptPos);
|
||||||
bool isTerrainLoaded(const CellPreloader::PositionCellGrid& position, double referenceTime) const;
|
bool isTerrainLoaded(const PositionCellGrid& position, double referenceTime) const;
|
||||||
void setTerrain(Terrain::World* terrain);
|
void setTerrain(Terrain::World* terrain);
|
||||||
|
|
||||||
void reportStats(unsigned int frameNumber, osg::Stats& stats) const;
|
void reportStats(unsigned int frameNumber, osg::Stats& stats) const;
|
||||||
|
|
16
apps/openmw/mwworld/positioncellgrid.hpp
Normal file
16
apps/openmw/mwworld/positioncellgrid.hpp
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
#ifndef OPENMW_APPS_OPENMW_MWWORLD_POSITIONCELLGRID_H
|
||||||
|
#define OPENMW_APPS_OPENMW_MWWORLD_POSITIONCELLGRID_H
|
||||||
|
|
||||||
|
#include <osg/Vec3f>
|
||||||
|
#include <osg/Vec4i>
|
||||||
|
|
||||||
|
namespace MWWorld
|
||||||
|
{
|
||||||
|
struct PositionCellGrid
|
||||||
|
{
|
||||||
|
osg::Vec3f mPosition;
|
||||||
|
osg::Vec4i mCellBounds;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -603,7 +603,7 @@ namespace MWWorld
|
||||||
mPreloader->setTerrain(mRendering.getTerrain());
|
mPreloader->setTerrain(mRendering.getTerrain());
|
||||||
if (mRendering.pagingUnlockCache())
|
if (mRendering.pagingUnlockCache())
|
||||||
mPreloader->abortTerrainPreloadExcept(nullptr);
|
mPreloader->abortTerrainPreloadExcept(nullptr);
|
||||||
if (!mPreloader->isTerrainLoaded(std::make_pair(pos, newGrid), mRendering.getReferenceTime()))
|
if (!mPreloader->isTerrainLoaded(PositionCellGrid{ pos, newGrid }, mRendering.getReferenceTime()))
|
||||||
preloadTerrain(pos, playerCellIndex.mWorldspace, true);
|
preloadTerrain(pos, playerCellIndex.mWorldspace, true);
|
||||||
mPagedRefs.clear();
|
mPagedRefs.clear();
|
||||||
mRendering.getPagedRefnums(newGrid, mPagedRefs);
|
mRendering.getPagedRefnums(newGrid, mPagedRefs);
|
||||||
|
@ -1093,26 +1093,25 @@ namespace MWWorld
|
||||||
osg::Vec3f predictedPos = playerPos + moved / dt * mPredictionTime;
|
osg::Vec3f predictedPos = playerPos + moved / dt * mPredictionTime;
|
||||||
|
|
||||||
if (mCurrentCell->isExterior())
|
if (mCurrentCell->isExterior())
|
||||||
exteriorPositions.emplace_back(
|
exteriorPositions.push_back(PositionCellGrid{
|
||||||
predictedPos, gridCenterToBounds(getNewGridCenter(predictedPos, &mCurrentGridCenter)));
|
predictedPos, gridCenterToBounds(getNewGridCenter(predictedPos, &mCurrentGridCenter)) });
|
||||||
|
|
||||||
mLastPlayerPos = playerPos;
|
mLastPlayerPos = playerPos;
|
||||||
|
|
||||||
if (mPreloadEnabled)
|
if (mPreloadEnabled)
|
||||||
{
|
{
|
||||||
if (mPreloadDoors)
|
if (mPreloadDoors)
|
||||||
preloadTeleportDoorDestinations(playerPos, predictedPos, exteriorPositions);
|
preloadTeleportDoorDestinations(playerPos, predictedPos);
|
||||||
if (mPreloadExteriorGrid)
|
if (mPreloadExteriorGrid)
|
||||||
preloadExteriorGrid(playerPos, predictedPos);
|
preloadExteriorGrid(playerPos, predictedPos);
|
||||||
if (mPreloadFastTravel)
|
if (mPreloadFastTravel)
|
||||||
preloadFastTravelDestinations(playerPos, predictedPos, exteriorPositions);
|
preloadFastTravelDestinations(playerPos, exteriorPositions);
|
||||||
}
|
}
|
||||||
|
|
||||||
mPreloader->setTerrainPreloadPositions(exteriorPositions);
|
mPreloader->setTerrainPreloadPositions(exteriorPositions);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Scene::preloadTeleportDoorDestinations(
|
void Scene::preloadTeleportDoorDestinations(const osg::Vec3f& playerPos, const osg::Vec3f& predictedPos)
|
||||||
const osg::Vec3f& playerPos, const osg::Vec3f& predictedPos, std::vector<PositionCellGrid>& exteriorPositions)
|
|
||||||
{
|
{
|
||||||
std::vector<MWWorld::ConstPtr> teleportDoors;
|
std::vector<MWWorld::ConstPtr> teleportDoors;
|
||||||
for (const MWWorld::CellStore* cellStore : mActiveCells)
|
for (const MWWorld::CellStore* cellStore : mActiveCells)
|
||||||
|
@ -1232,10 +1231,9 @@ namespace MWWorld
|
||||||
void Scene::preloadTerrain(const osg::Vec3f& pos, ESM::RefId worldspace, bool sync)
|
void Scene::preloadTerrain(const osg::Vec3f& pos, ESM::RefId worldspace, bool sync)
|
||||||
{
|
{
|
||||||
ESM::ExteriorCellLocation cellPos = ESM::positionToExteriorCellLocation(pos.x(), pos.y(), worldspace);
|
ESM::ExteriorCellLocation cellPos = ESM::positionToExteriorCellLocation(pos.x(), pos.y(), worldspace);
|
||||||
std::vector<PositionCellGrid> vec;
|
const PositionCellGrid position{ pos, gridCenterToBounds({ cellPos.mX, cellPos.mY }) };
|
||||||
vec.emplace_back(pos, gridCenterToBounds({ cellPos.mX, cellPos.mY }));
|
mPreloader->abortTerrainPreloadExcept(&position);
|
||||||
mPreloader->abortTerrainPreloadExcept(vec.data());
|
mPreloader->setTerrainPreloadPositions(std::span(&position, 1));
|
||||||
mPreloader->setTerrainPreloadPositions(vec);
|
|
||||||
if (!sync)
|
if (!sync)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -1249,7 +1247,7 @@ namespace MWWorld
|
||||||
|
|
||||||
void Scene::reloadTerrain()
|
void Scene::reloadTerrain()
|
||||||
{
|
{
|
||||||
mPreloader->setTerrainPreloadPositions(std::vector<PositionCellGrid>());
|
mPreloader->setTerrainPreloadPositions({});
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ListFastTravelDestinationsVisitor
|
struct ListFastTravelDestinationsVisitor
|
||||||
|
@ -1282,12 +1280,10 @@ namespace MWWorld
|
||||||
std::vector<ESM::Transport::Dest> mList;
|
std::vector<ESM::Transport::Dest> mList;
|
||||||
};
|
};
|
||||||
|
|
||||||
void Scene::preloadFastTravelDestinations(const osg::Vec3f& playerPos, const osg::Vec3f& /*predictedPos*/,
|
void Scene::preloadFastTravelDestinations(
|
||||||
std::vector<PositionCellGrid>& exteriorPositions) // ignore predictedPos here since opening dialogue with
|
const osg::Vec3f& playerPos, std::vector<PositionCellGrid>& exteriorPositions)
|
||||||
// travel service takes extra time
|
|
||||||
{
|
{
|
||||||
const MWWorld::ConstPtr player = mWorld.getPlayerPtr();
|
ListFastTravelDestinationsVisitor listVisitor(mPreloadDistance, playerPos);
|
||||||
ListFastTravelDestinationsVisitor listVisitor(mPreloadDistance, player.getRefData().getPosition().asVec3());
|
|
||||||
ESM::RefId extWorldspace = mWorld.getCurrentWorldspace();
|
ESM::RefId extWorldspace = mWorld.getCurrentWorldspace();
|
||||||
for (MWWorld::CellStore* cellStore : mActiveCells)
|
for (MWWorld::CellStore* cellStore : mActiveCells)
|
||||||
{
|
{
|
||||||
|
@ -1305,7 +1301,7 @@ namespace MWWorld
|
||||||
const ESM::ExteriorCellLocation cellIndex
|
const ESM::ExteriorCellLocation cellIndex
|
||||||
= ESM::positionToExteriorCellLocation(pos.x(), pos.y(), extWorldspace);
|
= ESM::positionToExteriorCellLocation(pos.x(), pos.y(), extWorldspace);
|
||||||
preloadCellWithSurroundings(mWorld.getWorldModel().getExterior(cellIndex));
|
preloadCellWithSurroundings(mWorld.getWorldModel().getExterior(cellIndex));
|
||||||
exteriorPositions.emplace_back(pos, gridCenterToBounds(getNewGridCenter(pos)));
|
exteriorPositions.push_back(PositionCellGrid{ pos, gridCenterToBounds(getNewGridCenter(pos)) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
#include <osg/Vec4i>
|
#include <osg/Vec4i>
|
||||||
#include <osg/ref_ptr>
|
#include <osg/ref_ptr>
|
||||||
|
|
||||||
|
#include "positioncellgrid.hpp"
|
||||||
#include "ptr.hpp"
|
#include "ptr.hpp"
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
@ -122,14 +123,11 @@ namespace MWWorld
|
||||||
|
|
||||||
void requestChangeCellGrid(const osg::Vec3f& position, const osg::Vec2i& cell, bool changeEvent = true);
|
void requestChangeCellGrid(const osg::Vec3f& position, const osg::Vec2i& cell, bool changeEvent = true);
|
||||||
|
|
||||||
typedef std::pair<osg::Vec3f, osg::Vec4i> PositionCellGrid;
|
|
||||||
|
|
||||||
void preloadCells(float dt);
|
void preloadCells(float dt);
|
||||||
void preloadTeleportDoorDestinations(const osg::Vec3f& playerPos, const osg::Vec3f& predictedPos,
|
void preloadTeleportDoorDestinations(const osg::Vec3f& playerPos, const osg::Vec3f& predictedPos);
|
||||||
std::vector<PositionCellGrid>& exteriorPositions);
|
|
||||||
void preloadExteriorGrid(const osg::Vec3f& playerPos, const osg::Vec3f& predictedPos);
|
void preloadExteriorGrid(const osg::Vec3f& playerPos, const osg::Vec3f& predictedPos);
|
||||||
void preloadFastTravelDestinations(const osg::Vec3f& playerPos, const osg::Vec3f& predictedPos,
|
void preloadFastTravelDestinations(
|
||||||
std::vector<PositionCellGrid>& exteriorPositions);
|
const osg::Vec3f& playerPos, std::vector<PositionCellGrid>& exteriorPositions);
|
||||||
|
|
||||||
osg::Vec4i gridCenterToBounds(const osg::Vec2i& centerCell) const;
|
osg::Vec4i gridCenterToBounds(const osg::Vec2i& centerCell) const;
|
||||||
osg::Vec2i getNewGridCenter(const osg::Vec3f& pos, const osg::Vec2i* currentGridCenter = nullptr) const;
|
osg::Vec2i getNewGridCenter(const osg::Vec3f& pos, const osg::Vec2i* currentGridCenter = nullptr) const;
|
||||||
|
|
|
@ -70,7 +70,6 @@ namespace Terrain
|
||||||
mNodes.clear();
|
mNodes.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const osg::Vec4i& getActiveGrid() const { return mActiveGrid; }
|
|
||||||
|
|
||||||
unsigned int getWorldUpdateRevision() const { return mWorldUpdateRevision; }
|
unsigned int getWorldUpdateRevision() const { return mWorldUpdateRevision; }
|
||||||
void setWorldUpdateRevision(int updateRevision) { mWorldUpdateRevision = updateRevision; }
|
void setWorldUpdateRevision(int updateRevision) { mWorldUpdateRevision = updateRevision; }
|
||||||
|
|
Loading…
Reference in a new issue