From Trajce Nikolov, support for use of PagedLOD in the txp plugin.
This commit is contained in:
parent
c199e74bfc
commit
fa8a06990d
@ -37,6 +37,9 @@
|
|||||||
|
|
||||||
bool osgDB::fileExists(const std::string& filename)
|
bool osgDB::fileExists(const std::string& filename)
|
||||||
{
|
{
|
||||||
|
// hack for getting TXP plugin to utilise PagedLOD.
|
||||||
|
if (getLowerCaseFileExtension(filename)=="txp") return true;
|
||||||
|
|
||||||
return access( filename.c_str(), F_OK ) == 0;
|
return access( filename.c_str(), F_OK ) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
#include <osgDB/Registry>
|
#include <osgDB/Registry>
|
||||||
#include <osgDB/Input>
|
#include <osgDB/Input>
|
||||||
#include <osgDB/Output>
|
#include <osgDB/Output>
|
||||||
|
#include <osg/ref_ptr>
|
||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
|
@ -16,14 +16,13 @@
|
|||||||
using namespace txp;
|
using namespace txp;
|
||||||
using namespace osg;
|
using namespace osg;
|
||||||
|
|
||||||
|
|
||||||
osgDB::ReaderWriter::ReadResult ReaderWriterTXP::readNode(const std::string& fileName, const osgDB::ReaderWriter::Options* options)
|
osgDB::ReaderWriter::ReadResult ReaderWriterTXP::readNode(const std::string& fileName, const osgDB::ReaderWriter::Options* options)
|
||||||
{
|
{
|
||||||
if( !acceptsExtension(osgDB::getFileExtension(fileName) ))
|
if( !acceptsExtension(osgDB::getFileExtension(fileName) ))
|
||||||
return ReadResult::FILE_NOT_HANDLED;
|
return ReadResult::FILE_NOT_HANDLED;
|
||||||
|
|
||||||
ref_ptr<TerrapageNode> pager = new TerrapageNode;
|
osg::ref_ptr<txp::TerrapageNode> pager = new TerrapageNode;
|
||||||
|
|
||||||
pager->setDatabaseName(fileName);
|
pager->setDatabaseName(fileName);
|
||||||
|
|
||||||
if (options)
|
if (options)
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
#include "TerrapageNode.h"
|
#include "TerrapageNode.h"
|
||||||
#include <osg/Notify>
|
#include <osg/Notify>
|
||||||
|
#include <osgDB/FileUtils>
|
||||||
|
#include <osgDB/FileNameUtils>
|
||||||
using namespace osg;
|
using namespace osg;
|
||||||
|
|
||||||
namespace txp
|
namespace txp
|
||||||
{
|
{
|
||||||
|
|
||||||
|
osg::ref_ptr<TrPageArchive> TerrapageNode::_archive = NULL;
|
||||||
|
|
||||||
TerrapageNode::TerrapageNode():
|
TerrapageNode::TerrapageNode():
|
||||||
_pageManager(0)
|
_pageManager(0)
|
||||||
{
|
{
|
||||||
setNumChildrenRequiringUpdateTraversal(1);
|
setNumChildrenRequiringUpdateTraversal(1);
|
||||||
|
_dbLoaded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
TerrapageNode::TerrapageNode(const TerrapageNode& pager,const osg::CopyOp&):
|
TerrapageNode::TerrapageNode(const TerrapageNode& pager,const osg::CopyOp&):
|
||||||
@ -21,6 +25,7 @@ TerrapageNode::TerrapageNode(const TerrapageNode& pager,const osg::CopyOp&):
|
|||||||
_lastRecordEyePoint(pager._lastRecordEyePoint)
|
_lastRecordEyePoint(pager._lastRecordEyePoint)
|
||||||
{
|
{
|
||||||
setNumChildrenRequiringUpdateTraversal(getNumChildrenRequiringUpdateTraversal()+1);
|
setNumChildrenRequiringUpdateTraversal(getNumChildrenRequiringUpdateTraversal()+1);
|
||||||
|
_dbLoaded = pager._dbLoaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
TerrapageNode::~TerrapageNode()
|
TerrapageNode::~TerrapageNode()
|
||||||
@ -49,6 +54,45 @@ void TerrapageNode::traverse(osg::NodeVisitor& nv)
|
|||||||
|
|
||||||
bool TerrapageNode::loadDatabase()
|
bool TerrapageNode::loadDatabase()
|
||||||
{
|
{
|
||||||
|
std::string name = osgDB::getSimpleFileName(_databaseName);
|
||||||
|
|
||||||
|
// Here we load subtiles for a tile
|
||||||
|
if (strncmp(name.c_str(),"subtiles",8)==0)
|
||||||
|
{
|
||||||
|
std::string path = osgDB::getFilePath(_databaseName);
|
||||||
|
_databaseName = path+"\\archive.txp";
|
||||||
|
|
||||||
|
int lod;
|
||||||
|
int x;
|
||||||
|
int y;
|
||||||
|
sscanf(name.c_str(),"subtiles%d_%dx%d",&lod,&x,&y);
|
||||||
|
|
||||||
|
float64 range;
|
||||||
|
TerrapageNode::_archive->GetHeader()->GetLodRange(lod+1,range);
|
||||||
|
|
||||||
|
trpg2dPoint tileSize;
|
||||||
|
TerrapageNode::_archive->GetHeader()->GetTileSize(lod+1,tileSize);
|
||||||
|
|
||||||
|
trpg2dPoint sw;
|
||||||
|
trpg2dPoint ne;
|
||||||
|
TerrapageNode::_archive->GetHeader()->GetExtents(sw,ne);
|
||||||
|
|
||||||
|
for (int ix = 0; ix < 2; ix++)
|
||||||
|
for (int iy = 0; iy < 2; iy++)
|
||||||
|
{
|
||||||
|
int tileX = x*2+ix;
|
||||||
|
int tileY = y*2+iy;
|
||||||
|
int tileLOD = lod+1;
|
||||||
|
|
||||||
|
int parentID;
|
||||||
|
addChild(TerrapageNode::_archive->LoadTile(tileX,tileY,tileLOD,parentID));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//std::cout << "subtiles paged in: " << x << " " << y << " " << lod << std::endl;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Open the TXP database
|
// Open the TXP database
|
||||||
TrPageArchive *txpArchive = new TrPageArchive();
|
TrPageArchive *txpArchive = new TrPageArchive();
|
||||||
if (!txpArchive->OpenFile(_databaseName.c_str()))
|
if (!txpArchive->OpenFile(_databaseName.c_str()))
|
||||||
@ -56,7 +100,7 @@ bool TerrapageNode::loadDatabase()
|
|||||||
osg::notify(osg::WARN)<<"Couldn't load TerraPage archive "<<_databaseName<<std::endl;
|
osg::notify(osg::WARN)<<"Couldn't load TerraPage archive "<<_databaseName<<std::endl;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Should be checking the return values
|
// Note: Should be checking the return values
|
||||||
txpArchive->LoadMaterials();
|
txpArchive->LoadMaterials();
|
||||||
|
|
||||||
@ -66,6 +110,21 @@ bool TerrapageNode::loadDatabase()
|
|||||||
// Note: Should be checking the return values
|
// Note: Should be checking the return values
|
||||||
txpArchive->LoadLightAttributes();
|
txpArchive->LoadLightAttributes();
|
||||||
|
|
||||||
|
if (TerrapageNode::_archive == NULL)
|
||||||
|
{
|
||||||
|
TerrapageNode::_archive = new TrPageArchive();
|
||||||
|
if (!TerrapageNode::_archive->OpenFile(_databaseName.c_str()))
|
||||||
|
{
|
||||||
|
osg::notify(osg::WARN)<<"Couldn't load interanal TerraPage archive "<<_databaseName<<std::endl;
|
||||||
|
TerrapageNode::_archive = NULL;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TerrapageNode::_archive->LoadMaterials();
|
||||||
|
TerrapageNode::_archive->LoadModels();
|
||||||
|
TerrapageNode::_archive->LoadLightAttributes();
|
||||||
|
}
|
||||||
|
|
||||||
// get the exents of the archive
|
// get the exents of the archive
|
||||||
const trpgHeader *head = txpArchive->GetHeader();
|
const trpgHeader *head = txpArchive->GetHeader();
|
||||||
trpg2dPoint sw,ne;
|
trpg2dPoint sw,ne;
|
||||||
@ -132,6 +191,8 @@ bool TerrapageNode::loadDatabase()
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_dbLoaded = true;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
namespace txp
|
namespace txp
|
||||||
{
|
{
|
||||||
|
class TrPageArchive;
|
||||||
class TerrapageNode : public osg::Group
|
class TerrapageNode : public osg::Group
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@ -54,6 +54,9 @@ class TerrapageNode : public osg::Group
|
|||||||
std::string _databaseOptions;
|
std::string _databaseOptions;
|
||||||
OSGPageManager* _pageManager;
|
OSGPageManager* _pageManager;
|
||||||
mutable osg::Vec3 _lastRecordEyePoint;
|
mutable osg::Vec3 _lastRecordEyePoint;
|
||||||
|
|
||||||
|
static osg::ref_ptr<TrPageArchive> _archive;
|
||||||
|
bool _dbLoaded;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -534,6 +534,7 @@ Group* TrPageArchive::LoadTile(int x,int y,int lod,int &parentID)
|
|||||||
if (!ReadTile(x,y,lod,buf))
|
if (!ReadTile(x,y,lod,buf))
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
|
parse->SetTile(x,y,lod);
|
||||||
Group *tile = parse->ParseScene(buf, m_gstates , m_models);
|
Group *tile = parse->ParseScene(buf, m_gstates , m_models);
|
||||||
if (tile)
|
if (tile)
|
||||||
{
|
{
|
||||||
@ -562,6 +563,7 @@ Group* TrPageArchive::LoadTile(Group *rootNode,
|
|||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
// Now parse it
|
// Now parse it
|
||||||
|
parse->SetTile(x,y,lod);
|
||||||
Group *gTile = parse->ParseScene(buf, m_gstates, m_models);
|
Group *gTile = parse->ParseScene(buf, m_gstates, m_models);
|
||||||
if (gTile && rootNode) {
|
if (gTile && rootNode) {
|
||||||
// Hook it into its parent
|
// Hook it into its parent
|
||||||
|
@ -47,7 +47,7 @@ namespace txp
|
|||||||
osg::Vec3 attitude;
|
osg::Vec3 attitude;
|
||||||
};
|
};
|
||||||
/// main class for loading terrapage archives
|
/// main class for loading terrapage archives
|
||||||
class TrPageArchive : public trpgr_Archive
|
class TrPageArchive : public trpgr_Archive, public osg::Referenced
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
TrPageArchive();
|
TrPageArchive();
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
#include <osg/Notify>
|
#include <osg/Notify>
|
||||||
#include <osg/PolygonOffset>
|
#include <osg/PolygonOffset>
|
||||||
#include <osg/MatrixTransform>
|
#include <osg/MatrixTransform>
|
||||||
|
#include <osg/PagedLOD>
|
||||||
#include <osgSim/LightPointNode>
|
#include <osgSim/LightPointNode>
|
||||||
#include <osg/Point>
|
#include <osg/Point>
|
||||||
|
|
||||||
@ -676,7 +676,8 @@ void* attachRead::Parse(trpgToken /*tok*/,trpgReadBuffer &buf)
|
|||||||
// This sets the parent ID for the current tile too
|
// This sets the parent ID for the current tile too
|
||||||
int32 parentID;
|
int32 parentID;
|
||||||
group.GetParentID(parentID);
|
group.GetParentID(parentID);
|
||||||
parse->SetParentID(parentID);
|
|
||||||
|
//parse->SetParentID(parentID); no need of this anymore. we force PagedLOD
|
||||||
return (void *) osg_Group;
|
return (void *) osg_Group;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -793,6 +794,10 @@ void* lodRead::Parse(trpgToken /*tok*/,trpgReadBuffer &buf)
|
|||||||
|
|
||||||
// Dump this group into the hierarchy
|
// Dump this group into the hierarchy
|
||||||
parse->AddIntoSceneGraph(osg_Lod);
|
parse->AddIntoSceneGraph(osg_Lod);
|
||||||
|
|
||||||
|
// Sets the current parent as potentional PagedLOD
|
||||||
|
parse->SetPotentionalPagedLOD(parse->GetCurrTop());
|
||||||
|
|
||||||
// Register for attachements
|
// Register for attachements
|
||||||
int32 id;
|
int32 id;
|
||||||
lod.GetID(id);
|
lod.GetID(id);
|
||||||
@ -982,6 +987,44 @@ trpgTileHeader *TrPageParser::GetTileHeaderRef()
|
|||||||
return &tileHead;
|
return &tileHead;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
// Converts to PagedLOD
|
||||||
|
void TrPageParser::ConvertToPagedLOD(osg::Group* group)
|
||||||
|
{
|
||||||
|
if (group->getNumChildren() == 2)
|
||||||
|
{
|
||||||
|
osg::LOD* loLOD = dynamic_cast<osg::LOD*>(group->getChild(0));
|
||||||
|
osg::LOD* hiLOD = dynamic_cast<osg::LOD*>(group->getChild(1));
|
||||||
|
|
||||||
|
if (loLOD && hiLOD)
|
||||||
|
{
|
||||||
|
osg::Group *g = dynamic_cast<osg::Group*>(hiLOD->getChild(0));
|
||||||
|
if (!g) return;
|
||||||
|
if (g->getNumChildren()) return;
|
||||||
|
|
||||||
|
char pagedLODfile[1024];
|
||||||
|
sprintf(pagedLODfile,
|
||||||
|
"%s\\subtiles%d_%dx%d.txp",
|
||||||
|
parent_->getDir(),
|
||||||
|
_tileLOD,
|
||||||
|
_tileX,
|
||||||
|
_tileY
|
||||||
|
);
|
||||||
|
|
||||||
|
osg::PagedLOD* pagedlod = new osg::PagedLOD;
|
||||||
|
|
||||||
|
pagedlod->addChild(loLOD->getChild(0),loLOD->getMinRange(0),loLOD->getMaxRange(0));
|
||||||
|
pagedlod->setRange(1,0.0f,hiLOD->getMaxRange(0));
|
||||||
|
pagedlod->setFileName(1,pagedLODfile);
|
||||||
|
pagedlod->setCenter(hiLOD->getCenter());
|
||||||
|
|
||||||
|
group->addChild(pagedlod);
|
||||||
|
|
||||||
|
group->removeChild(loLOD);
|
||||||
|
group->removeChild(hiLOD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
//----------------------------------------------------------------------------
|
//----------------------------------------------------------------------------
|
||||||
// Parse a buffer and return a (chunk of) Performer
|
// Parse a buffer and return a (chunk of) Performer
|
||||||
// scene graph.
|
// scene graph.
|
||||||
@ -1000,6 +1043,13 @@ Group *TrPageParser::ParseScene(trpgReadBuffer &buf,vector<ref_ptr<StateSet> > &
|
|||||||
notify(WARN) << "trpgFPParser::ParseScene failed to parse tile.\n";
|
notify(WARN) << "trpgFPParser::ParseScene failed to parse tile.\n";
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Puts PagedLODs on the right places
|
||||||
|
for (std::map<osg::Group*,int>::iterator i = _pagedLods.begin(); i != _pagedLods.end(); i++)
|
||||||
|
{
|
||||||
|
ConvertToPagedLOD((*i).first);
|
||||||
|
}
|
||||||
|
_pagedLods.clear();
|
||||||
|
|
||||||
Group *ret = top;
|
Group *ret = top;
|
||||||
top = currTop = NULL;
|
top = currTop = NULL;
|
||||||
|
@ -160,6 +160,18 @@ namespace txp
|
|||||||
|
|
||||||
DefferedLightAttribute& GetLightAttribute(int attr_index);
|
DefferedLightAttribute& GetLightAttribute(int attr_index);
|
||||||
|
|
||||||
|
// Sets the info about the tile that is being parsed
|
||||||
|
inline void SetTile(int x, int y, int lod)
|
||||||
|
{
|
||||||
|
_tileX = x; _tileY = y; _tileLOD = lod;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets a group as potentinal PagedLOD - see below
|
||||||
|
inline void SetPotentionalPagedLOD(osg::Group* group)
|
||||||
|
{
|
||||||
|
_pagedLods[group] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Called on start children
|
// Called on start children
|
||||||
bool StartChildren(void *);
|
bool StartChildren(void *);
|
||||||
@ -167,6 +179,25 @@ namespace txp
|
|||||||
// Called on end children
|
// Called on end children
|
||||||
bool EndChildren(void *);
|
bool EndChildren(void *);
|
||||||
|
|
||||||
|
// LOD parents
|
||||||
|
// These will help us to find the "LOD Bridges" in the scene graph. "LOD Bridge" is a
|
||||||
|
// group that holds two LOD nodes: one is the parent of the "current" LOD implementation of
|
||||||
|
// a tile, the other is parent of the quad of the higher-res LOD implementation of the same
|
||||||
|
// tile - it has four higher-res tiles. After a tile is loaded, we replace the "LOD bridge"
|
||||||
|
// with PagedLOD node
|
||||||
|
// nick@terrex.com
|
||||||
|
std::map<osg::Group*,int> _pagedLods;
|
||||||
|
|
||||||
|
// Converts to PagedLOD
|
||||||
|
// If the given group is "LOD Bridge" this method will convert it into appropriate PagedLOD
|
||||||
|
void ConvertToPagedLOD(osg::Group* group);
|
||||||
|
|
||||||
|
// Current tile that is being loaded
|
||||||
|
int _tileX;
|
||||||
|
int _tileY;
|
||||||
|
int _tileLOD;
|
||||||
|
double _tileRange;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
TrPageArchive* parent_; // The archive
|
TrPageArchive* parent_; // The archive
|
||||||
GeodeGroup *currTop; // Current parent group
|
GeodeGroup *currTop; // Current parent group
|
||||||
|
@ -251,15 +251,16 @@ bool OSGPageManager::ThreadLoop(PagingThread* t)
|
|||||||
//nextDelete.clear();
|
//nextDelete.clear();
|
||||||
|
|
||||||
{
|
{
|
||||||
osgGuard g(changeListMutex);
|
|
||||||
// Add to the unhook list
|
// Add to the unhook list
|
||||||
for(unsigned int kk = 0; kk < unhook.size();kk++)
|
for(unsigned int kk = 0; kk < unhook.size();kk++)
|
||||||
{
|
{
|
||||||
|
osgGuard g(changeListMutex);
|
||||||
toUnhook.push_back(unhook[kk]);
|
toUnhook.push_back(unhook[kk]);
|
||||||
}
|
}
|
||||||
// Also get the list of deletions while we're here
|
// Also get the list of deletions while we're here
|
||||||
// use the stl Luke :-) swap is constant time operation that do a = b; b.clear()
|
// use the stl Luke :-) swap is constant time operation that do a = b; b.clear()
|
||||||
// if a is empty which is our case
|
// if a is empty which is our case
|
||||||
|
osgGuard g(changeListMutex);
|
||||||
nextDelete.swap(toDelete);
|
nextDelete.swap(toDelete);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,7 +275,7 @@ bool OSGPageManager::ThreadLoop(PagingThread* t)
|
|||||||
|
|
||||||
//osg::notify(WARN) << "Tile to load :" << x << ' ' << y << ' ' << lod << std::endl;
|
//osg::notify(WARN) << "Tile to load :" << x << ' ' << y << ' ' << lod << std::endl;
|
||||||
//osg::notify(WARN) << "Position :" << loc.x << ' ' << loc.y << std::endl;
|
//osg::notify(WARN) << "Position :" << loc.x << ' ' << loc.y << std::endl;
|
||||||
LoadOneTile(tile);
|
if (lod==0) LoadOneTile(tile);
|
||||||
// Now add this tile to the merge list
|
// Now add this tile to the merge list
|
||||||
pageManage->AckLoad();
|
pageManage->AckLoad();
|
||||||
|
|
||||||
@ -326,11 +327,14 @@ bool OSGPageManager::MergeUpdateThread(osg::Group *rootNode)
|
|||||||
|
|
||||||
// Make local copies of the merge and unhook lists
|
// Make local copies of the merge and unhook lists
|
||||||
{
|
{
|
||||||
osgGuard g(changeListMutex);
|
|
||||||
// use the stl Luke :-) swap is constant time operation that do a = b; b.clear()
|
// use the stl Luke :-) swap is constant time operation that do a = b; b.clear()
|
||||||
// if a is empty which is our case
|
// if a is empty which is our case
|
||||||
mergeList.swap(toMerge);
|
//if (changeListMutex.trylock()==0)
|
||||||
unhookList.swap(toUnhook);
|
{
|
||||||
|
osgGuard g(changeListMutex);
|
||||||
|
mergeList.swap(toMerge);
|
||||||
|
unhookList.swap(toUnhook);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// visitor to go through unhooked subgraphs to release texture objects
|
// visitor to go through unhooked subgraphs to release texture objects
|
||||||
@ -353,7 +357,7 @@ bool OSGPageManager::MergeUpdateThread(osg::Group *rootNode)
|
|||||||
const osg::Node::ParentList &parents = unhookMe->getParents();
|
const osg::Node::ParentList &parents = unhookMe->getParents();
|
||||||
for (unsigned int pi=0;pi<parents.size();pi++) {
|
for (unsigned int pi=0;pi<parents.size();pi++) {
|
||||||
osg::Group *parent = parents[pi];
|
osg::Group *parent = parents[pi];
|
||||||
if(parent != rootNode)
|
//if(parent != rootNode)
|
||||||
{
|
{
|
||||||
//std::cout<<"removing "<<unhookMe<<" from "<<parent<<std::endl;
|
//std::cout<<"removing "<<unhookMe<<" from "<<parent<<std::endl;
|
||||||
parent->removeChild(unhookMe);
|
parent->removeChild(unhookMe);
|
||||||
@ -363,9 +367,12 @@ bool OSGPageManager::MergeUpdateThread(osg::Group *rootNode)
|
|||||||
|
|
||||||
// Append the unhooked things on to the list to delete
|
// Append the unhooked things on to the list to delete
|
||||||
{
|
{
|
||||||
osgGuard g(changeListMutex);
|
osgGuard g(changeListMutex);
|
||||||
for (unsigned int i = 0; i < unhookList.size();i++)
|
for (unsigned int i = 0; i < unhookList.size();i++)
|
||||||
|
{
|
||||||
|
|
||||||
toDelete.push_back(unhookList[i]);
|
toDelete.push_back(unhookList[i]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do the merging last
|
// Do the merging last
|
||||||
|
Loading…
Reference in New Issue
Block a user