瀏覽代碼

-Added test subfolder and updated CMakeLists for unit testing - Added a test case for the DrawTerrainOperation class(does not pass all tests successfully, 6 failures left to fix) - Fixed a few bugs

beegee1 12 年之前
父節點
當前提交
dd78205ce8

+ 8 - 0
CMakeLists.txt

@@ -16,6 +16,7 @@ set(VCMI_VERSION_PATCH 0)
 
 option(DISABLE_ERM "Disable compilation of ERM scripting module" ON)
 option(ENABLE_EDITOR "Enable compilation of map editor" OFF)
+option(ENABLE_TEST "Enable compilation of unit tests" OFF)
 
 ############################################
 #        Building section                  #
@@ -56,6 +57,10 @@ if (ENABLE_EDITOR)
 	find_package(Qt5Widgets REQUIRED)
 endif()
 
+if(ENABLE_TEST)
+	find_package(Boost 1.46.0 COMPONENTS unit_test_framework REQUIRED)
+endif()
+
 if(NOT WIN32)
 	set(FFmpeg_FIND_COMPONENTS AVFORMAT SWSCALE)
 	find_package(FFmpeg REQUIRED)
@@ -125,6 +130,9 @@ endif()
 if (ENABLE_EDITOR)
 	add_subdirectory(editor)
 endif()
+if(ENABLE_TEST)
+	add_subdirectory(test)
+endif()
 
 #######################################
 #    Installation section             #

+ 51 - 18
config/terrainViewPatterns.json

