ember/tests/TestTerrain.cpp
2023-06-03 10:32:31 +02:00

647 lines
16 KiB
C++

#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TestRunner.h>
#include <cppunit/extensions/HelperMacros.h>
#include <cppunit/BriefTestProgressListener.h>
#include <cppunit/TestResult.h>
#include "components/ogre/terrain/ICompilerTechniqueProvider.h"
#include "components/ogre/terrain/ITerrainPageBridge.h"
#include "components/ogre/terrain/TerrainHandler.h"
#include "components/ogre/terrain/Types.h"
#include "components/ogre/terrain/TerrainDefPoint.h"
#include "components/ogre/terrain/TerrainInfo.h"
#include "components/ogre/terrain/TerrainPageSurfaceCompiler.h"
#include "components/ogre/ILightning.h"
#include "framework/Exception.h"
#include "framework/TimeFrame.h"
#include "framework/MainLoopController.h"
#include <Eris/Entity.h>
#include <Eris/EventService.h>
#include <Atlas/Message/Element.h>
#include <Mercator/Terrain.h>
#include <wfmath/timestamp.h>
#include <wfmath/atlasconv.h>
#include <Ogre.h>
#include <sigc++/signal.h>
#include <sigc++/trackable.h>
#include <condition_variable>
using namespace Ember::OgreView;
using namespace Ember::OgreView::Terrain;
namespace Ember
{
class DummyTerrainTechnique : public TerrainPageSurfaceCompilerTechnique
{
virtual bool prepareMaterial()
{
return true;
}
virtual bool compileMaterial(Ogre::MaterialPtr material, std::set<std::string>& managedTextures) const
{
return true;
}
virtual bool compileCompositeMapMaterial(Ogre::MaterialPtr material, std::set<std::string>& managedTextures) const
{
return true;
}
virtual std::string getShadowTextureName(const Ogre::MaterialPtr& material) const
{
return "";
}
virtual bool requiresPregenShadow() const
{
return false;
}
};
class DummyCompilerTechniqueProvider: public ICompilerTechniqueProvider
{
public:
virtual TerrainPageSurfaceCompilerTechnique* createTechnique(const TerrainPageGeometryPtr& geometry, const SurfaceLayerStore& terrainPageSurfaces, const TerrainPageShadow* terrainPageShadow) const
{
return new DummyTerrainTechnique();
}
};
class DummyTerrainBridge: public ITerrainPageBridge
{
public:
bool pageReady;
DummyTerrainBridge() :
pageReady(false)
{
}
/**
* @brief Updates the Ogre mesh after changes to the underlying heightdata.
*
* Call this when the heightdata has changed and you want the Ogre representation to be updated to reflect this.
*/
virtual void updateTerrain(TerrainPageGeometry& geometry)
{
}
/**
* @brief Notifies class in the ogre side about the page being ready (after being created or so).
*/
virtual void terrainPageReady()
{
pageReady = true;
}
virtual bool isPageShown() const
{
return true;
}
};
class DummyLightning : public ILightning {
public:
/**
* @brief Gets the direction of the main light, in world space.
* @returns The direction of the main light, in world space.
*/
virtual WFMath::Vector<3> getMainLightDirection() const
{
return WFMath::Vector<3>::ZERO();
}
/**
* @brief Gets the default ambient light colour.
* @returns The default ambient light colour.
*/
virtual Ogre::ColourValue getAmbientLightColour() const
{
return Ogre::ColourValue::Black;
}
};
class DummyEntity: public Eris::Entity
{
public:
DummyEntity() :
Eris::Entity("0", 0)
{
}
Eris::TypeService* getTypeService() const
{
return 0;
}
void removeFromMovementPrediction()
{
}
void addToMovementPrediction()
{
}
Eris::Entity* getEntity(const std::string& id)
{
return 0;
}
void setAttr(const std::string &p, const Atlas::Message::Element &v)
{
Eris::Entity::setAttr(p, v);
}
void setFromMessage(const Atlas::Message::MapType& attrs)
{
beginUpdate();
Atlas::Message::MapType::const_iterator A;
for (A = attrs.begin(); A != attrs.end(); ++A) {
if (A->first == "loc" || A->first == "id" || A->first == "contains")
continue;
Eris::Entity::AttrMap::iterator I = m_attrs.find(A->first);
if ((I != m_attrs.end()) && (I->second == A->second))
continue;
setAttr(A->first, A->second);
}
endUpdate();
}
};
class EntityHolder
{
public:
Eris::Entity& entity;
EntityHolder(Eris::Entity& entity) :
entity(entity)
{
}
~EntityHolder()
{
entity.shutdown();
}
};
class Timer
{
public:
WFMath::TimeStamp startTime;
Timer() :
startTime(WFMath::TimeStamp::now())
{
}
bool hasElapsed(long milliseconds)
{
return (WFMath::TimeStamp::now() - startTime).milliseconds() > milliseconds;
}
};
/**
* @brief Base class for listening to events. After the event is triggered isCompleted() will return true.
*
* For background events, call waitForCompletion(...).
*/
class CompleteEventListener: public virtual sigc::trackable
{
protected:
std::condition_variable mCondition;
std::mutex mMutex;
int mCompletedCount;
void handleEvent()
{
{
std::lock_guard < std::mutex > lock(mMutex);
mCompletedCount++;
}
mCondition.notify_all();
}
public:
CompleteEventListener() :
mCompletedCount(0)
{
}
int waitForCompletion(long milliseconds)
{
std::unique_lock < std::mutex > lock(mMutex);
if (mCompletedCount) {
return true;
}
mCondition.wait_for(lock, std::chrono::milliseconds(milliseconds));
return mCompletedCount;
}
int getCompletedCount()
{
return mCompletedCount;
}
};
class WorldSizeChangedListener: public CompleteEventListener
{
protected:
void eventListener()
{
handleEvent();
}
public:
WorldSizeChangedListener(sigc::signal<void()>& event)
{
event.connect(sigc::mem_fun(*this, &WorldSizeChangedListener::eventListener));
}
};
class AfterTerrainUpdateListener: public CompleteEventListener
{
protected:
void eventListener(const std::vector<WFMath::AxisBox<2>>& areas, const std::set<TerrainPage*>& pages)
{
handleEvent();
}
public:
AfterTerrainUpdateListener(sigc::signal<void(const std::vector<WFMath::AxisBox<2>>&, const std::set<TerrainPage*>&)>& event)
{
event.connect(sigc::mem_fun(*this, &AfterTerrainUpdateListener::eventListener));
}
};
class TestTerrainHandler: public Terrain::TerrainHandler
{
public:
TestTerrainHandler(int pageIndexSize, ICompilerTechniqueProvider& compilerTechniqueProvider, Eris::EventService& es) :
Terrain::TerrainHandler(pageIndexSize, compilerTechniqueProvider, es)
{
mLightning = new DummyLightning();
}
Mercator::Terrain* getTerrain()
{
return mTerrain;
}
/**
* Checks the height, using an integer.
* This is because of the floats being used, we can't be 100% certain that a piece of terrain should be exactly the specified height.
*/
bool checkTerrainHeight(float x, float y, int height)
{
float realHeight = 0;
getHeight(TerrainPosition(x,y), realHeight);
return (int)(realHeight) == height;
}
};
class TerrainSetup
{
public:
boost::asio::io_service io_service;
Eris::EventService es;
DummyCompilerTechniqueProvider compilerTechniqueProvider;
TestTerrainHandler terrainHandler;
TerrainSetup() :
es(io_service), terrainHandler(513, compilerTechniqueProvider, es)
{
}
bool createBaseTerrain(float height)
{
WorldSizeChangedListener worldSizeChangedListener(terrainHandler.EventWorldSizeChanged);
TerrainDefPointStore terrainDefPoints;
terrainDefPoints.push_back(TerrainDefPoint(-1, -1, height));
terrainDefPoints.push_back(TerrainDefPoint(-1, 0, height));
terrainDefPoints.push_back(TerrainDefPoint(-1, 1, height));
terrainDefPoints.push_back(TerrainDefPoint(0, -1, height));
terrainDefPoints.push_back(TerrainDefPoint(0, 0, height));
terrainDefPoints.push_back(TerrainDefPoint(0, 1, height));
terrainDefPoints.push_back(TerrainDefPoint(1, -1, height));
terrainDefPoints.push_back(TerrainDefPoint(1, 0, height));
terrainDefPoints.push_back(TerrainDefPoint(1, 1, height));
terrainHandler.updateTerrain(terrainDefPoints);
Timer timer;
do {
es.processAllHandlers();
} while (!timer.hasElapsed(5000) && !worldSizeChangedListener.getCompletedCount());
return worldSizeChangedListener.getCompletedCount() > 0;
}
bool createPages()
{
DummyTerrainBridge* bridge1 = new DummyTerrainBridge();
DummyTerrainBridge* bridge2 = new DummyTerrainBridge();
DummyTerrainBridge* bridge3 = new DummyTerrainBridge();
DummyTerrainBridge* bridge4 = new DummyTerrainBridge();
terrainHandler.setUpTerrainPageAtIndex(TerrainIndex(0, 0), bridge1);
terrainHandler.setUpTerrainPageAtIndex(TerrainIndex(0, 1), bridge2);
terrainHandler.setUpTerrainPageAtIndex(TerrainIndex(-1, 0), bridge3);
terrainHandler.setUpTerrainPageAtIndex(TerrainIndex(-1, 1), bridge4);
{
Timer timer;
do {
es.processAllHandlers();
} while ((!timer.hasElapsed(5000)) && (!bridge1->pageReady || !bridge2->pageReady || !bridge3->pageReady || !bridge4->pageReady));
}
CPPUNIT_ASSERT(bridge1->pageReady);
CPPUNIT_ASSERT(bridge2->pageReady);
CPPUNIT_ASSERT(bridge3->pageReady);
CPPUNIT_ASSERT(bridge4->pageReady);
return true;
}
bool reloadTerrain()
{
AfterTerrainUpdateListener afterTerrainUpdateListener(terrainHandler.EventAfterTerrainUpdate);
std::vector<TerrainPosition> positions;
positions.push_back(TerrainPosition(-32, -32));
positions.push_back(TerrainPosition(-32, 32));
positions.push_back(TerrainPosition(32, 32));
positions.push_back(TerrainPosition(32, -32));
terrainHandler.reloadTerrain(positions);
{
Timer timer;
do {
es.processAllHandlers();
} while ((!timer.hasElapsed(5000)) && afterTerrainUpdateListener.getCompletedCount() < 4);
}
CPPUNIT_ASSERT(afterTerrainUpdateListener.getCompletedCount() == 4);
return true;
}
};
class TerrainTestCase: public CppUnit::TestFixture
{
CPPUNIT_TEST_SUITE( TerrainTestCase);
// CPPUNIT_TEST( testCreateTerrain);
// CPPUNIT_TEST( testAlterTerrain);
CPPUNIT_TEST( testApplyMod);
// CPPUNIT_TEST( testUpdateMod);
CPPUNIT_TEST_SUITE_END();
protected:
public:
void testCreateTerrain()
{
Ogre::Root root;
TerrainSetup terrainSetup;
float terrainHeight = 25.0f;
CPPUNIT_ASSERT(terrainSetup.createBaseTerrain(terrainHeight));
CPPUNIT_ASSERT(terrainSetup.createPages());
CPPUNIT_ASSERT(terrainSetup.reloadTerrain());
CPPUNIT_ASSERT(terrainSetup.terrainHandler.getTerrainInfo().getTotalNumberOfPagesX() == 2);
CPPUNIT_ASSERT(terrainSetup.terrainHandler.getTerrainInfo().getTotalNumberOfPagesY() == 2);
float height = 0;
CPPUNIT_ASSERT(terrainSetup.terrainHandler.getHeight(TerrainPosition(-1, -1), height));
CPPUNIT_ASSERT(height == terrainHeight);
}
void testAlterTerrain()
{
Ogre::Root root;
TerrainSetup terrainSetup;
Terrain::TerrainHandler& terrainHandler = terrainSetup.terrainHandler;
CPPUNIT_ASSERT(terrainSetup.createBaseTerrain(25.0f));
CPPUNIT_ASSERT(terrainSetup.createPages());
CPPUNIT_ASSERT(terrainSetup.reloadTerrain());
TerrainDefPointStore terrainDefPoints;
terrainDefPoints.push_back(TerrainDefPoint(-1, -1, -200));
terrainDefPoints.push_back(TerrainDefPoint(-1, 0, -200));
terrainDefPoints.push_back(TerrainDefPoint(0, -1, -200));
terrainDefPoints.push_back(TerrainDefPoint(0, 0, -200));
terrainHandler.updateTerrain(terrainDefPoints);
AfterTerrainUpdateListener afterTerrainUpdateListener(terrainHandler.EventAfterTerrainUpdate);
{
Timer timer;
do {
terrainSetup.es.processAllHandlers();
} while ((!timer.hasElapsed(5000)) && afterTerrainUpdateListener.getCompletedCount() < 4);
}
CPPUNIT_ASSERT(afterTerrainUpdateListener.getCompletedCount() == 4);
CPPUNIT_ASSERT(terrainHandler.getTerrainInfo().getTotalNumberOfPagesX() == 2);
CPPUNIT_ASSERT(terrainHandler.getTerrainInfo().getTotalNumberOfPagesY() == 2);
float height = 0;
CPPUNIT_ASSERT(terrainHandler.getHeight(TerrainPosition(-32, -32), height));
CPPUNIT_ASSERT(height == -200);
}
void testApplyMod()
{
bool shouldQuit, pollEris;
MainLoopController loopController(shouldQuit, pollEris);
Ogre::Root root;
TerrainSetup terrainSetup;
Terrain::TerrainHandler& terrainHandler = terrainSetup.terrainHandler;
DummyEntity entity;
EntityHolder entityHolder(entity);
entity.setAttr("pos", WFMath::Point<3>::ZERO().toAtlas());
float terrainHeight = 25.0f;
CPPUNIT_ASSERT(terrainSetup.createBaseTerrain(terrainHeight));
CPPUNIT_ASSERT(terrainSetup.createPages());
CPPUNIT_ASSERT(terrainSetup.reloadTerrain());
Atlas::Message::ListType polygon;
polygon.push_back(WFMath::Point<2>(-10, -10).toAtlas());
polygon.push_back(WFMath::Point<2>(-10, 10).toAtlas());
polygon.push_back(WFMath::Point<2>(10, 10).toAtlas());
polygon.push_back(WFMath::Point<2>(10, -10).toAtlas());
Atlas::Message::MapType shape;
shape["points"] = polygon;
shape["type"] = "polygon";
Atlas::Message::MapType mod;
mod["shape"] = shape;
mod["type"] = "levelmod";
mod["heightoffset"] = 2.0f;
entity.setAttr("terrainmod", mod);
OgreView::Terrain::TerrainMod terrainMod(entity);
terrainMod.init();
terrainHandler.addTerrainMod(&terrainMod);
}
void testUpdateMod()
{
Ogre::Root root;
TerrainSetup terrainSetup;
TestTerrainHandler& terrainHandler = terrainSetup.terrainHandler;
DummyEntity entity;
EntityHolder entityHolder(entity);
entity.setAttr("pos", WFMath::Point<3>::ZERO().toAtlas());
float terrainHeight = 25.0f;
CPPUNIT_ASSERT(terrainSetup.createBaseTerrain(terrainHeight));
CPPUNIT_ASSERT(terrainSetup.createPages());
CPPUNIT_ASSERT(terrainSetup.reloadTerrain());
Atlas::Message::ListType polygon;
polygon.push_back(WFMath::Point<2>(-10, -10).toAtlas());
polygon.push_back(WFMath::Point<2>(-10, 10).toAtlas());
polygon.push_back(WFMath::Point<2>(10, 10).toAtlas());
polygon.push_back(WFMath::Point<2>(10, -10).toAtlas());
Atlas::Message::MapType shape;
shape["points"] = polygon;
shape["type"] = "polygon";
Atlas::Message::MapType mod;
mod["shape"] = shape;
mod["type"] = "levelmod";
mod["heightoffset"] = 2.0f;
entity.setAttr("terrainmod", mod);
OgreView::Terrain::TerrainMod* terrainMod = new Terrain::TerrainMod(entity);
terrainMod->init();
{
AfterTerrainUpdateListener afterTerrainUpdateListener(terrainHandler.EventAfterTerrainUpdate);
//The shape should affect all four pages.
terrainHandler.addTerrainMod(terrainMod);
{
Timer timer;
do {
terrainSetup.es.processAllHandlers();
} while (afterTerrainUpdateListener.getCompletedCount() < 4);
}
CPPUNIT_ASSERT(afterTerrainUpdateListener.getCompletedCount() == 4);
CPPUNIT_ASSERT(terrainHandler.checkTerrainHeight(-5, -5, 2.0f));
CPPUNIT_ASSERT(terrainHandler.checkTerrainHeight(-5, 5, 2.0f));
CPPUNIT_ASSERT(terrainHandler.checkTerrainHeight(5, 5, 2.0f));
CPPUNIT_ASSERT(terrainHandler.checkTerrainHeight(5, -5, 2.0f));
}
mod["heightoffset"] = 20.0f;
//Setting the attribute should trigger a mod change
entity.setAttr("terrainmod", mod);
{
AfterTerrainUpdateListener afterTerrainUpdateListener(terrainHandler.EventAfterTerrainUpdate);
{
Timer timer;
do {
terrainSetup.es.processAllHandlers();
// } while ((!timer.hasElapsed(5000)) && afterTerrainUpdateListener.getCompletedCount() < 4);
} while (afterTerrainUpdateListener.getCompletedCount() < 4);
}
CPPUNIT_ASSERT(afterTerrainUpdateListener.getCompletedCount() == 4);
CPPUNIT_ASSERT(terrainHandler.checkTerrainHeight(-5, -5, 20.0f));
CPPUNIT_ASSERT(terrainHandler.checkTerrainHeight(-5, 5, 20.0f));
CPPUNIT_ASSERT(terrainHandler.checkTerrainHeight(5, 5, 20.0f));
CPPUNIT_ASSERT(terrainHandler.checkTerrainHeight(5, -5, 20.0f));
}
//Move the mod out of the way
entity.setAttr("pos", WFMath::Point<3>(40, 40, 0).toAtlas());
{
AfterTerrainUpdateListener afterTerrainUpdateListener(terrainHandler.EventAfterTerrainUpdate);
{
Timer timer;
do {
terrainSetup.es.processAllHandlers();
} while ((!timer.hasElapsed(5000)) && afterTerrainUpdateListener.getCompletedCount() < 4);
}
CPPUNIT_ASSERT(afterTerrainUpdateListener.getCompletedCount() == 4);
CPPUNIT_ASSERT(terrainHandler.checkTerrainHeight(-5, -5, 25));
CPPUNIT_ASSERT(terrainHandler.checkTerrainHeight(-5, 5, 25));
CPPUNIT_ASSERT(terrainHandler.checkTerrainHeight(5, 5, 25));
CPPUNIT_ASSERT(terrainHandler.checkTerrainHeight(5, -5, 25));
}
}
};
}
CPPUNIT_TEST_SUITE_REGISTRATION( Ember::TerrainTestCase);
int main(int argc, char **argv)
{
CppUnit::TextUi::TestRunner runner;
CppUnit::TestFactoryRegistry &registry = CppUnit::TestFactoryRegistry::getRegistry();
runner.addTest(registry.makeTest());
// Shows a message as each test starts
CppUnit::BriefTestProgressListener listener;
runner.eventManager().addListener(&listener);
bool wasSuccessful = runner.run("", false);
return !wasSuccessful;
}