@@ -45,6 +45,7 @@
 	[
 		// Standard transitions
 		{
+			"id" : "s1",
 			"data" :
 			[
 				"?", "?", "T",
@@ -54,6 +55,7 @@
 			"mapping" : "0-3, 20-23"
 		},
 		{
+			"id" : "s2",
 			"data" :
 			[
 				"?", "N", "N",
@@ -63,6 +65,7 @@
 			"mapping" : "4-7, 24-27"
 		},
 		{
+			"id" : "s3",
 			"data" :
 			[
 				"?", "T", "?",
@@ -72,6 +75,7 @@
 			"mapping" : "8-11, 28-31"
 		},
 		{
+			"id" : "s4",
 			"data" :
 			[
 				"N", "N", "N",
@@ -81,28 +85,30 @@
 			"mapping" : "12-15, 32-35"
 		},
 		{
+			"id" : "s5",
 			"data" :
 			[
-				"T", "T", "a-1,?",
+				"T", "T", "s5-1,?",
 				"T", "N", "N",
-				"a-1,?", "N", "N"
+				"s5-1,?", "N", "N"
 			],
 			"mapping" : "16-17, 36-37",
-			"id" : "a",
 			"minPoints" : 1
 		},
 		{
+			"id" : "s6",
 			"data" :
 			[
 				"N", "N", "N",
-				"N", "N", "a-1,N",
-				"N", "a-1,N", "T"
+				"N", "N", "s5-1,N",
+				"N", "s5-1,N", "T"
 			],
 			"mapping" : "18-19, 38-39",
 			"minPoints" : 1
 		},
 		// Mixed transitions
 		{
+			"id" : "m1",
 			"data" :
 			[
 				"T", "N", "N",
@@ -112,6 +118,7 @@
 			"mapping" : "40, 42"
 		},
 		{
+			"id" : "m2",
 			"data" :
 			[
 				"D", "N", "N",
@@ -121,6 +128,7 @@
 			"mapping" : "41"
 		},
 		{
+			"id" : "m3",
 			"data" :
 			[
 				"N", "N", "D,N",
@@ -130,6 +138,7 @@
 			"mapping" : "43"
 		},
 		{
+			"id" : "m4",
 			"data" :
 			[
 				"N", "N", "S",
@@ -139,6 +148,7 @@
 			"mapping" : "44"
 		},
 		{
+			"id" : "m5",
 			"data" :
 			[
 				"N", "N", "D,N",
@@ -148,6 +158,7 @@
 			"mapping" : "45"
 		},
 		{
+			"id" : "m6",
 			"data" :
 			[
 				"N", "N", "N",
@@ -157,6 +168,7 @@
 			"mapping" : "46"
 		},
 		{
+			"id" : "m7",
 			"data" :
 			[
 				"N", "N", "D,S,N",
@@ -166,6 +178,7 @@
 			"mapping" : "47"
 		},
 		{
+			"id" : "m8",
 			"data" :
 			[
 				"N", "N", "D",
@@ -176,6 +189,7 @@
 		},
 		// No transition
 		{
+			"id" : "n1",
 			"data" :
 			[
 				"N", "N", "N",
@@ -189,6 +203,7 @@
 	[
 		// Standard transitions
 		{
+			"id" : "s1",
 			"data" :
 			[
 				"?", "S", "S",
@@ -198,6 +213,7 @@
 			"mapping" : "0-3"
 		},
 		{
+			"id" : "s2",
 			"data" :
 			[
 				"?", "D", "D",
@@ -207,6 +223,7 @@
 			"mapping" : "4-7"
 		},
 		{
+			"id" : "s3",
 			"data" :
 			[
 				"?", "S", "?",
@@ -216,6 +233,7 @@
 			"mapping" : "8-11"
 		},
 		{
+			"id" : "s4",
 			"data" :
 			[
 				"D", "D", "D",
@@ -225,29 +243,30 @@
 			"mapping" : "12-15"
 		},
 		{
+			"id" : "s5",
 			"data" :
 			[
 				"S", "S", "D",
-				"S", "N", "b-1,D",
-				"D", "b-1,D", "D"
+				"S", "N", "s6-1,D",
+				"D", "s6-1,D", "D"
 			],
 			"mapping" : "16-17",
-			"id" : "a",
 			"minPoints" : 1
 		},
 		{
+			"id" : "s6",
 			"data" :
 			[
 				"D", "D", "D",
-				"D", "N", "a-1,D",
-				"D", "a-1,D", "S"
+				"D", "N", "s5-1,D",
+				"D", "s5-1,D", "S"
 			],
 			"mapping" : "18-19",
-			"id" : "b",
 			"minPoints" : 1
 		},
 		// Mixed transition
 		{
+			"id" : "m1",
 			"data" :
 			[
 				"S", "D", "D",
@@ -258,6 +277,7 @@
 		},
 		// No transition
 		{
+			"id" : "n1",
 			"data" :
 			[
 				"D", "D", "D",
@@ -270,6 +290,7 @@
 	"sand" :
 	[
 		{
+			"id" : "n1",
 			"data" :
 			[
 				"?", "?", "?",
@@ -283,6 +304,7 @@
 	[
 		// Standard transitions
 		{
+			"id" : "s1",
 			"data" :
 			[
 				"S", "S", "S",
@@ -292,6 +314,7 @@
 			"mapping" : "0-3"
 		},
 		{
+			"id" : "s2",
 			"data" :
 			[
 				"?", "N", "N",
@@ -301,6 +324,7 @@
 			"mapping" : "4-7"
 		},
 		{
+			"id" : "s3",
 			"data" :
 			[
 				"?", "S", "?",
@@ -310,6 +334,7 @@
 			"mapping" : "8-11"
 		},
 		{
+			"id" : "s4",
 			"data" :
 			[
 				"N", "N", "N",
@@ -319,6 +344,7 @@
 			"mapping" : "12-15"
 		},
 		{
+			"id" : "s5",
 			"data" :
 			[
 				"S", "S", "N",
@@ -326,20 +352,21 @@
 				"N", "N", "N"
 			],
 			"mapping" : "16-17",
-			"id" : "a"
 		},
 		{
+			"id" : "s6",
 			"data" :
 			[
 				"N", "N", "N",
-				"N", "N", "a-1,N",
-				"N", "a-1,N", "S"
+				"N", "N", "s5-1,N",
+				"N", "s5-1,N", "S"
 			],
 			"mapping" : "18-19",
 			"minPoints" : 1
 		},
 		// No transition
 		{
+			"id" : "n1",
 			"data" :
 			[
 				"N", "N", "N",
@@ -353,6 +380,7 @@
 	[
 		// No transition
 		{
+			"id" : "n1",
 			"data" :
 			[
 				"N", "N", "N",
@@ -363,6 +391,7 @@
 		},
 		// Standard transitions
 		{
+			"id" : "s1",
 			"data" :
 			[
 				"?", "S", "?",
@@ -373,6 +402,7 @@
 			"flipMode" : "diffImages"
 		},
 		{
+			"id" : "s2",
 			"data" :
 			[
 				"?", "N", "N",
@@ -383,6 +413,7 @@
 			"flipMode" : "diffImages"
 		},
 		{
+			"id" : "s3",
 			"data" :
 			[
 				"?", "S", "?",
@@ -393,6 +424,7 @@
 			"flipMode" : "diffImages"
 		},
 		{
+			"id" : "s4",
 			"data" :
 			[
 				"N", "N", "N",
@@ -403,6 +435,7 @@
 			"flipMode" : "diffImages"
 		},
 		{
+			"id" : "s5",
 			"data" :
 			[
 				"S", "S", "N",
@@ -410,15 +443,15 @@
 				"N", "N", "N"
 			],
 			"mapping" : "32-39",
-			"flipMode" : "diffImages",
-			"id" : "a"
+			"flipMode" : "diffImages"
 		},
 		{
+			"id" : "s6",
 			"data" :
 			[
 				"N", "N", "N",
-				"N", "N", "a-1,N",
-				"N", "a-1,N", "S"
+				"N", "N", "s5-1,N",
+				"N", "s5-1,N", "S"
 			],
 			"mapping" : "40-47",
 			"flipMode" : "diffImages",

+ 2 - 2
lib/logging/CLogger.h

@@ -258,7 +258,7 @@ public:
 	const CColorMapping & getColorMapping() const;
 	void setColorMapping(const CColorMapping & colorMapping);
 
-	void write(const LogRecord & record);
+	void write(const LogRecord & record) override;
 
 private:
 	CConsoleHandler * console;
@@ -281,7 +281,7 @@ public:
 	const CLogFormatter & getFormatter() const;
 	void setFormatter(const CLogFormatter & formatter);
 
-	void write(const LogRecord & record);
+	void write(const LogRecord & record) override;
 
 private:
 	std::ofstream file;

+ 103 - 21
lib/mapping/CMapEditManager.cpp

@@ -5,6 +5,77 @@
 #include "../filesystem/CResourceLoader.h"
 #include "../CDefObjInfoHandler.h"
 
+MapRect::MapRect() : x(0), y(0), z(0), width(0), height(0)
+{
+
+}
+
+MapRect::MapRect(int3 pos, si32 width, si32 height) : x(pos.x), y(pos.y), z(pos.z), width(width), height(height)
+{
+
+}
+
+MapRect MapRect::operator&(const MapRect & rect) const
+{
+	bool intersect = right() > rect.left() && rect.right() > left() &&
+			bottom() > rect.top() && rect.bottom() > top() &&
+			z == rect.z;
+	if(intersect)
+	{
+		MapRect ret;
+		ret.x = std::max(left(), rect.left());
+		ret.y = std::max(top(), rect.top());
+		ret.z = rect.z;
+		ret.width = std::min(right(), rect.right()) - ret.x;
+		ret.height = std::min(bottom(), rect.bottom()) - ret.y;
+		return ret;
+	}
+	else
+	{
+		return MapRect();
+	}
+}
+
+si32 MapRect::left() const
+{
+	return x;
+}
+
+si32 MapRect::right() const
+{
+	return x + width;
+}
+
+si32 MapRect::top() const
+{
+	return y;
+}
+
+si32 MapRect::bottom() const
+{
+	return y + height;
+}
+
+int3 MapRect::topLeft() const
+{
+	return int3(x, y, z);
+}
+
+int3 MapRect::topRight() const
+{
+	return int3(right(), y, z);
+}
+
+int3 MapRect::bottomLeft() const
+{
+	return int3(x, bottom(), z);
+}
+
+int3 MapRect::bottomRight() const
+{
+	return int3(right(), bottom(), z);
+}
+
 CMapOperation::CMapOperation(CMap * map) : map(map)
 {
 
@@ -181,12 +252,11 @@ CTerrainViewPatternConfig & CTerrainViewPatternConfig::get()
 CTerrainViewPatternConfig::CTerrainViewPatternConfig()
 {
 	const JsonNode config(ResourceID("config/terrainViewPatterns.json"));
-	const std::map<std::string, ETerrainGroup::ETerrainGroup> terGroups
-			= boost::assign::map_list_of("normal", ETerrainGroup::NORMAL)("dirt", ETerrainGroup::DIRT)
-			("sand", ETerrainGroup::SAND)("water", ETerrainGroup::WATER)("rock", ETerrainGroup::ROCK);
-	BOOST_FOREACH(auto terMapping, terGroups)
+	const auto & groupMap = config.Struct();
+	BOOST_FOREACH(const auto & groupPair, groupMap)
 	{
-		BOOST_FOREACH(const JsonNode & ptrnNode, config[terMapping.first].Vector())
+		auto terGroup = getTerrainGroup(groupPair.first);
+		BOOST_FOREACH(const JsonNode & ptrnNode, groupPair.second.Vector())
 		{
 			TerrainViewPattern pattern;
 
@@ -238,8 +308,8 @@ CTerrainViewPatternConfig::CTerrainViewPatternConfig()
 				pattern.flipMode = TerrainViewPattern::FLIP_MODE_SAME_IMAGE;
 			}
 
-			pattern.terGroup = terMapping.second;
-			patterns[terMapping.second].push_back(pattern);
+			pattern.terGroup = terGroup;
+			patterns[terGroup].push_back(pattern);
 		}
 	}
 }
@@ -249,6 +319,17 @@ CTerrainViewPatternConfig::~CTerrainViewPatternConfig()
 
 }
 
+ETerrainGroup::ETerrainGroup CTerrainViewPatternConfig::getTerrainGroup(const std::string & terGroup) const
+{
+	static const std::map<std::string, ETerrainGroup::ETerrainGroup> terGroups
+			= boost::assign::map_list_of("normal", ETerrainGroup::NORMAL)("dirt", ETerrainGroup::DIRT)
+			("sand", ETerrainGroup::SAND)("water", ETerrainGroup::WATER)("rock", ETerrainGroup::ROCK);
+	auto it = terGroups.find(terGroup);
+	if(it == terGroups.end()) throw std::runtime_error(boost::str(boost::format("Terrain group '%s' does not exist.") % terGroup));
+	return it->second;
+}
+
+
 const std::vector<TerrainViewPattern> & CTerrainViewPatternConfig::getPatternsForGroup(ETerrainGroup::ETerrainGroup terGroup) const
 {
 	return patterns.find(terGroup)->second;
@@ -275,18 +356,19 @@ DrawTerrainOperation::DrawTerrainOperation(CMap * map, const MapRect & rect, ETe
 
 void DrawTerrainOperation::execute()
 {
-	for(int i = rect.pos.x; i < rect.pos.x + rect.width; ++i)
+	for(int i = rect.x; i < rect.x + rect.width; ++i)
 	{
-		for(int j = rect.pos.y; j < rect.pos.y + rect.height; ++j)
+		for(int j = rect.y; j < rect.y + rect.height; ++j)
 		{
-			map->getTile(int3(i, j, rect.pos.z)).terType = terType;
+			map->getTile(int3(i, j, rect.z)).terType = terType;
 		}
 	}
 
 	//TODO there are situations where more tiles are affected implicitely
 	//TODO add coastal bit to extTileFlags appropriately
 
-	updateTerrainViews(MapRect(int3(rect.pos.x - 1, rect.pos.y - 1, rect.pos.z), rect.width + 2, rect.height + 2));
+	MapRect viewRect(int3(rect.x - 1, rect.y - 1, rect.z), rect.width + 2, rect.height + 2); // Has to overlap 1 tile around
+	updateTerrainViews(viewRect & MapRect(int3(0, 0, viewRect.z), map->width, map->height)); // Rect should not overlap map dimensions
 }
 
 void DrawTerrainOperation::undo()
@@ -306,12 +388,12 @@ std::string DrawTerrainOperation::getLabel() const
 
 void DrawTerrainOperation::updateTerrainViews(const MapRect & rect)
 {
-	for(int i = rect.pos.x; i < rect.pos.x + rect.width; ++i)
+	for(int i = rect.x; i < rect.x + rect.width; ++i)
 	{
-		for(int j = rect.pos.y; j < rect.pos.y + rect.height; ++j)
+		for(int j = rect.y; j < rect.y + rect.height; ++j)
 		{
 			const auto & patterns =
-					CTerrainViewPatternConfig::get().getPatternsForGroup(getTerrainGroup(map->getTile(int3(i, j, rect.pos.z)).terType));
+					CTerrainViewPatternConfig::get().getPatternsForGroup(getTerrainGroup(map->getTile(int3(i, j, rect.z)).terType));
 
 			// Detect a pattern which fits best
 			int bestPattern = -1, bestFlip = -1;
@@ -322,10 +404,10 @@ void DrawTerrainOperation::updateTerrainViews(const MapRect & rect)
 
 				for(int flip = 0; flip < 4; ++flip)
 				{
-					auto valRslt = validateTerrainView(int3(i, j, rect.pos.z), flip > 0 ? getFlippedPattern(pattern, flip) : pattern);
+					auto valRslt = validateTerrainView(int3(i, j, rect.z), flip > 0 ? getFlippedPattern(pattern, flip) : pattern);
 					if(valRslt.result)
 					{
-						logGlobal->debugStream() << "Pattern detected at pos " << i << "x" << j << "x" << rect.pos.z << ": P-Nr. " << k
+						logGlobal->debugStream() << "Pattern detected at pos " << i << "x" << j << "x" << rect.z << ": P-Nr. " << k
 							  << ", Flip " << flip << ", Repl. " << valRslt.transitionReplacement;
 
 						bestPattern = k;
@@ -338,7 +420,7 @@ void DrawTerrainOperation::updateTerrainViews(const MapRect & rect)
 			if(bestPattern == -1)
 			{
 				// This shouldn't be the case
-				logGlobal->warnStream() << "No pattern detected at pos " << i << "x" << j << "x" << rect.pos.z;
+				logGlobal->warnStream() << "No pattern detected at pos " << i << "x" << j << "x" << rect.z;
 				continue;
 			}
 
@@ -355,7 +437,7 @@ void DrawTerrainOperation::updateTerrainViews(const MapRect & rect)
 			}
 
 			// Set terrain view
-			auto & tile = map->getTile(int3(i, j, rect.pos.z));
+			auto & tile = map->getTile(int3(i, j, rect.z));
 			if(pattern.flipMode == TerrainViewPattern::FLIP_MODE_SAME_IMAGE)
 			{
 				tile.terView = gen->getInteger(mapping.first, mapping.second);
@@ -363,9 +445,9 @@ void DrawTerrainOperation::updateTerrainViews(const MapRect & rect)
 			}
 			else
 			{
-				int range = (mapping.second - mapping.first) / 4;
-				tile.terView = gen->getInteger(mapping.first + bestFlip * range,
-					mapping.first + (bestFlip + 1) * range - 1);
+				const int framesPerRot = 2;
+				int firstFrame = mapping.first + bestFlip * framesPerRot;
+				tile.terView = gen->getInteger(firstFrame, firstFrame + framesPerRot - 1);
 				tile.extTileFlags =	0;
 			}
 		}

+ 43 - 28
lib/mapping/CMapEditManager.h

@@ -18,28 +18,30 @@ class CGObjectInstance;
 class CTerrainViewPatternConfig;
 struct TerrainViewPattern;
 
-namespace ETerrainGroup
-{
-	enum ETerrainGroup
-	{
-		NORMAL,
-		DIRT,
-		SAND,
-		WATER,
-		ROCK
-	};
-}
-
 /// Represents a map rectangle.
 struct DLL_LINKAGE MapRect
 {
-	MapRect(int3 pos, si32 width, si32 height) : pos(pos), width(width), height(height) { };
-	int3 pos;
+	MapRect();
+	MapRect(int3 pos, si32 width, si32 height);
+	si32 x, y, z;
 	si32 width, height;
+
+	si32 left() const;
+	si32 right() const;
+	si32 top() const;
+	si32 bottom() const;
+
+	int3 topLeft() const; /// Top left corner of this rect.
+	int3 topRight() const; /// Top right corner of this rect.
+	int3 bottomLeft() const; /// Bottom left corner of this rect.
+	int3 bottomRight() const; /// Bottom right corner of this rect.
+
+	/// Returns a MapRect of the intersection of this rectangle and the given one.
+	MapRect operator&(const MapRect & rect) const;
 };
 
 /// The abstract base class CMapOperation defines an operation that can be executed, undone and redone.
-class CMapOperation
+class DLL_LINKAGE CMapOperation : public boost::noncopyable
 {
 public:
 	CMapOperation(CMap * map);
@@ -55,7 +57,7 @@ protected:
 };
 
 /// The CMapUndoManager provides the functionality to save operations and undo/redo them.
-class CMapUndoManager : boost::noncopyable
+class DLL_LINKAGE CMapUndoManager : boost::noncopyable
 {
 public:
 	CMapUndoManager();
@@ -77,8 +79,8 @@ public:
 private:
 	typedef std::list<unique_ptr<CMapOperation> > TStack;
 
-	inline void doOperation(TStack & fromStack, TStack & toStack, bool doUndo);
-	inline const CMapOperation * peek(const TStack & stack) const;
+	void doOperation(TStack & fromStack, TStack & toStack, bool doUndo);
+	const CMapOperation * peek(const TStack & stack) const;
 
 	TStack undoStack;
 	TStack redoStack;
@@ -113,9 +115,21 @@ private:
 /* Implementation/Detail classes, Private API */
 /* ---------------------------------------------------------------------------- */
 
+namespace ETerrainGroup
+{
+	enum ETerrainGroup
+	{
+		NORMAL,
+		DIRT,
+		SAND,
+		WATER,
+		ROCK
+	};
+}
+
 /// The terrain view pattern describes a specific composition of terrain tiles
 /// in a 3x3 matrix and notes which terrain view frame numbers can be used.
-struct TerrainViewPattern
+struct DLL_LINKAGE TerrainViewPattern
 {
 	struct WeightedRule
 	{
@@ -186,13 +200,14 @@ struct TerrainViewPattern
 };
 
 /// The terrain view pattern config loads pattern data from the filesystem.
-class CTerrainViewPatternConfig : public boost::noncopyable
+class DLL_LINKAGE CTerrainViewPatternConfig : public boost::noncopyable
 {
 public:
 	static CTerrainViewPatternConfig & get();
 
 	const std::vector<TerrainViewPattern> & getPatternsForGroup(ETerrainGroup::ETerrainGroup terGroup) const;
 	const TerrainViewPattern & getPatternById(ETerrainGroup::ETerrainGroup terGroup, const std::string & id) const;
+	ETerrainGroup::ETerrainGroup getTerrainGroup(const std::string & terGroup) const;
 
 private:
 	CTerrainViewPatternConfig();
@@ -208,10 +223,10 @@ class DrawTerrainOperation : public CMapOperation
 public:
 	DrawTerrainOperation(CMap * map, const MapRect & rect, ETerrainType terType, CRandomGenerator * gen);
 
-	void execute();
-	void undo();
-	void redo();
-	std::string getLabel() const;
+	void execute() override;
+	void undo() override;
+	void redo() override;
+	std::string getLabel() const override;
 
 private:
 	struct ValidationResult
@@ -246,10 +261,10 @@ class InsertObjectOperation : public CMapOperation
 public:
 	InsertObjectOperation(CMap * map, const int3 & pos, CGObjectInstance * obj);
 
-	void execute();
-	void undo();
-	void redo();
-	std::string getLabel() const;
+	void execute() override;
+	void undo() override;
+	void redo() override;
+	std::string getLabel() const override;
 
 private:
 	int3 pos;

+ 3 - 2
lib/rmg/CMapGenerator.cpp

@@ -357,7 +357,7 @@ void CMapGenOptions::CPlayerSettings::setPlayerType(EPlayerType::EPlayerType val
 	playerType = value;
 }
 
-CMapGenerator::CMapGenerator(const CMapGenOptions & mapGenOptions, int randomSeed) :
+CMapGenerator::CMapGenerator(const CMapGenOptions & mapGenOptions, int randomSeed /*= std::time(nullptr)*/) :
 	mapGenOptions(mapGenOptions), randomSeed(randomSeed)
 {
 	gen.seed(randomSeed);
@@ -376,6 +376,7 @@ std::unique_ptr<CMap> CMapGenerator::generate()
 
 	map = make_unique<CMap>();
 	editManager = map->getEditManager();
+	editManager->getUndoManager().setUndoRedoLimit(0);
 	addHeaderInfo();
 
 	genTerrain();
@@ -472,7 +473,7 @@ void CMapGenerator::addPlayerInfo()
 
 void CMapGenerator::genTerrain()
 {
-	map->initTerrain(); //FIXME nicer solution
+	map->initTerrain();
 	editManager->clearTerrain(&gen);
 	editManager->drawTerrain(MapRect(int3(10, 10, 0), 20, 30), ETerrainType::GRASS, &gen);
 }

+ 1 - 1
lib/rmg/CMapGenerator.h

@@ -169,7 +169,7 @@ public:
 class DLL_LINKAGE CMapGenerator
 {
 public:
-	CMapGenerator(const CMapGenOptions & mapGenOptions, int randomSeed);
+	explicit CMapGenerator(const CMapGenOptions & mapGenOptions, int randomSeed = std::time(nullptr));
 	~CMapGenerator(); // required due to unique_ptr
 
 	std::unique_ptr<CMap> generate();

+ 27 - 0
test/CMakeLists.txt

@@ -0,0 +1,27 @@
+cmake_minimum_required(VERSION 2.6)
+
+enable_testing()
+include_directories(${CMAKE_HOME_DIRECTORY} ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_HOME_DIRECTORY}/test)
+include_directories(${Boost_INCLUDE_DIRS})
+
+set(test_SRCS
+		StdInc.cpp
+		CVcmiTestConfig.cpp
+		CMapEditManagerTest.cpp
+)
+
+add_executable(vcmitest ${test_SRCS})
+target_link_libraries(vcmitest vcmi ${Boost_LIBRARIES})
+add_test(vcmitest vcmitest)
+
+# Files to copy to the build directory after compilation
+set(vcmitest_FILES
+		TerrainViewTest.h3m
+		terrainViewMappings.json
+)
+
+foreach(file ${vcmitest_FILES})
+	add_custom_command(TARGET vcmitest POST_BUILD
+	COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/${file}" $<TARGET_FILE_DIR:vcmitest>
+	)
+endforeach()

+ 83 - 0
test/CMapEditManagerTest.cpp

@@ -0,0 +1,83 @@
+
+/*
+ * CMapEditManagerTest.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#include "StdInc.h"
+#include <boost/test/unit_test.hpp>
+
+#include "../lib/mapping/CMapService.h"
+#include "../lib/mapping/CMap.h"
+#include "../lib/filesystem/CResourceLoader.h"
+#include "../lib/filesystem/CFilesystemLoader.h"
+#include "../lib/JsonNode.h"
+#include "../lib/mapping/CMapEditManager.h"
+#include "../lib/int3.h"
+#include "../lib/CRandomGenerator.h"
+
+BOOST_AUTO_TEST_CASE(CMapEditManager_DrawTerrain)
+{
+	try
+	{
+		// Load maps and json config
+		auto loader = make_shared<CFilesystemLoader>(".");
+		CResourceHandler::get()->addLoader("test/", loader, false);
+		const auto originalMap = CMapService::loadMap("test/TerrainViewTest");
+		auto map = CMapService::loadMap("test/TerrainViewTest");
+		logGlobal->infoStream() << "Loaded test map successfully.";
+
+		// Validate edit manager
+		auto editManager = map->getEditManager();
+		CRandomGenerator gen;
+		const JsonNode viewNode(ResourceID("test/terrainViewMappings", EResType::TEXT));
+		const auto & mappingsNode = viewNode["mappings"].Vector();
+		BOOST_FOREACH(const auto & node, mappingsNode)
+		{
+			// Get terrain group and id
+			const auto & patternStr = node["pattern"].String();
+			std::vector<std::string> patternParts;
+			boost::split(patternParts, patternStr, boost::is_any_of("."));
+			if(patternParts.size() != 2) throw std::runtime_error("A pattern should consist of two parts, the group and the id. Continue with next pattern.");
+			const auto & groupStr = patternParts[0];
+			const auto & id = patternParts[1];
+			auto terGroup = CTerrainViewPatternConfig::get().getTerrainGroup(groupStr);
+
+			// Get mapping range
+			const auto & pattern = CTerrainViewPatternConfig::get().getPatternById(terGroup, id);
+			const auto & mapping = pattern.mapping;
+
+			const auto & positionsNode = node["pos"].Vector();
+			BOOST_FOREACH(const auto & posNode, positionsNode)
+			{
+				const auto & posVector = posNode.Vector();
+				if(posVector.size() != 3) throw std::runtime_error("A position should consist of three values x,y,z. Continue with next position.");
+				int3 pos(posVector[0].Float(), posVector[1].Float(), posVector[2].Float());
+				logGlobal->infoStream() << boost::format("Test pattern '%s' on position x '%d', y '%d', z '%d'.") % patternStr % pos.x % pos.y % pos.z;
+				const auto & originalTile = originalMap->getTile(pos);
+				editManager->drawTerrain(MapRect(pos, 1, 1), originalTile.terType, &gen);
+				const auto & tile = map->getTile(pos);
+				bool isInRange = false;
+				BOOST_FOREACH(const auto & range, mapping)
+				{
+					if(tile.terView >= range.first && tile.terView <= range.second)
+					{
+						isInRange = true;
+						break;
+					}
+				}
+				BOOST_CHECK(isInRange);
+				if(!isInRange) logGlobal->errorStream() << "No or invalid pattern found for current position.";
+			}
+		}
+	}
+	catch(const std::exception & e)
+	{
+		logGlobal-> errorStream() << e.what();
+	}
+}

+ 37 - 0
test/CVcmiTestConfig.cpp

@@ -0,0 +1,37 @@
+
+/*
+ * CVcmiTestConfig.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#include "StdInc.h"
+#include "CVcmiTestConfig.h"
+
+#include "../lib/CConsoleHandler.h"
+#include "../lib/logging/CBasicLogConfigurator.h"
+#include "../lib/VCMIDirs.h"
+#include "../lib/VCMI_Lib.h"
+#include "../lib/logging/CLogger.h"
+#include "../lib/CConfigHandler.h"
+
+CVcmiTestConfig::CVcmiTestConfig()
+{
+	console = new CConsoleHandler;
+	CBasicLogConfigurator logConfig(VCMIDirs::get().localPath() + "/VCMI_Test_log.txt", console);
+	logConfig.configureDefault();
+	preinitDLL(console);
+	settings.init();
+	logConfig.configure();
+	loadDLLClasses();
+	logGlobal->infoStream() << "Initialized global test setup.";
+}
+
+CVcmiTestConfig::~CVcmiTestConfig()
+{
+	std::cout << "Ending global test tear-down." << std::endl;
+}

+ 20 - 0
test/CVcmiTestConfig.h

@@ -0,0 +1,20 @@
+
+/*
+ * CVcmiTestConfig.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#pragma once
+
+/// Global setup/tear down class for unit tests.
+class CVcmiTestConfig
+{
+public:
+	CVcmiTestConfig();
+	~CVcmiTestConfig();
+};

+ 7 - 0
test/StdInc.cpp

@@ -0,0 +1,7 @@
+// Creates the precompiled header
+#include "StdInc.h"
+
+#define BOOST_TEST_MODULE VcmiTest
+#include <boost/test/unit_test.hpp>
+#include "CVcmiTestConfig.h"
+BOOST_GLOBAL_FIXTURE(CVcmiTestConfig);

+ 8 - 0
test/StdInc.h

@@ -0,0 +1,8 @@
+#pragma once
+
+#include "../Global.h"
+
+#define BOOST_TEST_DYN_LINK
+
+
+

二進制
test/TerrainViewTest.h3m


+ 161 - 0
test/terrainViewMappings.json

@@ -0,0 +1,161 @@
+{
+	"mappings" : [
+		// Normal type
+		{
+			"pos" : [ [ 14,13,0 ], [ 4,34,0 ] ],
+			"pattern" : "normal.s1"
+		},
+		{
+			"pos" : [ [ 14,15,0 ], [ 29,19,0 ] ],
+			"pattern" : "normal.s2"
+		},
+		{
+			"pos" : [ [ 16,13,0 ], [ 23,30,0 ] ],
+			"pattern" : "normal.s3"
+		},
+		{
+			"pos" : [ [ 16,14,0 ], [ 29,20,0 ] ],
+			"pattern" : "normal.s4"
+		},
+		{
+			"pos" : [ [ 5,14,0 ], [ 31,13,0 ] ],
+			"pattern" : "normal.s5"
+		},
+		{
+			"pos" : [ [ 9,17,0 ], [ 3,17,0 ] ],
+			"pattern" : "normal.s6"
+		},
+		{
+			"pos" : [ [ 21,20,1 ], [ 28,27,1 ] ],
+			"pattern" : "normal.m1"
+		},
+		{
+			"pos" : [ [ 22,22,1 ] ],
+			"pattern" : "normal.m2"
+		},
+		{
+			"pos" : [ [ 12,31,0 ] ],
+			"pattern" : "normal.m3"
+		},
+		{
+			"pos" : [ [ 5,34,0 ] ],
+			"pattern" : "normal.m4"
+		},
+		{
+			"pos" : [ [ 12,34,0 ] ],
+			"pattern" : "normal.m5"
+		},
+		{
+			"pos" : [ [ 8,25,0 ] ],
+			"pattern" : "normal.m6"
+		},
+		{
+			"pos" : [ [ 5,32,0 ] ],
+			"pattern" : "normal.m7"
+		},
+		{
+			"pos" : [ [ 28,23,1 ] ],
+			"pattern" : "normal.m8"
+		},
+		{
+			"pos" : [ [ 7,23,0 ] ],
+			"pattern" : "normal.n1"
+		},
+		// Dirt type
+		{
+			"pos" : [ [ 22,26,0 ], [ 27,35,0 ] ],
+			"pattern" : "dirt.s1"
+		},
+		{
+			"pos" : [ [ 15,33,0 ], [ 19,34,0 ] ],
+			"pattern" : "dirt.s2"
+		},
+		{
+			"pos" : [ [ 24,32,0 ], [ 25,26,0 ] ],
+			"pattern" : "dirt.s3"
+		},
+		{
+			"pos" : [ [ 19,32,0 ] ],
+			"pattern" : "dirt.s4"
+		},
+		{
+			"pos" : [ [ 26,28,1 ] ],
+			"pattern" : "dirt.s5"
+		},
+		{
+			"pos" : [ [ 25,28,1 ] ],
+			"pattern" : "dirt.s6"
+		},
+		{
+			"pos" : [ [ 9,25,1 ] ],
+			"pattern" : "dirt.m1"
+		},
+		{
+			"pos" : [ [ 20,27,1 ] ],
+			"pattern" : "dirt.n1"
+		},
+		// Sand type
+		{
+			"pos" : [ [ 22,27,1 ] ],
+			"pattern" : "sand.n1"
+		},
+		// Water type
+		{
+			"pos" : [ [ 16,17,0 ] ],
+			"pattern" : "water.s1"
+		},
+		{
+			"pos" : [ [ 20,15,0 ] ],
+			"pattern" : "water.s2"
+		},
+		{
+			"pos" : [ [ 23,18,0 ] ],
+			"pattern" : "water.s3"
+		},
+		{
+			"pos" : [ [ 20,11,0 ] ],
+			"pattern" : "water.s4"
+		},
+		{
+			"pos" : [ [ 8,14,0 ] ],
+			"pattern" : "water.s5"
+		},
+		{
+			"pos" : [ [ 8,13,0 ] ],
+			"pattern" : "water.s6"
+		},
+		{
+			"pos" : [ [ 9,13,0 ] ],
+			"pattern" : "water.n1"
+		},
+		// Rock type
+		{
+			"pos" : [ [ 13,12,1 ] ],
+			"pattern" : "rock.n1"
+		},
+		{
+			"pos" : [ [ 11,21,1 ] ],
+			"pattern" : "rock.s1"
+		},
+		{
+			"pos" : [ [ 8,21,1 ] ],
+			"pattern" : "rock.s2"
+		},
+		{
+			"pos" : [ [ 10,12,1 ] ],
+			"pattern" : "rock.s3"
+		},
+		{
+			"pos" : [ [ 13,21,1 ] ],
+			"pattern" : "rock.s4"
+		},
+		{
+			"pos" : [ [ 10,22,1 ] ],
+			"pattern" : "rock.s5"
+		},
+		{
+			"pos" : [ [ 8,23,1 ] ],
+			"pattern" : "rock.s6"
+		},
+	]
+}