Browse Source

Merge pull request #972 from vcmi/cpp-map-editor

Cpp map editor
Andrii Danylchenko 3 years ago
parent
commit
b43e0ff9e4
78 changed files with 12673 additions and 20 deletions
  1. 1 1
      .travis.yml
  2. 6 2
      CMakeLists.txt
  3. 2 1
      CMakePresets.json
  4. 109 0
      config/schemas/terrain.json
  5. 2 12
      lib/rmg/CRmgTemplateStorage.cpp
  6. 2 4
      lib/rmg/CRmgTemplateStorage.h
  7. 777 0
      mapeditor/Animation.cpp
  8. 94 0
      mapeditor/Animation.h
  9. 160 0
      mapeditor/BitmapHandler.cpp
  10. 23 0
      mapeditor/BitmapHandler.h
  11. 136 0
      mapeditor/CMakeLists.txt
  12. 1 0
      mapeditor/StdInc.cpp
  13. 55 0
      mapeditor/StdInc.h
  14. 47 0
      mapeditor/generatorprogress.cpp
  15. 32 0
      mapeditor/generatorprogress.h
  16. 46 0
      mapeditor/generatorprogress.ui
  17. 345 0
      mapeditor/graphics.cpp
  18. 85 0
      mapeditor/graphics.h
  19. BIN
      mapeditor/icons/menu-game.png
  20. BIN
      mapeditor/icons/menu-mods.png
  21. BIN
      mapeditor/icons/menu-settings.png
  22. BIN
      mapeditor/icons/mod-delete.png
  23. BIN
      mapeditor/icons/mod-disabled.png
  24. BIN
      mapeditor/icons/mod-download.png
  25. BIN
      mapeditor/icons/mod-enabled.png
  26. BIN
      mapeditor/icons/mod-update.png
  27. 152 0
      mapeditor/inspector/armywidget.cpp
  28. 57 0
      mapeditor/inspector/armywidget.h
  29. 298 0
      mapeditor/inspector/armywidget.ui
  30. 849 0
      mapeditor/inspector/inspector.cpp
  31. 164 0
      mapeditor/inspector/inspector.h
  32. 63 0
      mapeditor/inspector/messagewidget.cpp
  33. 44 0
      mapeditor/inspector/messagewidget.h
  34. 33 0
      mapeditor/inspector/messagewidget.ui
  35. 193 0
      mapeditor/inspector/questwidget.cpp
  36. 52 0
      mapeditor/inspector/questwidget.h
  37. 50 0
      mapeditor/inspector/questwidget.ui
  38. 427 0
      mapeditor/inspector/rewardswidget.cpp
  39. 98 0
      mapeditor/inspector/rewardswidget.h
  40. 83 0
      mapeditor/inspector/rewardswidget.ui
  41. 250 0
      mapeditor/inspector/townbulidingswidget.cpp
  42. 63 0
      mapeditor/inspector/townbulidingswidget.h
  43. 49 0
      mapeditor/inspector/townbulidingswidget.ui
  44. 125 0
      mapeditor/jsonutils.cpp
  45. 22 0
      mapeditor/jsonutils.h
  46. 36 0
      mapeditor/launcherdirs.cpp
  47. 22 0
      mapeditor/launcherdirs.h
  48. 19 0
      mapeditor/main.cpp
  49. 1110 0
      mapeditor/mainwindow.cpp
  50. 145 0
      mapeditor/mainwindow.h
  51. 1120 0
      mapeditor/mainwindow.ui
  52. 508 0
      mapeditor/mapcontroller.cpp
  53. 69 0
      mapeditor/mapcontroller.h
  54. BIN
      mapeditor/mapeditor.ico
  55. 1 0
      mapeditor/mapeditor.rc
  56. 546 0
      mapeditor/maphandler.cpp
  57. 115 0
      mapeditor/maphandler.h
  58. 112 0
      mapeditor/mapsettings.cpp
  59. 34 0
      mapeditor/mapsettings.h
  60. 175 0
      mapeditor/mapsettings.ui
  61. 459 0
      mapeditor/mapview.cpp
  62. 140 0
      mapeditor/mapview.h
  63. 78 0
      mapeditor/objectbrowser.cpp
  64. 27 0
      mapeditor/objectbrowser.h
  65. 147 0
      mapeditor/playerparams.cpp
  66. 49 0
      mapeditor/playerparams.h
  67. 142 0
      mapeditor/playerparams.ui
  68. 89 0
      mapeditor/playersettings.cpp
  69. 41 0
      mapeditor/playersettings.h
  70. 117 0
      mapeditor/playersettings.ui
  71. 568 0
      mapeditor/scenelayer.cpp
  72. 190 0
      mapeditor/scenelayer.h
  73. 172 0
      mapeditor/validator.cpp
  74. 40 0
      mapeditor/validator.h
  75. 72 0
      mapeditor/validator.ui
  76. 417 0
      mapeditor/windownewmap.cpp
  77. 102 0
      mapeditor/windownewmap.h
  78. 816 0
      mapeditor/windownewmap.ui

+ 1 - 1
.travis.yml

@@ -41,7 +41,7 @@ addons:
     notification_email: [email protected]
     build_command_prepend: cov-configure --compiler clang-3.6 --comptype clangcc &&
       cov-configure --comptype clangcxx --compiler clang++-3.6 && cmake -G Ninja ..
-      -DCMAKE_BUILD_TYPE=DEBUG -DENABLE_LAUNCHER=0
+      -DCMAKE_BUILD_TYPE=DEBUG -DENABLE_LAUNCHER=0 -DENABLE_EDITOR=0
     build_command: ninja -j 3
     branch_pattern: coverity_scan
 

+ 6 - 2
CMakeLists.txt

@@ -65,6 +65,7 @@ endif()
 option(ENABLE_ERM "Enable compilation of ERM scripting module" OFF)
 option(ENABLE_LUA "Enable compilation of LUA scripting module" OFF)
 option(ENABLE_LAUNCHER "Enable compilation of launcher" ON)
+option(ENABLE_EDITOR "Enable compilation of map editor" ON)
 if(APPLE_IOS)
 	set(BUNDLE_IDENTIFIER_PREFIX "" CACHE STRING "Bundle identifier prefix")
 	set(APP_DISPLAY_NAME "VCMI" CACHE STRING "App name on the home screen")
@@ -324,7 +325,7 @@ if(TARGET SDL2_ttf::SDL2_ttf)
 endif()
 find_package(TBB REQUIRED)
 
-if(ENABLE_LAUNCHER)
+if(ENABLE_LAUNCHER OR ENABLE_EDITOR)
 	# Widgets finds its own dependencies (QtGui and QtCore).
 	find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets Network)
 	find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Network)
@@ -455,6 +456,9 @@ endif()
 if(ENABLE_LAUNCHER)
 	add_subdirectory(launcher)
 endif()
+if(ENABLE_EDITOR)
+	add_subdirectory(mapeditor)
+endif()
 add_subdirectory(client)
 add_subdirectory(server)
 add_subdirectory_with_folder("AI" AI)
@@ -494,7 +498,7 @@ if(WIN32)
 		set(debug_postfix d)
 	endif()
 
-	if(ENABLE_LAUNCHER)
+	if(ENABLE_LAUNCHER OR ENABLE_EDITOR)
 		get_target_property(QtCore_location Qt${QT_VERSION_MAJOR}::Core LOCATION)
 		get_filename_component(Qtbin_folder ${QtCore_location} PATH)
 		file(GLOB dep_files

+ 2 - 1
CMakePresets.json

@@ -119,7 +119,8 @@
             "cacheVariables": {
                 "CMAKE_SYSTEM_NAME": "iOS",
                 "FORCE_BUNDLED_FL": "ON",
-                "FORCE_BUNDLED_MINIZIP": "ON"
+                "FORCE_BUNDLED_MINIZIP": "ON",
+                "ENABLE_EDITOR" : "OFF"
             }
         },
         {

+ 109 - 0
config/schemas/terrain.json

@@ -0,0 +1,109 @@
+{
+	"type":"object",
+	"$schema": "http://json-schema.org/draft-04/schema",
+	"title" : "VCMI terrain format",
+	"description" : "Format used to define new terrains in VCMI",
+	"required" : [ "tiles", "code", "moveCost" ],
+
+	"additionalProperties" : false,
+	"properties":{
+		"moveCost":
+		{
+			"type": "number",
+			"description": "How many movement points needed to move hero"
+		},
+		"minimapUnblocked":
+		{
+			"type": "array",
+			"description": "Color of terrain on minimap without unpassable objects",
+			"minItems": 3,
+			"maxItems": 3,
+			"items":
+			{
+				"type": "number"
+			}
+		},
+		"minimapBlocked":
+		{
+			"type": "array",
+			"description": "Color of terrain on minimap with unpassable objects",
+			"minItems": 3,
+			"maxItems": 3,
+			"items":
+			{
+				"type": "number"
+			}
+		},
+		"music":
+		{
+			"type": "string",
+			"description": "Music filename to play on this terrain on adventure map"
+		},
+		"tiles":
+		{
+			"type": "string",
+			"description": "Name of file with graphicks",
+			"format": "defFile"
+		},
+		"type":
+		{
+			"type": "string",
+			"description": "Type of this terrain. Can be land, water, subterranean or rock",
+			"enum": ["LAND", "WATER", "SUB", "ROCK"]
+		},
+		"rockTerrain":
+		{
+			"type": "string",
+			"description": "The name of tock type terrain which will be used as borders in the underground"
+		},
+		"river":
+		{
+			"type": "string",
+			"description": "River type which should be used for that terrain",
+			"enum": ["", "rw", "ri", "rm", "rl"]
+		},
+		"horseSoundId":
+		{
+			"type": "number",
+			"description": "Id of horse sound to be played when hero is moving across terrain"
+		},
+		"text":
+		{
+			"type": "string",
+			"description": "Text to be shown when mouse if over terrain"
+		},
+		"code":
+		{
+			"type": "string",
+			"description": "Two-letters unique indentifier for this terrain. Used for terrain serializaion"
+		},
+		"battleFields":
+		{
+			"type": "array",
+			"description": "array of battleFields for this terrain",
+			"items":
+			{
+				"type": "string"
+			}
+		},
+		"prohibitTransitions":
+		{
+			"type": "array",
+			"description": "array or terrain names, which is prohibited to make transition from/to",
+			"items":
+			{
+				"type": "string"
+			}
+		},
+		"transitionRequired":
+		{
+			"type": "boolean",
+			"description": "If sand/dirt transition required from/to other terrains"
+		},
+		"terrainViewPatterns":
+		{
+			"type": "string",
+			"description": "Can be normal, dirt, water, rock"
+		}
+	}
+}

+ 2 - 12
lib/rmg/CRmgTemplateStorage.cpp

@@ -34,8 +34,6 @@ void CRmgTemplateStorage::loadObject(std::string scope, std::string name, const
 		templates[fullKey].setId(name);
 		templates[fullKey].serializeJson(handler);
 		templates[fullKey].validate();
-
-		templatesByName[name] = templates[fullKey];
 	}
 	catch(const std::exception & e)
 	{
@@ -55,22 +53,14 @@ std::vector<JsonNode> CRmgTemplateStorage::loadLegacyData(size_t dataSize)
 	//it would be cool to load old rmg.txt files
 }
 
-const CRmgTemplate * CRmgTemplateStorage::getTemplate(const std::string & templateFullId) const
+const CRmgTemplate * CRmgTemplateStorage::getTemplate(const std::string & templateName) const
 {
-	auto iter = templates.find(templateFullId);
+	auto iter = templates.find(templateName);
 	if(iter==templates.end())
 		return nullptr;
 	return &iter->second;
 }
 
-const CRmgTemplate * CRmgTemplateStorage::getTemplateByName(const std::string & templateName) const
-{
-	auto iter = templatesByName.find(templateName);
-	if(iter == templatesByName.end())
-		return nullptr;
-	return &iter->second;
-}
-
 std::vector<const CRmgTemplate *> CRmgTemplateStorage::getTemplates() const
 {
 	std::vector<const CRmgTemplate *> result;

+ 2 - 4
lib/rmg/CRmgTemplateStorage.h

@@ -31,13 +31,11 @@ public:
 	virtual void loadObject(std::string scope, std::string name, const JsonNode & data) override;
 	virtual void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override;
 	
-	const CRmgTemplate * getTemplate(const std::string & templateFullId) const;
-	const CRmgTemplate * getTemplateByName(const std::string & templateName) const;
+	const CRmgTemplate* getTemplate(const std::string & templateName) const;
 	std::vector<const CRmgTemplate *> getTemplates() const;
 
 private:
-	std::map<std::string, CRmgTemplate> templates; //FIXME: doesn't IHandlerBase cover this?
-	std::map<std::string, CRmgTemplate> templatesByName;
+	std::map<std::string, CRmgTemplate> templates;
 };
 
 

+ 777 - 0
mapeditor/Animation.cpp

@@ -0,0 +1,777 @@
+/*
+ * Animation.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
+ *
+ */
+
+//code is copied from vcmiclient/CAnimation.cpp with minimal changes
+
+#include "StdInc.h"
+#include "Animation.h"
+
+#include "BitmapHandler.h"
+
+#include "../lib/filesystem/Filesystem.h"
+#include "../lib/filesystem/ISimpleResourceLoader.h"
+#include "../lib/JsonNode.h"
+#include "../lib/CRandomGenerator.h"
+
+
+typedef std::map<size_t, std::vector<JsonNode>> source_map;
+
+/// Class for def loading
+/// After loading will store general info (palette and frame offsets) and pointer to file itself
+class DefFile
+{
+private:
+
+	struct SSpriteDef
+	{
+		ui32 size;
+		ui32 format;    /// format in which pixel data is stored
+		ui32 fullWidth; /// full width and height of frame, including borders
+		ui32 fullHeight;
+		ui32 width;     /// width and height of pixel data, borders excluded
+		ui32 height;
+		si32 leftMargin;
+		si32 topMargin;
+	};
+	//offset[group][frame] - offset of frame data in file
+	std::map<size_t, std::vector <size_t> > offset;
+
+	std::unique_ptr<ui8[]> data;
+	std::unique_ptr<QVector<QRgb>> palette;
+
+public:
+	DefFile(std::string Name);
+	~DefFile();
+
+	std::shared_ptr<QImage> loadFrame(size_t frame, size_t group) const;
+
+	const std::map<size_t, size_t> getEntries() const;
+};
+
+class ImageLoader
+{
+	QImage * image;
+	ui8 * lineStart;
+	ui8 * position;
+	QPoint spriteSize, margins, fullSize;
+public:
+	//load size raw pixels from data
+	inline void Load(size_t size, const ui8 * data);
+	//set size pixels to color
+	inline void Load(size_t size, ui8 color=0);
+	inline void EndLine();
+	//init image with these sizes and palette
+	inline void init(QPoint SpriteSize, QPoint Margins, QPoint FullSize);
+
+	ImageLoader(QImage * Img);
+	~ImageLoader();
+};
+
+// Extremely simple file cache. TODO: smarter, more general solution
+class FileCache
+{
+	static const int cacheSize = 50; //Max number of cached files
+	struct FileData
+	{
+		ResourceID             name;
+		size_t                 size;
+		std::unique_ptr<ui8[]> data;
+
+		std::unique_ptr<ui8[]> getCopy()
+		{
+			auto ret = std::unique_ptr<ui8[]>(new ui8[size]);
+			std::copy(data.get(), data.get() + size, ret.get());
+			return ret;
+		}
+		FileData(ResourceID name_, size_t size_, std::unique_ptr<ui8[]> data_):
+			name{std::move(name_)},
+			size{size_},
+			data{std::move(data_)}
+		{}
+	};
+
+	std::deque<FileData> cache;
+public:
+	std::unique_ptr<ui8[]> getCachedFile(ResourceID rid)
+	{
+		for(auto & file : cache)
+		{
+			if(file.name == rid)
+				return file.getCopy();
+		}
+		// Still here? Cache miss
+		if(cache.size() > cacheSize)
+			cache.pop_front();
+
+		auto data =  CResourceHandler::get()->load(rid)->readAll();
+
+		cache.emplace_back(std::move(rid), data.second, std::move(data.first));
+
+		return cache.back().getCopy();
+	}
+};
+
+enum class DefType : uint32_t
+{
+	SPELL = 0x40,
+	SPRITE = 0x41,
+	CREATURE = 0x42,
+	MAP = 0x43,
+	MAP_HERO = 0x44,
+	TERRAIN = 0x45,
+	CURSOR = 0x46,
+	INTERFACE = 0x47,
+	SPRITE_FRAME = 0x48,
+	BATTLE_HERO = 0x49
+};
+
+static FileCache animationCache;
+
+/*************************************************************************
+ *  DefFile, class used for def loading                                  *
+ *************************************************************************/
+
+DefFile::DefFile(std::string Name):
+	data(nullptr)
+{
+
+	#if 0
+	static QRgba H3_ORIG_PALETTE[8] =
+	{
+	   {  0, 255, 255, SDL_ALPHA_OPAQUE},
+	   {255, 150, 255, SDL_ALPHA_OPAQUE},
+	   {255, 100, 255, SDL_ALPHA_OPAQUE},
+	   {255,  50, 255, SDL_ALPHA_OPAQUE},
+	   {255,   0, 255, SDL_ALPHA_OPAQUE},
+	   {255, 255, 0,   SDL_ALPHA_OPAQUE},
+	   {180,   0, 255, SDL_ALPHA_OPAQUE},
+	   {  0, 255, 0,   SDL_ALPHA_OPAQUE}
+	};
+	#endif // 0
+
+	//First 8 colors in def palette used for transparency
+	static QRgb H3Palette[8] =
+	{
+		qRgba(0, 0, 0,   0), // 100% - transparency
+		qRgba(0, 0, 0,  32), //  75% - shadow border,
+		qRgba(0, 0, 0,  64), // TODO: find exact value
+		qRgba(0, 0, 0, 128), // TODO: for transparency
+		qRgba(0, 0, 0, 128), //  50% - shadow body
+		qRgba(0, 0, 0,   0), // 100% - selection highlight
+		qRgba(0, 0, 0, 128), //  50% - shadow body   below selection
+		qRgba(0, 0, 0,  64)  // 75% - shadow border below selection
+	};
+	data = animationCache.getCachedFile(ResourceID(std::string("SPRITES/") + Name, EResType::ANIMATION));
+
+	palette = std::make_unique<QVector<QRgb>>(256);
+	int it = 0;
+
+	ui32 type = read_le_u32(data.get() + it);
+	it += 4;
+	//int width  = read_le_u32(data + it); it+=4;//not used
+	//int height = read_le_u32(data + it); it+=4;
+	it += 8;
+	ui32 totalBlocks = read_le_u32(data.get() + it);
+	it += 4;
+
+	for (ui32 i= 0; i<256; i++)
+	{
+		ui8 c[3];
+		c[0] = data[it++];
+		c[1] = data[it++];
+		c[2] = data[it++];
+		(*palette)[i] = qRgba(c[0], c[1], c[2], 255);
+	}
+
+	switch(static_cast<DefType>(type))
+	{
+	case DefType::SPELL:
+		(*palette)[0] = H3Palette[0];
+		break;
+	case DefType::SPRITE:
+	case DefType::SPRITE_FRAME:
+		for(ui32 i= 0; i<8; i++)
+			(*palette)[i] = H3Palette[i];
+		break;
+	case DefType::CREATURE:
+		(*palette)[0] = H3Palette[0];
+		(*palette)[1] = H3Palette[1];
+		(*palette)[4] = H3Palette[4];
+		(*palette)[5] = H3Palette[5];
+		(*palette)[6] = H3Palette[6];
+		(*palette)[7] = H3Palette[7];
+		break;
+	case DefType::MAP:
+	case DefType::MAP_HERO:
+		(*palette)[0] = H3Palette[0];
+		(*palette)[1] = H3Palette[1];
+		(*palette)[4] = H3Palette[4];
+		//5 = owner flag, handled separately
+		break;
+	case DefType::TERRAIN:
+		(*palette)[0] = H3Palette[0];
+		(*palette)[1] = H3Palette[1];
+		(*palette)[2] = H3Palette[2];
+		(*palette)[3] = H3Palette[3];
+		(*palette)[4] = H3Palette[4];
+		break;
+	case DefType::CURSOR:
+		(*palette)[0] = H3Palette[0];
+		break;
+	case DefType::INTERFACE:
+		(*palette)[0] = H3Palette[0];
+		(*palette)[1] = H3Palette[1];
+		(*palette)[4] = H3Palette[4];
+		//player colors handled separately
+		//TODO: disallow colorizing other def types
+		break;
+	case DefType::BATTLE_HERO:
+		(*palette)[0] = H3Palette[0];
+		(*palette)[1] = H3Palette[1];
+		(*palette)[4] = H3Palette[4];
+		break;
+	default:
+		logAnim->error("Unknown def type %d in %s", type, Name);
+		break;
+	}
+
+
+	for (ui32 i=0; i<totalBlocks; i++)
+	{
+		size_t blockID = read_le_u32(data.get() + it);
+		it+=4;
+		size_t totalEntries = read_le_u32(data.get() + it);
+		it+=12;
+		//8 unknown bytes - skipping
+
+		//13 bytes for name of every frame in this block - not used, skipping
+		it+= 13 * (int)totalEntries;
+
+		for (ui32 j=0; j<totalEntries; j++)
+		{
+			size_t currOffset = read_le_u32(data.get() + it);
+			offset[blockID].push_back(currOffset);
+			it += 4;
+		}
+	}
+}
+
+std::shared_ptr<QImage> DefFile::loadFrame(size_t frame, size_t group) const
+{
+	std::map<size_t, std::vector <size_t> >::const_iterator it;
+	it = offset.find(group);
+	assert (it != offset.end());
+
+	const ui8 * FDef = data.get()+it->second[frame];
+
+	const SSpriteDef sd = * reinterpret_cast<const SSpriteDef *>(FDef);
+	SSpriteDef sprite;
+
+	sprite.format = read_le_u32(&sd.format);
+	sprite.fullWidth = read_le_u32(&sd.fullWidth);
+	sprite.fullHeight = read_le_u32(&sd.fullHeight);
+	sprite.width = read_le_u32(&sd.width);
+	sprite.height = read_le_u32(&sd.height);
+	sprite.leftMargin = read_le_u32(&sd.leftMargin);
+	sprite.topMargin = read_le_u32(&sd.topMargin);
+
+	ui32 currentOffset = sizeof(SSpriteDef);
+
+	//special case for some "old" format defs (SGTWMTA.DEF and SGTWMTB.DEF)
+
+	if(sprite.format == 1 && sprite.width > sprite.fullWidth && sprite.height > sprite.fullHeight)
+	{
+		sprite.leftMargin = 0;
+		sprite.topMargin = 0;
+		sprite.width = sprite.fullWidth;
+		sprite.height = sprite.fullHeight;
+
+		currentOffset -= 16;
+	}
+
+	const ui32 BaseOffset = currentOffset;
+
+	
+	std::shared_ptr<QImage> img = std::make_shared<QImage>(sprite.fullWidth, sprite.fullHeight, QImage::Format_Indexed8);
+	if(!img)
+		throw std::runtime_error("Image memory cannot be allocated");
+	
+	ImageLoader loader(img.get());
+	loader.init(QPoint(sprite.width, sprite.height),
+				QPoint(sprite.leftMargin, sprite.topMargin),
+				QPoint(sprite.fullWidth, sprite.fullHeight));
+
+	switch(sprite.format)
+	{
+	case 0:
+		{
+			//pixel data is not compressed, copy data to surface
+			for(ui32 i=0; i<sprite.height; i++)
+			{
+				loader.Load(sprite.width, FDef + currentOffset);
+				currentOffset += sprite.width;
+				loader.EndLine();
+			}
+			break;
+		}
+	case 1:
+		{
+			//for each line we have offset of pixel data
+			const ui32 * RWEntriesLoc = reinterpret_cast<const ui32 *>(FDef+currentOffset);
+			currentOffset += sizeof(ui32) * sprite.height;
+
+			for(ui32 i=0; i<sprite.height; i++)
+			{
+				//get position of the line
+				currentOffset=BaseOffset + read_le_u32(RWEntriesLoc + i);
+				ui32 TotalRowLength = 0;
+
+				while(TotalRowLength<sprite.width)
+				{
+					ui8 segmentType = FDef[currentOffset++];
+					ui32 length = FDef[currentOffset++] + 1;
+
+					if(segmentType==0xFF)//Raw data
+					{
+						loader.Load(length, FDef + currentOffset);
+						currentOffset+=length;
+					}
+					else// RLE
+					{
+						loader.Load(length, segmentType);
+					}
+					TotalRowLength += length;
+				}
+
+				loader.EndLine();
+			}
+			break;
+		}
+	case 2:
+		{
+			currentOffset = BaseOffset + read_le_u16(FDef + BaseOffset);
+
+			for(ui32 i=0; i<sprite.height; i++)
+			{
+				ui32 TotalRowLength=0;
+
+				while(TotalRowLength<sprite.width)
+				{
+					ui8 segment=FDef[currentOffset++];
+					ui8 code = segment / 32;
+					ui8 length = (segment & 31) + 1;
+
+					if(code==7)//Raw data
+					{
+						loader.Load(length, FDef + currentOffset);
+						currentOffset += length;
+					}
+					else//RLE
+					{
+						loader.Load(length, code);
+					}
+					TotalRowLength+=length;
+				}
+				loader.EndLine();
+			}
+			break;
+		}
+	case 3:
+		{
+			for(ui32 i=0; i<sprite.height; i++)
+			{
+				currentOffset = BaseOffset + read_le_u16(FDef + BaseOffset+i*2*(sprite.width/32));
+				ui32 TotalRowLength=0;
+
+				while(TotalRowLength<sprite.width)
+				{
+					ui8 segment = FDef[currentOffset++];
+					ui8 code = segment / 32;
+					ui8 length = (segment & 31) + 1;
+
+					if(code==7)//Raw data
+					{
+						loader.Load(length, FDef + currentOffset);
+						currentOffset += length;
+					}
+					else//RLE
+					{
+						loader.Load(length, code);
+					}
+					TotalRowLength += length;
+				}
+				loader.EndLine();
+			}
+			break;
+		}
+	default:
+	logGlobal->error("Error: unsupported format of def file: %d", sprite.format);
+		break;
+	}
+	
+	
+	img->setColorTable(*palette);
+	return img;
+}
+
+DefFile::~DefFile() = default;
+
+const std::map<size_t, size_t > DefFile::getEntries() const
+{
+	std::map<size_t, size_t > ret;
+
+	for (auto & elem : offset)
+		ret[elem.first] =  elem.second.size();
+	return ret;
+}
+
+/*************************************************************************
+ *  Classes for image loaders - helpers for loading from def files       *
+ *************************************************************************/
+
+ImageLoader::ImageLoader(QImage * Img):
+	image(Img),
+	lineStart(Img->bits()),
+	position(Img->bits())
+{
+	
+}
+
+void ImageLoader::init(QPoint SpriteSize, QPoint Margins, QPoint FullSize)
+{
+	spriteSize = SpriteSize;
+	margins = Margins;
+	fullSize = FullSize;
+	
+	memset((void *)image->bits(), 0, fullSize.y() * fullSize.x());
+	
+	lineStart = image->bits();
+	lineStart += margins.y() * fullSize.x() + margins.x();
+	position = lineStart;
+}
+
+inline void ImageLoader::Load(size_t size, const ui8 * data)
+{
+	if(size)
+	{
+		memcpy((void *)position, data, size);
+		position += size;
+	}
+}
+
+inline void ImageLoader::Load(size_t size, ui8 color)
+{
+	if(size)
+	{
+		memset((void *)position, color, size);
+		position += size;
+	}
+}
+
+inline void ImageLoader::EndLine()
+{
+	lineStart += fullSize.x();
+	position = lineStart;
+}
+
+ImageLoader::~ImageLoader()
+{
+}
+
+/*************************************************************************
+ *  Classes for images, support loading from file and drawing on surface *
+ *************************************************************************/
+
+std::shared_ptr<QImage> Animation::getFromExtraDef(std::string filename)
+{
+	size_t pos = filename.find(':');
+	if(pos == -1)
+		return nullptr;
+	Animation anim(filename.substr(0, pos));
+	pos++;
+	size_t frame = atoi(filename.c_str()+pos);
+	size_t group = 0;
+	pos = filename.find(':', pos);
+	if(pos != -1)
+	{
+		pos++;
+		group = frame;
+		frame = atoi(filename.c_str()+pos);
+	}
+	anim.load(frame ,group);
+	auto ret = anim.images[group][frame];
+	anim.images.clear();
+	return ret;
+}
+
+bool Animation::loadFrame(size_t frame, size_t group)
+{
+	if(size(group) <= frame)
+	{
+		printError(frame, group, "LoadFrame");
+		return false;
+	}
+
+	auto image = getImage(frame, group, false);
+	if(image)
+	{
+		return true;
+	}
+
+	//try to get image from def
+	if(source[group][frame].getType() == JsonNode::JsonType::DATA_NULL)
+	{
+		if(defFile)
+		{
+			auto frameList = defFile->getEntries();
+
+			if(vstd::contains(frameList, group) && frameList.at(group) > frame) // frame is present
+			{
+				images[group][frame] = defFile->loadFrame(frame, group);
+				return true;
+			}
+		}
+		return false;
+		// still here? image is missing
+
+		printError(frame, group, "LoadFrame");
+		images[group][frame] = std::make_shared<QImage>("DEFAULT");
+	}
+	else //load from separate file
+	{
+		images[group][frame] = getFromExtraDef(source[group][frame]["file"].String());;
+		return true;
+	}
+	return false;
+}
+
+bool Animation::unloadFrame(size_t frame, size_t group)
+{
+	auto image = getImage(frame, group, false);
+	if(image)
+	{
+		images[group].erase(frame);
+
+		if(images[group].empty())
+			images.erase(group);
+		return true;
+	}
+	return false;
+}
+
+void Animation::init()
+{
+	if(defFile)
+	{
+		const std::map<size_t, size_t> defEntries = defFile->getEntries();
+
+		for (auto & defEntry : defEntries)
+			source[defEntry.first].resize(defEntry.second);
+	}
+
+#if 0 //this code is not used but maybe requred if there will be configurable sprites
+	ResourceID resID(std::string("SPRITES/") + name, EResType::TEXT);
+
+	//if(vstd::contains(graphics->imageLists, resID.getName()))
+		//initFromJson(graphics->imageLists[resID.getName()]);
+
+	auto configList = CResourceHandler::get()->getResourcesWithName(resID);
+
+	for(auto & loader : configList)
+	{
+		auto stream = loader->load(resID);
+		std::unique_ptr<ui8[]> textData(new ui8[stream->getSize()]);
+		stream->read(textData.get(), stream->getSize());
+
+		const JsonNode config((char*)textData.get(), stream->getSize());
+
+		//initFromJson(config);
+	}
+#endif
+}
+
+void Animation::printError(size_t frame, size_t group, std::string type) const
+{
+	logGlobal->error("%s error: Request for frame not present in CAnimation! File name: %s, Group: %d, Frame: %d", type, name, group, frame);
+}
+
+Animation::Animation(std::string Name):
+	name(Name),
+	preloaded(false),
+	defFile()
+{
+	size_t dotPos = name.find_last_of('.');
+	if( dotPos!=-1 )
+		name.erase(dotPos);
+	std::transform(name.begin(), name.end(), name.begin(), toupper);
+
+	ResourceID resource(std::string("SPRITES/") + name, EResType::ANIMATION);
+
+	if(CResourceHandler::get()->existsResource(resource))
+		defFile = std::make_shared<DefFile>(name);
+
+	init();
+
+	if(source.empty())
+		logAnim->error("Animation %s failed to load", Name);
+}
+
+Animation::Animation():
+	name(""),
+	preloaded(false),
+	defFile()
+{
+	init();
+}
+
+Animation::~Animation() = default;
+
+void Animation::duplicateImage(const size_t sourceGroup, const size_t sourceFrame, const size_t targetGroup)
+{
+	if(!source.count(sourceGroup))
+	{
+		logAnim->error("Group %d missing in %s", sourceGroup, name);
+		return;
+	}
+
+	if(source[sourceGroup].size() <= sourceFrame)
+	{
+		logAnim->error("Frame [%d %d] missing in %s", sourceGroup, sourceFrame, name);
+		return;
+	}
+
+	//todo: clone actual loaded Image object
+	JsonNode clone(source[sourceGroup][sourceFrame]);
+
+	if(clone.getType() == JsonNode::JsonType::DATA_NULL)
+	{
+		std::string temp =  name+":"+boost::lexical_cast<std::string>(sourceGroup)+":"+boost::lexical_cast<std::string>(sourceFrame);
+		clone["file"].String() = temp;
+	}
+
+	source[targetGroup].push_back(clone);
+
+	size_t index = source[targetGroup].size() - 1;
+
+	if(preloaded)
+		load(index, targetGroup);
+}
+
+void Animation::setCustom(std::string filename, size_t frame, size_t group)
+{
+	if(source[group].size() <= frame)
+		source[group].resize(frame+1);
+	source[group][frame]["file"].String() = filename;
+	//FIXME: update image if already loaded
+}
+
+std::shared_ptr<QImage> Animation::getImage(size_t frame, size_t group, bool verbose) const
+{
+	auto groupIter = images.find(group);
+	if(groupIter != images.end())
+	{
+		auto imageIter = groupIter->second.find(frame);
+		if(imageIter != groupIter->second.end())
+			return imageIter->second;
+	}
+	if(verbose)
+		printError(frame, group, "GetImage");
+	return nullptr;
+}
+
+void Animation::load()
+{
+	for (auto & elem : source)
+		for (size_t image=0; image < elem.second.size(); image++)
+			loadFrame(image, elem.first);
+}
+
+void Animation::unload()
+{
+	for (auto & elem : source)
+		for (size_t image=0; image < elem.second.size(); image++)
+			unloadFrame(image, elem.first);
+
+}
+
+void Animation::preload()
+{
+	if(!preloaded)
+	{
+		preloaded = true;
+		load();
+	}
+}
+
+void Animation::loadGroup(size_t group)
+{
+	if(vstd::contains(source, group))
+		for (size_t image=0; image < source[group].size(); image++)
+			loadFrame(image, group);
+}
+
+void Animation::unloadGroup(size_t group)
+{
+	if(vstd::contains(source, group))
+		for (size_t image=0; image < source[group].size(); image++)
+			unloadFrame(image, group);
+}
+
+void Animation::load(size_t frame, size_t group)
+{
+	loadFrame(frame, group);
+}
+
+void Animation::unload(size_t frame, size_t group)
+{
+	unloadFrame(frame, group);
+}
+
+size_t Animation::size(size_t group) const
+{
+	auto iter = source.find(group);
+	if(iter != source.end())
+		return iter->second.size();
+	return 0;
+}
+
+void Animation::horizontalFlip()
+{
+	for(auto & group : images)
+		for(auto & image : group.second)
+			*image.second = image.second->transformed(QTransform::fromScale(-1, 1));
+}
+
+void Animation::verticalFlip()
+{
+	for(auto & group : images)
+		for(auto & image : group.second)
+			*image.second = image.second->transformed(QTransform::fromScale(1, -1));
+}
+
+void Animation::playerColored(PlayerColor player)
+{
+#if 0 //can be required in image preview?
+	for(auto & group : images)
+		for(auto & image : group.second)
+			image.second->playerColored(player);
+#endif
+}
+
+void Animation::createFlippedGroup(const size_t sourceGroup, const size_t targetGroup)
+{
+	for(size_t frame = 0; frame < size(sourceGroup); ++frame)
+	{
+		duplicateImage(sourceGroup, frame, targetGroup);
+
+		auto image = getImage(frame, targetGroup);
+		*image = image->transformed(QTransform::fromScale(1, -1));
+	}
+}

+ 94 - 0
mapeditor/Animation.h

@@ -0,0 +1,94 @@
+/*
+ * Animation.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
+//code is copied from vcmiclient/CAnimation.h with minimal changes
+
+#include "../lib/JsonNode.h"
+#include "../lib/GameConstants.h"
+#include <QRgb>
+#include <QImage>
+
+/*
+ * Base class for images, can be used for non-animation pictures as well
+ */
+
+class DefFile;
+/// Class for handling animation
+class Animation
+{
+private:
+	//source[group][position] - file with this frame, if string is empty - image located in def file
+	std::map<size_t, std::vector<JsonNode>> source;
+
+	//bitmap[group][position], store objects with loaded bitmaps
+	std::map<size_t, std::map<size_t, std::shared_ptr<QImage> > > images;
+
+	//animation file name
+	std::string name;
+
+	bool preloaded;
+
+	std::shared_ptr<DefFile> defFile;
+
+	//loader, will be called by load(), require opened def file for loading from it. Returns true if image is loaded
+	bool loadFrame(size_t frame, size_t group);
+
+	//unloadFrame, returns true if image has been unloaded ( either deleted or decreased refCount)
+	bool unloadFrame(size_t frame, size_t group);
+
+	//initialize animation from file
+	//void initFromJson(const JsonNode & input);
+	void init();
+
+	//to get rid of copy-pasting error message :]
+	void printError(size_t frame, size_t group, std::string type) const;
+
+	//not a very nice method to get image from another def file
+	//TODO: remove after implementing resource manager
+	std::shared_ptr<QImage> getFromExtraDef(std::string filename);
+
+public:
+	Animation(std::string Name);
+	Animation();
+	~Animation();
+
+	//duplicates frame at [sourceGroup, sourceFrame] as last frame in targetGroup
+	//and loads it if animation is preloaded
+	void duplicateImage(size_t sourceGroup, size_t sourceFrame, size_t targetGroup);
+
+	// adjust the color of the animation, used in battle spell effects, e.g. Cloned objects
+
+	//add custom surface to the selected position.
+	void setCustom(std::string filename, size_t frame, size_t group = 0);
+
+	std::shared_ptr<QImage> getImage(size_t frame, size_t group = 0, bool verbose = true) const;
+
+	//all available frames
+	void load();
+	void unload();
+	void preload();
+
+	//all frames from group
+	void loadGroup  (size_t group);
+	void unloadGroup(size_t group);
+
+	//single image
+	void load  (size_t frame, size_t group = 0);
+	void unload(size_t frame, size_t group = 0);
+
+	//total count of frames in group (including not loaded)
+	size_t size(size_t group = 0) const;
+
+	void horizontalFlip();
+	void verticalFlip();
+	void playerColored(PlayerColor player);
+
+	void createFlippedGroup(const size_t sourceGroup, const size_t targetGroup);
+};

+ 160 - 0
mapeditor/BitmapHandler.cpp

@@ -0,0 +1,160 @@
+/*
+ * BitmapHandler.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
+ *
+ */
+
+//code is copied from vcmiclient/CBitmapHandler.cpp with minimal changes
+
+#include "StdInc.h"
+#include "BitmapHandler.h"
+
+#include "../lib/filesystem/Filesystem.h"
+
+#include <QBitmap>
+#include <QImage>
+#include <QPixmap>
+
+namespace BitmapHandler
+{
+	QImage loadH3PCX(ui8 * data, size_t size);
+	
+	QImage loadBitmapFromDir(const std::string & path, const std::string & fname, bool setKey=true);
+
+	bool isPCX(const ui8 * header)//check whether file can be PCX according to header
+	{
+		ui32 fSize  = read_le_u32(header + 0);
+		ui32 width  = read_le_u32(header + 4);
+		ui32 height = read_le_u32(header + 8);
+		return fSize == width * height || fSize == width * height * 3;
+	}
+
+	enum Epcxformat
+	{
+		PCX8B,
+		PCX24B
+	};
+
+	QImage loadH3PCX(ui8 * pcx, size_t size)
+	{
+		//SDL_Surface * ret;
+		
+		Epcxformat format;
+		int it = 0;
+		
+		ui32 fSize = read_le_u32(pcx + it); it += 4;
+		ui32 width = read_le_u32(pcx + it); it += 4;
+		ui32 height = read_le_u32(pcx + it); it += 4;
+		
+		if(fSize==width*height*3)
+			format=PCX24B;
+		else if(fSize==width*height)
+			format=PCX8B;
+		else
+			return QImage();
+		
+		QSize qsize(width, height);
+		
+		if(format==PCX8B)
+		{
+			it = 0xC;
+			//auto bitmap = QBitmap::fromData(qsize, pcx + it);
+			QImage image(pcx + it, width, height, QImage::Format_Indexed8);
+			
+			//palette - last 256*3 bytes
+			QVector<QRgb> colorTable;
+			it = (int)size - 256 * 3;
+			for(int i = 0; i < 256; i++)
+			{
+				char bytes[3];
+				bytes[0] = pcx[it++];
+				bytes[1] = pcx[it++];
+				bytes[2] = pcx[it++];
+				colorTable.append(qRgb(bytes[0], bytes[1], bytes[2]));
+			}
+			image.setColorTable(colorTable);
+			return image;
+		}
+		else
+		{
+			QImage image(pcx + it, width, height, QImage::Format_RGB32);
+			return image;
+		}
+	}
+
+	QImage loadBitmapFromDir(const std::string & path, const std::string & fname, bool setKey)
+	{
+		if(!fname.size())
+		{
+			logGlobal->warn("Call to loadBitmap with void fname!");
+			return QImage();
+		}
+		if(!CResourceHandler::get()->existsResource(ResourceID(path + fname, EResType::IMAGE)))
+		{
+			return QImage();
+		}
+		
+		auto fullpath = CResourceHandler::get()->getResourceName(ResourceID(path + fname, EResType::IMAGE));
+		auto readFile = CResourceHandler::get()->load(ResourceID(path + fname, EResType::IMAGE))->readAll();
+		
+		if(isPCX(readFile.first.get()))
+		{//H3-style PCX
+			auto image = BitmapHandler::loadH3PCX(readFile.first.get(), readFile.second);
+			if(!image.isNull())
+			{
+				if(image.bitPlaneCount() == 1 && setKey)
+				{
+					QVector<QRgb> colorTable = image.colorTable();
+					colorTable[0] = qRgba(255, 255, 255, 0);
+					image.setColorTable(colorTable);
+				}
+			}
+			else
+			{
+				logGlobal->error("Failed to open %s as H3 PCX!", fname);
+			}
+			return image;
+		}
+		else
+		{ //loading via QImage
+			QImage image(QString::fromStdString(fullpath->make_preferred().string()));
+			if(!image.isNull())
+			{
+				if(image.bitPlaneCount() == 1)
+				{
+					//set correct value for alpha\unused channel
+					QVector<QRgb> colorTable = image.colorTable();
+					for(auto & c : colorTable)
+						c = qRgb(qRed(c), qGreen(c), qBlue(c));
+					image.setColorTable(colorTable);
+				}
+			}
+			else
+			{
+				logGlobal->error("Failed to open %s via QImage", fname);
+				return image;
+			}
+		}
+		return QImage();
+		// When modifying anything here please check use cases:
+		// 1) Vampire mansion in Necropolis (not 1st color is transparent)
+		// 2) Battle background when fighting on grass/dirt, topmost sky part (NO transparent color)
+		// 3) New objects that may use 24-bit images for icons (e.g. witchking arts)
+	}
+
+	QImage loadBitmap(const std::string & fname, bool setKey)
+	{
+		for(const auto dir : {"DATA/", "SPRITES/"})
+		{
+			auto image = loadBitmapFromDir(dir, fname, setKey);
+			if(!image.isNull())
+				return image;
+		}
+		logGlobal->error("Error: Failed to find file %s", fname);
+		return {};
+	}
+}

+ 23 - 0
mapeditor/BitmapHandler.h

@@ -0,0 +1,23 @@
+/*
+ * BitmapHandler.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
+//code is copied from vcmiclient/CBitmapHandler.h with minimal changes
+
+#define read_le_u16(p) (* reinterpret_cast<const ui16 *>(p))
+#define read_le_u32(p) (* reinterpret_cast<const ui32 *>(p))
+
+#include <QImage>
+
+namespace BitmapHandler
+{
+	//Load file from /DATA or /SPRITES
+	QImage loadBitmap(const std::string & fname, bool setKey = true);
+}
+

+ 136 - 0
mapeditor/CMakeLists.txt

@@ -0,0 +1,136 @@
+set(editor_SRCS
+		StdInc.cpp
+		main.cpp
+		launcherdirs.cpp
+		jsonutils.cpp
+		mainwindow.cpp
+		BitmapHandler.cpp
+		maphandler.cpp
+		Animation.cpp
+		graphics.cpp
+		windownewmap.cpp
+		generatorprogress.cpp
+		mapview.cpp
+		objectbrowser.cpp
+		mapsettings.cpp
+		playersettings.cpp
+		playerparams.cpp
+		scenelayer.cpp
+		mapcontroller.cpp
+		validator.cpp
+		inspector/inspector.cpp
+		inspector/townbulidingswidget.cpp
+		inspector/armywidget.cpp
+		inspector/messagewidget.cpp
+		inspector/rewardswidget.cpp
+		inspector/questwidget.cpp
+)
+
+set(editor_HEADERS
+		StdInc.h
+		launcherdirs.h
+		jsonutils.h
+		mainwindow.h
+		BitmapHandler.h
+		maphandler.h
+		Animation.h
+		graphics.h
+		windownewmap.h
+		generatorprogress.h
+		mapview.h
+		objectbrowser.h
+		mapsettings.h
+		playersettings.h
+		playerparams.h
+		scenelayer.h
+		mapcontroller.h
+		validator.h
+		inspector/inspector.h
+		inspector/townbulidingswidget.h
+		inspector/armywidget.h
+		inspector/messagewidget.h
+		inspector/rewardswidget.h
+		inspector/questwidget.h
+)
+
+set(editor_FORMS
+		mainwindow.ui
+		windownewmap.ui
+		generatorprogress.ui
+		mapsettings.ui
+		playersettings.ui
+		playerparams.ui
+		validator.ui
+		inspector/townbulidingswidget.ui
+		inspector/armywidget.ui
+		inspector/messagewidget.ui
+		inspector/rewardswidget.ui
+		inspector/questwidget.ui
+)
+
+assign_source_group(${editor_SRCS} ${editor_HEADERS} mapeditor.rc)
+
+# Tell CMake to run moc when necessary:
+set(CMAKE_AUTOMOC ON)
+
+if(POLICY CMP0071)
+	cmake_policy(SET CMP0071 NEW)
+endif()
+
+# As moc files are generated in the binary dir, tell CMake
+# to always look for includes there:
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+
+if(TARGET Qt6::Core)
+	qt_wrap_ui(editor_UI_HEADERS ${editor_FORMS})
+else()
+	qt5_wrap_ui(editor_UI_HEADERS ${editor_FORMS})
+endif()
+
+if(WIN32)
+	set(editor_ICON mapeditor.rc)
+endif()
+
+add_executable(vcmieditor WIN32 ${editor_SRCS} ${editor_HEADERS} ${editor_UI_HEADERS} ${editor_ICON})
+
+if(WIN32)
+	set_target_properties(vcmieditor
+		PROPERTIES
+			OUTPUT_NAME "VCMI_mapeditor"
+			PROJECT_LABEL "VCMI_mapeditor"
+	)
+
+	# FIXME: Can't to get CMP0020 working with Vcpkg and CMake 3.8.2
+	# So far I tried:
+	# - cmake_minimum_required set to 2.8.11 globally and in this file
+	# - cmake_policy in all possible places
+	# - used NO_POLICY_SCOPE to make sure no other parts reset policies
+	# Still nothing worked, warning kept appearing and WinMain didn't link automatically
+	target_link_libraries(vcmieditor Qt${QT_VERSION_MAJOR}::WinMain)
+endif()
+
+if(APPLE)
+	# This makes Xcode project prettier by moving vcmilauncher_autogen directory into vcmiclient subfolder
+	set_property(GLOBAL PROPERTY AUTOGEN_TARGETS_FOLDER vcmieditor)
+endif()
+
+target_link_libraries(vcmieditor vcmi Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network)
+target_include_directories(vcmieditor
+	PUBLIC	${CMAKE_CURRENT_SOURCE_DIR}
+)
+vcmi_set_output_dir(vcmieditor "")
+enable_pch(vcmieditor)
+
+# Copy to build directory for easier debugging
+add_custom_command(TARGET vcmieditor POST_BUILD
+	COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/mapeditor/icons
+	COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/mapeditor/icons ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/mapeditor/icons
+)
+
+install(TARGETS vcmieditor DESTINATION ${BIN_DIR})
+# copy whole directory
+install(DIRECTORY icons DESTINATION ${DATA_DIR}/mapeditor)
+# Install icons and desktop file on Linux
+if(NOT WIN32 AND NOT APPLE)
+	install(FILES "vcmilauncher.desktop" DESTINATION share/applications)
+endif()

+ 1 - 0
mapeditor/StdInc.cpp

@@ -0,0 +1 @@
+#include "StdInc.h"

+ 55 - 0
mapeditor/StdInc.h

@@ -0,0 +1,55 @@
+#pragma once
+
+#include "../Global.h"
+
+#define VCMI_EDITOR_VERSION "0.1"
+#define VCMI_EDITOR_NAME "VCMI Map Editor"
+
+#include <QtWidgets>
+#include <QStringList>
+#include <QSet>
+#include <QVector>
+#include <QList>
+#include <QString>
+#include <QFile>
+
+VCMI_LIB_USING_NAMESPACE
+
+using NumericPointer = typename std::conditional<sizeof(void *) == sizeof(unsigned long long),
+												 unsigned long long, unsigned int>::type;
+
+template<class Type>
+NumericPointer data_cast(Type * _pointer)
+{
+	static_assert(sizeof(Type *) == sizeof(NumericPointer),
+				  "Cannot compile for that architecture, see NumericPointer definition");
+
+	return reinterpret_cast<NumericPointer>(_pointer);
+}
+
+template<class Type>
+Type * data_cast(NumericPointer _numeric)
+{
+	static_assert(sizeof(Type *) == sizeof(NumericPointer),
+				  "Cannot compile for that architecture, see NumericPointer definition");
+
+	return reinterpret_cast<Type *>(_numeric);
+}
+
+inline QString pathToQString(const boost::filesystem::path & path)
+{
+#ifdef VCMI_WINDOWS
+	return QString::fromStdWString(path.wstring());
+#else
+	return QString::fromStdString(path.string());
+#endif
+}
+
+inline boost::filesystem::path qstringToPath(const QString & path)
+{
+#ifdef VCMI_WINDOWS
+	return boost::filesystem::path(path.toStdWString());
+#else
+	return boost::filesystem::path(path.toUtf8().data());
+#endif
+}

+ 47 - 0
mapeditor/generatorprogress.cpp

@@ -0,0 +1,47 @@
+/*
+ * generatorprogress.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 "generatorprogress.h"
+#include "ui_generatorprogress.h"
+#include <thread>
+#include <chrono>
+
+GeneratorProgress::GeneratorProgress(Load::Progress & source, QWidget *parent) :
+	QDialog(parent),
+	ui(new Ui::GeneratorProgress),
+	source(source)
+{
+	ui->setupUi(this);
+
+	setAttribute(Qt::WA_DeleteOnClose);
+
+	setWindowFlags(Qt::Window);
+
+	show();
+}
+
+GeneratorProgress::~GeneratorProgress()
+{
+	delete ui;
+}
+
+
+void GeneratorProgress::update()
+{
+	while(!source.finished())
+	{
+		int status = float(source.get()) / 2.55f;
+		ui->progressBar->setValue(status);
+		qApp->processEvents();
+	}
+
+	//delete source;
+	close();
+}

+ 32 - 0
mapeditor/generatorprogress.h

@@ -0,0 +1,32 @@
+/*
+ * generatorprogress.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
+
+#include <QDialog>
+#include "../lib/LoadProgress.h"
+
+namespace Ui {
+class GeneratorProgress;
+}
+
+class GeneratorProgress : public QDialog
+{
+	Q_OBJECT
+
+public:
+	explicit GeneratorProgress(Load::Progress & source, QWidget *parent = nullptr);
+	~GeneratorProgress();
+
+	void update();
+
+private:
+	Ui::GeneratorProgress *ui;
+	Load::Progress & source;
+};

+ 46 - 0
mapeditor/generatorprogress.ui

@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>GeneratorProgress</class>
+ <widget class="QDialog" name="GeneratorProgress">
+  <property name="windowModality">
+   <enum>Qt::ApplicationModal</enum>
+  </property>
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>60</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>400</width>
+    <height>60</height>
+   </size>
+  </property>
+  <property name="maximumSize">
+   <size>
+    <width>400</width>
+    <height>64</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Generating map</string>
+  </property>
+  <property name="modal">
+   <bool>true</bool>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="0" column="0">
+    <widget class="QProgressBar" name="progressBar">
+     <property name="value">
+      <number>0</number>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 345 - 0
mapeditor/graphics.cpp

@@ -0,0 +1,345 @@
+/*
+ * graphics.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
+ *
+ */
+
+//code is copied from vcmiclient/Graphics.cpp with minimal changes
+#include "StdInc.h"
+#include "graphics.h"
+
+#include <vcmi/Entity.h>
+#include <vcmi/ArtifactService.h>
+#include <vcmi/CreatureService.h>
+#include <vcmi/FactionService.h>
+#include <vcmi/HeroTypeService.h>
+#include <vcmi/SkillService.h>
+#include <vcmi/spells/Service.h>
+
+#include "../lib/filesystem/Filesystem.h"
+#include "../lib/filesystem/CBinaryReader.h"
+#include "Animation.h"
+#include "../lib/CThreadHelper.h"
+#include "../lib/CModHandler.h"
+#include "../lib/VCMI_Lib.h"
+#include "../CCallback.h"
+#include "../lib/CGeneralTextHandler.h"
+#include "BitmapHandler.h"
+#include "../lib/CGameState.h"
+#include "../lib/JsonNode.h"
+#include "../lib/CStopWatch.h"
+#include "../lib/mapObjects/CObjectClassesHandler.h"
+#include "../lib/mapObjects/CObjectHandler.h"
+#include "../lib/CHeroHandler.h"
+
+Graphics * graphics = nullptr;
+
+void Graphics::loadPaletteAndColors()
+{
+	auto textFile = CResourceHandler::get()->load(ResourceID("DATA/PLAYERS.PAL"))->readAll();
+	std::string pals((char*)textFile.first.get(), textFile.second);
+	
+	playerColorPalette.resize(256);
+	playerColors.resize(PlayerColor::PLAYER_LIMIT_I);
+	int startPoint = 24; //beginning byte; used to read
+	for(int i = 0; i < 256; ++i)
+	{
+		QColor col;
+		col.setRed(pals[startPoint++]);
+		col.setGreen(pals[startPoint++]);
+		col.setBlue(pals[startPoint++]);
+		col.setAlpha(255);
+		startPoint++;
+		playerColorPalette[i] = col.rgba();
+	}
+	
+	neutralColorPalette.resize(32);
+	
+	auto stream = CResourceHandler::get()->load(ResourceID("config/NEUTRAL.PAL"));
+	CBinaryReader reader(stream.get());
+	
+	for(int i = 0; i < 32; ++i)
+	{
+		QColor col;
+		col.setRed(reader.readUInt8());
+		col.setGreen(reader.readUInt8());
+		col.setBlue(reader.readUInt8());
+		col.setAlpha(255);
+		reader.readUInt8(); // this is "flags" entry, not alpha
+		neutralColorPalette[i] = col.rgba();
+	}
+	
+	//colors initialization
+	QColor colors[]  = {
+		{0xff,0,  0,    255},
+		{0x31,0x52,0xff,255},
+		{0x9c,0x73,0x52,255},
+		{0x42,0x94,0x29,255},
+		
+		{0xff,0x84,0,   255},
+		{0x8c,0x29,0xa5,255},
+		{0x09,0x9c,0xa5,255},
+		{0xc6,0x7b,0x8c,255}};
+	
+	for(int i=0;i<8;i++)
+	{
+		playerColors[i] = colors[i].rgba();
+	}
+	//gray
+	neutralColor = qRgba(0x84, 0x84, 0x84, 0xFF);
+}
+
+Graphics::Graphics()
+{
+#if 0
+	std::vector<Task> tasks; //preparing list of graphics to load
+	tasks += std::bind(&Graphics::loadFonts,this);
+	tasks += std::bind(&Graphics::loadPaletteAndColors,this);
+	tasks += std::bind(&Graphics::initializeBattleGraphics,this);
+	tasks += std::bind(&Graphics::loadErmuToPicture,this);
+	tasks += std::bind(&Graphics::initializeImageLists,this);
+	
+	CThreadHelper th(&tasks,std::max((ui32)1,boost::thread::hardware_concurrency()));
+	th.run();
+#else
+	loadPaletteAndColors();
+	initializeImageLists();
+#endif
+	
+	//(!) do not load any CAnimation here
+}
+
+Graphics::~Graphics()
+{
+}
+
+void Graphics::load()
+{
+	loadHeroAnimations();
+	loadHeroFlagAnimations();
+}
+
+void Graphics::loadHeroAnimations()
+{
+	for(auto & elem : VLC->heroh->classes.objects)
+	{
+		for(auto templ : VLC->objtypeh->getHandlerFor(Obj::HERO, elem->getIndex())->getTemplates())
+		{
+			if(!heroAnimations.count(templ->animationFile))
+				heroAnimations[templ->animationFile] = loadHeroAnimation(templ->animationFile);
+		}
+	}
+	
+	boatAnimations[0] = loadHeroAnimation("AB01_.DEF");
+	boatAnimations[1] = loadHeroAnimation("AB02_.DEF");
+	boatAnimations[2] = loadHeroAnimation("AB03_.DEF");
+	
+	
+	mapObjectAnimations["AB01_.DEF"] = boatAnimations[0];
+	mapObjectAnimations["AB02_.DEF"] = boatAnimations[1];
+	mapObjectAnimations["AB03_.DEF"] = boatAnimations[2];
+}
+void Graphics::loadHeroFlagAnimations()
+{
+	static const std::vector<std::string> HERO_FLAG_ANIMATIONS =
+	{
+		"AF00", "AF01","AF02","AF03",
+		"AF04", "AF05","AF06","AF07"
+	};
+	
+	static const std::vector< std::vector<std::string> > BOAT_FLAG_ANIMATIONS =
+	{
+		{
+			"ABF01L", "ABF01G", "ABF01R", "ABF01D",
+			"ABF01B", "ABF01P", "ABF01W", "ABF01K"
+		},
+		{
+			"ABF02L", "ABF02G", "ABF02R", "ABF02D",
+			"ABF02B", "ABF02P", "ABF02W", "ABF02K"
+		},
+		{
+			"ABF03L", "ABF03G", "ABF03R", "ABF03D",
+			"ABF03B", "ABF03P", "ABF03W", "ABF03K"
+		}
+	};
+	
+	for(const auto & name : HERO_FLAG_ANIMATIONS)
+		heroFlagAnimations.push_back(loadHeroFlagAnimation(name));
+	
+	for(int i = 0; i < BOAT_FLAG_ANIMATIONS.size(); i++)
+		for(const auto & name : BOAT_FLAG_ANIMATIONS[i])
+			boatFlagAnimations[i].push_back(loadHeroFlagAnimation(name));
+}
+
+std::shared_ptr<Animation> Graphics::loadHeroFlagAnimation(const std::string & name)
+{
+	//first - group number to be rotated, second - group number after rotation
+	static const std::vector<std::pair<int,int> > rotations =
+	{
+		{6,10}, {7,11}, {8,12}, {1,13},
+		{2,14}, {3,15}
+	};
+	
+	std::shared_ptr<Animation> anim = std::make_shared<Animation>(name);
+	anim->preload();
+	
+	for(const auto & rotation : rotations)
+	{
+		const int sourceGroup = rotation.first;
+		const int targetGroup = rotation.second;
+		
+		anim->createFlippedGroup(sourceGroup, targetGroup);
+	}
+	
+	return anim;
+}
+
+std::shared_ptr<Animation> Graphics::loadHeroAnimation(const std::string &name)
+{
+	//first - group number to be rotated, second - group number after rotation
+	static const std::vector<std::pair<int,int> > rotations =
+	{
+		{6,10}, {7,11}, {8,12}, {1,13},
+		{2,14}, {3,15}
+	};
+	
+	std::shared_ptr<Animation> anim = std::make_shared<Animation>(name);
+	anim->preload();
+	
+	
+	for(const auto & rotation : rotations)
+	{
+		const int sourceGroup = rotation.first;
+		const int targetGroup = rotation.second;
+		
+		anim->createFlippedGroup(sourceGroup, targetGroup);
+	}
+	
+	return anim;
+}
+
+void Graphics::blueToPlayersAdv(QImage * sur, PlayerColor player)
+{
+	if(sur->format() == QImage::Format_Indexed8)
+	{
+		auto palette = sur->colorTable();
+		if(player < PlayerColor::PLAYER_LIMIT)
+		{
+			for(int i = 0; i < 32; ++i)
+				palette[224 + i] = playerColorPalette[player.getNum() * 32 + i];
+		}
+		else if(player == PlayerColor::NEUTRAL)
+		{
+			palette = neutralColorPalette;
+		}
+		else
+		{
+			logGlobal->error("Wrong player id in blueToPlayersAdv (%s)!", player.getStr());
+			return;
+		}
+		//FIXME: not all player colored images have player palette at last 32 indexes
+		//NOTE: following code is much more correct but still not perfect (bugged with status bar)
+		sur->setColorTable(palette);
+	}
+	else
+	{
+		//TODO: implement. H3 method works only for images with palettes.
+		// Add some kind of player-colored overlay?
+		// Or keep palette approach here and replace only colors of specific value(s)
+		// Or just wait for OpenGL support?
+		logGlobal->warn("Image must have palette to be player-colored!");
+	}
+}
+
+std::shared_ptr<Animation> Graphics::getAnimation(const CGObjectInstance* obj)
+{
+	if(obj->ID == Obj::HERO)
+		return getHeroAnimation(obj->appearance);
+	return getAnimation(obj->appearance);
+}
+
+std::shared_ptr<Animation> Graphics::getHeroAnimation(const std::shared_ptr<const ObjectTemplate> info)
+{
+	if(info->animationFile.empty())
+	{
+		logGlobal->warn("Def name for hero (%d,%d) is empty!", info->id, info->subid);
+		return std::shared_ptr<Animation>();
+	}
+	
+	std::shared_ptr<Animation> ret = loadHeroAnimation(info->animationFile);
+	
+	//already loaded
+	if(ret)
+	{
+		ret->preload();
+		return ret;
+	}
+	
+	ret = std::make_shared<Animation>(info->animationFile);
+	heroAnimations[info->animationFile] = ret;
+	
+	ret->preload();
+	return ret;
+}
+
+std::shared_ptr<Animation> Graphics::getAnimation(const std::shared_ptr<const ObjectTemplate> info)
+{	
+	if(info->animationFile.empty())
+	{
+		logGlobal->warn("Def name for obj (%d,%d) is empty!", info->id, info->subid);
+		return std::shared_ptr<Animation>();
+	}
+	
+	std::shared_ptr<Animation> ret = mapObjectAnimations[info->animationFile];
+	
+	//already loaded
+	if(ret)
+	{
+		ret->preload();
+		return ret;
+	}
+	
+	ret = std::make_shared<Animation>(info->animationFile);
+	mapObjectAnimations[info->animationFile] = ret;
+	
+	ret->preload();
+	return ret;
+}
+
+void Graphics::addImageListEntry(size_t index, const std::string & listName, const std::string & imageName)
+{
+	if (!imageName.empty())
+	{
+		JsonNode entry;
+		entry["frame"].Integer() = index;
+		entry["file"].String() = imageName;
+		
+		imageLists["SPRITES/" + listName]["images"].Vector().push_back(entry);
+	}
+}
+
+void Graphics::addImageListEntries(const EntityService * service)
+{
+	auto cb = std::bind(&Graphics::addImageListEntry, this, _1, _2, _3);
+	
+	auto loopCb = [&](const Entity * entity, bool & stop)
+	{
+		entity->registerIcons(cb);
+	};
+	
+	service->forEachBase(loopCb);
+}
+
+void Graphics::initializeImageLists()
+{
+	addImageListEntries(VLC->creatures());
+	addImageListEntries(VLC->heroTypes());
+	addImageListEntries(VLC->artifacts());
+	addImageListEntries(VLC->factions());
+	addImageListEntries(VLC->spells());
+	addImageListEntries(VLC->skills());
+}

+ 85 - 0
mapeditor/graphics.h

@@ -0,0 +1,85 @@
+/*
+ * graphics.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
+//code is copied from vcmiclient/Graphics.h with minimal changes
+
+#include "../lib/GameConstants.h"
+#include <QImage>
+
+class CGHeroInstance;
+class CGTownInstance;
+class CHeroClass;
+struct InfoAboutHero;
+struct InfoAboutTown;
+class CGObjectInstance;
+class ObjectTemplate;
+class Animation;
+class EntityService;
+class JsonNode;
+
+/// Handles fonts, hero images, town images, various graphics
+class Graphics
+{
+	void addImageListEntry(size_t index, const std::string & listName, const std::string & imageName);
+	
+	void addImageListEntries(const EntityService * service);
+	
+	void initializeBattleGraphics();
+	void loadPaletteAndColors();
+	
+	void loadHeroAnimations();
+	//loads animation and adds required rotated frames
+	std::shared_ptr<Animation> loadHeroAnimation(const std::string &name);
+	
+	void loadHeroFlagAnimations();
+	
+	//loads animation and adds required rotated frames
+	std::shared_ptr<Animation> loadHeroFlagAnimation(const std::string &name);
+	
+	void loadErmuToPicture();
+	void loadFogOfWar();
+	void loadFonts();
+	void initializeImageLists();
+	
+public:
+	//various graphics
+	QVector<QRgb> playerColors; //array [8]
+	QRgb neutralColor;
+	QVector<QRgb> playerColorPalette; //palette to make interface colors good - array of size [256]
+	QVector<QRgb> neutralColorPalette;
+		
+	// [hero class def name]  //added group 10: up - left, 11 - left and 12 - left down // 13 - up-left standing; 14 - left standing; 15 - left down standing
+	std::map< std::string, std::shared_ptr<Animation> > heroAnimations;
+	std::vector< std::shared_ptr<Animation> > heroFlagAnimations;
+	
+	// [boat type: 0 .. 2]  //added group 10: up - left, 11 - left and 12 - left down // 13 - up-left standing; 14 - left standing; 15 - left down standing
+	std::array< std::shared_ptr<Animation>, 3> boatAnimations;
+	
+	std::array< std::vector<std::shared_ptr<Animation> >, 3> boatFlagAnimations;
+	
+	//all other objects (not hero or boat)
+	std::map< std::string, std::shared_ptr<Animation> > mapObjectAnimations;
+		
+	std::map<std::string, JsonNode> imageLists;
+		
+	//functions
+	Graphics();
+	~Graphics();
+	
+	void load();
+	
+	void blueToPlayersAdv(QImage * sur, PlayerColor player); //replaces blue interface colour with a color of player
+	
+	std::shared_ptr<Animation> getAnimation(const CGObjectInstance * obj);
+	std::shared_ptr<Animation> getAnimation(const std::shared_ptr<const ObjectTemplate> info);
+	std::shared_ptr<Animation> getHeroAnimation(const std::shared_ptr<const ObjectTemplate> info);
+};
+
+extern Graphics * graphics;

BIN
mapeditor/icons/menu-game.png


BIN
mapeditor/icons/menu-mods.png


BIN
mapeditor/icons/menu-settings.png


BIN
mapeditor/icons/mod-delete.png


BIN
mapeditor/icons/mod-disabled.png


BIN
mapeditor/icons/mod-download.png


BIN
mapeditor/icons/mod-enabled.png


BIN
mapeditor/icons/mod-update.png


+ 152 - 0
mapeditor/inspector/armywidget.cpp

@@ -0,0 +1,152 @@
+/*
+ * armywidget.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 "armywidget.h"
+#include "ui_armywidget.h"
+#include "CCreatureHandler.h"
+
+
+ArmyWidget::ArmyWidget(CArmedInstance & a, QWidget *parent) :
+	QDialog(parent),
+	army(a),
+	ui(new Ui::ArmyWidget)
+{
+	ui->setupUi(this);
+	
+	uiCounts[0] = ui->count0; uiSlots[0] = ui->slot0;
+	uiCounts[1] = ui->count1; uiSlots[1] = ui->slot1;
+	uiCounts[2] = ui->count2; uiSlots[2] = ui->slot2;
+	uiCounts[3] = ui->count3; uiSlots[3] = ui->slot3;
+	uiCounts[4] = ui->count4; uiSlots[4] = ui->slot4;
+	uiCounts[5] = ui->count5; uiSlots[5] = ui->slot5;
+	uiCounts[6] = ui->count6; uiSlots[6] = ui->slot6;
+	
+	for(int i = 0; i < TOTAL_SLOTS; ++i)
+	{
+		uiCounts[i]->setText("1");
+		uiSlots[i]->addItem("");
+		uiSlots[i]->setItemData(0, -1);
+		
+		for(int c = 0; c < VLC->creh->objects.size(); ++c)
+		{
+			auto creature = VLC->creh->objects[c];
+			uiSlots[i]->insertItem(c + 1, tr(creature->getPluralName().c_str()));
+			uiSlots[i]->setItemData(c + 1, creature->getId().getNum());
+		}
+	}
+	
+	ui->formationTight->setChecked(true);
+}
+
+int ArmyWidget::searchItemIndex(int slotId, CreatureID creId) const
+{
+	for(int i = 0; i < uiSlots[slotId]->count(); ++i)
+	{
+		if(creId.getNum() == uiSlots[slotId]->itemData(i).toInt())
+			return i;
+	}
+	return 0;
+}
+
+void ArmyWidget::obtainData()
+{
+	for(int i = 0; i < TOTAL_SLOTS; ++i)
+	{
+		if(army.hasStackAtSlot(SlotID(i)))
+		{
+			auto * creature = army.getCreature(SlotID(i));
+			uiSlots[i]->setCurrentIndex(searchItemIndex(i, creature->getId()));
+			uiCounts[i]->setText(QString::number(army.getStackCount(SlotID(i))));
+		}
+	}
+	
+	if(army.formation)
+		ui->formationTight->setChecked(true);
+	else
+		ui->formationWide->setChecked(true);
+}
+
+bool ArmyWidget::commitChanges()
+{
+	bool isArmed = false;
+	for(int i = 0; i < TOTAL_SLOTS; ++i)
+	{
+		CreatureID creId(uiSlots[i]->itemData(uiSlots[i]->currentIndex()).toInt());
+		if(creId == -1)
+		{
+			if(army.hasStackAtSlot(SlotID(i)))
+				army.eraseStack(SlotID(i));
+		}
+		else
+		{
+			isArmed = true;
+			int amount = uiCounts[i]->text().toInt();
+			if(amount)
+			{
+				army.setCreature(SlotID(i), creId, amount);
+			}
+			else
+			{
+				if(army.hasStackAtSlot(SlotID(i)))
+					army.eraseStack(SlotID(i));
+				army.putStack(SlotID(i), new CStackInstance(creId, amount, false));
+			}
+		}
+	}
+	
+	army.setFormation(ui->formationTight->isChecked());
+	return isArmed;
+}
+
+ArmyWidget::~ArmyWidget()
+{
+	delete ui;
+}
+
+
+
+ArmyDelegate::ArmyDelegate(CArmedInstance & t): army(t), QStyledItemDelegate()
+{
+}
+
+QWidget * ArmyDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
+{
+	return new ArmyWidget(army, parent);
+}
+
+void ArmyDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
+{
+	if(auto * ed = qobject_cast<ArmyWidget *>(editor))
+	{
+		ed->obtainData();
+	}
+	else
+	{
+		QStyledItemDelegate::setEditorData(editor, index);
+	}
+}
+
+void ArmyDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
+{
+	if(auto * ed = qobject_cast<ArmyWidget *>(editor))
+	{
+		auto isArmed = ed->commitChanges();
+		model->setData(index, "dummy");
+		if(isArmed)
+			model->setData(index, "HAS ARMY");
+		else
+			model->setData(index, "");
+	}
+	else
+	{
+		QStyledItemDelegate::setModelData(editor, model, index);
+	}
+}

+ 57 - 0
mapeditor/inspector/armywidget.h

@@ -0,0 +1,57 @@
+/*
+ * armywidget.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
+
+#include "../StdInc.h"
+#include <QDialog>
+#include "../lib/mapObjects/CArmedInstance.h"
+
+const int TOTAL_SLOTS = 7;
+
+namespace Ui {
+class ArmyWidget;
+}
+
+class ArmyWidget : public QDialog
+{
+	Q_OBJECT
+
+public:
+	explicit ArmyWidget(CArmedInstance &, QWidget *parent = nullptr);
+	~ArmyWidget();
+	
+	void obtainData();
+	bool commitChanges();
+
+private:
+	int searchItemIndex(int slotId, CreatureID creId) const;
+	
+	Ui::ArmyWidget *ui;
+	CArmedInstance & army;
+	std::array<QLineEdit*, TOTAL_SLOTS> uiCounts;
+	std::array<QComboBox*, TOTAL_SLOTS> uiSlots;
+};
+
+class ArmyDelegate : public QStyledItemDelegate
+{
+	Q_OBJECT
+public:
+	using QStyledItemDelegate::QStyledItemDelegate;
+	
+	ArmyDelegate(CArmedInstance &);
+	
+	QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
+	void setEditorData(QWidget *editor, const QModelIndex &index) const override;
+	void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
+	
+private:
+	CArmedInstance & army;
+};
+

+ 298 - 0
mapeditor/inspector/armywidget.ui

@@ -0,0 +1,298 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ArmyWidget</class>
+ <widget class="QDialog" name="ArmyWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>318</width>
+    <height>314</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>318</width>
+    <height>314</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Army settings</string>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="6" column="0">
+    <widget class="QComboBox" name="slot6">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+    </widget>
+   </item>
+   <item row="3" column="0">
+    <widget class="QComboBox" name="slot3">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="0">
+    <widget class="QComboBox" name="slot2">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+    </widget>
+   </item>
+   <item row="6" column="1">
+    <widget class="QLineEdit" name="count6">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="minimumSize">
+      <size>
+       <width>30</width>
+       <height>0</height>
+      </size>
+     </property>
+     <property name="maximumSize">
+      <size>
+       <width>50</width>
+       <height>16777215</height>
+      </size>
+     </property>
+     <property name="inputMethodHints">
+      <set>Qt::ImhDigitsOnly</set>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="1">
+    <widget class="QLineEdit" name="count2">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="minimumSize">
+      <size>
+       <width>30</width>
+       <height>0</height>
+      </size>
+     </property>
+     <property name="maximumSize">
+      <size>
+       <width>50</width>
+       <height>16777215</height>
+      </size>
+     </property>
+     <property name="inputMethodHints">
+      <set>Qt::ImhDigitsOnly</set>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="1">
+    <widget class="QLineEdit" name="count1">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="minimumSize">
+      <size>
+       <width>30</width>
+       <height>0</height>
+      </size>
+     </property>
+     <property name="maximumSize">
+      <size>
+       <width>50</width>
+       <height>16777215</height>
+      </size>
+     </property>
+     <property name="inputMethodHints">
+      <set>Qt::ImhDigitsOnly</set>
+     </property>
+    </widget>
+   </item>
+   <item row="5" column="1">
+    <widget class="QLineEdit" name="count5">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="minimumSize">
+      <size>
+       <width>30</width>
+       <height>0</height>
+      </size>
+     </property>
+     <property name="maximumSize">
+      <size>
+       <width>50</width>
+       <height>16777215</height>
+      </size>
+     </property>
+     <property name="inputMethodHints">
+      <set>Qt::ImhDigitsOnly</set>
+     </property>
+    </widget>
+   </item>
+   <item row="7" column="0">
+    <widget class="QRadioButton" name="formationWide">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="text">
+      <string>Wide formation</string>
+     </property>
+    </widget>
+   </item>
+   <item row="3" column="1">
+    <widget class="QLineEdit" name="count3">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="minimumSize">
+      <size>
+       <width>30</width>
+       <height>0</height>
+      </size>
+     </property>
+     <property name="maximumSize">
+      <size>
+       <width>50</width>
+       <height>16777215</height>
+      </size>
+     </property>
+     <property name="inputMethodHints">
+      <set>Qt::ImhDigitsOnly</set>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="0">
+    <widget class="QComboBox" name="slot1">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="0">
+    <widget class="QComboBox" name="slot0">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+    </widget>
+   </item>
+   <item row="4" column="0">
+    <widget class="QComboBox" name="slot4">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+    </widget>
+   </item>
+   <item row="4" column="1">
+    <widget class="QLineEdit" name="count4">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="minimumSize">
+      <size>
+       <width>30</width>
+       <height>0</height>
+      </size>
+     </property>
+     <property name="maximumSize">
+      <size>
+       <width>50</width>
+       <height>16777215</height>
+      </size>
+     </property>
+     <property name="inputMethodHints">
+      <set>Qt::ImhDigitsOnly</set>
+     </property>
+    </widget>
+   </item>
+   <item row="5" column="0">
+    <widget class="QComboBox" name="slot5">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="1">
+    <widget class="QLineEdit" name="count0">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="minimumSize">
+      <size>
+       <width>30</width>
+       <height>0</height>
+      </size>
+     </property>
+     <property name="maximumSize">
+      <size>
+       <width>50</width>
+       <height>16777215</height>
+      </size>
+     </property>
+     <property name="inputMethodHints">
+      <set>Qt::ImhDigitsOnly</set>
+     </property>
+    </widget>
+   </item>
+   <item row="8" column="0">
+    <widget class="QRadioButton" name="formationTight">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="text">
+      <string>Tight formation</string>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 849 - 0
mapeditor/inspector/inspector.cpp

@@ -0,0 +1,849 @@
+/*
+ * inspector.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 "inspector.h"
+#include "../lib/CArtHandler.h"
+#include "../lib/spells/CSpellHandler.h"
+#include "../lib/CHeroHandler.h"
+#include "../lib/CRandomGenerator.h"
+#include "../lib/mapObjects/CObjectClassesHandler.h"
+#include "../lib/mapping/CMap.h"
+
+#include "townbulidingswidget.h"
+#include "armywidget.h"
+#include "messagewidget.h"
+#include "rewardswidget.h"
+#include "questwidget.h"
+
+//===============IMPLEMENT OBJECT INITIALIZATION FUNCTIONS================
+Initializer::Initializer(CGObjectInstance * o, const PlayerColor & pl) : defaultPlayer(pl)
+{
+	logGlobal->info("New object instance initialized");
+///IMPORTANT! initialize order should be from base objects to derived objects
+	INIT_OBJ_TYPE(CGResource);
+	INIT_OBJ_TYPE(CGArtifact);
+	INIT_OBJ_TYPE(CArmedInstance);
+	INIT_OBJ_TYPE(CGShipyard);
+	INIT_OBJ_TYPE(CGGarrison);
+	INIT_OBJ_TYPE(CGMine);
+	INIT_OBJ_TYPE(CGDwelling);
+	INIT_OBJ_TYPE(CGTownInstance);
+	INIT_OBJ_TYPE(CGCreature);
+	INIT_OBJ_TYPE(CGHeroInstance);
+	INIT_OBJ_TYPE(CGSignBottle);
+	INIT_OBJ_TYPE(CGLighthouse);
+	//INIT_OBJ_TYPE(CGPandoraBox);
+	//INIT_OBJ_TYPE(CGEvent);
+	//INIT_OBJ_TYPE(CGSeerHut);
+}
+
+bool stringToBool(const QString & s)
+{
+	if(s == "TRUE")
+		return true;
+	//if(s == "FALSE")
+	return false;
+}
+
+void Initializer::initialize(CArmedInstance * o)
+{
+	if(!o) return;
+}
+
+void Initializer::initialize(CGSignBottle * o)
+{
+	if(!o) return;
+}
+
+void Initializer::initialize(CGCreature * o)
+{
+	if(!o) return;
+	
+	o->character = CGCreature::Character::HOSTILE;
+	o->putStack(SlotID(0), new CStackInstance(CreatureID(o->subID), 0, false));
+}
+
+void Initializer::initialize(CGDwelling * o)
+{
+	if(!o) return;
+	
+	o->tempOwner = defaultPlayer;
+	
+	switch(o->ID)
+	{
+		case Obj::RANDOM_DWELLING:
+		case Obj::RANDOM_DWELLING_LVL:
+		case Obj::RANDOM_DWELLING_FACTION:
+			o->initRandomObjectInfo();
+	}
+}
+
+void Initializer::initialize(CGGarrison * o)
+{
+	if(!o) return;
+	
+	o->tempOwner = defaultPlayer;
+	o->removableUnits = true;
+}
+
+void Initializer::initialize(CGShipyard * o)
+{
+	if(!o) return;
+	
+	o->tempOwner = defaultPlayer;
+}
+
+void Initializer::initialize(CGLighthouse * o)
+{
+	if(!o) return;
+	
+	o->tempOwner = defaultPlayer;
+}
+
+void Initializer::initialize(CGHeroInstance * o)
+{
+	if(!o) return;
+	
+	o->tempOwner = defaultPlayer;
+	if(o->ID == Obj::PRISON)
+		o->tempOwner = PlayerColor::NEUTRAL;
+	
+	if(o->ID == Obj::HERO)
+	{
+		for(auto t : VLC->heroh->objects)
+		{
+			if(t->heroClass == VLC->heroh->classes.objects[o->subID].get())
+			{
+				o->type = VLC->heroh->objects[o->subID];
+				break;
+			}
+		}
+	}
+	
+	if(!o->type)
+		o->type = VLC->heroh->objects.at(o->subID);
+	
+	o->name = o->type->getName();
+	o->sex = o->type->sex;
+	o->biography = o->type->biography;
+	o->portrait = o->type->imageIndex;
+	o->randomizeArmy(o->type->heroClass->faction);
+}
+
+void Initializer::initialize(CGTownInstance * o)
+{
+	if(!o) return;
+
+	const std::vector<std::string> castleLevels{"village", "fort", "citadel", "castle", "capitol"};
+	int lvl = vstd::find_pos(castleLevels, o->appearance->stringID);
+	o->builtBuildings.insert(BuildingID::DEFAULT);
+	if(lvl > -1) o->builtBuildings.insert(BuildingID::TAVERN);
+	if(lvl > 0) o->builtBuildings.insert(BuildingID::FORT);
+	if(lvl > 1) o->builtBuildings.insert(BuildingID::CITADEL);
+	if(lvl > 2) o->builtBuildings.insert(BuildingID::CASTLE);
+	if(lvl > 3) o->builtBuildings.insert(BuildingID::CAPITOL);
+
+	for(auto spell : VLC->spellh->objects) //add all regular spells to town
+	{
+		if(!spell->isSpecial() && !spell->isCreatureAbility())
+			o->possibleSpells.push_back(spell->id);
+	}
+}
+
+void Initializer::initialize(CGArtifact * o)
+{
+	if(!o) return;
+	
+	if(o->ID == Obj::SPELL_SCROLL)
+	{
+		std::vector<SpellID> out;
+		for(auto spell : VLC->spellh->objects) //spellh size appears to be greater (?)
+		{
+			//if(map->isAllowedSpell(spell->id))
+			{
+				out.push_back(spell->id);
+			}
+		}
+		auto a = CArtifactInstance::createScroll(*RandomGeneratorUtil::nextItem(out, CRandomGenerator::getDefault()));
+		o->storedArtifact = a;
+	}
+}
+
+void Initializer::initialize(CGMine * o)
+{
+	if(!o) return;
+	
+	o->tempOwner = defaultPlayer;
+	o->producedResource = Res::ERes(o->subID);
+	o->producedQuantity = o->defaultResProduction();
+}
+
+void Initializer::initialize(CGResource * o)
+{
+	if(!o) return;
+	
+	o->amount = CGResource::RANDOM_AMOUNT;
+}
+
+//===============IMPLEMENT PROPERTIES SETUP===============================
+void Inspector::updateProperties(CArmedInstance * o)
+{
+	if(!o) return;
+	
+	auto * delegate = new ArmyDelegate(*o);
+	addProperty("Army", PropertyEditorPlaceholder(), delegate, false);
+}
+
+void Inspector::updateProperties(CGDwelling * o)
+{
+	if(!o) return;
+	
+	addProperty("Owner", o->tempOwner, false);
+}
+
+void Inspector::updateProperties(CGLighthouse * o)
+{
+	if(!o) return;
+	
+	addProperty("Owner", o->tempOwner, false);
+}
+
+void Inspector::updateProperties(CGGarrison * o)
+{
+	if(!o) return;
+	
+	addProperty("Owner", o->tempOwner, false);
+	addProperty("Removable units", o->removableUnits, InspectorDelegate::boolDelegate(), false);
+}
+
+void Inspector::updateProperties(CGShipyard * o)
+{
+	if(!o) return;
+	
+	addProperty("Owner", o->tempOwner, false);
+}
+
+void Inspector::updateProperties(CGHeroInstance * o)
+{
+	if(!o) return;
+	
+	addProperty("Owner", o->tempOwner, o->ID == Obj::PRISON); //field is not editable for prison
+	addProperty<int>("Experience", o->exp, false);
+	addProperty("Hero class", o->type->heroClass->getName(), true);
+	
+	{ //Sex
+		auto * delegate = new InspectorDelegate;
+		delegate->options << "MALE" << "FEMALE";
+		addProperty<std::string>("Sex", (o->sex ? "FEMALE" : "MALE"), delegate , false);
+	}
+	addProperty("Name", o->name, false);
+	addProperty("Biography", o->biography, new MessageDelegate, false);
+	addProperty("Portrait", o->portrait, false);
+	
+	{ //Hero type
+		auto * delegate = new InspectorDelegate;
+		for(int i = 0; i < VLC->heroh->objects.size(); ++i)
+		{
+			if(map->allowedHeroes.at(i))
+			{
+				if(o->ID == Obj::PRISON || (o->type && VLC->heroh->objects[i]->heroClass->getIndex() == o->type->heroClass->getIndex()))
+					delegate->options << QObject::tr(VLC->heroh->objects[i]->getName().c_str());
+			}
+		}
+		addProperty("Hero type", o->type->getName(), delegate, false);
+	}
+}
+
+void Inspector::updateProperties(CGTownInstance * o)
+{
+	if(!o) return;
+	
+	addProperty("Town name", o->name, false);
+	
+	auto * delegate = new TownBuildingsDelegate(*o);
+	addProperty("Buildings", PropertyEditorPlaceholder(), delegate, false);
+}
+
+void Inspector::updateProperties(CGArtifact * o)
+{
+	if(!o) return;
+	
+	addProperty("Message", o->message, false);
+	
+	CArtifactInstance * instance = o->storedArtifact;
+	if(instance)
+	{
+		SpellID spellId = instance->getGivenSpellID();
+		if(spellId != -1)
+		{
+			auto * delegate = new InspectorDelegate;
+			for(auto spell : VLC->spellh->objects)
+			{
+				//if(map->isAllowedSpell(spell->id))
+				delegate->options << QObject::tr(spell->name.c_str());
+			}
+			addProperty("Spell", VLC->spellh->objects[spellId]->name, delegate, false);
+		}
+	}
+}
+
+void Inspector::updateProperties(CGMine * o)
+{
+	if(!o) return;
+	
+	addProperty("Owner", o->tempOwner, false);
+	addProperty("Resource", o->producedResource);
+	addProperty("Productivity", o->producedQuantity, false);
+}
+
+void Inspector::updateProperties(CGResource * o)
+{
+	if(!o) return;
+	
+	addProperty("Amount", o->amount, false);
+	addProperty("Message", o->message, false);
+}
+
+void Inspector::updateProperties(CGSignBottle * o)
+{
+	if(!o) return;
+	
+	addProperty("Message", o->message, new MessageDelegate, false);
+}
+
+void Inspector::updateProperties(CGCreature * o)
+{
+	if(!o) return;
+	
+	addProperty("Message", o->message, false);
+	{ //Character
+		auto * delegate = new InspectorDelegate;
+		delegate->options << "COMPLIANT" << "FRIENDLY" << "AGRESSIVE" << "HOSTILE" << "SAVAGE";
+		addProperty<CGCreature::Character>("Character", (CGCreature::Character)o->character, delegate, false);
+	}
+	addProperty("Never flees", o->neverFlees, InspectorDelegate::boolDelegate(), false);
+	addProperty("Not growing", o->notGrowingTeam, InspectorDelegate::boolDelegate(), false);
+	addProperty("Artifact reward", o->gainedArtifact); //TODO: implement in setProperty
+	addProperty("Army", PropertyEditorPlaceholder(), true);
+	addProperty("Amount", o->stacks[SlotID(0)]->count, false);
+	//addProperty("Resources reward", o->resources); //TODO: implement in setProperty
+}
+
+void Inspector::updateProperties(CGPandoraBox * o)
+{
+	if(!o) return;
+	
+	addProperty("Message", o->message, new MessageDelegate, false);
+	
+	auto * delegate = new RewardsPandoraDelegate(*map, *o);
+	addProperty("Reward", PropertyEditorPlaceholder(), delegate, false);
+}
+
+void Inspector::updateProperties(CGEvent * o)
+{
+	if(!o) return;
+	
+	addProperty("Remove after", o->removeAfterVisit, InspectorDelegate::boolDelegate(), false);
+	addProperty("Human trigger", o->humanActivate, InspectorDelegate::boolDelegate(), false);
+	addProperty("Cpu trigger", o->computerActivate, InspectorDelegate::boolDelegate(), false);
+	//ui8 availableFor; //players whom this event is available for
+}
+
+void Inspector::updateProperties(CGSeerHut * o)
+{
+	if(!o) return;
+
+	{ //Mission type
+		auto * delegate = new InspectorDelegate;
+		delegate->options << "Reach level" << "Stats" << "Kill hero" << "Kill creature" << "Artifact" << "Army" << "Resources" << "Hero" << "Player";
+		addProperty<CQuest::Emission>("Mission type", o->quest->missionType, delegate, false);
+	}
+	
+	addProperty("First visit text", o->quest->firstVisitText, new MessageDelegate, false);
+	addProperty("Next visit text", o->quest->nextVisitText, new MessageDelegate, false);
+	addProperty("Completed text", o->quest->completedText, new MessageDelegate, false);
+	
+	{ //Quest
+		auto * delegate = new QuestDelegate(*map, *o);
+		addProperty("Quest", PropertyEditorPlaceholder(), delegate, false);
+	}
+	
+	{ //Reward
+		auto * delegate = new RewardsSeerhutDelegate(*map, *o);
+		addProperty("Reward", PropertyEditorPlaceholder(), delegate, false);
+	}
+}
+
+void Inspector::updateProperties()
+{
+	if(!obj)
+		return;
+	table->setRowCount(0); //cleanup table
+	
+	addProperty("Indentifier", obj);
+	addProperty("ID", obj->ID.getNum());
+	addProperty("SubID", obj->subID);
+	addProperty("InstanceName", obj->instanceName);
+	addProperty("TypeName", obj->typeName);
+	addProperty("SubTypeName", obj->subTypeName);
+	
+	if(!dynamic_cast<CGHeroInstance*>(obj))
+	{
+		auto factory = VLC->objtypeh->getHandlerFor(obj->ID, obj->subID);
+		addProperty("IsStatic", factory->isStaticObject());
+	}
+	
+	auto * delegate = new InspectorDelegate();
+	delegate->options << "NEUTRAL";
+	for(int p = 0; p < map->players.size(); ++p)
+		if(map->players[p].canAnyonePlay())
+			delegate->options << QString("PLAYER %1").arg(p);
+	addProperty("Owner", obj->tempOwner, delegate, true);
+	
+	UPDATE_OBJ_PROPERTIES(CArmedInstance);
+	UPDATE_OBJ_PROPERTIES(CGResource);
+	UPDATE_OBJ_PROPERTIES(CGArtifact);
+	UPDATE_OBJ_PROPERTIES(CGMine);
+	UPDATE_OBJ_PROPERTIES(CGGarrison);
+	UPDATE_OBJ_PROPERTIES(CGShipyard);
+	UPDATE_OBJ_PROPERTIES(CGDwelling);
+	UPDATE_OBJ_PROPERTIES(CGTownInstance);
+	UPDATE_OBJ_PROPERTIES(CGCreature);
+	UPDATE_OBJ_PROPERTIES(CGHeroInstance);
+	UPDATE_OBJ_PROPERTIES(CGSignBottle);
+	UPDATE_OBJ_PROPERTIES(CGLighthouse);
+	UPDATE_OBJ_PROPERTIES(CGPandoraBox);
+	UPDATE_OBJ_PROPERTIES(CGEvent);
+	UPDATE_OBJ_PROPERTIES(CGSeerHut);
+	
+	table->show();
+}
+
+//===============IMPLEMENT PROPERTY UPDATE================================
+void Inspector::setProperty(const QString & key, const QVariant & value)
+{
+	if(!obj)
+		return;
+	
+	if(key == "Owner")
+	{
+		PlayerColor owner(value.toString().mid(6).toInt()); //receiving PLAYER N, N has index 6
+		if(value == "NEUTRAL")
+			owner = PlayerColor::NEUTRAL;
+		if(value == "UNFLAGGABLE")
+			owner = PlayerColor::UNFLAGGABLE;
+		obj->tempOwner = owner;
+	}
+	
+	SET_PROPERTIES(CArmedInstance);
+	SET_PROPERTIES(CGTownInstance);
+	SET_PROPERTIES(CGArtifact);
+	SET_PROPERTIES(CGMine);
+	SET_PROPERTIES(CGResource);
+	SET_PROPERTIES(CGDwelling);
+	SET_PROPERTIES(CGGarrison);
+	SET_PROPERTIES(CGCreature);
+	SET_PROPERTIES(CGHeroInstance);
+	SET_PROPERTIES(CGShipyard);
+	SET_PROPERTIES(CGSignBottle);
+	SET_PROPERTIES(CGLighthouse);
+	SET_PROPERTIES(CGPandoraBox);
+	SET_PROPERTIES(CGEvent);
+	SET_PROPERTIES(CGSeerHut);
+}
+
+void Inspector::setProperty(CArmedInstance * o, const QString & key, const QVariant & value)
+{
+	if(!o) return;
+}
+
+void Inspector::setProperty(CGLighthouse * o, const QString & key, const QVariant & value)
+{
+	if(!o) return;
+}
+
+void Inspector::setProperty(CGPandoraBox * o, const QString & key, const QVariant & value)
+{
+	if(!o) return;
+}
+
+void Inspector::setProperty(CGEvent * o, const QString & key, const QVariant & value)
+{
+	if(!o) return;
+	
+	if(key == "Remove after")
+		o->removeAfterVisit = stringToBool(value.toString());
+	
+	if(key == "Human trigger")
+		o->humanActivate = stringToBool(value.toString());
+	
+	if(key == "Cpu trigger")
+		o->computerActivate = stringToBool(value.toString());
+}
+
+void Inspector::setProperty(CGTownInstance * o, const QString & key, const QVariant & value)
+{
+	if(!o) return;
+	
+	if(key == "Town name")
+		o->name = value.toString().toStdString();
+}
+
+void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVariant & value)
+{
+	if(!o) return;
+	
+	if(key == "Message")
+		o->message = value.toString().toStdString();
+}
+
+void Inspector::setProperty(CGMine * o, const QString & key, const QVariant & value)
+{
+	if(!o) return;
+	
+	if(key == "Productivity")
+		o->producedQuantity = value.toString().toInt();
+}
+
+void Inspector::setProperty(CGArtifact * o, const QString & key, const QVariant & value)
+{
+	if(!o) return;
+	
+	if(key == "Message")
+		o->message = value.toString().toStdString();
+	
+	if(o->storedArtifact && key == "Spell")
+	{
+		for(auto spell : VLC->spellh->objects)
+		{
+			if(spell->name == value.toString().toStdString())
+			{
+				o->storedArtifact = CArtifactInstance::createScroll(spell->getId());
+				break;
+			}
+		}
+	}
+}
+
+void Inspector::setProperty(CGDwelling * o, const QString & key, const QVariant & value)
+{
+	if(!o) return;
+}
+
+void Inspector::setProperty(CGGarrison * o, const QString & key, const QVariant & value)
+{
+	if(!o) return;
+	
+	if(key == "Removable units")
+		o->removableUnits = stringToBool(value.toString());
+}
+
+void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVariant & value)
+{
+	if(!o) return;
+	
+	if(key == "Sex")
+		o->sex = value.toString() == "MALE" ? 0 : 1;
+	
+	if(key == "Name")
+		o->name = value.toString().toStdString();
+	
+	if(key == "Hero type")
+	{
+		for(auto t : VLC->heroh->objects)
+		{
+			if(t->getName() == value.toString().toStdString())
+				o->type = t.get();
+		}
+		o->name = o->type->getName();
+		o->sex = o->type->sex;
+		o->biography = o->type->biography;
+		o->portrait = o->type->imageIndex;
+		o->randomizeArmy(o->type->heroClass->faction);
+		updateProperties(); //updating other properties after change
+	}
+}
+
+void Inspector::setProperty(CGShipyard * o, const QString & key, const QVariant & value)
+{
+	if(!o) return;
+}
+
+void Inspector::setProperty(CGResource * o, const QString & key, const QVariant & value)
+{
+	if(!o) return;
+	
+	if(key == "Amount")
+		o->amount = value.toString().toInt();
+}
+
+void Inspector::setProperty(CGCreature * o, const QString & key, const QVariant & value)
+{
+	if(!o) return;
+	
+	if(key == "Message")
+		o->message = value.toString().toStdString();
+	if(key == "Character")
+	{
+		//COMPLIANT = 0, FRIENDLY = 1, AGRESSIVE = 2, HOSTILE = 3, SAVAGE = 4
+		if(value == "COMPLIANT")
+			o->character = CGCreature::Character::COMPLIANT;
+		if(value == "FRIENDLY")
+			o->character = CGCreature::Character::FRIENDLY;
+		if(value == "AGRESSIVE")
+			o->character = CGCreature::Character::AGRESSIVE;
+		if(value == "HOSTILE")
+			o->character = CGCreature::Character::HOSTILE;
+		if(value == "SAVAGE")
+			o->character = CGCreature::Character::SAVAGE;
+	}
+	if(key == "Never flees")
+		o->neverFlees = stringToBool(value.toString());
+	if(key == "Not growing")
+		o->notGrowingTeam = stringToBool(value.toString());
+	if(key == "Amount")
+		o->stacks[SlotID(0)]->count = value.toString().toInt();
+}
+
+void Inspector::setProperty(CGSeerHut * o, const QString & key, const QVariant & value)
+{
+	if(!o) return;
+	
+	if(key == "Mission type")
+	{
+		if(value == "Reach level")
+			o->quest->missionType = CQuest::Emission::MISSION_LEVEL;
+		if(value == "Stats")
+			o->quest->missionType = CQuest::Emission::MISSION_PRIMARY_STAT;
+		if(value == "Kill hero")
+			o->quest->missionType = CQuest::Emission::MISSION_KILL_HERO;
+		if(value == "Kill creature")
+			o->quest->missionType = CQuest::Emission::MISSION_KILL_CREATURE;
+		if(value == "Artifact")
+			o->quest->missionType = CQuest::Emission::MISSION_ART;
+		if(value == "Army")
+			o->quest->missionType = CQuest::Emission::MISSION_ARMY;
+		if(value == "Resources")
+			o->quest->missionType = CQuest::Emission::MISSION_RESOURCES;
+		if(value == "Hero")
+			o->quest->missionType = CQuest::Emission::MISSION_HERO;
+		if(value == "Player")
+			o->quest->missionType = CQuest::Emission::MISSION_PLAYER;
+	}
+	
+	if(key == "First visit text")
+		o->quest->firstVisitText = value.toString().toStdString();
+	if(key == "Next visit text")
+		o->quest->nextVisitText = value.toString().toStdString();
+	if(key == "Completed text")
+		o->quest->completedText = value.toString().toStdString();
+}
+
+
+//===============IMPLEMENT PROPERTY VALUE TYPE============================
+QTableWidgetItem * Inspector::addProperty(CGObjectInstance * value)
+{
+	return new QTableWidgetItem(QString::number(data_cast<CGObjectInstance>(value)));
+}
+
+QTableWidgetItem * Inspector::addProperty(Inspector::PropertyEditorPlaceholder value)
+{
+	auto item = new QTableWidgetItem("");
+	item->setData(Qt::UserRole, QString("PropertyEditor"));
+	return item;
+}
+
+QTableWidgetItem * Inspector::addProperty(unsigned int value)
+{
+	return new QTableWidgetItem(QString::number(value));
+}
+
+QTableWidgetItem * Inspector::addProperty(int value)
+{
+	return new QTableWidgetItem(QString::number(value));
+}
+
+QTableWidgetItem * Inspector::addProperty(bool value)
+{
+	return new QTableWidgetItem(value ? "TRUE" : "FALSE");
+}
+
+QTableWidgetItem * Inspector::addProperty(const std::string & value)
+{
+	return addProperty(QString::fromStdString(value));
+}
+
+QTableWidgetItem * Inspector::addProperty(const QString & value)
+{
+	return new QTableWidgetItem(value);
+}
+
+QTableWidgetItem * Inspector::addProperty(const int3 & value)
+{
+	return new QTableWidgetItem(QString("(%1, %2, %3)").arg(value.x, value.y, value.z));
+}
+
+QTableWidgetItem * Inspector::addProperty(const PlayerColor & value)
+{
+	auto str = QString("PLAYER %1").arg(value.getNum());
+	if(value == PlayerColor::NEUTRAL)
+		str = "NEUTRAL";
+	if(value == PlayerColor::UNFLAGGABLE)
+		str = "UNFLAGGABLE";
+	return new QTableWidgetItem(str);
+}
+
+QTableWidgetItem * Inspector::addProperty(const Res::ERes & value)
+{
+	QString str;
+	switch (value) {
+		case Res::ERes::WOOD:
+			str = "WOOD";
+			break;
+		case Res::ERes::ORE:
+			str = "ORE";
+			break;
+		case Res::ERes::SULFUR:
+			str = "SULFUR";
+			break;
+		case Res::ERes::GEMS:
+			str = "GEMS";
+			break;
+		case Res::ERes::MERCURY:
+			str = "MERCURY";
+			break;
+		case Res::ERes::CRYSTAL:
+			str = "CRYSTAL";
+			break;
+		case Res::ERes::GOLD:
+			str = "GOLD";
+			break;
+		default:
+			break;
+	}
+	return new QTableWidgetItem(str);
+}
+
+QTableWidgetItem * Inspector::addProperty(CGCreature::Character value)
+{
+	QString str;
+	switch (value) {
+		case CGCreature::Character::COMPLIANT:
+			str = "COMPLIANT";
+			break;
+		case CGCreature::Character::FRIENDLY:
+			str = "FRIENDLY";
+			break;
+		case CGCreature::Character::AGRESSIVE:
+			str = "AGRESSIVE";
+			break;
+		case CGCreature::Character::HOSTILE:
+			str = "HOSTILE";
+			break;
+		case CGCreature::Character::SAVAGE:
+			str = "SAVAGE";
+			break;
+		default:
+			break;
+	}
+	return new QTableWidgetItem(str);
+}
+
+QTableWidgetItem * Inspector::addProperty(CQuest::Emission value)
+{
+	QString str;
+	switch (value) {
+		case CQuest::Emission::MISSION_LEVEL:
+			str = "Reach level";
+			break;
+		case CQuest::Emission::MISSION_PRIMARY_STAT:
+			str = "Stats";
+			break;
+		case CQuest::Emission::MISSION_KILL_HERO:
+			str = "Kill hero";
+			break;
+		case CQuest::Emission::MISSION_KILL_CREATURE:
+			str = "Kill creature";
+			break;
+		case CQuest::Emission::MISSION_ART:
+			str = "Artifact";
+			break;
+		case CQuest::Emission::MISSION_ARMY:
+			str = "Army";
+			break;
+		case CQuest::Emission::MISSION_RESOURCES:
+			str = "Resources";
+			break;
+		case CQuest::Emission::MISSION_HERO:
+			str = "Hero";
+			break;
+		case CQuest::Emission::MISSION_PLAYER:
+			str = "Player";
+			break;
+		case CQuest::Emission::MISSION_KEYMASTER:
+			str = "Key master";
+			break;
+		default:
+			break;
+	}
+	return new QTableWidgetItem(str);
+}
+
+//========================================================================
+
+Inspector::Inspector(CMap * m, CGObjectInstance * o, QTableWidget * t): obj(o), table(t), map(m)
+{
+}
+
+/*
+ * Delegates
+ */
+
+InspectorDelegate * InspectorDelegate::boolDelegate()
+{
+	auto * d = new InspectorDelegate;
+	d->options << "TRUE" << "FALSE";
+	return d;
+}
+
+QWidget * InspectorDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
+{
+	return new QComboBox(parent);
+}
+
+void InspectorDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
+{
+	if(QComboBox *ed = qobject_cast<QComboBox *>(editor))
+	{
+		ed->addItems(options);
+	}
+	else
+	{
+		QStyledItemDelegate::setEditorData(editor, index);
+	}
+}
+
+void InspectorDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
+{
+	if(QComboBox *ed = qobject_cast<QComboBox *>(editor))
+	{
+		if(!options.isEmpty())
+		{
+			QMap<int, QVariant> data;
+			data[0] = options[ed->currentIndex()];
+			model->setItemData(index, data);
+		}
+	}
+	else
+	{
+		QStyledItemDelegate::setModelData(editor, model, index);
+	}
+}
+

+ 164 - 0
mapeditor/inspector/inspector.h

@@ -0,0 +1,164 @@
+/*
+ * inspector.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
+
+#include "../StdInc.h"
+#include <QTableWidget>
+#include <QTableWidgetItem>
+#include <QStyledItemDelegate>
+#include "../lib/int3.h"
+#include "../lib/GameConstants.h"
+#include "../lib/mapObjects/MapObjects.h"
+#include "../lib/ResourceSet.h"
+
+#define DECLARE_OBJ_TYPE(x) void initialize(x*);
+#define DECLARE_OBJ_PROPERTY_METHODS(x) \
+void updateProperties(x*); \
+void setProperty(x*, const QString &, const QVariant &);
+
+#define INIT_OBJ_TYPE(x) initialize(dynamic_cast<x*>(o))
+#define UPDATE_OBJ_PROPERTIES(x) updateProperties(dynamic_cast<x*>(obj))
+#define SET_PROPERTIES(x) setProperty(dynamic_cast<x*>(obj), key, value)
+
+
+class Initializer
+{
+public:
+	//===============DECLARE MAP OBJECTS======================================
+	DECLARE_OBJ_TYPE(CArmedInstance);
+	DECLARE_OBJ_TYPE(CGShipyard);
+	DECLARE_OBJ_TYPE(CGTownInstance);
+	DECLARE_OBJ_TYPE(CGArtifact);
+	DECLARE_OBJ_TYPE(CGMine);
+	DECLARE_OBJ_TYPE(CGResource);
+	DECLARE_OBJ_TYPE(CGDwelling);
+	DECLARE_OBJ_TYPE(CGGarrison);
+	DECLARE_OBJ_TYPE(CGHeroInstance);
+	DECLARE_OBJ_TYPE(CGCreature);
+	DECLARE_OBJ_TYPE(CGSignBottle);
+	DECLARE_OBJ_TYPE(CGLighthouse);
+	//DECLARE_OBJ_TYPE(CGEvent);
+	//DECLARE_OBJ_TYPE(CGPandoraBox);
+	//DECLARE_OBJ_TYPE(CGSeerHut);
+	
+	Initializer(CGObjectInstance *, const PlayerColor &);
+
+private:
+	PlayerColor defaultPlayer;
+};
+
+class Inspector
+{
+protected:
+	struct PropertyEditorPlaceholder {};
+	
+//===============DECLARE PROPERTIES SETUP=================================
+	DECLARE_OBJ_PROPERTY_METHODS(CArmedInstance);
+	DECLARE_OBJ_PROPERTY_METHODS(CGTownInstance);
+	DECLARE_OBJ_PROPERTY_METHODS(CGShipyard);
+	DECLARE_OBJ_PROPERTY_METHODS(CGArtifact);
+	DECLARE_OBJ_PROPERTY_METHODS(CGMine);
+	DECLARE_OBJ_PROPERTY_METHODS(CGResource);
+	DECLARE_OBJ_PROPERTY_METHODS(CGDwelling);
+	DECLARE_OBJ_PROPERTY_METHODS(CGGarrison);
+	DECLARE_OBJ_PROPERTY_METHODS(CGHeroInstance);
+	DECLARE_OBJ_PROPERTY_METHODS(CGCreature);
+	DECLARE_OBJ_PROPERTY_METHODS(CGSignBottle);
+	DECLARE_OBJ_PROPERTY_METHODS(CGLighthouse);
+	DECLARE_OBJ_PROPERTY_METHODS(CGPandoraBox);
+	DECLARE_OBJ_PROPERTY_METHODS(CGEvent);
+	DECLARE_OBJ_PROPERTY_METHODS(CGSeerHut);
+
+//===============DECLARE PROPERTY VALUE TYPE==============================
+	QTableWidgetItem * addProperty(unsigned int value);
+	QTableWidgetItem * addProperty(int value);
+	QTableWidgetItem * addProperty(const std::string & value);
+	QTableWidgetItem * addProperty(const QString & value);
+	QTableWidgetItem * addProperty(const int3 & value);
+	QTableWidgetItem * addProperty(const PlayerColor & value);
+	QTableWidgetItem * addProperty(const Res::ERes & value);
+	QTableWidgetItem * addProperty(bool value);
+	QTableWidgetItem * addProperty(CGObjectInstance * value);
+	QTableWidgetItem * addProperty(CGCreature::Character value);
+	QTableWidgetItem * addProperty(CQuest::Emission value);
+	QTableWidgetItem * addProperty(PropertyEditorPlaceholder value);
+	
+//===============END OF DECLARATION=======================================
+	
+public:
+	Inspector(CMap *, CGObjectInstance *, QTableWidget *);
+
+	void setProperty(const QString & key, const QVariant & value);
+
+	void updateProperties();
+	
+protected:
+
+	template<class T>
+	void addProperty(const QString & key, const T & value, QAbstractItemDelegate * delegate, bool restricted)
+	{
+		auto * itemValue = addProperty(value);
+		if(restricted)
+			itemValue->setFlags(Qt::NoItemFlags);
+		
+		QTableWidgetItem * itemKey = nullptr;
+		if(keyItems.contains(key))
+		{
+			itemKey = keyItems[key];
+			table->setItem(table->row(itemKey), 1, itemValue);
+			if(delegate)
+				table->setItemDelegateForRow(table->row(itemKey), delegate);
+		}
+		else
+		{
+			itemKey = new QTableWidgetItem(key);
+			itemKey->setFlags(Qt::NoItemFlags);
+			keyItems[key] = itemKey;
+			
+			table->setRowCount(row + 1);
+			table->setItem(row, 0, itemKey);
+			table->setItem(row, 1, itemValue);
+			table->setItemDelegateForRow(row, delegate);
+			++row;
+		}
+	}
+	
+	template<class T>
+	void addProperty(const QString & key, const T & value, bool restricted = true)
+	{
+		addProperty<T>(key, value, nullptr, restricted);
+	}
+
+protected:
+	int row = 0;
+	QTableWidget * table;
+	CGObjectInstance * obj;
+	QMap<QString, QTableWidgetItem*> keyItems;
+	CMap * map;
+};
+
+
+
+
+class InspectorDelegate : public QStyledItemDelegate
+{
+	Q_OBJECT
+public:
+	static InspectorDelegate * boolDelegate();
+	
+	using QStyledItemDelegate::QStyledItemDelegate;
+
+	QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
+	void setEditorData(QWidget *editor, const QModelIndex &index) const override;
+	void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
+	
+	QStringList options;
+};
+

+ 63 - 0
mapeditor/inspector/messagewidget.cpp

@@ -0,0 +1,63 @@
+/*
+ * messagewidget.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 "messagewidget.h"
+#include "ui_messagewidget.h"
+
+MessageWidget::MessageWidget(QWidget *parent) :
+	QDialog(parent),
+	ui(new Ui::MessageWidget)
+{
+	ui->setupUi(this);
+}
+
+MessageWidget::~MessageWidget()
+{
+	delete ui;
+}
+
+void MessageWidget::setMessage(const QString & m)
+{
+	ui->messageEdit->setPlainText(m);
+}
+
+QString MessageWidget::getMessage() const
+{
+	return ui->messageEdit->toPlainText();
+}
+
+QWidget * MessageDelegate::createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const
+{
+	return new MessageWidget(parent);
+}
+
+void MessageDelegate::setEditorData(QWidget * editor, const QModelIndex & index) const
+{
+	if(auto *ed = qobject_cast<MessageWidget *>(editor))
+	{
+		ed->setMessage(index.data().toString());
+	}
+	else
+	{
+		QStyledItemDelegate::setEditorData(editor, index);
+	}
+}
+
+void MessageDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const
+{
+	if(auto *ed = qobject_cast<MessageWidget *>(editor))
+	{
+		model->setData(index, ed->getMessage());
+	}
+	else
+	{
+		QStyledItemDelegate::setModelData(editor, model, index);
+	}
+}

+ 44 - 0
mapeditor/inspector/messagewidget.h

@@ -0,0 +1,44 @@
+/*
+ * messagewidget.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
+#include "../StdInc.h"
+#include <QDialog>
+
+namespace Ui {
+class MessageWidget;
+}
+
+class MessageWidget : public QDialog
+{
+	Q_OBJECT
+
+public:
+	explicit MessageWidget(QWidget *parent = nullptr);
+	~MessageWidget();
+	
+	void setMessage(const QString &);
+	QString getMessage() const;
+
+private:
+	Ui::MessageWidget *ui;
+};
+
+
+class MessageDelegate : public QStyledItemDelegate
+{
+	Q_OBJECT
+public:
+	using QStyledItemDelegate::QStyledItemDelegate;
+	
+	QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
+	void setEditorData(QWidget *editor, const QModelIndex &index) const override;
+	void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
+};
+

+ 33 - 0
mapeditor/inspector/messagewidget.ui

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MessageWidget</class>
+ <widget class="QDialog" name="MessageWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>306</width>
+    <height>201</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>306</width>
+    <height>201</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Message</string>
+  </property>
+  <property name="modal">
+   <bool>true</bool>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="0" column="0">
+    <widget class="QPlainTextEdit" name="messageEdit"/>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 193 - 0
mapeditor/inspector/questwidget.cpp

@@ -0,0 +1,193 @@
+/*
+ * questwidget.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 "questwidget.h"
+#include "ui_questwidget.h"
+#include "../lib/VCMI_Lib.h"
+#include "../lib/CSkillHandler.h"
+#include "../lib/CArtHandler.h"
+#include "../lib/CCreatureHandler.h"
+#include "../lib/CHeroHandler.h"
+#include "../lib/StringConstants.h"
+
+QuestWidget::QuestWidget(const CMap & _map, CGSeerHut & _sh, QWidget *parent) :
+	QDialog(parent),
+	map(_map),
+	seerhut(_sh),
+	ui(new Ui::QuestWidget)
+{
+	ui->setupUi(this);
+}
+
+QuestWidget::~QuestWidget()
+{
+	delete ui;
+}
+
+void QuestWidget::obtainData()
+{
+	assert(seerhut.quest);
+	bool activeId = false;
+	bool activeAmount = false;
+	switch(seerhut.quest->missionType) {
+		case CQuest::Emission::MISSION_LEVEL:
+			activeAmount = true;
+			ui->targetId->addItem("Reach level");
+			ui->targetAmount->setText(QString::number(seerhut.quest->m13489val));
+			break;
+		case CQuest::Emission::MISSION_PRIMARY_STAT:
+			activeId = true;
+			activeAmount = true;
+			for(auto s : PrimarySkill::names)
+				ui->targetId->addItem(QString::fromStdString(s));
+			for(int i = 0; i < seerhut.quest->m2stats.size(); ++i)
+			{
+				if(seerhut.quest->m2stats[i] > 0)
+				{
+					ui->targetId->setCurrentIndex(i);
+					ui->targetAmount->setText(QString::number(seerhut.quest->m2stats[i]));
+					break; //TODO: support multiple stats
+				}
+			}
+			break;
+		case CQuest::Emission::MISSION_KILL_HERO:
+			activeId = true;
+			//TODO: implement
+			break;
+		case CQuest::Emission::MISSION_KILL_CREATURE:
+			activeId = true;
+			//TODO: implement
+			break;
+		case CQuest::Emission::MISSION_ART:
+			activeId = true;
+			for(int i = 0; i < map.allowedArtifact.size(); ++i)
+				ui->targetId->addItem(QString::fromStdString(VLC->arth->objects.at(i)->getName()));
+			if(!seerhut.quest->m5arts.empty())
+				ui->targetId->setCurrentIndex(seerhut.quest->m5arts.front());
+			//TODO: support multiple artifacts
+			break;
+		case CQuest::Emission::MISSION_ARMY:
+			activeId = true;
+			activeAmount = true;
+			break;
+		case CQuest::Emission::MISSION_RESOURCES:
+			activeId = true;
+			activeAmount = true;
+			for(auto s : GameConstants::RESOURCE_NAMES)
+				ui->targetId->addItem(QString::fromStdString(s));
+			for(int i = 0; i < seerhut.quest->m7resources.size(); ++i)
+			{
+				if(seerhut.quest->m7resources[i] > 0)
+				{
+					ui->targetId->setCurrentIndex(i);
+					ui->targetAmount->setText(QString::number(seerhut.quest->m7resources[i]));
+					break; //TODO: support multiple resources
+				}
+			}
+			break;
+		case CQuest::Emission::MISSION_HERO:
+			activeId = true;
+			for(int i = 0; i < map.allowedHeroes.size(); ++i)
+				ui->targetId->addItem(QString::fromStdString(VLC->heroh->objects.at(i)->getName()));
+			ui->targetId->setCurrentIndex(seerhut.quest->m13489val);
+			break;
+		case CQuest::Emission::MISSION_PLAYER:
+			activeId = true;
+			for(auto s : GameConstants::PLAYER_COLOR_NAMES)
+				ui->targetId->addItem(QString::fromStdString(s));
+			ui->targetId->setCurrentIndex(seerhut.quest->m13489val);
+			break;
+		case CQuest::Emission::MISSION_KEYMASTER:
+			break;
+		default:
+			break;
+	}
+	
+	ui->targetId->setEnabled(activeId);
+	ui->targetAmount->setEnabled(activeAmount);
+}
+
+QString QuestWidget::commitChanges()
+{
+	assert(seerhut.quest);
+	switch(seerhut.quest->missionType) {
+		case CQuest::Emission::MISSION_LEVEL:
+			seerhut.quest->m13489val = ui->targetAmount->text().toInt();
+			return QString("Reach lvl ").append(ui->targetAmount->text());
+		case CQuest::Emission::MISSION_PRIMARY_STAT:
+			seerhut.quest->m2stats.resize(sizeof(PrimarySkill::names), 0);
+			seerhut.quest->m2stats[ui->targetId->currentIndex()] = ui->targetAmount->text().toInt();
+			//TODO: support multiple stats
+			return ui->targetId->currentText().append(ui->targetAmount->text());
+		case CQuest::Emission::MISSION_KILL_HERO:
+			//TODO: implement
+			return QString("N/A");
+		case CQuest::Emission::MISSION_KILL_CREATURE:
+			//TODO: implement
+			return QString("N/A");
+		case CQuest::Emission::MISSION_ART:
+			seerhut.quest->m5arts.clear();
+			seerhut.quest->m5arts.push_back(ui->targetId->currentIndex());
+			//TODO: support multiple artifacts
+			return ui->targetId->currentText();
+		case CQuest::Emission::MISSION_ARMY:
+			//TODO: implement
+			return QString("N/A");
+		case CQuest::Emission::MISSION_RESOURCES:
+			seerhut.quest->m7resources.resize(sizeof(GameConstants::RESOURCE_NAMES), 0);
+			seerhut.quest->m7resources[ui->targetId->currentIndex()] = ui->targetAmount->text().toInt();
+			//TODO: support resources
+			return ui->targetId->currentText().append(ui->targetAmount->text());
+		case CQuest::Emission::MISSION_HERO:
+			seerhut.quest->m13489val = ui->targetId->currentIndex();
+			return ui->targetId->currentText();
+		case CQuest::Emission::MISSION_PLAYER:
+			seerhut.quest->m13489val = ui->targetId->currentIndex();
+			return ui->targetId->currentText();
+		case CQuest::Emission::MISSION_KEYMASTER:
+			return QString("N/A");
+		default:
+			return QString("N/A");
+	}
+}
+
+QuestDelegate::QuestDelegate(const CMap & m, CGSeerHut & t): map(m), seerhut(t), QStyledItemDelegate()
+{
+}
+
+QWidget * QuestDelegate::createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const
+{
+	return new QuestWidget(map, seerhut, parent);
+}
+
+void QuestDelegate::setEditorData(QWidget * editor, const QModelIndex & index) const
+{
+	if(auto *ed = qobject_cast<QuestWidget *>(editor))
+	{
+		ed->obtainData();
+	}
+	else
+	{
+		QStyledItemDelegate::setEditorData(editor, index);
+	}
+}
+
+void QuestDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const
+{
+	if(auto *ed = qobject_cast<QuestWidget *>(editor))
+	{
+		auto quest = ed->commitChanges();
+		model->setData(index, quest);
+	}
+	else
+	{
+		QStyledItemDelegate::setModelData(editor, model, index);
+	}
+}

+ 52 - 0
mapeditor/inspector/questwidget.h

@@ -0,0 +1,52 @@
+/*
+ * questwidget.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
+#include "../StdInc.h"
+#include <QDialog>
+#include "../lib/mapObjects/CQuest.h"
+#include "../lib/mapping/CMap.h"
+
+namespace Ui {
+class QuestWidget;
+}
+
+class QuestWidget : public QDialog
+{
+	Q_OBJECT
+
+public:
+	explicit QuestWidget(const CMap &, CGSeerHut &, QWidget *parent = nullptr);
+	~QuestWidget();
+	
+	void obtainData();
+	QString commitChanges();
+
+private:
+	CGSeerHut & seerhut;
+	const CMap & map;
+	Ui::QuestWidget *ui;
+};
+
+class QuestDelegate : public QStyledItemDelegate
+{
+	Q_OBJECT
+public:
+	using QStyledItemDelegate::QStyledItemDelegate;
+	
+	QuestDelegate(const CMap &, CGSeerHut &);
+	
+	QWidget * createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const override;
+	void setEditorData(QWidget * editor, const QModelIndex & index) const override;
+	void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override;
+	
+private:
+	CGSeerHut & seerhut;
+	const CMap & map;
+};

+ 50 - 0
mapeditor/inspector/questwidget.ui

@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>QuestWidget</class>
+ <widget class="QDialog" name="QuestWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>429</width>
+    <height>89</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Mission goal</string>
+  </property>
+  <layout class="QHBoxLayout" name="horizontalLayout">
+   <item>
+    <widget class="QComboBox" name="targetId">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLineEdit" name="targetAmount">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="maximumSize">
+      <size>
+       <width>60</width>
+       <height>16777215</height>
+      </size>
+     </property>
+     <property name="inputMethodHints">
+      <set>Qt::ImhDigitsOnly</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 427 - 0
mapeditor/inspector/rewardswidget.cpp

@@ -0,0 +1,427 @@
+/*
+ * rewardswidget.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 "rewardswidget.h"
+#include "ui_rewardswidget.h"
+#include "../lib/VCMI_Lib.h"
+#include "../lib/CSkillHandler.h"
+#include "../lib/spells/CSpellHandler.h"
+#include "../lib/CArtHandler.h"
+#include "../lib/CCreatureHandler.h"
+#include "../lib/StringConstants.h"
+
+RewardsWidget::RewardsWidget(const CMap & m, CGPandoraBox & p, QWidget *parent) :
+	QDialog(parent),
+	map(m),
+	pandora(&p),
+	seerhut(nullptr),
+	ui(new Ui::RewardsWidget)
+{
+	ui->setupUi(this);
+	
+	for(auto & type : rewardTypes)
+		ui->rewardType->addItem(QString::fromStdString(type));
+}
+
+RewardsWidget::RewardsWidget(const CMap & m, CGSeerHut & p, QWidget *parent) :
+	QDialog(parent),
+	map(m),
+	pandora(nullptr),
+	seerhut(&p),
+	ui(new Ui::RewardsWidget)
+{
+	ui->setupUi(this);
+	
+	for(auto & type : rewardTypes)
+		ui->rewardType->addItem(QString::fromStdString(type));
+}
+
+RewardsWidget::~RewardsWidget()
+{
+	delete ui;
+}
+
+QList<QString> RewardsWidget::getListForType(RewardType typeId)
+{
+	assert(typeId < rewardTypes.size());
+	QList<QString> result;
+	
+	switch (typeId) {
+		case RewardType::RESOURCE:
+			//to convert string to index WOOD = 0, MERCURY, ORE, SULFUR, CRYSTAL, GEMS, GOLD, MITHRIL,
+			result.append("Wood");
+			result.append("Mercury");
+			result.append("Ore");
+			result.append("Sulfur");
+			result.append("Crystals");
+			result.append("Gems");
+			result.append("Gold");
+			break;
+			
+		case RewardType::PRIMARY_SKILL:
+			for(auto s : PrimarySkill::names)
+				result.append(QString::fromStdString(s));
+			break;
+			
+		case RewardType::SECONDARY_SKILL:
+			for(int i = 0; i < map.allowedAbilities.size(); ++i)
+			{
+				if(map.allowedAbilities[i])
+					result.append(QString::fromStdString(VLC->skillh->objects.at(i)->getName()));
+			}
+			break;
+			
+		case RewardType::ARTIFACT:
+			for(int i = 0; i < map.allowedArtifact.size(); ++i)
+			{
+				if(map.allowedArtifact[i])
+					result.append(QString::fromStdString(VLC->arth->objects.at(i)->getName()));
+			}
+			break;
+			
+		case RewardType::SPELL:
+			for(int i = 0; i < map.allowedSpell.size(); ++i)
+			{
+				if(map.allowedSpell[i])
+					result.append(QString::fromStdString(VLC->spellh->objects.at(i)->getName()));
+			}
+			break;
+			
+		case RewardType::CREATURE:
+			for(auto creature : VLC->creh->objects)
+			{
+				result.append(QString::fromStdString(creature->getName()));
+			}
+			break;
+	}
+	return result;
+}
+
+void RewardsWidget::on_rewardType_activated(int index)
+{
+	ui->rewardList->clear();
+	ui->rewardList->setEnabled(true);
+	assert(index < rewardTypes.size());
+	
+	auto l = getListForType(RewardType(index));
+	if(l.empty())
+		ui->rewardList->setEnabled(false);
+	
+	for(auto & s : l)
+		ui->rewardList->addItem(s);
+}
+
+void RewardsWidget::obtainData()
+{
+	if(pandora)
+	{
+		if(pandora->gainedExp > 0)
+			addReward(RewardType::EXPERIENCE, 0, pandora->gainedExp);
+		if(pandora->manaDiff)
+			addReward(RewardType::MANA, 0, pandora->manaDiff);
+		if(pandora->moraleDiff)
+			addReward(RewardType::MORALE, 0, pandora->moraleDiff);
+		if(pandora->luckDiff)
+			addReward(RewardType::LUCK, 0, pandora->luckDiff);
+		if(pandora->resources.nonZero())
+		{
+			for(Res::ResourceSet::nziterator resiter(pandora->resources); resiter.valid(); ++resiter)
+				addReward(RewardType::RESOURCE, resiter->resType, resiter->resVal);
+		}
+		for(int idx = 0; idx < pandora->primskills.size(); ++idx)
+		{
+			if(pandora->primskills[idx])
+				addReward(RewardType::PRIMARY_SKILL, idx, pandora->primskills[idx]);
+		}
+		assert(pandora->abilities.size() == pandora->abilityLevels.size());
+		for(int idx = 0; idx < pandora->abilities.size(); ++idx)
+		{
+			addReward(RewardType::SECONDARY_SKILL, pandora->abilities[idx].getNum(), pandora->abilityLevels[idx]);
+		}
+		for(auto art : pandora->artifacts)
+		{
+			addReward(RewardType::ARTIFACT, art.getNum(), 1);
+		}
+		for(auto spell : pandora->spells)
+		{
+			addReward(RewardType::SPELL, spell.getNum(), 1);
+		}
+		for(int i = 0; i < pandora->creatures.Slots().size(); ++i)
+		{
+			if(auto c = pandora->creatures.getCreature(SlotID(i)))
+				addReward(RewardType::CREATURE, c->getId(), pandora->creatures.getStackCount(SlotID(i)));
+		}
+	}
+	
+	if(seerhut)
+	{
+		switch(seerhut->rewardType)
+		{
+			case CGSeerHut::ERewardType::EXPERIENCE:
+				addReward(RewardType::EXPERIENCE, 0, seerhut->rVal);
+				break;
+				
+			case CGSeerHut::ERewardType::MANA_POINTS:
+				addReward(RewardType::MANA, 0, seerhut->rVal);
+				break;
+				
+			case CGSeerHut::ERewardType::MORALE_BONUS:
+				addReward(RewardType::MORALE, 0, seerhut->rVal);
+				break;
+				
+			case CGSeerHut::ERewardType::LUCK_BONUS:
+				addReward(RewardType::LUCK, 0, seerhut->rVal);
+				break;
+				
+			case CGSeerHut::ERewardType::RESOURCES:
+				addReward(RewardType::RESOURCE, seerhut->rID, seerhut->rVal);
+				break;
+				
+			case CGSeerHut::ERewardType::PRIMARY_SKILL:
+				addReward(RewardType::PRIMARY_SKILL, seerhut->rID, seerhut->rVal);
+				break;
+				
+			case CGSeerHut::ERewardType::SECONDARY_SKILL:
+				addReward(RewardType::SECONDARY_SKILL, seerhut->rID, seerhut->rVal);
+				break;
+				
+			case CGSeerHut::ERewardType::ARTIFACT:
+				addReward(RewardType::ARTIFACT, seerhut->rID, seerhut->rVal);
+				break;
+				
+			case CGSeerHut::ERewardType::SPELL:
+				addReward(RewardType::SPELL, seerhut->rID, seerhut->rVal);
+				break;
+				
+			case CGSeerHut::ERewardType::CREATURE:
+				addReward(RewardType::CREATURE, seerhut->rID, seerhut->rVal);
+				break;
+				
+			default:
+				break;
+		}
+	}
+}
+
+bool RewardsWidget::commitChanges()
+{
+	bool haveRewards = false;
+	if(pandora)
+	{
+		pandora->abilities.clear();
+		pandora->abilityLevels.clear();
+		pandora->primskills.resize(GameConstants::PRIMARY_SKILLS, 0);
+		pandora->resources = Res::ResourceSet();
+		pandora->artifacts.clear();
+		pandora->spells.clear();
+		pandora->creatures.clear();
+		
+		for(int row = 0; row < rewards; ++row)
+		{
+			haveRewards = true;
+			int typeId = ui->rewardsTable->item(row, 0)->data(Qt::UserRole).toInt();
+			int listId = ui->rewardsTable->item(row, 1) ? ui->rewardsTable->item(row, 1)->data(Qt::UserRole).toInt() : 0;
+			int amount = ui->rewardsTable->item(row, 2)->data(Qt::UserRole).toInt();
+			switch(typeId)
+			{
+				case RewardType::EXPERIENCE:
+					pandora->gainedExp = amount;
+					break;
+					
+				case RewardType::MANA:
+					pandora->manaDiff = amount;
+					break;
+					
+				case RewardType::MORALE:
+					pandora->moraleDiff = amount;
+					break;
+					
+				case RewardType::LUCK:
+					pandora->luckDiff = amount;
+					break;
+					
+				case RewardType::RESOURCE:
+					pandora->resources.at(listId) = amount;
+					break;
+					
+				case RewardType::PRIMARY_SKILL:
+					pandora->primskills[listId] = amount;
+					break;
+					
+				case RewardType::SECONDARY_SKILL:
+					pandora->abilities.push_back(SecondarySkill(listId));
+					pandora->abilityLevels.push_back(amount);
+					break;
+					
+				case RewardType::ARTIFACT:
+					pandora->artifacts.push_back(ArtifactID(listId));
+					break;
+					
+				case RewardType::SPELL:
+					pandora->spells.push_back(SpellID(listId));
+					break;
+					
+				case RewardType::CREATURE:
+					auto slot = pandora->creatures.getFreeSlot();
+					if(slot != SlotID() && amount > 0)
+						pandora->creatures.addToSlot(slot, CreatureID(listId), amount);
+					break;
+			}
+		}
+	}
+	if(seerhut)
+	{
+		for(int row = 0; row < rewards; ++row)
+		{
+			haveRewards = true;
+			int typeId = ui->rewardsTable->item(row, 0)->data(Qt::UserRole).toInt();
+			int listId = ui->rewardsTable->item(row, 1) ? ui->rewardsTable->item(row, 1)->data(Qt::UserRole).toInt() : 0;
+			int amount = ui->rewardsTable->item(row, 2)->data(Qt::UserRole).toInt();
+			seerhut->rewardType = CGSeerHut::ERewardType(typeId + 1);
+			seerhut->rID = listId;
+			seerhut->rVal = amount;
+		}
+	}
+	return haveRewards;
+}
+
+void RewardsWidget::on_rewardList_activated(int index)
+{
+	ui->rewardAmount->setText(QStringLiteral("1"));
+}
+
+void RewardsWidget::addReward(RewardsWidget::RewardType typeId, int listId, int amount)
+{
+	//for seerhut there could be the only one reward
+	if(!pandora && seerhut && rewards)
+		return;
+	
+	ui->rewardsTable->setRowCount(++rewards);
+	
+	auto itemType = new QTableWidgetItem(QString::fromStdString(rewardTypes[typeId]));
+	itemType->setData(Qt::UserRole, typeId);
+	ui->rewardsTable->setItem(rewards - 1, 0, itemType);
+	
+	auto l = getListForType(typeId);
+	if(!l.empty())
+	{
+		auto itemCurr = new QTableWidgetItem(getListForType(typeId)[listId]);
+		itemCurr->setData(Qt::UserRole, listId);
+		ui->rewardsTable->setItem(rewards - 1, 1, itemCurr);
+	}
+	
+	QString am = QString::number(amount);
+	switch(ui->rewardType->currentIndex())
+	{
+		case 6:
+			if(amount <= 1)
+				am = "Basic";
+			if(amount == 2)
+				am = "Advanced";
+			if(amount >= 3)
+				am = "Expert";
+			break;
+			
+		case 7:
+		case 8:
+			am = "";
+			amount = 1;
+			break;
+	}
+	auto itemCount = new QTableWidgetItem(am);
+	itemCount->setData(Qt::UserRole, amount);
+	ui->rewardsTable->setItem(rewards - 1, 2, itemCount);
+}
+
+
+void RewardsWidget::on_buttonAdd_clicked()
+{
+	addReward(RewardType(ui->rewardType->currentIndex()), ui->rewardList->currentIndex(), ui->rewardAmount->text().toInt());
+}
+
+
+void RewardsWidget::on_buttonRemove_clicked()
+{
+	auto currentRow = ui->rewardsTable->currentRow();
+	if(currentRow != -1)
+	{
+		ui->rewardsTable->removeRow(currentRow);
+		--rewards;
+	}
+}
+
+
+void RewardsWidget::on_buttonClear_clicked()
+{
+	ui->rewardsTable->clear();
+	rewards = 0;
+}
+
+
+void RewardsWidget::on_rewardsTable_itemSelectionChanged()
+{
+	/*auto type = ui->rewardsTable->item(ui->rewardsTable->currentRow(), 0);
+	ui->rewardType->setCurrentIndex(type->data(Qt::UserRole).toInt());
+	ui->rewardType->activated(ui->rewardType->currentIndex());
+	
+	type = ui->rewardsTable->item(ui->rewardsTable->currentRow(), 1);
+	ui->rewardList->setCurrentIndex(type->data(Qt::UserRole).toInt());
+	ui->rewardList->activated(ui->rewardList->currentIndex());
+	
+	type = ui->rewardsTable->item(ui->rewardsTable->currentRow(), 2);
+	ui->rewardAmount->setText(QString::number(type->data(Qt::UserRole).toInt()));*/
+}
+
+void RewardsDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
+{
+	if(auto * ed = qobject_cast<RewardsWidget *>(editor))
+	{
+		ed->obtainData();
+	}
+	else
+	{
+		QStyledItemDelegate::setEditorData(editor, index);
+	}
+}
+
+void RewardsDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
+{
+	if(auto * ed = qobject_cast<RewardsWidget *>(editor))
+	{
+		auto hasReward = ed->commitChanges();
+		model->setData(index, "dummy");
+		if(hasReward)
+			model->setData(index, "HAS REWARD");
+		else
+			model->setData(index, "");
+	}
+	else
+	{
+		QStyledItemDelegate::setModelData(editor, model, index);
+	}
+}
+
+RewardsPandoraDelegate::RewardsPandoraDelegate(const CMap & m, CGPandoraBox & t): map(m), pandora(t), RewardsDelegate()
+{
+}
+
+QWidget * RewardsPandoraDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
+{
+	return new RewardsWidget(map, pandora, parent);
+}
+
+RewardsSeerhutDelegate::RewardsSeerhutDelegate(const CMap & m, CGSeerHut & t): map(m), seerhut(t), RewardsDelegate()
+{
+}
+
+QWidget * RewardsSeerhutDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
+{
+	return new RewardsWidget(map, seerhut, parent);
+}

+ 98 - 0
mapeditor/inspector/rewardswidget.h

@@ -0,0 +1,98 @@
+/*
+ * rewardswidget.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
+#include "../StdInc.h"
+#include <QDialog>
+#include "../lib/mapObjects/CGPandoraBox.h"
+#include "../lib/mapObjects/CQuest.h"
+#include "../lib/mapping/CMap.h"
+
+namespace Ui {
+class RewardsWidget;
+}
+
+const std::array<std::string, 10> rewardTypes{"Experience", "Mana", "Morale", "Luck", "Resource", "Primary skill", "Secondary skill", "Artifact", "Spell", "Creature"};
+
+class RewardsWidget : public QDialog
+{
+	Q_OBJECT
+
+public:
+	enum RewardType
+	{
+		EXPERIENCE = 0, MANA, MORALE, LUCK, RESOURCE, PRIMARY_SKILL, SECONDARY_SKILL, ARTIFACT, SPELL, CREATURE
+	};
+	
+	explicit RewardsWidget(const CMap &, CGPandoraBox &, QWidget *parent = nullptr);
+	explicit RewardsWidget(const CMap &, CGSeerHut &, QWidget *parent = nullptr);
+	~RewardsWidget();
+	
+	void obtainData();
+	bool commitChanges();
+
+private slots:
+	void on_rewardType_activated(int index);
+
+	void on_rewardList_activated(int index);
+
+	void on_buttonAdd_clicked();
+
+	void on_buttonRemove_clicked();
+
+	void on_buttonClear_clicked();
+
+	void on_rewardsTable_itemSelectionChanged();
+
+private:
+	void addReward(RewardType typeId, int listId, int amount);
+	QList<QString> getListForType(RewardType typeId);
+	
+	Ui::RewardsWidget *ui;
+	CGPandoraBox * pandora;
+	CGSeerHut * seerhut;
+	const CMap & map;
+	int rewards = 0;
+};
+
+class RewardsDelegate : public QStyledItemDelegate
+{
+	Q_OBJECT
+public:
+	using QStyledItemDelegate::QStyledItemDelegate;
+	
+	void setEditorData(QWidget *editor, const QModelIndex &index) const override;
+	void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
+};
+
+class RewardsPandoraDelegate : public RewardsDelegate
+{
+	Q_OBJECT
+public:
+	RewardsPandoraDelegate(const CMap &, CGPandoraBox &);
+	
+	QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
+	
+private:
+	CGPandoraBox & pandora;
+	const CMap & map;
+};
+
+class RewardsSeerhutDelegate : public RewardsDelegate
+{
+	Q_OBJECT
+public:
+	RewardsSeerhutDelegate(const CMap &, CGSeerHut &);
+	
+	QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
+	
+private:
+	CGSeerHut & seerhut;
+	const CMap & map;
+};

+ 83 - 0
mapeditor/inspector/rewardswidget.ui

@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>RewardsWidget</class>
+ <widget class="QDialog" name="RewardsWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>645</width>
+    <height>335</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Rewards</string>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="0" column="1">
+    <widget class="QPushButton" name="buttonRemove">
+     <property name="text">
+      <string>Remove selected</string>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="4">
+    <widget class="QLineEdit" name="rewardAmount">
+     <property name="maximumSize">
+      <size>
+       <width>80</width>
+       <height>16777215</height>
+      </size>
+     </property>
+     <property name="inputMethodHints">
+      <set>Qt::ImhDigitsOnly</set>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="2">
+    <widget class="QPushButton" name="buttonClear">
+     <property name="text">
+      <string>Delete all</string>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="0">
+    <widget class="QPushButton" name="buttonAdd">
+     <property name="text">
+      <string>Add or change</string>
+     </property>
+    </widget>
+   </item>
+   <item row="3" column="0" colspan="5">
+    <widget class="QTableWidget" name="rewardsTable">
+     <property name="editTriggers">
+      <set>QAbstractItemView::NoEditTriggers</set>
+     </property>
+     <property name="selectionMode">
+      <enum>QAbstractItemView::SingleSelection</enum>
+     </property>
+     <property name="selectionBehavior">
+      <enum>QAbstractItemView::SelectRows</enum>
+     </property>
+     <property name="columnCount">
+      <number>3</number>
+     </property>
+     <attribute name="horizontalHeaderVisible">
+      <bool>false</bool>
+     </attribute>
+     <column/>
+     <column/>
+     <column/>
+    </widget>
+   </item>
+   <item row="2" column="1" colspan="3">
+    <widget class="QComboBox" name="rewardList"/>
+   </item>
+   <item row="2" column="0">
+    <widget class="QComboBox" name="rewardType"/>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 250 - 0
mapeditor/inspector/townbulidingswidget.cpp

@@ -0,0 +1,250 @@
+/*
+ * townbuildingswidget.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 "townbulidingswidget.h"
+#include "ui_townbulidingswidget.h"
+#include "../lib/CModHandler.h"
+#include "../lib/CGeneralTextHandler.h"
+
+std::string defaultBuildingIdConversion(BuildingID bId)
+{
+	switch(bId)
+	{
+		case BuildingID::DEFAULT: return "DEFAULT";
+		case BuildingID::MAGES_GUILD_1: return "MAGES_GUILD_1";
+		case BuildingID::MAGES_GUILD_2: return "MAGES_GUILD_2";
+		case BuildingID::MAGES_GUILD_3: return "MAGES_GUILD_3";
+		case BuildingID::MAGES_GUILD_4: return "MAGES_GUILD_4";
+		case BuildingID::MAGES_GUILD_5: return "MAGES_GUILD_5";
+		case BuildingID::TAVERN: return "TAVERN";
+		case BuildingID::SHIPYARD: return "SHIPYARD";
+		case BuildingID::FORT: return "FORT";
+		case BuildingID::CITADEL: return "CITADEL";
+		case BuildingID::CASTLE: return "CASTLE";
+		case BuildingID::VILLAGE_HALL: return "VILLAGE_HALL";
+		case BuildingID::TOWN_HALL: return "TOWN_HALL";
+		case BuildingID::CITY_HALL: return "CITY_HALL";
+		case BuildingID::CAPITOL: return "CAPITOL";
+		case BuildingID::MARKETPLACE: return "MARKETPLACE";
+		case BuildingID::RESOURCE_SILO: return "RESOURCE_SILO";
+		case BuildingID::BLACKSMITH: return "BLACKSMITH";
+		case BuildingID::SPECIAL_1: return "SPECIAL_1";
+		case BuildingID::SPECIAL_2: return "SPECIAL_2";
+		case BuildingID::SPECIAL_3: return "SPECIAL_3";
+		case BuildingID::SPECIAL_4: return "SPECIAL_4";
+		case BuildingID::HORDE_1: return "HORDE_1";
+		case BuildingID::HORDE_1_UPGR: return "HORDE_1_UPGR";
+		case BuildingID::HORDE_2: return "HORDE_2";
+		case BuildingID::HORDE_2_UPGR: return "HORDE_2_UPGR";
+		case BuildingID::SHIP: return "SHIP";
+		case BuildingID::GRAIL: return "GRAIL";
+		case BuildingID::EXTRA_TOWN_HALL: return "EXTRA_TOWN_HALL";
+		case BuildingID::EXTRA_CITY_HALL: return "EXTRA_CITY_HALL";
+		case BuildingID::EXTRA_CAPITOL: return "EXTRA_CAPITOL";
+		case BuildingID::DWELL_LVL_1: return "DWELL_LVL_1";
+		case BuildingID::DWELL_LVL_2: return "DWELL_LVL_2";
+		case BuildingID::DWELL_LVL_3: return "DWELL_LVL_3";
+		case BuildingID::DWELL_LVL_4: return "DWELL_LVL_4";
+		case BuildingID::DWELL_LVL_5: return "DWELL_LVL_5";
+		case BuildingID::DWELL_LVL_6: return "DWELL_LVL_6";
+		case BuildingID::DWELL_LVL_7: return "DWELL_LVL_7";
+		case BuildingID::DWELL_LVL_1_UP: return "DWELL_LVL_1_UP";
+		case BuildingID::DWELL_LVL_2_UP: return "DWELL_LVL_2_UP";
+		case BuildingID::DWELL_LVL_3_UP: return "DWELL_LVL_3_UP";
+		case BuildingID::DWELL_LVL_4_UP: return "DWELL_LVL_4_UP";
+		case BuildingID::DWELL_LVL_5_UP: return "DWELL_LVL_5_UP";
+		case BuildingID::DWELL_LVL_6_UP: return "DWELL_LVL_6_UP";
+		case BuildingID::DWELL_LVL_7_UP: return "DWELL_LVL_7_UP";
+		default:
+			return "UNKNOWN";
+	}
+}
+
+TownBulidingsWidget::TownBulidingsWidget(CGTownInstance & t, QWidget *parent) :
+	town(t),
+	QDialog(parent),
+	ui(new Ui::TownBulidingsWidget)
+{
+	ui->setupUi(this);
+	ui->treeView->setModel(&model);
+	//ui->treeView->setColumnCount(3);
+	model.setHorizontalHeaderLabels(QStringList() << QStringLiteral("Type") << QStringLiteral("Enabled") << QStringLiteral("Built"));
+	
+	//setAttribute(Qt::WA_DeleteOnClose);
+}
+
+TownBulidingsWidget::~TownBulidingsWidget()
+{
+	delete ui;
+}
+
+QStandardItem * TownBulidingsWidget::addBuilding(const CTown & ctown, int bId, std::set<si32> & remaining)
+{
+	BuildingID buildingId(bId);
+	const CBuilding * building = ctown.buildings.at(buildingId);
+	if(!building)
+	{
+		remaining.erase(bId);
+		return nullptr;
+	}
+	
+	QString name = tr(building->Name().c_str());
+	
+	if(name.isEmpty())
+		name = QString::fromStdString(defaultBuildingIdConversion(buildingId));
+	
+	QList<QStandardItem *> checks;
+	
+	checks << new QStandardItem(name);
+	checks.back()->setData(bId, Qt::UserRole);
+	
+	checks << new QStandardItem;
+	checks.back()->setCheckable(true);
+	checks.back()->setCheckState(town.forbiddenBuildings.count(buildingId) ? Qt::Unchecked : Qt::Checked);
+	checks.back()->setData(bId, Qt::UserRole);
+	
+	checks << new QStandardItem;
+	checks.back()->setCheckable(true);
+	checks.back()->setCheckState(town.builtBuildings.count(buildingId) ? Qt::Checked : Qt::Unchecked);
+	checks.back()->setData(bId, Qt::UserRole);
+	
+	if(building->getBase() == buildingId)
+	{
+		model.appendRow(checks);
+	}
+	else
+	{
+		QStandardItem * parent = nullptr;
+		std::vector<QModelIndex> stack;
+		stack.push_back(QModelIndex());
+		while(!parent && !stack.empty())
+		{
+			auto pindex = stack.back();
+			stack.pop_back();
+			for(int i = 0; i < model.rowCount(pindex); ++i)
+			{
+				QModelIndex index = model.index(i, 0, pindex);
+				if(building->upgrade == model.itemFromIndex(index)->data(Qt::UserRole).toInt())
+				{
+					parent = model.itemFromIndex(index);
+					break;
+				}
+				if(model.hasChildren(index))
+					stack.push_back(index);
+			}
+		}
+		
+		if(!parent)
+			parent = addBuilding(ctown, building->upgrade.getNum(), remaining);
+		
+		if(!parent)
+		{
+			remaining.erase(bId);
+			return nullptr;
+		}
+		
+		parent->appendRow(checks);
+	}
+	
+	remaining.erase(bId);
+	return checks.front();
+}
+
+void TownBulidingsWidget::addBuildings(const CTown & ctown)
+{
+	auto buildings = ctown.getAllBuildings();
+	while(!buildings.empty())
+	{
+		addBuilding(ctown, *buildings.begin(), buildings);
+	}
+	ui->treeView->resizeColumnToContents(0);
+	ui->treeView->resizeColumnToContents(1);
+	ui->treeView->resizeColumnToContents(2);
+}
+
+std::set<BuildingID> TownBulidingsWidget::getBuildingsFromModel(int modelColumn, Qt::CheckState checkState)
+{
+	std::set<BuildingID> result;
+	for(int i = 0; i < model.rowCount(); ++i)
+	{
+		if(auto * item = model.item(i, modelColumn))
+			if(item->checkState() == checkState)
+				result.emplace(item->data(Qt::UserRole).toInt());
+	}
+	
+	return result;
+}
+
+std::set<BuildingID> TownBulidingsWidget::getForbiddenBuildings()
+{
+	return getBuildingsFromModel(1, Qt::Unchecked);
+}
+
+std::set<BuildingID> TownBulidingsWidget::getBuiltBuildings()
+{
+	return getBuildingsFromModel(2, Qt::Checked);
+}
+
+void TownBulidingsWidget::on_treeView_expanded(const QModelIndex &index)
+{
+	ui->treeView->resizeColumnToContents(0);
+}
+
+void TownBulidingsWidget::on_treeView_collapsed(const QModelIndex &index)
+{
+	ui->treeView->resizeColumnToContents(0);
+}
+
+
+TownBuildingsDelegate::TownBuildingsDelegate(CGTownInstance & t): town(t), QStyledItemDelegate()
+{
+}
+
+QWidget * TownBuildingsDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
+{
+	return new TownBulidingsWidget(town, parent);
+}
+
+void TownBuildingsDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
+{
+	if(auto * ed = qobject_cast<TownBulidingsWidget *>(editor))
+	{
+		auto * ctown = town.town;
+		if(!ctown)
+			ctown = VLC->townh->randomTown;
+		if(!ctown)
+			throw std::runtime_error("No Town defined for type selected");
+		
+		ed->addBuildings(*ctown);
+	}
+	else
+	{
+		QStyledItemDelegate::setEditorData(editor, index);
+	}
+}
+
+void TownBuildingsDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
+{
+	if(auto * ed = qobject_cast<TownBulidingsWidget *>(editor))
+	{
+		town.forbiddenBuildings = ed->getForbiddenBuildings();
+		town.builtBuildings = ed->getBuiltBuildings();
+		
+		auto data = model->itemData(index);
+		model->setData(index, "dummy");
+		model->setItemData(index, data); //dummy change to trigger signal
+	}
+	else
+	{
+		QStyledItemDelegate::setModelData(editor, model, index);
+	}
+}
+
+

+ 63 - 0
mapeditor/inspector/townbulidingswidget.h

@@ -0,0 +1,63 @@
+/*
+ * townbuildingswidget.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
+
+#include "../StdInc.h"
+#include <QDialog>
+#include "../lib/mapObjects/CGTownInstance.h"
+
+namespace Ui {
+class TownBulidingsWidget;
+}
+
+class TownBulidingsWidget : public QDialog
+{
+	Q_OBJECT
+
+	QStandardItem * addBuilding(const CTown & ctown, int bId, std::set<si32> & remaining);
+	
+public:
+	explicit TownBulidingsWidget(CGTownInstance &, QWidget *parent = nullptr);
+	~TownBulidingsWidget();
+	
+	void addBuildings(const CTown & ctown);
+	std::set<BuildingID> getForbiddenBuildings();
+	std::set<BuildingID> getBuiltBuildings();
+
+private slots:
+	void on_treeView_expanded(const QModelIndex &index);
+
+	void on_treeView_collapsed(const QModelIndex &index);
+
+private:
+	std::set<BuildingID> getBuildingsFromModel(int modelColumn, Qt::CheckState checkState);
+	
+	Ui::TownBulidingsWidget *ui;
+	CGTownInstance & town;
+	mutable QStandardItemModel model;
+};
+
+class TownBuildingsDelegate : public QStyledItemDelegate
+{
+	Q_OBJECT
+public:
+	using QStyledItemDelegate::QStyledItemDelegate;
+	
+	TownBuildingsDelegate(CGTownInstance &);
+	
+	QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
+	void setEditorData(QWidget *editor, const QModelIndex &index) const override;
+	void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
+	
+private:
+	CGTownInstance & town;
+	//std::set<BuildingID>
+};
+

+ 49 - 0
mapeditor/inspector/townbulidingswidget.ui

@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>TownBulidingsWidget</class>
+ <widget class="QDialog" name="TownBulidingsWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>480</width>
+    <height>280</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>480</width>
+    <height>280</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Buildings</string>
+  </property>
+  <property name="modal">
+   <bool>true</bool>
+  </property>
+  <layout class="QHBoxLayout" name="horizontalLayout">
+   <item>
+    <widget class="QTreeView" name="treeView">
+     <property name="editTriggers">
+      <set>QAbstractItemView::NoEditTriggers</set>
+     </property>
+     <property name="alternatingRowColors">
+      <bool>true</bool>
+     </property>
+     <attribute name="headerCascadingSectionResizes">
+      <bool>true</bool>
+     </attribute>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 125 - 0
mapeditor/jsonutils.cpp

@@ -0,0 +1,125 @@
+/*
+ * jsonutils.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 "jsonutils.h"
+#include "../lib/filesystem/FileStream.h"
+
+static QVariantMap JsonToMap(const JsonMap & json)
+{
+	QVariantMap map;
+	for(auto & entry : json)
+	{
+		map.insert(QString::fromUtf8(entry.first.c_str()), JsonUtils::toVariant(entry.second));
+	}
+	return map;
+}
+
+static QVariantList JsonToList(const JsonVector & json)
+{
+	QVariantList list;
+	for(auto & entry : json)
+	{
+		list.push_back(JsonUtils::toVariant(entry));
+	}
+	return list;
+}
+
+static JsonVector VariantToList(QVariantList variant)
+{
+	JsonVector vector;
+	for(auto & entry : variant)
+	{
+		vector.push_back(JsonUtils::toJson(entry));
+	}
+	return vector;
+}
+
+static JsonMap VariantToMap(QVariantMap variant)
+{
+	JsonMap map;
+	for(auto & entry : variant.toStdMap())
+	{
+		map[entry.first.toUtf8().data()] = JsonUtils::toJson(entry.second);
+	}
+	return map;
+}
+
+namespace JsonUtils
+{
+
+QVariant toVariant(const JsonNode & node)
+{
+	switch(node.getType())
+	{
+		break;
+	case JsonNode::JsonType::DATA_NULL:
+		return QVariant();
+		break;
+	case JsonNode::JsonType::DATA_BOOL:
+		return QVariant(node.Bool());
+		break;
+	case JsonNode::JsonType::DATA_FLOAT:
+		return QVariant(node.Float());
+		break;
+	case JsonNode::JsonType::DATA_STRING:
+		return QVariant(QString::fromUtf8(node.String().c_str()));
+		break;
+	case JsonNode::JsonType::DATA_VECTOR:
+		return JsonToList(node.Vector());
+		break;
+	case JsonNode::JsonType::DATA_STRUCT:
+		return JsonToMap(node.Struct());
+	}
+	return QVariant();
+}
+
+QVariant JsonFromFile(QString filename)
+{
+	QFile file(filename);
+	file.open(QFile::ReadOnly);
+	auto data = file.readAll();
+
+	if(data.size() == 0)
+	{
+		logGlobal->error("Failed to open file %s", filename.toUtf8().data());
+		return QVariant();
+	}
+	else
+	{
+		JsonNode node(data.data(), data.size());
+		return toVariant(node);
+	}
+}
+
+JsonNode toJson(QVariant object)
+{
+	JsonNode ret;
+
+	if(object.canConvert<QVariantMap>())
+		ret.Struct() = VariantToMap(object.toMap());
+	else if(object.canConvert<QVariantList>())
+		ret.Vector() = VariantToList(object.toList());
+	else if(object.userType() == QMetaType::QString)
+		ret.String() = object.toString().toUtf8().data();
+	else if(object.userType() == QMetaType::Bool)
+		ret.Bool() = object.toBool();
+	else if(object.canConvert<double>())
+		ret.Float() = object.toFloat();
+
+	return ret;
+}
+
+void JsonToFile(QString filename, QVariant object)
+{
+	FileStream file(qstringToPath(filename), std::ios::out | std::ios_base::binary);
+	file << toJson(object).toJson();
+}
+
+}

+ 22 - 0
mapeditor/jsonutils.h

@@ -0,0 +1,22 @@
+/*
+ * jsonutils.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
+
+#include <QVariant>
+#include "../lib/JsonNode.h"
+
+namespace JsonUtils
+{
+QVariant toVariant(const JsonNode & node);
+QVariant JsonFromFile(QString filename);
+
+JsonNode toJson(QVariant object);
+void JsonToFile(QString filename, QVariant object);
+}

+ 36 - 0
mapeditor/launcherdirs.cpp

@@ -0,0 +1,36 @@
+/*
+ * launcherdirs.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 "launcherdirs.h"
+
+#include "../lib/VCMIDirs.h"
+
+static CLauncherDirs launcherDirsGlobal;
+
+CLauncherDirs::CLauncherDirs()
+{
+	QDir().mkdir(downloadsPath());
+	QDir().mkdir(modsPath());
+}
+
+CLauncherDirs & CLauncherDirs::get()
+{
+	return launcherDirsGlobal;
+}
+
+QString CLauncherDirs::downloadsPath()
+{
+	return pathToQString(VCMIDirs::get().userCachePath() / "downloads");
+}
+
+QString CLauncherDirs::modsPath()
+{
+	return pathToQString(VCMIDirs::get().userDataPath() / "Mods");
+}

+ 22 - 0
mapeditor/launcherdirs.h

@@ -0,0 +1,22 @@
+/*
+ * launcherdirs.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
+
+/// similar to lib/VCMIDirs, controls where all launcher-related data will be stored
+class CLauncherDirs
+{
+public:
+	CLauncherDirs();
+
+	static CLauncherDirs & get();
+
+	QString downloadsPath();
+	QString modsPath();
+};

+ 19 - 0
mapeditor/main.cpp

@@ -0,0 +1,19 @@
+/*
+ * main.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 <QApplication>
+#include "StdInc.h"
+#include "mainwindow.h"
+
+int main(int argc, char * argv[])
+{
+	QApplication vcmieditor(argc, argv);
+	MainWindow mainWindow;
+	return vcmieditor.exec();
+}

+ 1110 - 0
mapeditor/mainwindow.cpp

@@ -0,0 +1,1110 @@
+/*
+ * mainwindow.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 "mainwindow.h"
+#include "ui_mainwindow.h"
+
+#include <QFileDialog>
+#include <QFile>
+#include <QMessageBox>
+#include <QFileInfo>
+
+#include "../lib/VCMIDirs.h"
+#include "../lib/VCMI_Lib.h"
+#include "../lib/logging/CBasicLogConfigurator.h"
+#include "../lib/CConfigHandler.h"
+#include "../lib/filesystem/Filesystem.h"
+#include "../lib/GameConstants.h"
+#include "../lib/mapping/CMapService.h"
+#include "../lib/mapping/CMap.h"
+#include "../lib/mapping/CMapEditManager.h"
+#include "../lib/Terrain.h"
+#include "../lib/mapObjects/CObjectClassesHandler.h"
+#include "../lib/filesystem/CFilesystemLoader.h"
+
+#include "maphandler.h"
+#include "graphics.h"
+#include "windownewmap.h"
+#include "objectbrowser.h"
+#include "inspector/inspector.h"
+#include "mapsettings.h"
+#include "playersettings.h"
+#include "validator.h"
+
+static CBasicLogConfigurator * logConfig;
+
+QJsonValue jsonFromPixmap(const QPixmap &p)
+{
+  QBuffer buffer;
+  buffer.open(QIODevice::WriteOnly);
+  p.save(&buffer, "PNG");
+  auto const encoded = buffer.data().toBase64();
+  return {QLatin1String(encoded)};
+}
+
+QPixmap pixmapFromJson(const QJsonValue &val)
+{
+  auto const encoded = val.toString().toLatin1();
+  QPixmap p;
+  p.loadFromData(QByteArray::fromBase64(encoded), "PNG");
+  return p;
+}
+
+void init()
+{
+	loadDLLClasses();
+	logGlobal->info("Initializing VCMI_Lib");
+}
+
+void MainWindow::loadUserSettings()
+{
+	//load window settings
+	QSettings s(Ui::teamName, Ui::appName);
+
+	auto size = s.value(mainWindowSizeSetting).toSize();
+	if (size.isValid())
+	{
+		resize(size);
+	}
+	auto position = s.value(mainWindowPositionSetting).toPoint();
+	if (!position.isNull())
+	{
+		move(position);
+	}
+}
+
+void MainWindow::saveUserSettings()
+{
+	QSettings s(Ui::teamName, Ui::appName);
+	s.setValue(mainWindowSizeSetting, size());
+	s.setValue(mainWindowPositionSetting, pos());
+}
+
+MainWindow::MainWindow(QWidget *parent) :
+	QMainWindow(parent),
+	ui(new Ui::MainWindow),
+	controller(this)
+{
+	ui->setupUi(this);
+	loadUserSettings(); //For example window size
+	setTitle();
+	
+	// Set current working dir to executable folder.
+	// This is important on Mac for relative paths to work inside DMG.
+	QDir::setCurrent(QApplication::applicationDirPath());
+
+	//configure logging
+	const boost::filesystem::path logPath = VCMIDirs::get().userLogsPath() / "VCMI_Editor_log.txt";
+	console = new CConsoleHandler();
+	logConfig = new CBasicLogConfigurator(logPath, console);
+	logConfig->configureDefault();
+	logGlobal->info("The log file will be saved to %s", logPath);
+	
+	//init
+	preinitDLL(::console);
+	settings.init();
+	
+	// Initialize logging based on settings
+	logConfig->configure();
+	logGlobal->debug("settings = %s", settings.toJsonNode().toJson());
+	
+	// Some basic data validation to produce better error messages in cases of incorrect install
+	auto testFile = [](std::string filename, std::string message) -> bool
+	{
+		if (CResourceHandler::get()->existsResource(ResourceID(filename)))
+			return true;
+		
+		logGlobal->error("Error: %s was not found!", message);
+		return false;
+	};
+	
+	if(!testFile("DATA/HELP.TXT", "Heroes III data") ||
+	   !testFile("MODS/VCMI/MOD.JSON", "VCMI data"))
+	{
+		QApplication::quit();
+	}
+	
+	conf.init();
+	logGlobal->info("Loading settings");
+	
+	init();
+	
+	graphics = new Graphics(); // should be before curh->init()
+	graphics->load();//must be after Content loading but should be in main thread
+	
+	ui->mapView->setScene(controller.scene(0));
+	ui->mapView->setController(&controller);
+	ui->mapView->setOptimizationFlags(QGraphicsView::DontSavePainterState | QGraphicsView::DontAdjustForAntialiasing);
+	connect(ui->mapView, &MapView::openObjectProperties, this, &MainWindow::loadInspector);
+	
+	ui->minimapView->setScene(controller.miniScene(0));
+	ui->minimapView->setController(&controller);
+	connect(ui->minimapView, &MinimapView::cameraPositionChanged, ui->mapView, &MapView::cameraChanged);
+
+	scenePreview = new QGraphicsScene(this);
+	ui->objectPreview->setScene(scenePreview);
+
+	//loading objects
+	loadObjectsTree();
+	
+	ui->tabWidget->setCurrentIndex(0);
+	
+	for(int i = 0; i < PlayerColor::PLAYER_LIMIT.getNum(); ++i)
+	{
+		connect(getActionPlayer(PlayerColor(i)), &QAction::toggled, this, [&, i](){switchDefaultPlayer(PlayerColor(i));});
+	}
+	connect(getActionPlayer(PlayerColor::NEUTRAL), &QAction::toggled, this, [&](){switchDefaultPlayer(PlayerColor::NEUTRAL);});
+	onPlayersChanged();
+	
+	show();
+	
+	//Load map from command line
+	if(qApp->arguments().size() >= 2)
+		openMap(qApp->arguments().at(1));
+}
+
+MainWindow::~MainWindow()
+{
+	saveUserSettings(); //save window size etc.
+	delete ui;
+}
+
+bool MainWindow::getAnswerAboutUnsavedChanges()
+{
+	if(unsaved)
+	{
+		auto sure = QMessageBox::question(this, "Confirmation", "Unsaved changes will be lost, are you sure?");
+		if(sure == QMessageBox::No)
+		{
+			return false;
+		}
+	}
+	return true;
+}
+
+void MainWindow::closeEvent(QCloseEvent *event)
+{
+	if(getAnswerAboutUnsavedChanges())
+		QMainWindow::closeEvent(event);
+	else
+		event->ignore();
+}
+
+void MainWindow::setStatusMessage(const QString & status)
+{
+	statusBar()->showMessage(status);
+}
+
+void MainWindow::setTitle()
+{
+	QString title = QString("%1%2 - %3 (v%4)").arg(filename, unsaved ? "*" : "", VCMI_EDITOR_NAME, VCMI_EDITOR_VERSION);
+	setWindowTitle(title);
+}
+
+void MainWindow::mapChanged()
+{
+	unsaved = true;
+	setTitle();
+}
+
+void MainWindow::initializeMap(bool isNew)
+{
+	unsaved = isNew;
+	if(isNew)
+		filename.clear();
+	setTitle();
+
+	mapLevel = 0;
+	ui->mapView->setScene(controller.scene(mapLevel));
+	ui->minimapView->setScene(controller.miniScene(mapLevel));
+	ui->minimapView->dimensions();
+	
+	setStatusMessage(QString("Scene objects: %1").arg(ui->mapView->scene()->items().size()));
+
+	//enable settings
+	ui->actionMapSettings->setEnabled(true);
+	ui->actionPlayers_settings->setEnabled(true);
+	
+	onPlayersChanged();
+}
+
+bool MainWindow::openMap(const QString & filenameSelect)
+{
+	QFileInfo fi(filenameSelect);
+	std::string fname = fi.fileName().toStdString();
+	std::string fdir = fi.dir().path().toStdString();
+	
+	ResourceID resId("MAPEDITOR/" + fname, EResType::MAP);
+	
+	//addFilesystem takes care about memory deallocation if case of failure, no memory leak here
+	auto * mapEditorFilesystem = new CFilesystemLoader("MAPEDITOR/", fdir, 0);
+	CResourceHandler::removeFilesystem("local", "mapEditor");
+	CResourceHandler::addFilesystem("local", "mapEditor", mapEditorFilesystem);
+	
+	if(!CResourceHandler::get("mapEditor")->existsResource(resId))
+	{
+		QMessageBox::warning(this, "Failed to open map", "Cannot open map from this folder");
+		return false;
+	}
+	
+	CMapService mapService;
+	try
+	{
+		controller.setMap(mapService.loadMap(resId));
+	}
+	catch(const std::exception & e)
+	{
+		QMessageBox::critical(this, "Failed to open map", e.what());
+		return false;
+	}
+	
+	filename = filenameSelect;
+	initializeMap(controller.map()->version != EMapFormat::VCMI);
+	return true;
+}
+
+void MainWindow::on_actionOpen_triggered()
+{
+	if(!getAnswerAboutUnsavedChanges())
+		return;
+	
+	auto filenameSelect = QFileDialog::getOpenFileName(this, tr("Open map"),
+		QString::fromStdString(VCMIDirs::get().userCachePath().make_preferred().string()),
+		tr("All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m)"));
+	if(filenameSelect.isEmpty())
+		return;
+	
+	openMap(filenameSelect);
+}
+
+void MainWindow::saveMap()
+{
+	if(!controller.map())
+		return;
+
+	if(!unsaved)
+		return;
+	
+	//validate map
+	auto issues = Validator::validate(controller.map());
+	bool critical = false;
+	for(auto & issue : issues)
+		critical |= issue.critical;
+	
+	if(!issues.empty())
+	{
+		if(critical)
+			QMessageBox::warning(this, "Map validation", "Map has critical problems and most probably will not be playable. Open Validator from the Map menu to see issues found");
+		else
+			QMessageBox::information(this, "Map validation", "Map has some errors. Open Validator from the Map menu to see issues found");
+	}
+
+	CMapService mapService;
+	try
+	{
+		mapService.saveMap(controller.getMapUniquePtr(), filename.toStdString());
+	}
+	catch(const std::exception & e)
+	{
+		QMessageBox::critical(this, "Failed to save map", e.what());
+		return;
+	}
+	
+	unsaved = false;
+	setTitle();
+}
+
+void MainWindow::on_actionSave_as_triggered()
+{
+	if(!controller.map())
+		return;
+
+	auto filenameSelect = QFileDialog::getSaveFileName(this, tr("Save map"), "", tr("VCMI maps (*.vmap)"));
+
+	if(filenameSelect.isNull())
+		return;
+
+	if(filenameSelect == filename)
+		return;
+
+	filename = filenameSelect;
+
+	saveMap();
+}
+
+
+void MainWindow::on_actionNew_triggered()
+{
+	if(getAnswerAboutUnsavedChanges())
+		new WindowNewMap(this);
+}
+
+void MainWindow::on_actionSave_triggered()
+{
+	if(!controller.map())
+		return;
+
+	if(filename.isNull())
+	{
+		auto filenameSelect = QFileDialog::getSaveFileName(this, tr("Save map"), "", tr("VCMI maps (*.vmap)"));
+
+		if(filenameSelect.isNull())
+			return;
+
+		filename = filenameSelect;
+	}
+
+	saveMap();
+}
+
+void MainWindow::terrainButtonClicked(TerrainId terrain)
+{
+	controller.commitTerrainChange(mapLevel, terrain);
+}
+
+void MainWindow::roadOrRiverButtonClicked(ui8 type, bool isRoad)
+{
+	controller.commitRoadOrRiverChange(mapLevel, type, isRoad);
+}
+
+void MainWindow::addGroupIntoCatalog(const std::string & groupName, bool staticOnly)
+{
+	auto knownObjects = VLC->objtypeh->knownObjects();
+	for(auto ID : knownObjects)
+	{
+		if(catalog.count(ID))
+			continue;
+
+		addGroupIntoCatalog(groupName, true, staticOnly, ID);
+	}
+}
+
+void MainWindow::addGroupIntoCatalog(const std::string & groupName, bool useCustomName, bool staticOnly, int ID)
+{
+	QStandardItem * itemGroup = nullptr;
+	auto itms = objectsModel.findItems(QString::fromStdString(groupName));
+	if(itms.empty())
+	{
+		itemGroup = new QStandardItem(QString::fromStdString(groupName));
+		objectsModel.appendRow(itemGroup);
+	}
+	else
+	{
+		itemGroup = itms.front();
+	}
+
+	auto knownSubObjects = VLC->objtypeh->knownSubObjects(ID);
+	for(auto secondaryID : knownSubObjects)
+	{
+		auto factory = VLC->objtypeh->getHandlerFor(ID, secondaryID);
+		auto templates = factory->getTemplates();
+		bool singleTemplate = templates.size() == 1;
+		if(staticOnly && !factory->isStaticObject())
+			continue;
+
+		auto subGroupName = QString::fromStdString(factory->subTypeName);
+		auto customName = factory->getCustomName();
+		if(customName)
+			subGroupName = tr(customName->c_str());
+		
+		auto * itemType = new QStandardItem(subGroupName);
+		for(int templateId = 0; templateId < templates.size(); ++templateId)
+		{
+			auto templ = templates[templateId];
+
+			//selecting file
+			const std::string & afile = templ->editorAnimationFile.empty() ? templ->animationFile : templ->editorAnimationFile;
+
+			//creating picture
+			QPixmap preview(128, 128);
+			preview.fill(QColor(255, 255, 255));
+			QPainter painter(&preview);
+			Animation animation(afile);
+			animation.preload();
+			auto picture = animation.getImage(0);
+			if(picture && picture->width() && picture->height())
+			{
+				qreal xscale = qreal(128) / qreal(picture->width()), yscale = qreal(128) / qreal(picture->height());
+				qreal scale = std::min(xscale, yscale);
+				painter.scale(scale, scale);
+				painter.drawImage(QPoint(0, 0), *picture);
+			}
+
+			//add parameters
+			QJsonObject data{{"id", QJsonValue(ID)},
+							 {"subid", QJsonValue(secondaryID)},
+							 {"template", QJsonValue(templateId)},
+							 {"animationEditor", QString::fromStdString(templ->editorAnimationFile)},
+							 {"animation", QString::fromStdString(templ->animationFile)},
+							 {"preview", jsonFromPixmap(preview)}};
+			
+			//create object to extract name
+			std::unique_ptr<CGObjectInstance> temporaryObj(factory->create(templ));
+			QString translated = useCustomName ? tr(temporaryObj->getObjectName().c_str()) : subGroupName;
+
+			//do not have extra level
+			if(singleTemplate)
+			{
+				if(useCustomName)
+					itemType->setText(translated);
+				itemType->setIcon(QIcon(preview));
+				itemType->setData(data);
+			}
+			else
+			{
+				if(useCustomName)
+					itemType->setText(translated);
+				auto * item = new QStandardItem(QIcon(preview), QString::fromStdString(templ->stringID));
+				item->setData(data);
+				itemType->appendRow(item);
+			}
+		}
+		itemGroup->appendRow(itemType);
+		catalog.insert(ID);
+	}
+}
+
+void MainWindow::loadObjectsTree()
+{
+	try
+	{
+	ui->terrainFilterCombo->addItem("");
+	//adding terrains
+	for(auto & terrain : VLC->terrainTypeHandler->terrains())
+	{
+		QPushButton *b = new QPushButton(QString::fromStdString(terrain.name));
+		ui->terrainLayout->addWidget(b);
+		connect(b, &QPushButton::clicked, this, [this, terrain]{ terrainButtonClicked(terrain.id); });
+
+		//filter
+		ui->terrainFilterCombo->addItem(QString::fromStdString(terrain));
+	}
+	//add spacer to keep terrain button on the top
+	ui->terrainLayout->addItem(new QSpacerItem(20, 20, QSizePolicy::Minimum, QSizePolicy::Expanding));
+	//adding roads
+	for(auto & road : VLC->terrainTypeHandler->roads())
+	{
+		QPushButton *b = new QPushButton(QString::fromStdString(road.fileName));
+		ui->roadLayout->addWidget(b);
+		connect(b, &QPushButton::clicked, this, [this, road]{ roadOrRiverButtonClicked(road.id, true); });
+	}
+	//add spacer to keep terrain button on the top
+	ui->roadLayout->addItem(new QSpacerItem(20, 20, QSizePolicy::Minimum, QSizePolicy::Expanding));
+	//adding rivers
+	for(auto & river : VLC->terrainTypeHandler->rivers())
+	{
+		QPushButton *b = new QPushButton(QString::fromStdString(river.fileName));
+		ui->riverLayout->addWidget(b);
+		connect(b, &QPushButton::clicked, this, [this, river]{ roadOrRiverButtonClicked(river.id, false); });
+	}
+	//add spacer to keep terrain button on the top
+	ui->riverLayout->addItem(new QSpacerItem(20, 20, QSizePolicy::Minimum, QSizePolicy::Expanding));
+
+	if(objectBrowser)
+		throw std::runtime_error("object browser exists");
+
+	//model
+	objectsModel.setHorizontalHeaderLabels(QStringList() << tr("Type"));
+	objectBrowser = new ObjectBrowser(this);
+	objectBrowser->setSourceModel(&objectsModel);
+	objectBrowser->setDynamicSortFilter(false);
+	objectBrowser->setRecursiveFilteringEnabled(true);
+	ui->treeView->setModel(objectBrowser);
+	ui->treeView->setSelectionBehavior(QAbstractItemView::SelectItems);
+	ui->treeView->setSelectionMode(QAbstractItemView::SingleSelection);
+	connect(ui->treeView->selectionModel(), SIGNAL(currentChanged(const QModelIndex &, const QModelIndex &)), this, SLOT(treeViewSelected(const QModelIndex &, const QModelIndex &)));
+
+
+	//adding objects
+	addGroupIntoCatalog("TOWNS", false, false, Obj::TOWN);
+	addGroupIntoCatalog("TOWNS", false, false, Obj::RANDOM_TOWN);
+	addGroupIntoCatalog("TOWNS", true, false, Obj::SHIPYARD);
+	addGroupIntoCatalog("TOWNS", true, false, Obj::GARRISON);
+	addGroupIntoCatalog("TOWNS", true, false, Obj::GARRISON2);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::ALTAR_OF_SACRIFICE);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::ARENA);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::BLACK_MARKET);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::BUOY);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::CARTOGRAPHER);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::SWAN_POND);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::COVER_OF_DARKNESS);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::CORPSE);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::FAERIE_RING);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::FOUNTAIN_OF_FORTUNE);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::FOUNTAIN_OF_YOUTH);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::GARDEN_OF_REVELATION);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::HILL_FORT);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::IDOL_OF_FORTUNE);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::LIBRARY_OF_ENLIGHTENMENT);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::LIGHTHOUSE);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::SCHOOL_OF_MAGIC);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::MAGIC_SPRING);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::MAGIC_WELL);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::MERCENARY_CAMP);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::MERMAID);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::MYSTICAL_GARDEN);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::OASIS);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::LEAN_TO);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::OBELISK);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::REDWOOD_OBSERVATORY);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::PILLAR_OF_FIRE);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::STAR_AXIS);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::RALLY_FLAG);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::WATERING_HOLE);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::SCHOLAR);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::SHRINE_OF_MAGIC_INCANTATION);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::SHRINE_OF_MAGIC_GESTURE);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::SHRINE_OF_MAGIC_THOUGHT);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::SIRENS);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::STABLES);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::TAVERN);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::TEMPLE);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::DEN_OF_THIEVES);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::TRADING_POST);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::TRADING_POST_SNOW);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::LEARNING_STONE);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::TREE_OF_KNOWLEDGE);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::UNIVERSITY);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::WAGON);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::SCHOOL_OF_WAR);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::WAR_MACHINE_FACTORY);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::WARRIORS_TOMB);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::WITCH_HUT);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::FREELANCERS_GUILD);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::SANCTUARY);
+	addGroupIntoCatalog("OBJECTS", true, false, Obj::MARLETTO_TOWER);
+	addGroupIntoCatalog("HEROES", true, false, Obj::PRISON);
+	addGroupIntoCatalog("HEROES", false, false, Obj::HERO);
+	addGroupIntoCatalog("HEROES", false, false, Obj::RANDOM_HERO);
+	addGroupIntoCatalog("HEROES", false, false, Obj::HERO_PLACEHOLDER);
+	addGroupIntoCatalog("HEROES", false, false, Obj::BOAT);
+	addGroupIntoCatalog("ARTIFACTS", true, false, Obj::ARTIFACT);
+	addGroupIntoCatalog("ARTIFACTS", false, false, Obj::RANDOM_ART);
+	addGroupIntoCatalog("ARTIFACTS", false, false, Obj::RANDOM_TREASURE_ART);
+	addGroupIntoCatalog("ARTIFACTS", false, false, Obj::RANDOM_MINOR_ART);
+	addGroupIntoCatalog("ARTIFACTS", false, false, Obj::RANDOM_MAJOR_ART);
+	addGroupIntoCatalog("ARTIFACTS", false, false, Obj::RANDOM_RELIC_ART);
+	addGroupIntoCatalog("ARTIFACTS", true, false, Obj::SPELL_SCROLL);
+	addGroupIntoCatalog("ARTIFACTS", true, false, Obj::PANDORAS_BOX);
+	addGroupIntoCatalog("RESOURCES", true, false, Obj::RANDOM_RESOURCE);
+	addGroupIntoCatalog("RESOURCES", false, false, Obj::RESOURCE);
+	addGroupIntoCatalog("RESOURCES", true, false, Obj::SEA_CHEST);
+	addGroupIntoCatalog("RESOURCES", true, false, Obj::TREASURE_CHEST);
+	addGroupIntoCatalog("RESOURCES", true, false, Obj::CAMPFIRE);
+	addGroupIntoCatalog("RESOURCES", true, false, Obj::SHIPWRECK_SURVIVOR);
+	addGroupIntoCatalog("RESOURCES", true, false, Obj::FLOTSAM);
+	addGroupIntoCatalog("BANKS", true, false, Obj::CREATURE_BANK);
+	addGroupIntoCatalog("BANKS", true, false, Obj::DRAGON_UTOPIA);
+	addGroupIntoCatalog("BANKS", true, false, Obj::CRYPT);
+	addGroupIntoCatalog("BANKS", true, false, Obj::DERELICT_SHIP);
+	addGroupIntoCatalog("BANKS", true, false, Obj::PYRAMID);
+	addGroupIntoCatalog("BANKS", true, false, Obj::SHIPWRECK);
+	addGroupIntoCatalog("DWELLINGS", true, false, Obj::CREATURE_GENERATOR1);
+	addGroupIntoCatalog("DWELLINGS", true, false, Obj::CREATURE_GENERATOR2);
+	addGroupIntoCatalog("DWELLINGS", true, false, Obj::CREATURE_GENERATOR3);
+	addGroupIntoCatalog("DWELLINGS", true, false, Obj::CREATURE_GENERATOR4);
+	addGroupIntoCatalog("DWELLINGS", true, false, Obj::REFUGEE_CAMP);
+	addGroupIntoCatalog("DWELLINGS", false, false, Obj::RANDOM_DWELLING);
+	addGroupIntoCatalog("DWELLINGS", false, false, Obj::RANDOM_DWELLING_LVL);
+	addGroupIntoCatalog("DWELLINGS", false, false, Obj::RANDOM_DWELLING_FACTION);
+	addGroupIntoCatalog("GROUNDS", true, false, Obj::CURSED_GROUND1);
+	addGroupIntoCatalog("GROUNDS", true, false, Obj::MAGIC_PLAINS1);
+	addGroupIntoCatalog("GROUNDS", true, false, Obj::CLOVER_FIELD);
+	addGroupIntoCatalog("GROUNDS", true, false, Obj::CURSED_GROUND2);
+	addGroupIntoCatalog("GROUNDS", true, false, Obj::EVIL_FOG);
+	addGroupIntoCatalog("GROUNDS", true, false, Obj::FAVORABLE_WINDS);
+	addGroupIntoCatalog("GROUNDS", true, false, Obj::FIERY_FIELDS);
+	addGroupIntoCatalog("GROUNDS", true, false, Obj::HOLY_GROUNDS);
+	addGroupIntoCatalog("GROUNDS", true, false, Obj::LUCID_POOLS);
+	addGroupIntoCatalog("GROUNDS", true, false, Obj::MAGIC_CLOUDS);
+	addGroupIntoCatalog("GROUNDS", true, false, Obj::MAGIC_PLAINS2);
+	addGroupIntoCatalog("GROUNDS", true, false, Obj::ROCKLANDS);
+	addGroupIntoCatalog("GROUNDS", true, false, Obj::HOLE);
+	addGroupIntoCatalog("TELEPORTS", true, false, Obj::MONOLITH_ONE_WAY_ENTRANCE);
+	addGroupIntoCatalog("TELEPORTS", true, false, Obj::MONOLITH_ONE_WAY_EXIT);
+	addGroupIntoCatalog("TELEPORTS", true, false, Obj::MONOLITH_TWO_WAY);
+	addGroupIntoCatalog("TELEPORTS", true, false, Obj::SUBTERRANEAN_GATE);
+	addGroupIntoCatalog("TELEPORTS", true, false, Obj::WHIRLPOOL);
+	addGroupIntoCatalog("MINES", true, false, Obj::MINE);
+	addGroupIntoCatalog("MINES", false, false, Obj::ABANDONED_MINE);
+	addGroupIntoCatalog("MINES", true, false, Obj::WINDMILL);
+	addGroupIntoCatalog("MINES", true, false, Obj::WATER_WHEEL);
+	addGroupIntoCatalog("TRIGGERS", true, false, Obj::EVENT);
+	addGroupIntoCatalog("TRIGGERS", true, false, Obj::GRAIL);
+	addGroupIntoCatalog("TRIGGERS", true, false, Obj::SIGN);
+	addGroupIntoCatalog("TRIGGERS", true, false, Obj::OCEAN_BOTTLE);
+	addGroupIntoCatalog("MONSTERS", false, false, Obj::MONSTER);
+	addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER);
+	addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L1);
+	addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L2);
+	addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L3);
+	addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L4);
+	addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L5);
+	addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L6);
+	addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L7);
+	addGroupIntoCatalog("QUESTS", true, false, Obj::SEER_HUT);
+	addGroupIntoCatalog("QUESTS", true, false, Obj::BORDER_GATE);
+	addGroupIntoCatalog("QUESTS", true, false, Obj::QUEST_GUARD);
+	addGroupIntoCatalog("QUESTS", true, false, Obj::HUT_OF_MAGI);
+	addGroupIntoCatalog("QUESTS", true, false, Obj::EYE_OF_MAGI);
+	addGroupIntoCatalog("QUESTS", true, false, Obj::BORDERGUARD);
+	addGroupIntoCatalog("QUESTS", true, false, Obj::KEYMASTER);
+	addGroupIntoCatalog("wog object", true, false, Obj::WOG_OBJECT);
+	addGroupIntoCatalog("OBSTACLES", true);
+	addGroupIntoCatalog("OTHER", false);
+	}
+	catch(const std::exception & e)
+	{
+		QMessageBox::critical(this, "Mods loading problem", "Critical error during Mods loading. Disable invalid mods and restart.");
+	}
+}
+
+void MainWindow::on_actionLevel_triggered()
+{
+	if(controller.map() && controller.map()->twoLevel)
+	{
+		mapLevel = mapLevel ? 0 : 1;
+		ui->mapView->setScene(controller.scene(mapLevel));
+		ui->minimapView->setScene(controller.miniScene(mapLevel));
+		if (mapLevel == 0)
+		{
+			ui->actionLevel->setToolTip(tr("View underground"));
+		}
+		else
+		{
+			ui->actionLevel->setToolTip(tr("View surface"));
+		}
+	}
+}
+
+void MainWindow::on_actionUndo_triggered()
+{
+	QString str("Undo clicked");
+	statusBar()->showMessage(str, 1000);
+
+	if (controller.map())
+	{
+		controller.undo();
+	}
+}
+
+void MainWindow::on_actionRedo_triggered()
+{
+	QString str("Redo clicked");
+	displayStatus(str);
+
+	if (controller.map())
+	{
+		controller.redo();
+	}
+}
+
+void MainWindow::on_actionPass_triggered(bool checked)
+{
+	QString str("Passability clicked");
+	displayStatus(str);
+
+	if(controller.map())
+	{
+		controller.scene(0)->passabilityView.show(checked);
+		controller.scene(1)->passabilityView.show(checked);
+	}
+}
+
+
+void MainWindow::on_actionGrid_triggered(bool checked)
+{
+	QString str("Grid clicked");
+	displayStatus(str);
+
+	if(controller.map())
+	{
+		controller.scene(0)->gridView.show(checked);
+		controller.scene(1)->gridView.show(checked);
+	}
+}
+
+void MainWindow::changeBrushState(int idx)
+{
+
+}
+
+void MainWindow::on_toolBrush_clicked(bool checked)
+{
+	//ui->toolBrush->setChecked(false);
+	ui->toolBrush2->setChecked(false);
+	ui->toolBrush4->setChecked(false);
+	ui->toolArea->setChecked(false);
+	ui->toolLasso->setChecked(false);
+
+	if(checked)
+		ui->mapView->selectionTool = MapView::SelectionTool::Brush;
+	else
+		ui->mapView->selectionTool = MapView::SelectionTool::None;
+	
+	ui->tabWidget->setCurrentIndex(0);
+}
+
+void MainWindow::on_toolBrush2_clicked(bool checked)
+{
+	ui->toolBrush->setChecked(false);
+	//ui->toolBrush2->setChecked(false);
+	ui->toolBrush4->setChecked(false);
+	ui->toolArea->setChecked(false);
+	ui->toolLasso->setChecked(false);
+
+	if(checked)
+		ui->mapView->selectionTool = MapView::SelectionTool::Brush2;
+	else
+		ui->mapView->selectionTool = MapView::SelectionTool::None;
+	
+	ui->tabWidget->setCurrentIndex(0);
+}
+
+
+void MainWindow::on_toolBrush4_clicked(bool checked)
+{
+	ui->toolBrush->setChecked(false);
+	ui->toolBrush2->setChecked(false);
+	//ui->toolBrush4->setChecked(false);
+	ui->toolArea->setChecked(false);
+	ui->toolLasso->setChecked(false);
+
+	if(checked)
+		ui->mapView->selectionTool = MapView::SelectionTool::Brush4;
+	else
+		ui->mapView->selectionTool = MapView::SelectionTool::None;
+	
+	ui->tabWidget->setCurrentIndex(0);
+}
+
+void MainWindow::on_toolArea_clicked(bool checked)
+{
+	ui->toolBrush->setChecked(false);
+	ui->toolBrush2->setChecked(false);
+	ui->toolBrush4->setChecked(false);
+	//ui->toolArea->setChecked(false);
+	ui->toolLasso->setChecked(false);
+
+	if(checked)
+		ui->mapView->selectionTool = MapView::SelectionTool::Area;
+	else
+		ui->mapView->selectionTool = MapView::SelectionTool::None;
+	
+	ui->tabWidget->setCurrentIndex(0);
+}
+
+void MainWindow::on_actionErase_triggered()
+{
+	on_toolErase_clicked();
+}
+
+void MainWindow::on_toolErase_clicked()
+{
+	if(controller.map())
+	{
+		controller.commitObjectErase(mapLevel);
+	}
+	ui->tabWidget->setCurrentIndex(0);
+}
+
+void MainWindow::preparePreview(const QModelIndex &index, bool createNew)
+{
+	scenePreview->clear();
+
+	auto data = objectsModel.itemFromIndex(objectBrowser->mapToSource(index))->data().toJsonObject();
+
+	if(!data.empty())
+	{
+		auto preview = data["preview"];
+		if(preview != QJsonValue::Undefined)
+		{
+			QPixmap objPreview = pixmapFromJson(preview);
+			scenePreview->addPixmap(objPreview);
+
+			auto objId = data["id"].toInt();
+			auto objSubId = data["subid"].toInt();
+			auto templateId = data["template"].toInt();
+
+			if(controller.discardObject(mapLevel) || createNew)
+			{
+				auto factory = VLC->objtypeh->getHandlerFor(objId, objSubId);
+				auto templ = factory->getTemplates()[templateId];
+				controller.createObject(mapLevel, factory->create(templ));
+			}
+		}
+	}
+}
+
+
+void MainWindow::treeViewSelected(const QModelIndex & index, const QModelIndex & deselected)
+{
+	preparePreview(index, false);
+}
+
+
+void MainWindow::on_treeView_activated(const QModelIndex &index)
+{
+	ui->toolBrush->setChecked(false);
+	ui->toolBrush2->setChecked(false);
+	ui->toolBrush4->setChecked(false);
+	ui->toolArea->setChecked(false);
+	ui->toolLasso->setChecked(false);
+	ui->mapView->selectionTool = MapView::SelectionTool::None;
+
+	preparePreview(index, true);
+}
+
+
+void MainWindow::on_terrainFilterCombo_currentTextChanged(const QString &arg1)
+{
+	if(!objectBrowser)
+		return;
+
+	objectBrowser->terrain = arg1.isEmpty() ? Terrain::ANY_TERRAIN : VLC->terrainTypeHandler->getInfoByName(arg1.toStdString())->id;
+	objectBrowser->invalidate();
+	objectBrowser->sort(0);
+}
+
+
+void MainWindow::on_filter_textChanged(const QString &arg1)
+{
+	if(!objectBrowser)
+		return;
+
+	objectBrowser->filter = arg1;
+	objectBrowser->invalidate();
+	objectBrowser->sort(0);
+}
+
+
+void MainWindow::on_actionFill_triggered()
+{
+	QString str("Fill clicked");
+	displayStatus(str);
+
+	if(!controller.map())
+		return;
+
+	controller.commitObstacleFill(mapLevel);
+}
+
+void MainWindow::loadInspector(CGObjectInstance * obj, bool switchTab)
+{
+	if(switchTab)
+		ui->tabWidget->setCurrentIndex(1);
+	Inspector inspector(controller.map(), obj, ui->inspectorWidget);
+	inspector.updateProperties();
+}
+
+void MainWindow::on_inspectorWidget_itemChanged(QTableWidgetItem *item)
+{
+	if(!item->isSelected())
+		return;
+
+	int r = item->row();
+	int c = item->column();
+	if(c < 1)
+		return;
+
+	auto * tableWidget = item->tableWidget();
+
+	//get identifier
+	auto identifier = tableWidget->item(0, 1)->text();
+	CGObjectInstance * obj = data_cast<CGObjectInstance>(identifier.toLongLong());
+
+	//get parameter name
+	auto param = tableWidget->item(r, c - 1)->text();
+
+	//set parameter
+	Inspector inspector(controller.map(), obj, tableWidget);
+	inspector.setProperty(param, item->text());
+	controller.commitObjectChange(mapLevel);
+}
+
+void MainWindow::on_actionMapSettings_triggered()
+{
+	auto settingsDialog = new MapSettings(controller, this);
+	settingsDialog->setWindowModality(Qt::WindowModal);
+	settingsDialog->setModal(true);
+}
+
+
+void MainWindow::on_actionPlayers_settings_triggered()
+{
+	auto settingsDialog = new PlayerSettings(controller, this);
+	settingsDialog->setWindowModality(Qt::WindowModal);
+	settingsDialog->setModal(true);
+	connect(settingsDialog, &QDialog::finished, this, &MainWindow::onPlayersChanged);
+}
+
+QAction * MainWindow::getActionPlayer(const PlayerColor & player)
+{
+	if(player.getNum() == 0) return ui->actionPlayer_1;
+	if(player.getNum() == 1) return ui->actionPlayer_2;
+	if(player.getNum() == 2) return ui->actionPlayer_3;
+	if(player.getNum() == 3) return ui->actionPlayer_4;
+	if(player.getNum() == 4) return ui->actionPlayer_5;
+	if(player.getNum() == 5) return ui->actionPlayer_6;
+	if(player.getNum() == 6) return ui->actionPlayer_7;
+	if(player.getNum() == 7) return ui->actionPlayer_8;
+	return ui->actionNeutral;
+}
+
+void MainWindow::switchDefaultPlayer(const PlayerColor & player)
+{
+	if(controller.defaultPlayer == player)
+		return;
+	
+	ui->actionNeutral->blockSignals(true);
+	ui->actionNeutral->setChecked(PlayerColor::NEUTRAL == player);
+	ui->actionNeutral->blockSignals(false);
+	for(int i = 0; i < PlayerColor::PLAYER_LIMIT.getNum(); ++i)
+	{
+		getActionPlayer(PlayerColor(i))->blockSignals(true);
+		getActionPlayer(PlayerColor(i))->setChecked(PlayerColor(i) == player);
+		getActionPlayer(PlayerColor(i))->blockSignals(false);
+	}
+	controller.defaultPlayer = player;
+}
+
+void MainWindow::onPlayersChanged()
+{
+	if(controller.map())
+	{
+		getActionPlayer(PlayerColor::NEUTRAL)->setEnabled(true);
+		for(int i = 0; i < controller.map()->players.size(); ++i)
+			getActionPlayer(PlayerColor(i))->setEnabled(controller.map()->players.at(i).canAnyonePlay());
+		if(!getActionPlayer(controller.defaultPlayer)->isEnabled() || controller.defaultPlayer == PlayerColor::NEUTRAL)
+			switchDefaultPlayer(PlayerColor::NEUTRAL);
+	}
+	else
+	{
+		for(int i = 0; i < PlayerColor::PLAYER_LIMIT.getNum(); ++i)
+			getActionPlayer(PlayerColor(i))->setEnabled(false);
+		getActionPlayer(PlayerColor::NEUTRAL)->setEnabled(false);
+	}
+	
+}
+
+
+
+void MainWindow::enableUndo(bool enable)
+{
+	ui->actionUndo->setEnabled(enable);
+}
+
+void MainWindow::enableRedo(bool enable)
+{
+	ui->actionRedo->setEnabled(enable);
+}
+
+void MainWindow::onSelectionMade(int level, bool anythingSelected)
+{
+	if (level == mapLevel)
+	{
+		auto info = QString::asprintf("Selection on layer %d: %b", level, anythingSelected ? "true" : "false");
+		setStatusMessage(info);
+
+		ui->actionErase->setEnabled(anythingSelected);
+		ui->toolErase->setEnabled(anythingSelected);
+	}
+}
+void MainWindow::displayStatus(const QString& message, int timeout /* = 2000 */)
+{
+	statusBar()->showMessage(message, timeout);
+}
+
+void MainWindow::on_actionValidate_triggered()
+{
+	new Validator(controller.map(), this);
+}
+
+
+void MainWindow::on_actionUpdate_appearance_triggered()
+{
+	if(!controller.map())
+		return;
+	
+	if(controller.scene(mapLevel)->selectionObjectsView.getSelection().empty())
+	{
+		QMessageBox::information(this, "Update appearance", "No objects selected");
+		return;
+	}
+	
+	if(QMessageBox::Yes != QMessageBox::question(this, "Update appearance", "This operation is irreversible. Do you want to continue?"))
+		return;
+	
+	controller.scene(mapLevel)->selectionTerrainView.clear();
+	
+	int errors = 0;
+	std::set<CGObjectInstance*> staticObjects;
+	for(auto * obj : controller.scene(mapLevel)->selectionObjectsView.getSelection())
+	{
+		auto handler = VLC->objtypeh->getHandlerFor(obj->ID, obj->subID);
+		if(!controller.map()->isInTheMap(obj->visitablePos()))
+		{
+			++errors;
+			continue;
+		}
+		
+		auto * terrain = controller.map()->getTile(obj->visitablePos()).terType;
+		
+		if(handler->isStaticObject())
+		{
+			staticObjects.insert(obj);
+			if(obj->appearance->canBePlacedAt(terrain->id))
+			{
+				controller.scene(mapLevel)->selectionObjectsView.deselectObject(obj);
+				continue;
+			}
+			
+			for(auto & offset : obj->appearance->getBlockedOffsets())
+				controller.scene(mapLevel)->selectionTerrainView.select(obj->pos + offset);
+		}
+		else
+		{
+			auto app = handler->getOverride(terrain->id, obj);
+			if(!app)
+			{
+				if(obj->appearance->canBePlacedAt(terrain->id))
+					continue;
+				
+				auto templates = handler->getTemplates(terrain->id);
+				if(templates.empty())
+				{
+					++errors;
+					continue;
+				}
+				app = templates.front();
+			}
+			auto tiles = controller.mapHandler()->getTilesUnderObject(obj);
+			obj->appearance = app;
+			controller.mapHandler()->invalidate(tiles);
+			controller.mapHandler()->invalidate(obj);
+			controller.scene(mapLevel)->selectionObjectsView.deselectObject(obj);
+		}
+	}
+	controller.commitObjectChange(mapLevel);
+	controller.commitObjectErase(mapLevel);
+	controller.commitObstacleFill(mapLevel);
+	
+	
+	if(errors)
+		QMessageBox::warning(this, "Update appearance", QString("Errors occured. %1 objects were not updated").arg(errors));
+}
+
+
+void MainWindow::on_actionRecreate_obstacles_triggered()
+{
+
+}
+

+ 145 - 0
mapeditor/mainwindow.h

@@ -0,0 +1,145 @@
+#pragma once
+
+#include <QMainWindow>
+#include <QGraphicsScene>
+#include <QStandardItemModel>
+#include "mapcontroller.h"
+#include "../lib/Terrain.h"
+
+
+class CMap;
+class ObjectBrowser;
+class CGObjectInstance;
+
+namespace Ui
+{
+	class MainWindow;
+	const QString teamName = "VCMI Team";
+	const QString appName = "VCMI Map Editor";
+}
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+	const QString mainWindowSizeSetting = "MainWindow/Size";
+	const QString mainWindowPositionSetting = "MainWindow/Position";
+
+public:
+    explicit MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+	void initializeMap(bool isNew);
+
+	void saveMap();
+	bool openMap(const QString &);
+	
+	MapView * mapView();
+
+	void loadObjectsTree();
+
+	void setStatusMessage(const QString & status);
+
+	int getMapLevel() const {return mapLevel;}
+	
+	MapController controller;
+
+private slots:
+	void on_actionOpen_triggered();
+
+	void on_actionSave_as_triggered();
+
+	void on_actionNew_triggered();
+
+	void on_actionLevel_triggered();
+
+	void on_actionSave_triggered();
+
+	void on_actionErase_triggered();
+	
+	void on_actionUndo_triggered();
+
+	void on_actionRedo_triggered();
+
+	void on_actionPass_triggered(bool checked);
+
+	void on_actionGrid_triggered(bool checked);
+
+	void on_toolBrush_clicked(bool checked);
+
+	void on_toolArea_clicked(bool checked);
+
+	void terrainButtonClicked(TerrainId terrain);
+	void roadOrRiverButtonClicked(ui8 type, bool isRoad);
+
+	void on_toolErase_clicked();
+
+	void on_treeView_activated(const QModelIndex &index);
+
+	void on_terrainFilterCombo_currentTextChanged(const QString &arg1);
+
+	void on_filter_textChanged(const QString &arg1);
+
+	void on_actionFill_triggered();
+
+	void on_toolBrush2_clicked(bool checked);
+
+	void on_toolBrush4_clicked(bool checked);
+
+	void on_inspectorWidget_itemChanged(QTableWidgetItem *item);
+
+	void on_actionMapSettings_triggered();
+
+	void on_actionPlayers_settings_triggered();
+
+	void on_actionValidate_triggered();
+
+	void on_actionUpdate_appearance_triggered();
+
+	void on_actionRecreate_obstacles_triggered();
+	
+	void switchDefaultPlayer(const PlayerColor &);
+
+public slots:
+
+	void treeViewSelected(const QModelIndex &selected, const QModelIndex &deselected);
+	void loadInspector(CGObjectInstance * obj, bool switchTab);
+	void mapChanged();
+	void enableUndo(bool enable);
+	void enableRedo(bool enable);
+	void onSelectionMade(int level, bool anythingSelected);
+	void onPlayersChanged();
+
+	void displayStatus(const QString& message, int timeout = 2000);
+
+private:
+	void preparePreview(const QModelIndex &index, bool createNew);
+	void addGroupIntoCatalog(const std::string & groupName, bool staticOnly);
+	void addGroupIntoCatalog(const std::string & groupName, bool useCustomName, bool staticOnly, int ID);
+	
+	QAction * getActionPlayer(const PlayerColor &);
+
+	void changeBrushState(int idx);
+	void setTitle();
+	
+	void closeEvent(QCloseEvent *event) override;
+	
+	bool getAnswerAboutUnsavedChanges();
+
+	void loadUserSettings();
+	void saveUserSettings();
+
+private:
+    Ui::MainWindow * ui;
+	ObjectBrowser * objectBrowser = nullptr;
+	QGraphicsScene * scenePreview;
+	
+	QString filename;
+	bool unsaved = false;
+
+	QStandardItemModel objectsModel;
+
+	int mapLevel = 0;
+
+	std::set<int> catalog;
+};

+ 1120 - 0
mapeditor/mainwindow.ui

@@ -0,0 +1,1120 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>1024</width>
+    <height>768</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>VCMI Map Editor</string>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <layout class="QGridLayout" name="gridLayout">
+    <property name="leftMargin">
+     <number>2</number>
+    </property>
+    <property name="topMargin">
+     <number>2</number>
+    </property>
+    <property name="rightMargin">
+     <number>2</number>
+    </property>
+    <property name="bottomMargin">
+     <number>2</number>
+    </property>
+    <item row="0" column="0">
+     <widget class="MapView" name="mapView">
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <property name="mouseTracking">
+       <bool>true</bool>
+      </property>
+      <property name="sizeAdjustPolicy">
+       <enum>QAbstractScrollArea::AdjustToContents</enum>
+      </property>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QMenuBar" name="menubar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>1024</width>
+     <height>22</height>
+    </rect>
+   </property>
+   <widget class="QMenu" name="menuFile">
+    <property name="title">
+     <string>File</string>
+    </property>
+    <addaction name="actionNew"/>
+    <addaction name="actionOpen"/>
+    <addaction name="actionSave"/>
+    <addaction name="actionSave_as"/>
+   </widget>
+   <widget class="QMenu" name="menuMap">
+    <property name="title">
+     <string>Map</string>
+    </property>
+    <addaction name="actionMapSettings"/>
+    <addaction name="actionPlayers_settings"/>
+    <addaction name="actionValidate"/>
+    <addaction name="actionUpdate_appearance"/>
+    <addaction name="actionRecreate_obstacles"/>
+   </widget>
+   <widget class="QMenu" name="menuEdit">
+    <property name="title">
+     <string>Edit</string>
+    </property>
+    <addaction name="actionUndo"/>
+    <addaction name="actionRedo"/>
+    <addaction name="actionErase"/>
+   </widget>
+   <widget class="QMenu" name="menuView">
+    <property name="title">
+     <string>View</string>
+    </property>
+    <addaction name="actionLevel"/>
+    <addaction name="actionGrid"/>
+    <addaction name="actionPass"/>
+   </widget>
+   <widget class="QMenu" name="menuPlayer">
+    <property name="title">
+     <string>Player</string>
+    </property>
+    <addaction name="actionNeutral"/>
+    <addaction name="actionPlayer_1"/>
+    <addaction name="actionPlayer_2"/>
+    <addaction name="actionPlayer_3"/>
+    <addaction name="actionPlayer_4"/>
+    <addaction name="actionPlayer_5"/>
+    <addaction name="actionPlayer_6"/>
+    <addaction name="actionPlayer_7"/>
+    <addaction name="actionPlayer_8"/>
+   </widget>
+   <addaction name="menuFile"/>
+   <addaction name="menuEdit"/>
+   <addaction name="menuView"/>
+   <addaction name="menuMap"/>
+   <addaction name="menuPlayer"/>
+  </widget>
+  <widget class="QToolBar" name="toolBar">
+   <property name="windowTitle">
+    <string>toolBar</string>
+   </property>
+   <attribute name="toolBarArea">
+    <enum>TopToolBarArea</enum>
+   </attribute>
+   <attribute name="toolBarBreak">
+    <bool>false</bool>
+   </attribute>
+   <addaction name="actionNew"/>
+   <addaction name="actionOpen"/>
+   <addaction name="actionSave"/>
+   <addaction name="separator"/>
+   <addaction name="actionLevel"/>
+   <addaction name="actionGrid"/>
+   <addaction name="actionPass"/>
+   <addaction name="separator"/>
+   <addaction name="actionErase"/>
+   <addaction name="actionCut"/>
+   <addaction name="actionCopy"/>
+   <addaction name="actionPaste"/>
+   <addaction name="separator"/>
+   <addaction name="actionFill"/>
+  </widget>
+  <widget class="QDockWidget" name="dockWidget_2">
+   <property name="sizePolicy">
+    <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+     <horstretch>0</horstretch>
+     <verstretch>0</verstretch>
+    </sizepolicy>
+   </property>
+   <property name="minimumSize">
+    <size>
+     <width>192</width>
+     <height>214</height>
+    </size>
+   </property>
+   <property name="maximumSize">
+    <size>
+     <width>192</width>
+     <height>214</height>
+    </size>
+   </property>
+   <attribute name="dockWidgetArea">
+    <number>2</number>
+   </attribute>
+   <widget class="QWidget" name="dockWidgetContents_2">
+    <property name="maximumSize">
+     <size>
+      <width>524287</width>
+      <height>16777215</height>
+     </size>
+    </property>
+    <layout class="QVBoxLayout" name="verticalLayout_6">
+     <property name="leftMargin">
+      <number>0</number>
+     </property>
+     <property name="topMargin">
+      <number>0</number>
+     </property>
+     <property name="rightMargin">
+      <number>0</number>
+     </property>
+     <property name="bottomMargin">
+      <number>0</number>
+     </property>
+     <item>
+      <widget class="MinimapView" name="minimapView">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="minimumSize">
+        <size>
+         <width>192</width>
+         <height>192</height>
+        </size>
+       </property>
+       <property name="maximumSize">
+        <size>
+         <width>192</width>
+         <height>192</height>
+        </size>
+       </property>
+       <property name="verticalScrollBarPolicy">
+        <enum>Qt::ScrollBarAlwaysOff</enum>
+       </property>
+       <property name="horizontalScrollBarPolicy">
+        <enum>Qt::ScrollBarAlwaysOff</enum>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </widget>
+  </widget>
+  <widget class="QDockWidget" name="dockWidget_3">
+   <property name="sizePolicy">
+    <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+     <horstretch>0</horstretch>
+     <verstretch>0</verstretch>
+    </sizepolicy>
+   </property>
+   <property name="minimumSize">
+    <size>
+     <width>268</width>
+     <height>196</height>
+    </size>
+   </property>
+   <property name="maximumSize">
+    <size>
+     <width>524287</width>
+     <height>524287</height>
+    </size>
+   </property>
+   <attribute name="dockWidgetArea">
+    <number>2</number>
+   </attribute>
+   <widget class="QWidget" name="dockWidgetContents_3">
+    <property name="sizePolicy">
+     <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+      <horstretch>0</horstretch>
+      <verstretch>0</verstretch>
+     </sizepolicy>
+    </property>
+    <layout class="QVBoxLayout" name="verticalLayout_7">
+     <property name="leftMargin">
+      <number>0</number>
+     </property>
+     <property name="topMargin">
+      <number>0</number>
+     </property>
+     <property name="rightMargin">
+      <number>0</number>
+     </property>
+     <property name="bottomMargin">
+      <number>0</number>
+     </property>
+     <item>
+      <widget class="QTabWidget" name="tabWidget">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Minimum" vsizetype="Expanding">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="currentIndex">
+        <number>0</number>
+       </property>
+       <widget class="QWidget" name="tab_2">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <attribute name="title">
+         <string>Browser</string>
+        </attribute>
+        <layout class="QVBoxLayout" name="verticalLayout_2">
+         <property name="spacing">
+          <number>0</number>
+         </property>
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <property name="rightMargin">
+          <number>0</number>
+         </property>
+         <property name="bottomMargin">
+          <number>0</number>
+         </property>
+         <item>
+          <widget class="QComboBox" name="terrainFilterCombo">
+           <property name="sizePolicy">
+            <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+             <horstretch>0</horstretch>
+             <verstretch>0</verstretch>
+            </sizepolicy>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QLineEdit" name="filter"/>
+         </item>
+         <item>
+          <widget class="QTreeView" name="treeView">
+           <property name="sizePolicy">
+            <sizepolicy hsizetype="Minimum" vsizetype="Expanding">
+             <horstretch>0</horstretch>
+             <verstretch>0</verstretch>
+            </sizepolicy>
+           </property>
+           <property name="mouseTracking">
+            <bool>false</bool>
+           </property>
+           <property name="focusPolicy">
+            <enum>Qt::ClickFocus</enum>
+           </property>
+           <property name="editTriggers">
+            <set>QAbstractItemView::NoEditTriggers</set>
+           </property>
+           <property name="dragDropMode">
+            <enum>QAbstractItemView::NoDragDrop</enum>
+           </property>
+           <property name="selectionBehavior">
+            <enum>QAbstractItemView::SelectItems</enum>
+           </property>
+           <property name="iconSize">
+            <size>
+             <width>32</width>
+             <height>32</height>
+            </size>
+           </property>
+           <property name="indentation">
+            <number>12</number>
+           </property>
+           <property name="sortingEnabled">
+            <bool>true</bool>
+           </property>
+           <property name="headerHidden">
+            <bool>true</bool>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </widget>
+       <widget class="QWidget" name="tab">
+        <attribute name="title">
+         <string>Inspector</string>
+        </attribute>
+        <layout class="QVBoxLayout" name="verticalLayout_3">
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <property name="rightMargin">
+          <number>0</number>
+         </property>
+         <property name="bottomMargin">
+          <number>0</number>
+         </property>
+         <item>
+          <widget class="QTableWidget" name="inspectorWidget">
+           <property name="font">
+            <font>
+             <pointsize>10</pointsize>
+            </font>
+           </property>
+           <property name="editTriggers">
+            <set>QAbstractItemView::AnyKeyPressed|QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
+           </property>
+           <property name="selectionMode">
+            <enum>QAbstractItemView::SingleSelection</enum>
+           </property>
+           <property name="columnCount">
+            <number>2</number>
+           </property>
+           <attribute name="verticalHeaderVisible">
+            <bool>false</bool>
+           </attribute>
+           <attribute name="verticalHeaderDefaultSectionSize">
+            <number>20</number>
+           </attribute>
+           <column>
+            <property name="text">
+             <string>Property</string>
+            </property>
+           </column>
+           <column>
+            <property name="text">
+             <string>Value</string>
+            </property>
+           </column>
+          </widget>
+         </item>
+        </layout>
+       </widget>
+      </widget>
+     </item>
+    </layout>
+   </widget>
+  </widget>
+  <widget class="QStatusBar" name="statusbar"/>
+  <widget class="QDockWidget" name="dockWidget">
+   <property name="sizePolicy">
+    <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+     <horstretch>0</horstretch>
+     <verstretch>0</verstretch>
+    </sizepolicy>
+   </property>
+   <property name="minimumSize">
+    <size>
+     <width>128</width>
+     <height>496</height>
+    </size>
+   </property>
+   <attribute name="dockWidgetArea">
+    <number>1</number>
+   </attribute>
+   <widget class="QWidget" name="dockWidgetContents">
+    <property name="sizePolicy">
+     <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+      <horstretch>0</horstretch>
+      <verstretch>0</verstretch>
+     </sizepolicy>
+    </property>
+    <property name="minimumSize">
+     <size>
+      <width>0</width>
+      <height>0</height>
+     </size>
+    </property>
+    <property name="maximumSize">
+     <size>
+      <width>16777215</width>
+      <height>16777215</height>
+     </size>
+    </property>
+    <layout class="QVBoxLayout" name="verticalLayout_8">
+     <property name="leftMargin">
+      <number>0</number>
+     </property>
+     <property name="topMargin">
+      <number>0</number>
+     </property>
+     <property name="rightMargin">
+      <number>0</number>
+     </property>
+     <property name="bottomMargin">
+      <number>0</number>
+     </property>
+     <item>
+      <widget class="QGroupBox" name="groupBox">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="maximumSize">
+        <size>
+         <width>16777215</width>
+         <height>16777215</height>
+        </size>
+       </property>
+       <property name="title">
+        <string>Brush</string>
+       </property>
+       <layout class="QFormLayout" name="formLayout">
+        <item row="0" column="0">
+         <widget class="QPushButton" name="toolBrush">
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="minimumSize">
+           <size>
+            <width>40</width>
+            <height>40</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>40</width>
+            <height>40</height>
+           </size>
+          </property>
+          <property name="text">
+           <string>1</string>
+          </property>
+          <property name="checkable">
+           <bool>true</bool>
+          </property>
+          <property name="flat">
+           <bool>false</bool>
+          </property>
+         </widget>
+        </item>
+        <item row="0" column="1">
+         <widget class="QPushButton" name="toolBrush2">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="minimumSize">
+           <size>
+            <width>40</width>
+            <height>40</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>40</width>
+            <height>40</height>
+           </size>
+          </property>
+          <property name="text">
+           <string>2</string>
+          </property>
+          <property name="checkable">
+           <bool>true</bool>
+          </property>
+          <property name="flat">
+           <bool>false</bool>
+          </property>
+         </widget>
+        </item>
+        <item row="1" column="0">
+         <widget class="QPushButton" name="toolBrush4">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="minimumSize">
+           <size>
+            <width>40</width>
+            <height>40</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>40</width>
+            <height>40</height>
+           </size>
+          </property>
+          <property name="text">
+           <string>4</string>
+          </property>
+          <property name="checkable">
+           <bool>true</bool>
+          </property>
+          <property name="flat">
+           <bool>false</bool>
+          </property>
+         </widget>
+        </item>
+        <item row="1" column="1">
+         <widget class="QPushButton" name="toolArea">
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="minimumSize">
+           <size>
+            <width>40</width>
+            <height>40</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>40</width>
+            <height>40</height>
+           </size>
+          </property>
+          <property name="text">
+           <string>[]</string>
+          </property>
+          <property name="checkable">
+           <bool>true</bool>
+          </property>
+          <property name="flat">
+           <bool>false</bool>
+          </property>
+         </widget>
+        </item>
+        <item row="2" column="0">
+         <widget class="QPushButton" name="toolLasso">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="minimumSize">
+           <size>
+            <width>40</width>
+            <height>40</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>40</width>
+            <height>40</height>
+           </size>
+          </property>
+          <property name="text">
+           <string>O</string>
+          </property>
+          <property name="checkable">
+           <bool>true</bool>
+          </property>
+          <property name="flat">
+           <bool>false</bool>
+          </property>
+         </widget>
+        </item>
+        <item row="2" column="1">
+         <widget class="QPushButton" name="toolErase">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="minimumSize">
+           <size>
+            <width>40</width>
+            <height>40</height>
+           </size>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>40</width>
+            <height>40</height>
+           </size>
+          </property>
+          <property name="text">
+           <string>E</string>
+          </property>
+          <property name="checkable">
+           <bool>false</bool>
+          </property>
+          <property name="flat">
+           <bool>false</bool>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </widget>
+     </item>
+     <item>
+      <widget class="QToolBox" name="toolBox">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="maximumSize">
+        <size>
+         <width>16777215</width>
+         <height>16777215</height>
+        </size>
+       </property>
+       <property name="currentIndex">
+        <number>0</number>
+       </property>
+       <widget class="QWidget" name="terrainPage">
+        <property name="geometry">
+         <rect>
+          <x>0</x>
+          <y>0</y>
+          <width>128</width>
+          <height>271</height>
+         </rect>
+        </property>
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <attribute name="label">
+         <string>Terrains</string>
+        </attribute>
+        <layout class="QVBoxLayout" name="verticalLayout">
+         <property name="spacing">
+          <number>1</number>
+         </property>
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <property name="rightMargin">
+          <number>0</number>
+         </property>
+         <property name="bottomMargin">
+          <number>0</number>
+         </property>
+         <item>
+          <layout class="QVBoxLayout" name="terrainLayout">
+           <property name="spacing">
+            <number>1</number>
+           </property>
+          </layout>
+         </item>
+        </layout>
+       </widget>
+       <widget class="QWidget" name="roadPage">
+        <property name="geometry">
+         <rect>
+          <x>0</x>
+          <y>0</y>
+          <width>128</width>
+          <height>271</height>
+         </rect>
+        </property>
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <attribute name="label">
+         <string>Roads</string>
+        </attribute>
+        <layout class="QVBoxLayout" name="verticalLayout_4">
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <property name="rightMargin">
+          <number>0</number>
+         </property>
+         <property name="bottomMargin">
+          <number>0</number>
+         </property>
+         <item>
+          <layout class="QVBoxLayout" name="roadLayout"/>
+         </item>
+        </layout>
+       </widget>
+       <widget class="QWidget" name="riverPage">
+        <property name="geometry">
+         <rect>
+          <x>0</x>
+          <y>0</y>
+          <width>128</width>
+          <height>271</height>
+         </rect>
+        </property>
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <attribute name="label">
+         <string>Rivers</string>
+        </attribute>
+        <layout class="QVBoxLayout" name="verticalLayout_5">
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <property name="rightMargin">
+          <number>0</number>
+         </property>
+         <property name="bottomMargin">
+          <number>0</number>
+         </property>
+         <item>
+          <layout class="QVBoxLayout" name="riverLayout"/>
+         </item>
+        </layout>
+       </widget>
+      </widget>
+     </item>
+     <item>
+      <widget class="QGraphicsView" name="objectPreview">
+       <property name="minimumSize">
+        <size>
+         <width>128</width>
+         <height>128</height>
+        </size>
+       </property>
+       <property name="maximumSize">
+        <size>
+         <width>128</width>
+         <height>128</height>
+        </size>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </widget>
+  </widget>
+  <action name="actionOpen">
+   <property name="text">
+    <string>Open</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+O</string>
+   </property>
+  </action>
+  <action name="actionSave">
+   <property name="text">
+    <string>Save</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+S</string>
+   </property>
+  </action>
+  <action name="actionNew">
+   <property name="text">
+    <string>New</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+N</string>
+   </property>
+  </action>
+  <action name="actionSave_as">
+   <property name="text">
+    <string>Save as</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+Shift+S</string>
+   </property>
+  </action>
+  <action name="actionLevel">
+   <property name="text">
+    <string>U/G</string>
+   </property>
+   <property name="toolTip">
+    <string>View underground</string>
+   </property>
+   <property name="shortcut">
+    <string>U</string>
+   </property>
+  </action>
+  <action name="actionPass">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Pass</string>
+   </property>
+   <property name="shortcut">
+    <string>P</string>
+   </property>
+  </action>
+  <action name="actionCut">
+   <property name="text">
+    <string>Cut</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+X</string>
+   </property>
+  </action>
+  <action name="actionCopy">
+   <property name="text">
+    <string>Copy</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+C</string>
+   </property>
+  </action>
+  <action name="actionPaste">
+   <property name="text">
+    <string>Paste</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+V</string>
+   </property>
+  </action>
+  <action name="actionFill">
+   <property name="text">
+    <string>Fill</string>
+   </property>
+   <property name="toolTip">
+    <string>Fills the selection with obstacles</string>
+   </property>
+   <property name="shortcut">
+    <string>F</string>
+   </property>
+  </action>
+  <action name="actionGrid">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Grid</string>
+   </property>
+   <property name="shortcut">
+    <string>G</string>
+   </property>
+  </action>
+  <action name="actionMapSettings">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="text">
+    <string>General</string>
+   </property>
+   <property name="toolTip">
+    <string>Map title and description</string>
+   </property>
+  </action>
+  <action name="actionPlayers_settings">
+   <property name="text">
+    <string>Players settings</string>
+   </property>
+  </action>
+  <action name="actionUndo">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="text">
+    <string>Undo</string>
+   </property>
+   <property name="iconText">
+    <string>Undo</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+Z</string>
+   </property>
+   <property name="shortcutVisibleInContextMenu">
+    <bool>true</bool>
+   </property>
+  </action>
+  <action name="actionRedo">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="text">
+    <string>Redo</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+Y</string>
+   </property>
+   <property name="shortcutVisibleInContextMenu">
+    <bool>true</bool>
+   </property>
+  </action>
+  <action name="actionErase">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="text">
+    <string>Erase</string>
+   </property>
+   <property name="shortcut">
+    <string>Backspace, Del</string>
+   </property>
+  </action>
+  <action name="actionNeutral">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Neutral</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+0</string>
+   </property>
+  </action>
+  <action name="actionValidate">
+   <property name="text">
+    <string>Validate</string>
+   </property>
+  </action>
+  <action name="actionUpdate_appearance">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="text">
+    <string>Update appearance</string>
+   </property>
+  </action>
+  <action name="actionRecreate_obstacles">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="text">
+    <string>Recreate obstacles</string>
+   </property>
+  </action>
+  <action name="actionPlayer_1">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Player 1</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+1</string>
+   </property>
+  </action>
+  <action name="actionPlayer_2">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Player 2</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+2</string>
+   </property>
+  </action>
+  <action name="actionPlayer_3">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Player 3</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+3</string>
+   </property>
+  </action>
+  <action name="actionPlayer_4">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Player 4</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+4</string>
+   </property>
+  </action>
+  <action name="actionPlayer_5">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Player 5</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+5</string>
+   </property>
+  </action>
+  <action name="actionPlayer_6">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Player 6</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+6</string>
+   </property>
+  </action>
+  <action name="actionPlayer_7">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Player 7</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+7</string>
+   </property>
+  </action>
+  <action name="actionPlayer_8">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Player 8</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+8</string>
+   </property>
+  </action>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>MapView</class>
+   <extends>QGraphicsView</extends>
+   <header>mapview.h</header>
+  </customwidget>
+  <customwidget>
+   <class>MinimapView</class>
+   <extends>QGraphicsView</extends>
+   <header>mapview.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+ <slots>
+  <signal>enableUndo(bool)</signal>
+  <signal>enableRedo(bool)</signal>
+ </slots>
+</ui>

+ 508 - 0
mapeditor/mapcontroller.cpp

@@ -0,0 +1,508 @@
+/*
+ * mapcontroller.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 "mapcontroller.h"
+
+#include "../lib/GameConstants.h"
+#include "../lib/mapping/CMapService.h"
+#include "../lib/mapping/CMap.h"
+#include "../lib/mapping/CMapEditManager.h"
+#include "../lib/Terrain.h"
+#include "../lib/mapObjects/CObjectClassesHandler.h"
+#include "../lib/rmg/ObstaclePlacer.h"
+#include "../lib/CSkillHandler.h"
+#include "../lib/spells/CSpellHandler.h"
+#include "../lib/CHeroHandler.h"
+#include "mapview.h"
+#include "scenelayer.h"
+#include "maphandler.h"
+#include "mainwindow.h"
+#include "inspector/inspector.h"
+
+
+MapController::MapController(MainWindow * m): main(m)
+{
+	for(int i : {0, 1})
+	{
+		_scenes[i].reset(new MapScene(i));
+		_miniscenes[i].reset(new MinimapScene(i));
+	}
+	connectScenes();
+}
+
+void MapController::connectScenes()
+{
+	for (int level = 0; level <= 1; level++)
+	{
+		//selections for both layers will be handled separately
+		QObject::connect(_scenes[level].get(), &MapScene::selected, [this, level](bool anythingSelected)
+		{
+			main->onSelectionMade(level, anythingSelected);
+		});
+	}
+}
+
+MapController::~MapController()
+{
+}
+
+const std::unique_ptr<CMap> & MapController::getMapUniquePtr() const
+{
+	return _map;
+}
+
+CMap * MapController::map()
+{
+	return _map.get();
+}
+
+MapHandler * MapController::mapHandler()
+{
+	return _mapHandler.get();
+}
+
+MapScene * MapController::scene(int level)
+{
+	return _scenes[level].get();
+}
+
+MinimapScene * MapController::miniScene(int level)
+{
+	return _miniscenes[level].get();
+}
+
+void MapController::repairMap()
+{
+	//there might be extra skills, arts and spells not imported from map
+	if(VLC->skillh->getDefaultAllowed().size() > map()->allowedAbilities.size())
+	{
+		map()->allowedAbilities.resize(VLC->skillh->getDefaultAllowed().size());
+	}
+	if(VLC->arth->getDefaultAllowed().size() > map()->allowedArtifact.size())
+	{
+		map()->allowedArtifact.resize(VLC->arth->getDefaultAllowed().size());
+	}
+	if(VLC->spellh->getDefaultAllowed().size() > map()->allowedSpell.size())
+	{
+		map()->allowedSpell.resize(VLC->spellh->getDefaultAllowed().size());
+	}
+	if(VLC->heroh->getDefaultAllowed().size() > map()->allowedHeroes.size())
+	{
+		map()->allowedHeroes.resize(VLC->heroh->getDefaultAllowed().size());
+	}
+	
+	//fix owners for objects
+	for(auto obj : _map->objects)
+	{
+		//setup proper names (hero name will be fixed later
+		if(obj->ID != Obj::HERO && obj->ID != Obj::PRISON && (obj->typeName.empty() || obj->subTypeName.empty()))
+		{
+			auto handler = VLC->objtypeh->getHandlerFor(obj->ID, obj->subID);
+			obj->typeName = handler->typeName;
+			obj->subTypeName = handler->subTypeName;
+		}
+		//fix flags
+		if(obj->getOwner() == PlayerColor::UNFLAGGABLE)
+		{
+			if(dynamic_cast<CGMine*>(obj.get()) ||
+			   dynamic_cast<CGDwelling*>(obj.get()) ||
+			   dynamic_cast<CGTownInstance*>(obj.get()) ||
+			   dynamic_cast<CGGarrison*>(obj.get()) ||
+			   dynamic_cast<CGShipyard*>(obj.get()) ||
+			   dynamic_cast<CGLighthouse*>(obj.get()) ||
+			   dynamic_cast<CGHeroInstance*>(obj.get()))
+				obj->tempOwner = PlayerColor::NEUTRAL;
+		}
+		//fix hero instance
+		if(auto * nih = dynamic_cast<CGHeroInstance*>(obj.get()))
+		{
+			map()->allowedHeroes.at(nih->subID) = true;
+			auto type = VLC->heroh->objects[nih->subID];
+			assert(type->heroClass);
+			//TODO: find a way to get proper type name
+			if(obj->ID == Obj::HERO)
+				nih->typeName = "hero";
+			if(obj->ID == Obj::PRISON)
+				nih->typeName = "prison";
+			nih->subTypeName = type->heroClass->identifier;
+			
+			nih->type = type;
+			if(nih->name.empty())
+				nih->name = nih->type->name;
+			if(nih->biography.empty())
+				nih->biography = nih->type->biography;
+			
+			if(nih->ID == Obj::HERO) //not prison
+				nih->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front();
+			//fix spells
+			if(nih->spellbookContainsSpell(SpellID::PRESET))
+			{
+				nih->removeSpellFromSpellbook(SpellID::PRESET);
+			}
+			else
+			{
+				for(auto spellID : type->spells)
+					nih->addSpellToSpellbook(spellID);
+			}
+			//fix portrait
+			if(nih->portrait < 0 || nih->portrait == 255)
+				nih->portrait = type->imageIndex;
+		}
+		//fix town instance
+		if(auto * tnh = dynamic_cast<CGTownInstance*>(obj.get()))
+		{
+			if(tnh->getTown())
+			{
+				vstd::erase_if(tnh->builtBuildings, [tnh](BuildingID bid)
+				{
+					return !tnh->getTown()->buildings.count(bid);
+				});
+				vstd::erase_if(tnh->forbiddenBuildings, [tnh](BuildingID bid)
+				{
+					return !tnh->getTown()->buildings.count(bid);
+				});
+			}
+		}
+		//fix spell scrolls
+		if(auto * art = dynamic_cast<CGArtifact*>(obj.get()))
+		{
+			if(art->ID == Obj::SPELL_SCROLL && !art->storedArtifact)
+			{
+				std::vector<SpellID> out;
+				for(auto spell : VLC->spellh->objects) //spellh size appears to be greater (?)
+				{
+					//if(map->isAllowedSpell(spell->id))
+					{
+						out.push_back(spell->id);
+					}
+				}
+				auto a = CArtifactInstance::createScroll(*RandomGeneratorUtil::nextItem(out, CRandomGenerator::getDefault()));
+				art->storedArtifact = a;
+			}
+			else
+				map()->allowedArtifact.at(art->subID) = true;
+		}
+	}
+}
+
+void MapController::setMap(std::unique_ptr<CMap> cmap)
+{
+	_map = std::move(cmap);
+	
+	repairMap();
+	
+	for(int i : {0, 1})
+	{
+		_scenes[i].reset(new MapScene(i));
+		_miniscenes[i].reset(new MinimapScene(i));
+	}
+	resetMapHandler();
+	sceneForceUpdate();
+
+	connectScenes();
+
+	_map->getEditManager()->getUndoManager().setUndoCallback([this](bool allowUndo, bool allowRedo)
+		{
+			main->enableUndo(allowUndo);
+			main->enableRedo(allowRedo);
+		}
+	);
+}
+
+void MapController::sceneForceUpdate()
+{
+	_scenes[0]->updateViews();
+	_miniscenes[0]->updateViews();
+	if(_map->twoLevel)
+	{
+		_scenes[1]->updateViews();
+		_miniscenes[1]->updateViews();
+	}
+}
+
+void MapController::sceneForceUpdate(int level)
+{
+	_scenes[level]->updateViews();
+	_miniscenes[level]->updateViews();
+}
+
+void MapController::resetMapHandler()
+{
+	if(!_mapHandler)
+		_mapHandler.reset(new MapHandler());
+	_mapHandler->reset(map());
+	for(int i : {0, 1})
+	{
+		_scenes[i]->initialize(*this);
+		_miniscenes[i]->initialize(*this);
+	}
+}
+
+void MapController::commitTerrainChange(int level, const TerrainId & terrain)
+{
+	std::vector<int3> v(_scenes[level]->selectionTerrainView.selection().begin(),
+						_scenes[level]->selectionTerrainView.selection().end());
+	if(v.empty())
+		return;
+	
+	_scenes[level]->selectionTerrainView.clear();
+	_scenes[level]->selectionTerrainView.draw();
+	
+	_map->getEditManager()->getTerrainSelection().setSelection(v);
+	_map->getEditManager()->drawTerrain(terrain, &CRandomGenerator::getDefault());
+	
+	for(auto & t : v)
+		_scenes[level]->terrainView.setDirty(t);
+	_scenes[level]->terrainView.draw();
+	
+	_miniscenes[level]->updateViews();
+	main->mapChanged();
+}
+
+void MapController::commitRoadOrRiverChange(int level, ui8 type, bool isRoad)
+{
+	std::vector<int3> v(_scenes[level]->selectionTerrainView.selection().begin(),
+						_scenes[level]->selectionTerrainView.selection().end());
+	if(v.empty())
+		return;
+	
+	_scenes[level]->selectionTerrainView.clear();
+	_scenes[level]->selectionTerrainView.draw();
+	
+	_map->getEditManager()->getTerrainSelection().setSelection(v);
+	if(isRoad)
+		_map->getEditManager()->drawRoad(RoadId(type), &CRandomGenerator::getDefault());
+	else
+		_map->getEditManager()->drawRiver(RiverId(type), &CRandomGenerator::getDefault());
+	
+	for(auto & t : v)
+		_scenes[level]->terrainView.setDirty(t);
+	_scenes[level]->terrainView.draw();
+	
+	_miniscenes[level]->updateViews();
+	main->mapChanged();
+}
+
+void MapController::commitObjectErase(int level)
+{
+	auto selectedObjects = _scenes[level]->selectionObjectsView.getSelection();
+	if (selectedObjects.size() > 1)
+	{
+		//mass erase => undo in one operation
+		_map->getEditManager()->removeObjects(selectedObjects);
+	}
+	else if (selectedObjects.size() == 1)
+	{
+		_map->getEditManager()->removeObject(*selectedObjects.begin());
+	}
+	else //nothing to erase - shouldn't be here
+	{
+		return;
+	}
+
+	for (auto obj : selectedObjects)
+	{
+		//invalidate tiles under objects
+		_mapHandler->invalidate(_mapHandler->getTilesUnderObject(obj));
+	}
+
+	_scenes[level]->selectionObjectsView.clear();
+	_scenes[level]->objectsView.draw();
+	_scenes[level]->selectionObjectsView.draw();
+	_scenes[level]->passabilityView.update();
+	
+	_miniscenes[level]->updateViews();
+	main->mapChanged();
+}
+
+bool MapController::discardObject(int level) const
+{
+	_scenes[level]->selectionObjectsView.clear();
+	if(_scenes[level]->selectionObjectsView.newObject)
+	{
+		delete _scenes[level]->selectionObjectsView.newObject;
+		_scenes[level]->selectionObjectsView.newObject = nullptr;
+		_scenes[level]->selectionObjectsView.shift = QPoint(0, 0);
+		_scenes[level]->selectionObjectsView.selectionMode = SelectionObjectsLayer::NOTHING;
+		_scenes[level]->selectionObjectsView.draw();
+		return true;
+	}
+	return false;
+}
+
+void MapController::createObject(int level, CGObjectInstance * obj) const
+{
+	_scenes[level]->selectionObjectsView.newObject = obj;
+	_scenes[level]->selectionObjectsView.selectionMode = SelectionObjectsLayer::MOVEMENT;
+	_scenes[level]->selectionObjectsView.draw();
+}
+
+void MapController::commitObstacleFill(int level)
+{
+	auto selection = _scenes[level]->selectionTerrainView.selection();
+	if(selection.empty())
+		return;
+	
+	//split by zones
+	std::map<TerrainId, ObstacleProxy> terrainSelected;
+	for(auto & t : selection)
+	{
+		auto tl = _map->getTile(t);
+		if(tl.blocked || tl.visitable)
+			continue;
+		
+		terrainSelected[tl.terType->id].blockedArea.add(t);
+	}
+	
+	for(auto & sel : terrainSelected)
+	{
+		sel.second.collectPossibleObstacles(sel.first);
+		sel.second.placeObstacles(_map.get(), CRandomGenerator::getDefault());
+	}
+
+	_mapHandler->invalidateObjects();
+	
+	_scenes[level]->selectionTerrainView.clear();
+	_scenes[level]->selectionTerrainView.draw();
+	_scenes[level]->objectsView.draw();
+	_scenes[level]->passabilityView.update();
+	
+	_miniscenes[level]->updateViews();
+	main->mapChanged();
+}
+
+void MapController::commitObjectChange(int level)
+{	
+	//for( auto * o : _scenes[level]->selectionObjectsView.getSelection())
+		//_mapHandler->invalidate(o);
+	
+	_scenes[level]->objectsView.draw();
+	_scenes[level]->selectionObjectsView.draw();
+	_scenes[level]->passabilityView.update();
+	
+	_miniscenes[level]->updateViews();
+	main->mapChanged();
+}
+
+
+void MapController::commitChangeWithoutRedraw()
+{
+	//DO NOT REDRAW
+	main->mapChanged();
+}
+
+void MapController::commitObjectShift(int level)
+{
+	auto shift = _scenes[level]->selectionObjectsView.shift;
+	bool makeShift = !shift.isNull();
+	if(makeShift)
+	{
+		for(auto * obj : _scenes[level]->selectionObjectsView.getSelection())
+		{
+			int3 pos = obj->pos;
+			pos.z = level;
+			pos.x += shift.x(); pos.y += shift.y();
+			
+			auto prevPositions = _mapHandler->getTilesUnderObject(obj);
+			_map->getEditManager()->moveObject(obj, pos);
+			_mapHandler->invalidate(prevPositions);
+			_mapHandler->invalidate(obj);
+		}
+	}
+	
+	_scenes[level]->selectionObjectsView.newObject = nullptr;
+	_scenes[level]->selectionObjectsView.shift = QPoint(0, 0);
+	_scenes[level]->selectionObjectsView.selectionMode = SelectionObjectsLayer::NOTHING;
+	
+	if(makeShift)
+	{
+		_scenes[level]->objectsView.draw();
+		_scenes[level]->selectionObjectsView.draw();
+		_scenes[level]->passabilityView.update();
+		
+		_miniscenes[level]->updateViews();
+		main->mapChanged();
+	}
+}
+
+void MapController::commitObjectCreate(int level)
+{
+	auto * newObj = _scenes[level]->selectionObjectsView.newObject;
+	if(!newObj)
+		return;
+	
+	auto shift = _scenes[level]->selectionObjectsView.shift;
+	
+	int3 pos = newObj->pos;
+	pos.z = level;
+	pos.x += shift.x(); pos.y += shift.y();
+	
+	newObj->pos = pos;
+	
+	Initializer init(newObj, defaultPlayer);
+	
+	_map->getEditManager()->insertObject(newObj);
+	_mapHandler->invalidate(newObj);
+	
+	_scenes[level]->selectionObjectsView.newObject = nullptr;
+	_scenes[level]->selectionObjectsView.shift = QPoint(0, 0);
+	_scenes[level]->selectionObjectsView.selectionMode = SelectionObjectsLayer::NOTHING;
+	_scenes[level]->objectsView.draw();
+	_scenes[level]->selectionObjectsView.draw();
+	_scenes[level]->passabilityView.update();
+	
+	_miniscenes[level]->updateViews();
+	main->mapChanged();
+}
+
+bool MapController::canPlaceObject(int level, CGObjectInstance * newObj, QString & error) const
+{
+	//find all objects of such type
+	int objCounter = 0;
+	for(auto o : _map->objects)
+	{
+		if(o->ID == newObj->ID && o->subID == newObj->subID)
+		{
+			++objCounter;
+		}
+	}
+	
+	if(newObj->ID == Obj::GRAIL && objCounter >= 1) //special case for grail
+	{
+		auto typeName = QString::fromStdString(newObj->typeName);
+		auto subTypeName = QString::fromStdString(newObj->subTypeName);
+		error = QString("There can be only one grail object on the map");
+		return false; //maplimit reached
+	}
+	
+	if(defaultPlayer == PlayerColor::NEUTRAL && (newObj->ID == Obj::HERO || newObj->ID == Obj::RANDOM_HERO))
+	{
+		error = "Hero cannot be created as NEUTRAL";
+		return false;
+	}
+	
+	return true;
+}
+
+void MapController::undo()
+{
+	_map->getEditManager()->getUndoManager().undo();
+	resetMapHandler();
+	sceneForceUpdate();
+	main->mapChanged();
+}
+
+void MapController::redo()
+{
+	_map->getEditManager()->getUndoManager().redo();
+	resetMapHandler();
+	sceneForceUpdate();
+	main->mapChanged();
+}

+ 69 - 0
mapeditor/mapcontroller.h

@@ -0,0 +1,69 @@
+/*
+ * mapcontroller.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
+
+#include "maphandler.h"
+#include "mapview.h"
+#include "../lib/mapping/CMap.h"
+#include "../lib/Terrain.h"
+
+class MainWindow;
+class MapController
+{
+public:
+	MapController(MainWindow *);
+	MapController(const MapController &) = delete;
+	MapController(const MapController &&) = delete;
+	~MapController();
+	
+	void setMap(std::unique_ptr<CMap>);
+	
+	void repairMap();
+	
+	const std::unique_ptr<CMap> & getMapUniquePtr() const; //to be used for map saving
+	CMap * map();
+	MapHandler * mapHandler();
+	MapScene * scene(int level);
+	MinimapScene * miniScene(int level);
+	
+	void resetMapHandler();
+	
+	void sceneForceUpdate();
+	void sceneForceUpdate(int level);
+	
+	void commitTerrainChange(int level, const TerrainId & terrain);
+	void commitRoadOrRiverChange(int level, ui8 type, bool isRoad);
+	void commitObjectErase(const CGObjectInstance* obj);
+	void commitObjectErase(int level);
+	void commitObstacleFill(int level);
+	void commitChangeWithoutRedraw();
+	void commitObjectShift(int level);
+	void commitObjectCreate(int level);
+	void commitObjectChange(int level);
+	
+	bool discardObject(int level) const;
+	void createObject(int level, CGObjectInstance * obj) const;
+	bool canPlaceObject(int level, CGObjectInstance * obj, QString & error) const;
+
+	void undo();
+	void redo();
+	
+	PlayerColor defaultPlayer;
+	
+private:
+	std::unique_ptr<CMap> _map;
+	std::unique_ptr<MapHandler> _mapHandler;
+	MainWindow * main;
+	mutable std::array<std::unique_ptr<MapScene>, 2> _scenes;
+	mutable std::array<std::unique_ptr<MinimapScene>, 2> _miniscenes;
+
+	void connectScenes();
+};

BIN
mapeditor/mapeditor.ico


+ 1 - 0
mapeditor/mapeditor.rc

@@ -0,0 +1 @@
+IDI_ICON1   ICON  "mapeditor.ico"

+ 546 - 0
mapeditor/maphandler.cpp

@@ -0,0 +1,546 @@
+/*
+ * maphandler.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
+ *
+ */
+
+//code is copied from vcmiclient/mapHandler.cpp with minimal changes
+#include "StdInc.h"
+#include "maphandler.h"
+#include "graphics.h"
+#include "../lib/mapping/CMap.h"
+#include "../lib/mapObjects/CGHeroInstance.h"
+#include "../lib/mapObjects/CObjectClassesHandler.h"
+#include "../lib/CHeroHandler.h"
+#include "../lib/CTownHandler.h"
+#include "../lib/CModHandler.h"
+#include "../lib/mapping/CMap.h"
+#include "../lib/GameConstants.h"
+#include "../lib/JsonDetail.h"
+
+const int tileSize = 32;
+
+static bool objectBlitOrderSorter(const TileObject & a, const TileObject & b)
+{
+	return MapHandler::compareObjectBlitOrder(a.obj, b.obj);
+}
+
+int MapHandler::index(int x, int y, int z) const
+{
+	return z * (sizes.x * sizes.y) + y * sizes.x + x;
+}
+
+int MapHandler::index(const int3 & p) const
+{
+	return index(p.x, p.y, p.z);
+}
+
+MapHandler::MapHandler()
+{
+	initTerrainGraphics();
+	logGlobal->info("\tPreparing terrain, roads, rivers, borders");
+}
+
+void MapHandler::reset(const CMap * Map)
+{
+	ttiles.clear();
+	map = Map;
+	
+	//sizes of terrain
+	sizes.x = map->width;
+	sizes.y = map->height;
+	sizes.z = map->twoLevel ? 2 : 1;
+	
+	initObjectRects();
+	logGlobal->info("\tMaking object rects");
+}
+
+void MapHandler::initTerrainGraphics()
+{
+	auto loadFlipped = [](TFlippedAnimations & animation, TFlippedCache & cache, const std::map<std::string, std::string> & files)
+	{
+		for(auto & type : files)
+		{
+			animation[type.first] = make_unique<Animation>(type.second);
+			animation[type.first]->preload();
+			const size_t views = animation[type.first]->size(0);
+			cache[type.first].resize(views);
+			
+			for(int j = 0; j < views; j++)
+				cache[type.first][j] = animation[type.first]->getImage(j);
+		}
+	};
+	
+	std::map<std::string, std::string> terrainFiles;
+	std::map<std::string, std::string> roadFiles;
+	std::map<std::string, std::string> riverFiles;
+	for(const auto & terrain : VLC->terrainTypeHandler->terrains())
+	{
+		terrainFiles[terrain.name] = terrain.tilesFilename;
+	}
+	for(const auto & river : VLC->terrainTypeHandler->rivers())
+	{
+		riverFiles[river.fileName] = river.fileName;
+	}
+	for(const auto & road : VLC->terrainTypeHandler->roads())
+	{
+		roadFiles[road.fileName] = road.fileName;
+	}
+	
+	loadFlipped(terrainAnimations, terrainImages, terrainFiles);
+	loadFlipped(riverAnimations, riverImages, riverFiles);
+	loadFlipped(roadAnimations, roadImages, roadFiles);
+}
+
+void MapHandler::drawTerrainTile(QPainter & painter, int x, int y, int z)
+{
+	auto & tinfo = map->getTile(int3(x, y, z));
+	ui8 rotation = tinfo.extTileFlags % 4;
+	
+	//TODO: use ui8 instead of string key
+	auto terrainName = tinfo.terType->name;
+	
+	if(terrainImages.at(terrainName).size() <= tinfo.terView)
+		return;
+	
+	bool hflip = (rotation == 1 || rotation == 3), vflip = (rotation == 2 || rotation == 3);
+	painter.drawImage(x * tileSize, y * tileSize, terrainImages.at(terrainName)[tinfo.terView]->mirrored(hflip, vflip));
+}
+
+void MapHandler::drawRoad(QPainter & painter, int x, int y, int z)
+{
+	auto & tinfo = map->getTile(int3(x, y, z));
+	auto * tinfoUpper = map->isInTheMap(int3(x, y - 1, z)) ? &map->getTile(int3(x, y - 1, z)) : nullptr;
+	
+	if(tinfoUpper && tinfoUpper->roadType->id != Road::NO_ROAD)
+	{
+		auto roadName = tinfoUpper->roadType->fileName;
+		QRect source(0, tileSize / 2, tileSize, tileSize / 2);
+		ui8 rotation = (tinfoUpper->extTileFlags >> 4) % 4;
+		bool hflip = (rotation == 1 || rotation == 3), vflip = (rotation == 2 || rotation == 3);
+		if(roadImages.at(roadName).size() > tinfoUpper->roadDir)
+		{
+			painter.drawImage(QPoint(x * tileSize, y * tileSize), roadImages.at(roadName)[tinfoUpper->roadDir]->mirrored(hflip, vflip), source);
+		}
+	}
+	
+	if(tinfo.roadType->id != Road::NO_ROAD) //print road from this tile
+	{
+		auto roadName = tinfo.roadType->fileName;
+		QRect source(0, 0, tileSize, tileSize / 2);
+		ui8 rotation = (tinfo.extTileFlags >> 4) % 4;
+		bool hflip = (rotation == 1 || rotation == 3), vflip = (rotation == 2 || rotation == 3);
+		if(roadImages.at(roadName).size() > tinfo.roadDir)
+		{
+			painter.drawImage(QPoint(x * tileSize, y * tileSize + tileSize / 2), roadImages.at(roadName)[tinfo.roadDir]->mirrored(hflip, vflip), source);
+		}
+	}
+}
+
+void MapHandler::drawRiver(QPainter & painter, int x, int y, int z)
+{
+	auto & tinfo = map->getTile(int3(x, y, z));
+
+	if(tinfo.riverType->id == River::NO_RIVER)
+		return;
+	
+	//TODO: use ui8 instead of string key
+	auto riverName = tinfo.riverType->fileName;
+
+	if(riverImages.at(riverName).size() <= tinfo.riverDir)
+		return;
+
+	ui8 rotation = (tinfo.extTileFlags >> 2) % 4;
+	bool hflip = (rotation == 1 || rotation == 3), vflip = (rotation == 2 || rotation == 3);
+
+	painter.drawImage(x * tileSize, y * tileSize, riverImages.at(riverName)[tinfo.riverDir]->mirrored(hflip, vflip));
+}
+
+void setPlayerColor(QImage * sur, PlayerColor player)
+{
+	if(player == PlayerColor::UNFLAGGABLE)
+		return;
+	if(sur->format() == QImage::Format_Indexed8)
+	{
+		QRgb color = graphics->neutralColor;
+		if(player != PlayerColor::NEUTRAL && player < PlayerColor::PLAYER_LIMIT)
+			color = graphics->playerColors.at(player.getNum());
+
+		sur->setColor(5, color);
+	}
+	else
+		logGlobal->warn("Warning, setPlayerColor called on not 8bpp surface!");
+}
+
+void MapHandler::initObjectRects()
+{
+	ttiles.resize(sizes.x * sizes.y * sizes.z);
+	
+	//initializing objects / rects
+	for(const CGObjectInstance * elem : map->objects)
+	{
+		CGObjectInstance *obj = const_cast<CGObjectInstance *>(elem);
+		if(	!obj
+		   || (obj->ID==Obj::HERO && static_cast<const CGHeroInstance*>(obj)->inTownGarrison) //garrisoned hero
+		   || (obj->ID==Obj::BOAT && static_cast<const CGBoat*>(obj)->hero)) //boat with hero (hero graphics is used)
+		{
+			continue;
+		}
+		
+		std::shared_ptr<Animation> animation = graphics->getAnimation(obj);
+		
+		//no animation at all
+		if(!animation)
+			continue;
+		
+		//empty animation
+		if(animation->size(0) == 0)
+			continue;
+		
+		auto image = animation->getImage(0, obj->ID == Obj::HERO ? 2 : 0);
+		if(!image)
+		{
+			//workaround for prisons
+			image = animation->getImage(0, 0);
+			if(!image)
+				continue;
+		}
+			
+		
+		for(int fx=0; fx < obj->getWidth(); ++fx)
+		{
+			for(int fy=0; fy < obj->getHeight(); ++fy)
+			{
+				int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z);
+				QRect cr(image->width() - fx * tileSize - tileSize,
+						 image->height() - fy * tileSize - tileSize,
+						 image->width(),
+						 image->height());
+				
+				TileObject toAdd(obj, cr);
+				
+				if( map->isInTheMap(currTile) && // within map
+				   cr.x() + cr.width() > 0 &&           // image has data on this tile
+				   cr.y() + cr.height() > 0)
+				{
+					ttiles[index(currTile)].push_back(toAdd);
+				}
+			}
+		}
+	}
+	
+	for(auto & tt : ttiles)
+	{
+		stable_sort(tt.begin(), tt.end(), objectBlitOrderSorter);
+	}
+}
+
+bool MapHandler::compareObjectBlitOrder(const CGObjectInstance * a, const CGObjectInstance * b)
+{
+	if (!a)
+		return true;
+	if (!b)
+		return false;
+	if (a->appearance->printPriority != b->appearance->printPriority)
+		return a->appearance->printPriority > b->appearance->printPriority;
+	
+	if(a->pos.y != b->pos.y)
+		return a->pos.y < b->pos.y;
+	
+	if(b->ID == Obj::HERO && a->ID != Obj::HERO)
+		return true;
+	if(b->ID != Obj::HERO && a->ID == Obj::HERO)
+		return false;
+	
+	if(!a->isVisitable() && b->isVisitable())
+		return true;
+	if(!b->isVisitable() && a->isVisitable())
+		return false;
+	if(a->pos.x < b->pos.x)
+		return true;
+	return false;
+}
+
+TileObject::TileObject(CGObjectInstance * obj_, QRect rect_)
+: obj(obj_),
+rect(rect_)
+{
+}
+
+TileObject::~TileObject()
+{
+}
+
+std::shared_ptr<QImage> MapHandler::findFlagBitmap(const CGHeroInstance * hero, int anim, const PlayerColor color, int group) const
+{
+	if(!hero || hero->boat)
+		return std::shared_ptr<QImage>();
+	
+	return findFlagBitmapInternal(graphics->heroFlagAnimations.at(color.getNum()), anim, group, hero->moveDir, !hero->isStanding);
+}
+
+std::shared_ptr<QImage> MapHandler::findFlagBitmapInternal(std::shared_ptr<Animation> animation, int anim, int group, ui8 dir, bool moving) const
+{
+	size_t groupSize = animation->size(group);
+	if(groupSize == 0)
+		return nullptr;
+	
+	if(moving)
+		return animation->getImage(anim % groupSize, group);
+	else
+		return animation->getImage((anim / 4) % groupSize, group);
+}
+
+MapHandler::AnimBitmapHolder MapHandler::findObjectBitmap(const CGObjectInstance * obj, int anim, int group) const
+{
+	if(!obj)
+		return MapHandler::AnimBitmapHolder();
+
+	// normal object
+	std::shared_ptr<Animation> animation = graphics->getAnimation(obj);
+	size_t groupSize = animation->size(group);
+	if(groupSize == 0)
+		return MapHandler::AnimBitmapHolder();
+	
+	animation->playerColored(obj->tempOwner);
+	auto bitmap = animation->getImage(anim % groupSize, group);
+	
+	if(!bitmap)
+		return MapHandler::AnimBitmapHolder();
+
+	setPlayerColor(bitmap.get(), obj->tempOwner);
+	
+	return MapHandler::AnimBitmapHolder(bitmap);
+}
+
+std::vector<TileObject> & MapHandler::getObjects(int x, int y, int z)
+{
+	return ttiles[index(x, y, z)];
+}
+
+void MapHandler::drawObjects(QPainter & painter, int x, int y, int z)
+{
+	for(auto & object : getObjects(x, y, z))
+	{
+		const CGObjectInstance * obj = object.obj;
+		if(!obj)
+		{
+			logGlobal->error("Stray map object that isn't fading");
+			return;
+		}
+
+		uint8_t animationFrame = 0;
+
+		auto objData = findObjectBitmap(obj, animationFrame, obj->ID == Obj::HERO ? 2 : 0);
+		if(obj->ID == Obj::HERO && obj->tempOwner.isValidPlayer())
+			objData.flagBitmap = findFlagBitmap(dynamic_cast<const CGHeroInstance*>(obj), 0, obj->tempOwner, 4);
+		
+		if(objData.objBitmap)
+		{
+			auto pos = obj->getPosition();
+
+			painter.drawImage(QPoint(x * tileSize, y * tileSize), *objData.objBitmap, object.rect);
+			
+			if(objData.flagBitmap)
+			{
+				if(x == pos.x && y == pos.y)
+					painter.drawImage(QPoint((x - 2) * tileSize, (y - 1) * tileSize), *objData.flagBitmap);
+			}
+		}
+	}
+}
+
+void MapHandler::drawObject(QPainter & painter, const TileObject & object)
+{
+	const CGObjectInstance * obj = object.obj;
+	if (!obj)
+	{
+		logGlobal->error("Stray map object that isn't fading");
+		return;
+	}
+
+	uint8_t animationFrame = 0;
+
+	auto objData = findObjectBitmap(obj, animationFrame, obj->ID == Obj::HERO ? 2 : 0);
+	if(obj->ID == Obj::HERO && obj->tempOwner.isValidPlayer())
+		objData.flagBitmap = findFlagBitmap(dynamic_cast<const CGHeroInstance*>(obj), 0, obj->tempOwner, 0);
+	
+	if (objData.objBitmap)
+	{
+		auto pos = obj->getPosition();
+
+		painter.drawImage(pos.x * tileSize - object.rect.x(), pos.y * tileSize - object.rect.y(), *objData.objBitmap);
+		
+		if (objData.flagBitmap)
+		{
+			if(object.rect.x() == pos.x && object.rect.y() == pos.y)
+				painter.drawImage(pos.x * tileSize - object.rect.x(), pos.y * tileSize - object.rect.y(), *objData.flagBitmap);
+		}
+	}
+}
+
+
+void MapHandler::drawObjectAt(QPainter & painter, const CGObjectInstance * obj, int x, int y)
+{
+	if (!obj)
+	{
+		logGlobal->error("Stray map object that isn't fading");
+		return;
+	}
+
+	uint8_t animationFrame = 0;
+
+	auto objData = findObjectBitmap(obj, animationFrame, obj->ID == Obj::HERO ? 2 : 0);
+	if(obj->ID == Obj::HERO && obj->tempOwner.isValidPlayer())
+		objData.flagBitmap = findFlagBitmap(dynamic_cast<const CGHeroInstance*>(obj), 0, obj->tempOwner, 4);
+	
+	if (objData.objBitmap)
+	{
+		painter.drawImage(QPoint((x + 1) * 32 - objData.objBitmap->width(), (y + 1) * 32 - objData.objBitmap->height()), *objData.objBitmap);
+		
+		if (objData.flagBitmap)
+			painter.drawImage(QPoint((x + 1) * 32 - objData.objBitmap->width(), (y + 1) * 32 - objData.objBitmap->height()), *objData.flagBitmap);
+	}
+}
+
+QRgb MapHandler::getTileColor(int x, int y, int z)
+{
+	// if object at tile is owned - it will be colored as its owner
+	for(auto & object : getObjects(x, y, z))
+	{
+		if(!object.obj->getBlockedPos().count(int3(x, y, z)))
+			continue;
+		
+		PlayerColor player = object.obj->getOwner();
+		if(player == PlayerColor::NEUTRAL)
+			return graphics->neutralColor;
+		else
+			if (player < PlayerColor::PLAYER_LIMIT)
+				return graphics->playerColors[player.getNum()];
+	}
+	
+	// else - use terrain color (blocked version or normal)
+	
+	auto & tile = map->getTile(int3(x, y, z));
+	
+	auto color = tile.terType->minimapUnblocked;
+	if (tile.blocked && (!tile.visitable))
+		color = tile.terType->minimapBlocked;
+	
+	return qRgb(color[0], color[1], color[2]);
+}
+
+void MapHandler::drawMinimapTile(QPainter & painter, int x, int y, int z)
+{
+	painter.setPen(getTileColor(x, y, z));
+	painter.drawPoint(x, y);
+}
+
+void MapHandler::invalidate(int x, int y, int z)
+{
+	auto & objects = getObjects(x, y, z);
+	
+	for(auto obj = objects.begin(); obj != objects.end();)
+	{
+		//object was removed
+		if(std::find(map->objects.begin(), map->objects.end(), obj->obj) == map->objects.end())
+		{
+			obj = objects.erase(obj);
+			continue;
+		}
+			
+		//object was moved
+		auto & pos = obj->obj->pos;
+		if(pos.z != z || pos.x < x || pos.y < y || pos.x - obj->obj->getWidth() >= x || pos.y - obj->obj->getHeight() >= y)
+		{
+			obj = objects.erase(obj);
+			continue;
+		}
+		
+		++obj;
+	}
+	
+	stable_sort(objects.begin(), objects.end(), objectBlitOrderSorter);
+}
+
+void MapHandler::invalidate(CGObjectInstance * obj)
+{
+	std::shared_ptr<Animation> animation = graphics->getAnimation(obj);
+		
+	//no animation at all or empty animation
+	if(!animation || animation->size(0) == 0)
+		return;
+		
+	auto image = animation->getImage(0, obj->ID == Obj::HERO ? 2 : 0);
+	if(!image)
+		return;
+	
+	for(int fx=0; fx < obj->getWidth(); ++fx)
+	{
+		for(int fy=0; fy < obj->getHeight(); ++fy)
+		{
+			//object presented on the tile
+			int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z);
+			QRect cr(image->width() - fx * tileSize - tileSize, image->height() - fy * tileSize - tileSize, image->width(), image->height());
+			
+			if( map->isInTheMap(currTile) && // within map
+			   cr.x() + cr.width() > 0 &&           // image has data on this tile
+			   cr.y() + cr.height() > 0)
+			{
+				auto & objects = ttiles[index(currTile)];
+				bool found = false;
+				for(auto & o : objects)
+				{
+					if(o.obj == obj)
+					{
+						o.rect = cr;
+						found = true;
+						break;
+					}
+				}
+				if(!found)
+					objects.emplace_back(obj, cr);
+				
+				stable_sort(objects.begin(), objects.end(), objectBlitOrderSorter);
+			}
+		}
+	}
+}
+
+std::vector<int3> MapHandler::getTilesUnderObject(CGObjectInstance * obj) const
+{
+	std::vector<int3> result;
+	for(int fx=0; fx < obj->getWidth(); ++fx)
+	{
+		for(int fy=0; fy < obj->getHeight(); ++fy)
+		{
+			//object presented on the tile
+			int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z);
+			if(map->isInTheMap(currTile)) // within map
+			{
+				result.push_back(currTile);
+			}
+		}
+	}
+	return result;
+}
+
+void MapHandler::invalidateObjects()
+{
+	for(auto obj : map->objects)
+	{
+		invalidate(obj);
+	}
+}
+
+void MapHandler::invalidate(const std::vector<int3> & tiles)
+{
+	for(auto & currTile : tiles)
+	{
+		invalidate(currTile.x, currTile.y, currTile.z);
+	}
+}

+ 115 - 0
mapeditor/maphandler.h

@@ -0,0 +1,115 @@
+/*
+ * maphandler.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
+//code is copied from vcmiclient/mapHandler.h with minimal changes
+
+#include "StdInc.h"
+#include "../lib/mapping/CMap.h"
+#include "Animation.h"
+
+#include <QImage>
+#include <QPixmap>
+#include <QRect>
+
+class CGObjectInstance;
+class CGBoat;
+class PlayerColor;
+
+struct TileObject
+{
+	CGObjectInstance *obj;
+	QRect rect;
+	
+	TileObject(CGObjectInstance *obj_, QRect rect_);
+	~TileObject();
+};
+
+using TileObjects = std::vector<TileObject>; //pointers to objects being on this tile with rects to be easier to blit this tile on screen
+
+class MapHandler
+{
+public:
+	struct AnimBitmapHolder
+	{
+		std::shared_ptr<QImage> objBitmap; // main object bitmap
+		std::shared_ptr<QImage> flagBitmap; // flag bitmap for the object (probably only for heroes and boats with heroes)
+		
+		AnimBitmapHolder(std::shared_ptr<QImage> objBitmap_ = nullptr, std::shared_ptr<QImage> flagBitmap_ = nullptr)
+		: objBitmap(objBitmap_),
+		flagBitmap(flagBitmap_)
+		{}
+	};
+	
+private:
+	
+	int index(int x, int y, int z) const;
+	int index(const int3 &) const;
+		
+	std::shared_ptr<QImage> findFlagBitmapInternal(std::shared_ptr<Animation> animation, int anim, int group, ui8 dir, bool moving) const;
+	std::shared_ptr<QImage> findFlagBitmap(const CGHeroInstance * obj, int anim, const PlayerColor color, int group) const;
+	AnimBitmapHolder findObjectBitmap(const CGObjectInstance * obj, int anim, int group = 0) const;
+	
+	//FIXME: unique_ptr should be enough, but fails to compile in MSVS 2013
+	typedef std::map<std::string, std::shared_ptr<Animation>> TFlippedAnimations; //[type, rotation]
+	typedef std::map<std::string, std::vector<std::shared_ptr<QImage>>> TFlippedCache;//[type, view type, rotation]
+	
+	TFlippedAnimations terrainAnimations;//[terrain type, rotation]
+	TFlippedCache terrainImages;//[terrain type, view type, rotation]
+	
+	TFlippedAnimations roadAnimations;//[road type, rotation]
+	TFlippedCache roadImages;//[road type, view type, rotation]
+	
+	TFlippedAnimations riverAnimations;//[river type, rotation]
+	TFlippedCache riverImages;//[river type, view type, rotation]
+	
+	std::vector<TileObjects> ttiles; //informations about map tiles
+	int3 sizes; //map size (x = width, y = height, z = number of levels)
+	const CMap * map;
+		
+	enum class EMapCacheType : char
+	{
+		TERRAIN, OBJECTS, ROADS, RIVERS, FOW, HEROES, HERO_FLAGS, FRAME, AFTER_LAST
+	};
+	
+	void initObjectRects();
+	void initTerrainGraphics();
+	QRgb getTileColor(int x, int y, int z);
+		
+public:
+	MapHandler();
+	~MapHandler() = default;
+	
+	void reset(const CMap * Map);
+
+	void updateWater();
+	
+	void drawTerrainTile(QPainter & painter, int x, int y, int z);
+	/// draws a river segment on current tile
+	void drawRiver(QPainter & painter, int x, int y, int z);
+	/// draws a road segment on current tile
+	void drawRoad(QPainter & painter, int x, int y, int z);
+	
+	void invalidate(int x, int y, int z); //invalidates all objects in particular tile
+	void invalidate(CGObjectInstance *); //invalidates object rects
+	void invalidate(const std::vector<int3> &); //invalidates all tiles
+	void invalidateObjects(); //invalidates all objects on the map
+	std::vector<int3> getTilesUnderObject(CGObjectInstance *) const;
+	
+	/// draws all objects on current tile (higher-level logic, unlike other draw*** methods)
+	void drawObjects(QPainter & painter, int x, int y, int z);
+	void drawObject(QPainter & painter, const TileObject & object);
+	void drawObjectAt(QPainter & painter, const CGObjectInstance * object, int x, int y);
+	std::vector<TileObject> & getObjects(int x, int y, int z);
+	
+	void drawMinimapTile(QPainter & painter, int x, int y, int z);
+
+	static bool compareObjectBlitOrder(const CGObjectInstance * a, const CGObjectInstance * b);
+};

+ 112 - 0
mapeditor/mapsettings.cpp

@@ -0,0 +1,112 @@
+/*
+ * mapsettings.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 "mapsettings.h"
+#include "ui_mapsettings.h"
+#include "mainwindow.h"
+
+#include "../lib/CSkillHandler.h"
+#include "../lib/spells/CSpellHandler.h"
+#include "../lib/CArtHandler.h"
+#include "../lib/CHeroHandler.h"
+
+MapSettings::MapSettings(MapController & ctrl, QWidget *parent) :
+	QDialog(parent),
+	ui(new Ui::MapSettings),
+	controller(ctrl)
+{
+	ui->setupUi(this);
+
+	assert(controller.map());
+
+	ui->mapNameEdit->setText(tr(controller.map()->name.c_str()));
+	ui->mapDescriptionEdit->setPlainText(tr(controller.map()->description.c_str()));
+	
+	show();
+	
+	
+	for(int i = 0; i < controller.map()->allowedAbilities.size(); ++i)
+	{
+		auto * item = new QListWidgetItem(QString::fromStdString(VLC->skillh->objects[i]->getName()));
+		item->setData(Qt::UserRole, QVariant::fromValue(i));
+		item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
+		item->setCheckState(controller.map()->allowedAbilities[i] ? Qt::Checked : Qt::Unchecked);
+		ui->listAbilities->addItem(item);
+	}
+	for(int i = 0; i < controller.map()->allowedSpell.size(); ++i)
+	{
+		auto * item = new QListWidgetItem(QString::fromStdString(VLC->spellh->objects[i]->getName()));
+		item->setData(Qt::UserRole, QVariant::fromValue(i));
+		item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
+		item->setCheckState(controller.map()->allowedSpell[i] ? Qt::Checked : Qt::Unchecked);
+		ui->listSpells->addItem(item);
+	}
+	for(int i = 0; i < controller.map()->allowedArtifact.size(); ++i)
+	{
+		auto * item = new QListWidgetItem(QString::fromStdString(VLC->arth->objects[i]->getName()));
+		item->setData(Qt::UserRole, QVariant::fromValue(i));
+		item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
+		item->setCheckState(controller.map()->allowedArtifact[i] ? Qt::Checked : Qt::Unchecked);
+		ui->listArts->addItem(item);
+	}
+	for(int i = 0; i < controller.map()->allowedHeroes.size(); ++i)
+	{
+		auto * item = new QListWidgetItem(QString::fromStdString(VLC->heroh->objects[i]->getName()));
+		item->setData(Qt::UserRole, QVariant::fromValue(i));
+		item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
+		item->setCheckState(controller.map()->allowedHeroes[i] ? Qt::Checked : Qt::Unchecked);
+		ui->listHeroes->addItem(item);
+	}
+
+	//ui8 difficulty;
+	//ui8 levelLimit;
+
+	//std::string victoryMessage;
+	//std::string defeatMessage;
+	//ui16 victoryIconIndex;
+	//ui16 defeatIconIndex;
+
+	//std::vector<PlayerInfo> players; /// The default size of the vector is PlayerColor::PLAYER_LIMIT.
+}
+
+MapSettings::~MapSettings()
+{
+	delete ui;
+}
+
+void MapSettings::on_pushButton_clicked()
+{
+	controller.map()->name = ui->mapNameEdit->text().toStdString();
+	controller.map()->description = ui->mapDescriptionEdit->toPlainText().toStdString();
+	controller.commitChangeWithoutRedraw();
+	
+	for(int i = 0; i < controller.map()->allowedAbilities.size(); ++i)
+	{
+		auto * item = ui->listAbilities->item(i);
+		controller.map()->allowedAbilities[i] = item->checkState() == Qt::Checked;
+	}
+	for(int i = 0; i < controller.map()->allowedSpell.size(); ++i)
+	{
+		auto * item = ui->listSpells->item(i);
+		controller.map()->allowedSpell[i] = item->checkState() == Qt::Checked;
+	}
+	for(int i = 0; i < controller.map()->allowedArtifact.size(); ++i)
+	{
+		auto * item = ui->listArts->item(i);
+		controller.map()->allowedArtifact[i] = item->checkState() == Qt::Checked;
+	}
+	for(int i = 0; i < controller.map()->allowedHeroes.size(); ++i)
+	{
+		auto * item = ui->listHeroes->item(i);
+		controller.map()->allowedHeroes[i] = item->checkState() == Qt::Checked;
+	}
+	
+	close();
+}

+ 34 - 0
mapeditor/mapsettings.h

@@ -0,0 +1,34 @@
+/*
+ * mapsettings.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
+
+#include <QDialog>
+#include "mapcontroller.h"
+
+namespace Ui {
+class MapSettings;
+}
+
+class MapSettings : public QDialog
+{
+	Q_OBJECT
+
+public:
+	explicit MapSettings(MapController & controller, QWidget *parent = nullptr);
+	~MapSettings();
+
+private slots:
+	void on_pushButton_clicked();
+
+private:
+	Ui::MapSettings *ui;
+	MapController & controller;
+};

+ 175 - 0
mapeditor/mapsettings.ui

@@ -0,0 +1,175 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MapSettings</class>
+ <widget class="QDialog" name="MapSettings">
+  <property name="windowModality">
+   <enum>Qt::ApplicationModal</enum>
+  </property>
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>454</width>
+    <height>480</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="windowTitle">
+   <string>Map settings</string>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="0" column="0" colspan="2">
+    <widget class="QTabWidget" name="tabWidget">
+     <property name="currentIndex">
+      <number>4</number>
+     </property>
+     <widget class="QWidget" name="tab">
+      <attribute name="title">
+       <string>General</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout">
+       <item>
+        <widget class="QLabel" name="label">
+         <property name="text">
+          <string>Map name</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLineEdit" name="mapNameEdit"/>
+       </item>
+       <item>
+        <widget class="QLabel" name="label_2">
+         <property name="text">
+          <string>Map description</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPlainTextEdit" name="mapDescriptionEdit"/>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tab_2">
+      <attribute name="title">
+       <string>Abilities</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_2">
+       <item>
+        <widget class="QListWidget" name="listAbilities">
+         <property name="horizontalScrollMode">
+          <enum>QAbstractItemView::ScrollPerItem</enum>
+         </property>
+         <property name="isWrapping" stdset="0">
+          <bool>true</bool>
+         </property>
+         <property name="resizeMode">
+          <enum>QListView::Adjust</enum>
+         </property>
+         <property name="layoutMode">
+          <enum>QListView::Batched</enum>
+         </property>
+         <property name="batchSize">
+          <number>30</number>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tab_3">
+      <attribute name="title">
+       <string>Spells</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_3">
+       <item>
+        <widget class="QListWidget" name="listSpells">
+         <property name="editTriggers">
+          <set>QAbstractItemView::NoEditTriggers</set>
+         </property>
+         <property name="horizontalScrollMode">
+          <enum>QAbstractItemView::ScrollPerItem</enum>
+         </property>
+         <property name="isWrapping" stdset="0">
+          <bool>true</bool>
+         </property>
+         <property name="layoutMode">
+          <enum>QListView::Batched</enum>
+         </property>
+         <property name="batchSize">
+          <number>30</number>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tab_4">
+      <attribute name="title">
+       <string>Artifacts</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_4">
+       <item>
+        <widget class="QListWidget" name="listArts">
+         <property name="editTriggers">
+          <set>QAbstractItemView::NoEditTriggers</set>
+         </property>
+         <property name="horizontalScrollMode">
+          <enum>QAbstractItemView::ScrollPerItem</enum>
+         </property>
+         <property name="isWrapping" stdset="0">
+          <bool>true</bool>
+         </property>
+         <property name="layoutMode">
+          <enum>QListView::Batched</enum>
+         </property>
+         <property name="batchSize">
+          <number>30</number>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tab_5">
+      <attribute name="title">
+       <string>Heroes</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_5">
+       <item>
+        <widget class="QListWidget" name="listHeroes">
+         <property name="editTriggers">
+          <set>QAbstractItemView::NoEditTriggers</set>
+         </property>
+         <property name="horizontalScrollMode">
+          <enum>QAbstractItemView::ScrollPerItem</enum>
+         </property>
+         <property name="isWrapping" stdset="0">
+          <bool>true</bool>
+         </property>
+         <property name="layoutMode">
+          <enum>QListView::Batched</enum>
+         </property>
+         <property name="batchSize">
+          <number>30</number>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+   <item row="1" column="1">
+    <widget class="QPushButton" name="pushButton">
+     <property name="text">
+      <string>Ok</string>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 459 - 0
mapeditor/mapview.cpp

@@ -0,0 +1,459 @@
+/*
+ * mapview.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 "mapview.h"
+#include "mainwindow.h"
+#include <QGraphicsSceneMouseEvent>
+#include "mapcontroller.h"
+
+MinimapView::MinimapView(QWidget * parent):
+	QGraphicsView(parent)
+{
+	// Disable scrollbars
+	setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+}
+
+void MinimapView::dimensions()
+{
+	fitInView(0, 0, controller->map()->width, controller->map()->height, Qt::KeepAspectRatio);
+}
+
+void MinimapView::setController(MapController * ctrl)
+{
+	controller = ctrl;
+}
+
+void MinimapView::mouseMoveEvent(QMouseEvent *mouseEvent)
+{
+	this->update();
+	
+	auto * sc = static_cast<MinimapScene*>(scene());
+	if(!sc)
+		return;
+	
+	int w = sc->viewport.viewportWidth();
+	int h = sc->viewport.viewportHeight();
+	auto pos = mapToScene(mouseEvent->pos());
+	pos.setX(pos.x() - w / 2);
+	pos.setY(pos.y() - h / 2);
+	
+	QPointF point = pos * 32;
+			
+	emit cameraPositionChanged(point);
+}
+
+void MinimapView::mousePressEvent(QMouseEvent* event)
+{
+	mouseMoveEvent(event);
+}
+
+MapView::MapView(QWidget * parent):
+	QGraphicsView(parent),
+	selectionTool(MapView::SelectionTool::None)
+{
+}
+
+void MapView::cameraChanged(const QPointF & pos)
+{
+	//ui->mapView->translate(pos.x(), pos.y());
+	horizontalScrollBar()->setValue(pos.x());
+	verticalScrollBar()->setValue(pos.y());
+}
+
+
+void MapView::setController(MapController * ctrl)
+{
+	controller = ctrl;
+}
+
+void MapView::mouseMoveEvent(QMouseEvent *mouseEvent)
+{
+	this->update();
+
+	auto * sc = static_cast<MapScene*>(scene());
+	if(!sc || !controller->map())
+		return;
+
+	auto pos = mapToScene(mouseEvent->pos()); //TODO: do we need to check size?
+	int3 tile(pos.x() / 32, pos.y() / 32, sc->level);
+
+	if(tile == tilePrev) //do not redraw
+		return;
+
+	tilePrev = tile;
+
+	//TODO: cast parent->parent to MainWindow in order to show coordinates or another way to do it?
+	//main->setStatusMessage(QString("x: %1 y: %2").arg(tile.x, tile.y));
+
+	switch(selectionTool)
+	{
+	case MapView::SelectionTool::Brush:
+		if(mouseEvent->buttons() & Qt::RightButton)
+			sc->selectionTerrainView.erase(tile);
+		else if(mouseEvent->buttons() == Qt::LeftButton)
+			sc->selectionTerrainView.select(tile);
+		sc->selectionTerrainView.draw();
+		break;
+
+	case MapView::SelectionTool::Brush2:
+		{
+			std::array<int3, 4> extra{ int3{0, 0, 0}, int3{1, 0, 0}, int3{0, 1, 0}, int3{1, 1, 0} };
+			for(auto & e : extra)
+			{
+				if(mouseEvent->buttons() & Qt::RightButton)
+					sc->selectionTerrainView.erase(tile + e);
+				else if(mouseEvent->buttons() == Qt::LeftButton)
+					sc->selectionTerrainView.select(tile + e);
+			}
+		}
+		sc->selectionTerrainView.draw();
+		break;
+
+	case MapView::SelectionTool::Brush4:
+	{
+		std::array<int3, 16> extra{
+			int3{-1, -1, 0}, int3{0, -1, 0}, int3{1, -1, 0}, int3{2, -1, 0},
+			int3{-1, 0, 0}, int3{0, 0, 0}, int3{1, 0, 0}, int3{2, 0, 0},
+			int3{-1, 1, 0}, int3{0, 1, 0}, int3{1, 1, 0}, int3{2, 1, 0},
+			int3{-1, 2, 0}, int3{0, 2, 0}, int3{1, 2, 0}, int3{2, 2, 0}
+		};
+		for(auto & e : extra)
+		{
+			if(mouseEvent->buttons() & Qt::RightButton)
+				sc->selectionTerrainView.erase(tile + e);
+			else if(mouseEvent->buttons() == Qt::LeftButton)
+				sc->selectionTerrainView.select(tile + e);
+		}
+	}
+		sc->selectionTerrainView.draw();
+		break;
+
+	case MapView::SelectionTool::Area:
+		if(mouseEvent->buttons() & Qt::RightButton || !(mouseEvent->buttons() & Qt::LeftButton))
+			break;
+
+		sc->selectionTerrainView.clear();
+		for(int j = std::min(tile.y, tileStart.y); j < std::max(tile.y, tileStart.y); ++j)
+		{
+			for(int i = std::min(tile.x, tileStart.x); i < std::max(tile.x, tileStart.x); ++i)
+			{
+				sc->selectionTerrainView.select(int3(i, j, sc->level));
+			}
+		}
+		sc->selectionTerrainView.draw();
+		break;
+
+	case MapView::SelectionTool::None:
+		if(mouseEvent->buttons() & Qt::RightButton)
+			break;
+
+		auto sh = tile - tileStart;
+		sc->selectionObjectsView.shift = QPoint(sh.x, sh.y);
+
+		if(sh.x || sh.y)
+		{
+			if(sc->selectionObjectsView.newObject)
+			{
+				sc->selectionObjectsView.shift = QPoint(tile.x, tile.y);
+				sc->selectionObjectsView.selectObject(sc->selectionObjectsView.newObject);
+				sc->selectionObjectsView.selectionMode = SelectionObjectsLayer::MOVEMENT;
+			}
+			else if(mouseEvent->buttons() & Qt::LeftButton)
+			{
+				if(sc->selectionObjectsView.selectionMode == SelectionObjectsLayer::SELECTION)
+				{
+					sc->selectionObjectsView.clear();
+					sc->selectionObjectsView.selectObjects(tileStart.x, tileStart.y, tile.x, tile.y);
+				}
+			}
+		}
+
+		sc->selectionObjectsView.draw();
+		break;
+	}
+}
+
+void MapView::mousePressEvent(QMouseEvent *event)
+{
+	this->update();
+
+	auto * sc = static_cast<MapScene*>(scene());
+	if(!sc || !controller->map())
+		return;
+
+	mouseStart = mapToScene(event->pos());
+	tileStart = tilePrev = int3(mouseStart.x() / 32, mouseStart.y() / 32, sc->level);
+
+	if(sc->selectionTerrainView.selection().count(tileStart))
+		pressedOnSelected = true;
+	else
+		pressedOnSelected = false;
+
+	switch(selectionTool)
+	{
+	case MapView::SelectionTool::Brush:
+		sc->selectionObjectsView.clear();
+		sc->selectionObjectsView.draw();
+
+		if(event->button() == Qt::RightButton)
+			sc->selectionTerrainView.erase(tileStart);
+		else if(event->button() == Qt::LeftButton)
+			sc->selectionTerrainView.select(tileStart);
+		sc->selectionTerrainView.draw();
+		break;
+
+	case MapView::SelectionTool::Brush2:
+		sc->selectionObjectsView.clear();
+		sc->selectionObjectsView.draw();
+	{
+		std::array<int3, 4> extra{ int3{0, 0, 0}, int3{1, 0, 0}, int3{0, 1, 0}, int3{1, 1, 0} };
+		for(auto & e : extra)
+		{
+			if(event->button() == Qt::RightButton)
+				sc->selectionTerrainView.erase(tileStart + e);
+			else if(event->button() == Qt::LeftButton)
+				sc->selectionTerrainView.select(tileStart + e);
+		}
+	}
+		sc->selectionTerrainView.draw();
+		break;
+
+	case MapView::SelectionTool::Brush4:
+		sc->selectionObjectsView.clear();
+		sc->selectionObjectsView.draw();
+	{
+		std::array<int3, 16> extra{
+			int3{-1, -1, 0}, int3{0, -1, 0}, int3{1, -1, 0}, int3{2, -1, 0},
+			int3{-1, 0, 0}, int3{0, 0, 0}, int3{1, 0, 0}, int3{2, 0, 0},
+			int3{-1, 1, 0}, int3{0, 1, 0}, int3{1, 1, 0}, int3{2, 1, 0},
+			int3{-1, 2, 0}, int3{0, 2, 0}, int3{1, 2, 0}, int3{2, 2, 0}
+		};
+		for(auto & e : extra)
+		{
+			if(event->button() == Qt::RightButton)
+				sc->selectionTerrainView.erase(tileStart + e);
+			else if(event->button() == Qt::LeftButton)
+				sc->selectionTerrainView.select(tileStart + e);
+		}
+	}
+		sc->selectionTerrainView.draw();
+		break;
+
+	case MapView::SelectionTool::Area:
+		if(event->button() == Qt::RightButton)
+			break;
+
+		sc->selectionTerrainView.clear();
+		sc->selectionTerrainView.draw();
+		sc->selectionObjectsView.clear();
+		sc->selectionObjectsView.draw();
+		break;
+
+	case MapView::SelectionTool::None:
+		sc->selectionTerrainView.clear();
+		sc->selectionTerrainView.draw();
+
+		if(sc->selectionObjectsView.newObject && sc->selectionObjectsView.isSelected(sc->selectionObjectsView.newObject))
+		{
+			if(event->button() == Qt::RightButton)
+				controller->discardObject(sc->level);
+		}
+		else
+		{
+			if(event->button() == Qt::LeftButton)
+			{
+				auto * obj = sc->selectionObjectsView.selectObjectAt(tileStart.x, tileStart.y);
+				if(obj)
+				{
+					if(sc->selectionObjectsView.isSelected(obj))
+					{
+						if(qApp->keyboardModifiers() & Qt::ControlModifier)
+						{
+							sc->selectionObjectsView.deselectObject(obj);
+							sc->selectionObjectsView.selectionMode = SelectionObjectsLayer::SELECTION;
+						}
+						else
+							sc->selectionObjectsView.selectionMode = SelectionObjectsLayer::MOVEMENT;
+					}
+					else
+					{
+						if(!(qApp->keyboardModifiers() & Qt::ControlModifier))
+							sc->selectionObjectsView.clear();
+						sc->selectionObjectsView.selectionMode = SelectionObjectsLayer::MOVEMENT;
+						sc->selectionObjectsView.selectObject(obj);
+					}
+				}
+				else
+				{
+					sc->selectionObjectsView.clear();
+					sc->selectionObjectsView.selectionMode = SelectionObjectsLayer::SELECTION;
+				}
+			}
+			sc->selectionObjectsView.shift = QPoint(0, 0);
+			sc->selectionObjectsView.draw();
+		}
+		break;
+	}
+
+	//main->setStatusMessage(QString("x: %1 y: %2").arg(QString::number(event->pos().x()), QString::number(event->pos().y())));
+}
+
+void MapView::mouseReleaseEvent(QMouseEvent *event)
+{
+	this->update();
+
+	auto * sc = static_cast<MapScene*>(scene());
+	if(!sc || !controller->map())
+		return;
+
+	switch(selectionTool)
+	{
+	case MapView::SelectionTool::None:
+		if(event->button() == Qt::RightButton)
+			break;
+		//switch position
+		bool tab = false;
+		if(sc->selectionObjectsView.selectionMode == SelectionObjectsLayer::MOVEMENT)
+		{
+			if(sc->selectionObjectsView.newObject)
+			{
+				QString errorMsg;
+				if(controller->canPlaceObject(sc->level, sc->selectionObjectsView.newObject, errorMsg))
+					controller->commitObjectCreate(sc->level);
+				else
+				{
+					QMessageBox::information(this, "Can't place object", errorMsg);
+					break;
+				}
+			}
+			else
+				controller->commitObjectShift(sc->level);
+		}
+		else
+		{
+			sc->selectionObjectsView.selectionMode = SelectionObjectsLayer::NOTHING;
+			sc->selectionObjectsView.shift = QPoint(0, 0);
+			sc->selectionObjectsView.draw();
+			tab = true;
+			//check if we have only one object
+		}
+		auto selection = sc->selectionObjectsView.getSelection();
+		if(selection.size() == 1)
+		{
+			emit openObjectProperties(*selection.begin(), tab);
+		}
+		break;
+	}
+}
+
+bool MapView::viewportEvent(QEvent *event)
+{
+	if(auto * sc = static_cast<MapScene*>(scene()))
+	{
+		//auto rect = sceneRect();
+		auto rect = mapToScene(viewport()->geometry()).boundingRect();
+		controller->miniScene(sc->level)->viewport.setViewport(rect.x() / 32, rect.y() / 32, rect.width() / 32, rect.height() / 32);
+	}
+	return QGraphicsView::viewportEvent(event);
+}
+
+MapSceneBase::MapSceneBase(int lvl):
+	QGraphicsScene(nullptr),
+	level(lvl)
+{
+}
+
+void MapSceneBase::initialize(MapController & controller)
+{
+	for(auto * layer : getAbstractLayers())
+		layer->initialize(controller);
+}
+
+void MapSceneBase::updateViews()
+{
+	for(auto * layer : getAbstractLayers())
+		layer->update();
+}
+
+MapScene::MapScene(int lvl):
+	MapSceneBase(lvl),
+	gridView(this),
+	passabilityView(this),
+	selectionTerrainView(this),
+	terrainView(this),
+	objectsView(this),
+	selectionObjectsView(this),
+	isTerrainSelected(false),
+	isObjectSelected(false)
+{
+	connect(&selectionTerrainView, &SelectionTerrainLayer::selectionMade, this, &MapScene::terrainSelected);
+	connect(&selectionObjectsView, &SelectionObjectsLayer::selectionMade, this, &MapScene::objectSelected);
+}
+
+std::list<AbstractLayer *> MapScene::getAbstractLayers()
+{
+	//sequence is important because it defines rendering order
+	return {
+		&terrainView,
+		&objectsView,
+		&gridView,
+		&passabilityView,
+		&selectionTerrainView,
+		&selectionObjectsView
+	};
+}
+
+void MapScene::updateViews()
+{
+	MapSceneBase::updateViews();
+
+	terrainView.show(true);
+	objectsView.show(true);
+	selectionTerrainView.show(true);
+	selectionObjectsView.show(true);
+}
+
+void MapScene::terrainSelected(bool anythingSelected)
+{
+	isTerrainSelected = anythingSelected;
+	emit selected(isTerrainSelected || isObjectSelected);
+}
+
+void MapScene::objectSelected(bool anythingSelected)
+{
+	isObjectSelected = anythingSelected;
+	emit selected(isTerrainSelected || isObjectSelected);
+}
+
+MinimapScene::MinimapScene(int lvl):
+	MapSceneBase(lvl),
+	minimapView(this),
+	viewport(this)
+{
+}
+
+std::list<AbstractLayer *> MinimapScene::getAbstractLayers()
+{
+	//sequence is important because it defines rendering order
+	return {
+		&minimapView,
+		&viewport
+	};
+}
+
+void MinimapScene::updateViews()
+{
+	MapSceneBase::updateViews();
+	
+	minimapView.show(true);
+	viewport.show(true);
+}

+ 140 - 0
mapeditor/mapview.h

@@ -0,0 +1,140 @@
+/*
+ * mapview.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
+
+#include <QGraphicsScene>
+#include <QGraphicsView>
+#include "scenelayer.h"
+#include "../lib/int3.h"
+
+
+class CGObjectInstance;
+class MainWindow;
+class MapController;
+
+class MapSceneBase : public QGraphicsScene
+{
+	Q_OBJECT;
+public:
+	MapSceneBase(int lvl);
+	
+	const int level;
+	
+	virtual void updateViews();
+	virtual void initialize(MapController &);
+	
+protected:
+	virtual std::list<AbstractLayer *> getAbstractLayers() = 0;
+};
+
+class MinimapScene : public MapSceneBase
+{
+public:
+	MinimapScene(int lvl);
+	
+	void updateViews() override;
+	
+	MinimapLayer minimapView;
+	MinimapViewLayer viewport;
+	
+protected:
+	std::list<AbstractLayer *> getAbstractLayers() override;
+};
+
+class MapScene : public MapSceneBase
+{
+	Q_OBJECT
+public:
+	MapScene(int lvl);
+	
+	void updateViews() override;
+	
+	GridLayer gridView;
+	PassabilityLayer passabilityView;
+	SelectionTerrainLayer selectionTerrainView;
+	TerrainLayer terrainView;
+	ObjectsLayer objectsView;
+	SelectionObjectsLayer selectionObjectsView;
+
+signals:
+	void selected(bool anything);
+
+public slots:
+	void terrainSelected(bool anything);
+	void objectSelected(bool anything);
+	
+protected:
+	std::list<AbstractLayer *> getAbstractLayers() override;
+
+	bool isTerrainSelected;
+	bool isObjectSelected;
+
+};
+
+class MapView : public QGraphicsView
+{
+	Q_OBJECT
+public:
+	enum class SelectionTool
+	{
+		None, Brush, Brush2, Brush4, Area, Lasso
+	};
+
+public:
+	MapView(QWidget * parent);
+	void setController(MapController *);
+
+	SelectionTool selectionTool;
+
+public slots:
+	void mouseMoveEvent(QMouseEvent * mouseEvent) override;
+	void mousePressEvent(QMouseEvent *event) override;
+	void mouseReleaseEvent(QMouseEvent *event) override;
+	
+	void cameraChanged(const QPointF & pos);
+	
+signals:
+	void openObjectProperties(CGObjectInstance *, bool switchTab);
+	//void viewportChanged(const QRectF & rect);
+
+protected:
+	bool viewportEvent(QEvent *event) override;
+	
+private:
+	MapController * controller = nullptr;
+	QPointF mouseStart;
+	int3 tileStart;
+	int3 tilePrev;
+	bool pressedOnSelected;
+};
+
+class MinimapView : public QGraphicsView
+{
+	Q_OBJECT
+public:
+	MinimapView(QWidget * parent);
+	void setController(MapController *);
+	
+	void dimensions();
+	
+public slots:
+	void mouseMoveEvent(QMouseEvent * mouseEvent) override;
+	void mousePressEvent(QMouseEvent* event) override;
+	
+signals:
+	void cameraPositionChanged(const QPointF & newPosition);
+	
+private:
+	MapController * controller = nullptr;
+	
+	int displayWidth = 192;
+	int displayHeight = 192;
+};

+ 78 - 0
mapeditor/objectbrowser.cpp

@@ -0,0 +1,78 @@
+/*
+ * objectbrowser.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 "objectbrowser.h"
+#include "../lib/mapObjects/CObjectClassesHandler.h"
+
+ObjectBrowser::ObjectBrowser(QObject *parent)
+	: QSortFilterProxyModel{parent}, terrain(Terrain::ANY_TERRAIN)
+{
+}
+
+bool ObjectBrowser::filterAcceptsRow(int source_row, const QModelIndex & source_parent) const
+{
+	bool result = QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
+
+	QModelIndex currentIndex = sourceModel()->index(source_row, 0, source_parent);
+	int childCount = currentIndex.model()->rowCount(currentIndex);
+	if(childCount)
+		return false;
+
+	auto item = dynamic_cast<QStandardItemModel*>(sourceModel())->itemFromIndex(currentIndex);
+	if(!item)
+		return result;
+
+	if(!filterAcceptsRowText(source_row, source_parent))
+		return false;
+
+	if(terrain == Terrain::ANY_TERRAIN)
+		return result;
+
+	auto data = item->data().toJsonObject();
+	if(data.empty())
+		return result;
+
+	auto objIdJson = data["id"];
+	if(objIdJson == QJsonValue::Undefined)
+		return result;
+
+	auto objId = data["id"].toInt();
+	auto objSubId = data["subid"].toInt();
+	auto templateId = data["template"].toInt();
+
+	auto factory = VLC->objtypeh->getHandlerFor(objId, objSubId);
+	auto templ = factory->getTemplates()[templateId];
+
+	result = result & templ->canBePlacedAt(terrain);
+
+	//if we are here, just text filter will be applied
+	return result;
+}
+
+bool ObjectBrowser::filterAcceptsRowText(int source_row, const QModelIndex &source_parent) const
+{
+	if(source_parent.isValid())
+	{
+		if(filterAcceptsRowText(source_parent.row(), source_parent.parent()))
+			return true;
+	}
+
+	QModelIndex index = sourceModel()->index(source_row, 0 ,source_parent);
+	if(!index.isValid())
+		return false;
+
+	auto item = dynamic_cast<QStandardItemModel*>(sourceModel())->itemFromIndex(index);
+	if(!item)
+		return false;
+
+	return (filter.isNull() || filter.isEmpty() || item->text().contains(filter, Qt::CaseInsensitive));
+}
+

+ 27 - 0
mapeditor/objectbrowser.h

@@ -0,0 +1,27 @@
+/*
+ * objectbrowser.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
+
+#include <QSortFilterProxyModel>
+#include "../lib/Terrain.h"
+
+class ObjectBrowser : public QSortFilterProxyModel
+{
+public:
+	explicit ObjectBrowser(QObject *parent = nullptr);
+
+	TerrainId terrain;
+	QString filter;
+
+protected:
+	bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override;
+	bool filterAcceptsRowText(int source_row, const QModelIndex &source_parent) const;
+};

+ 147 - 0
mapeditor/playerparams.cpp

@@ -0,0 +1,147 @@
+/*
+ * playerparams.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 "playerparams.h"
+#include "ui_playerparams.h"
+#include "../lib/CTownHandler.h"
+
+PlayerParams::PlayerParams(MapController & ctrl, int playerId, QWidget *parent) :
+	QWidget(parent),
+	ui(new Ui::PlayerParams),
+	controller(ctrl)
+{
+	ui->setupUi(this);
+
+	playerColor = playerId;
+	assert(controller.map()->players.size() > playerColor);
+	playerInfo = controller.map()->players[playerColor];
+	
+	//load factions
+	for(auto idx : VLC->townh->getAllowedFactions())
+	{
+		CFaction * faction = VLC->townh->objects.at(idx);
+		auto * item = new QListWidgetItem(QString::fromStdString(faction->name));
+		item->setData(Qt::UserRole, QVariant::fromValue(idx));
+		item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
+		ui->allowedFactions->addItem(item);
+		if(playerInfo.allowedFactions.count(idx))
+			item->setCheckState(Qt::Checked);
+		else
+			item->setCheckState(Qt::Unchecked);
+	}
+	QObject::connect(ui->allowedFactions, SIGNAL(itemChanged(QListWidgetItem*)),
+					 this, SLOT(allowedFactionsCheck(QListWidgetItem*)));
+
+	//load checks
+	bool canHumanPlay = playerInfo.canHumanPlay; //need variable to restore after signal received
+	playerInfo.canComputerPlay = true; //computer always can play
+	ui->radioCpu->setChecked(true);
+	if(canHumanPlay)
+		ui->radioHuman->setChecked(true);
+	if(playerInfo.isFactionRandom)
+		ui->randomFaction->setChecked(true);
+	if(playerInfo.generateHeroAtMainTown)
+		ui->generateHero->setChecked(true);
+
+	//load towns
+	int foundMainTown = -1;
+	for(int i = 0, townIndex = 0; i < controller.map()->objects.size(); ++i)
+	{
+		if(auto town = dynamic_cast<CGTownInstance*>(controller.map()->objects[i].get()))
+		{
+			auto * ctown = town->town;
+			if(!ctown)
+				ctown = VLC->townh->randomTown;
+			if(ctown && town->getOwner().getNum() == playerColor)
+			{
+				if(playerInfo.hasMainTown && playerInfo.posOfMainTown == town->pos)
+					foundMainTown = townIndex;
+				const auto name = ctown->faction ? town->getObjectName() : town->name + ", (random)";
+				ui->mainTown->addItem(tr(name.c_str()), QVariant::fromValue(i));
+				++townIndex;
+			}
+		}
+	}
+	
+	if(foundMainTown > -1)
+	{
+		ui->mainTown->setCurrentIndex(foundMainTown + 1);
+	}
+	else
+	{
+		ui->generateHero->setEnabled(false);
+		playerInfo.hasMainTown = false;
+		playerInfo.generateHeroAtMainTown = false;
+		playerInfo.posOfMainTown = int3(-1, -1, -1);
+	}
+
+	ui->playerColor->setTitle(tr("Player ID: %1").arg(playerId));
+	show();
+}
+
+PlayerParams::~PlayerParams()
+{
+	delete ui;
+}
+
+void PlayerParams::on_radioHuman_toggled(bool checked)
+{
+	if(checked)
+		playerInfo.canHumanPlay = true;
+}
+
+
+void PlayerParams::on_radioCpu_toggled(bool checked)
+{
+	if(checked)
+		playerInfo.canHumanPlay = false;
+}
+
+
+void PlayerParams::on_generateHero_stateChanged(int arg1)
+{
+	playerInfo.generateHeroAtMainTown = ui->generateHero->isChecked();
+}
+
+
+void PlayerParams::on_randomFaction_stateChanged(int arg1)
+{
+	playerInfo.isFactionRandom = ui->randomFaction->isChecked();
+}
+
+
+void PlayerParams::allowedFactionsCheck(QListWidgetItem * item)
+{
+	if(item->checkState() == Qt::Checked)
+		playerInfo.allowedFactions.insert(item->data(Qt::UserRole).toInt());
+	else
+		playerInfo.allowedFactions.erase(item->data(Qt::UserRole).toInt());
+}
+
+
+void PlayerParams::on_mainTown_activated(int index)
+{
+	if(index == 0) //default
+	{
+		ui->generateHero->setEnabled(false);
+		ui->generateHero->setChecked(false);
+		playerInfo.hasMainTown = false;
+		playerInfo.posOfMainTown = int3(-1, -1, -1);
+	}
+	else
+	{
+		ui->generateHero->setEnabled(true);
+		auto town = controller.map()->objects.at(ui->mainTown->currentData().toInt());
+		playerInfo.hasMainTown = true;
+		playerInfo.posOfMainTown = town->pos;
+	}
+}
+

+ 49 - 0
mapeditor/playerparams.h

@@ -0,0 +1,49 @@
+/*
+ * playerparams.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
+
+#include <QWidget>
+#include "../lib/mapping/CMap.h"
+#include "mapcontroller.h"
+
+namespace Ui {
+class PlayerParams;
+}
+
+class PlayerParams : public QWidget
+{
+	Q_OBJECT
+
+public:
+	explicit PlayerParams(MapController & controller, int playerId, QWidget *parent = nullptr);
+	~PlayerParams();
+
+	PlayerInfo playerInfo;
+	int playerColor;
+
+private slots:
+	void on_radioHuman_toggled(bool checked);
+
+	void on_radioCpu_toggled(bool checked);
+
+	void on_mainTown_activated(int index);
+
+	void on_generateHero_stateChanged(int arg1);
+
+	void on_randomFaction_stateChanged(int arg1);
+	
+	void allowedFactionsCheck(QListWidgetItem *);
+
+private:
+	Ui::PlayerParams *ui;
+	
+	MapController & controller;
+};

+ 142 - 0
mapeditor/playerparams.ui

@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PlayerParams</class>
+ <widget class="QWidget" name="PlayerParams">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>505</width>
+    <height>160</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="maximumSize">
+   <size>
+    <width>16777215</width>
+    <height>160</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QGroupBox" name="playerColor">
+     <property name="maximumSize">
+      <size>
+       <width>16777215</width>
+       <height>160</height>
+      </size>
+     </property>
+     <property name="title">
+      <string>GroupBox</string>
+     </property>
+     <layout class="QGridLayout" name="gridLayout">
+      <item row="3" column="0">
+       <widget class="QComboBox" name="teamId">
+        <item>
+         <property name="text">
+          <string>No team</string>
+         </property>
+        </item>
+       </widget>
+      </item>
+      <item row="0" column="0">
+       <widget class="QRadioButton" name="radioHuman">
+        <property name="text">
+         <string>Human/CPU</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="0">
+       <widget class="QRadioButton" name="radioCpu">
+        <property name="text">
+         <string>CPU only</string>
+        </property>
+       </widget>
+      </item>
+      <item row="2" column="0">
+       <widget class="QLabel" name="label_2">
+        <property name="text">
+         <string>Team</string>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="1">
+       <widget class="QLabel" name="label_3">
+        <property name="text">
+         <string>Main town</string>
+        </property>
+       </widget>
+      </item>
+      <item row="3" column="2">
+       <widget class="QCheckBox" name="randomFaction">
+        <property name="text">
+         <string>Random faction</string>
+        </property>
+       </widget>
+      </item>
+      <item row="3" column="1">
+       <widget class="QCheckBox" name="generateHero">
+        <property name="text">
+         <string>Generate hero at main</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="1">
+       <widget class="QComboBox" name="mainTown">
+        <item>
+         <property name="text">
+          <string>(default)</string>
+         </property>
+        </item>
+       </widget>
+      </item>
+      <item row="0" column="2" rowspan="3">
+       <widget class="QListWidget" name="allowedFactions">
+        <property name="enabled">
+         <bool>true</bool>
+        </property>
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="focusPolicy">
+         <enum>Qt::ClickFocus</enum>
+        </property>
+        <property name="editTriggers">
+         <set>QAbstractItemView::NoEditTriggers</set>
+        </property>
+        <property name="selectionMode">
+         <enum>QAbstractItemView::NoSelection</enum>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 89 - 0
mapeditor/playersettings.cpp

@@ -0,0 +1,89 @@
+/*
+ * playersettings.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 "playersettings.h"
+#include "ui_playersettings.h"
+#include "playerparams.h"
+#include "mainwindow.h"
+
+PlayerSettings::PlayerSettings(MapController & ctrl, QWidget *parent) :
+	QDialog(parent),
+	ui(new Ui::PlayerSettings),
+	controller(ctrl)
+{
+	ui->setupUi(this);
+	show();
+
+	int players = 0;
+	const int minAllowedPlayers = 2;
+	for(auto & p : controller.map()->players)
+	{
+		if(p.canAnyonePlay())
+		{
+			paramWidgets.push_back(new PlayerParams(controller, players));
+			ui->playersLayout->addWidget(paramWidgets.back());
+			++players;
+		}
+	}
+
+	if(players < minAllowedPlayers)
+		ui->playersCount->setCurrentText("");
+	else
+		ui->playersCount->setCurrentIndex(players - minAllowedPlayers);
+	
+	setAttribute(Qt::WA_DeleteOnClose);
+}
+
+PlayerSettings::~PlayerSettings()
+{
+	delete ui;
+}
+
+void PlayerSettings::on_playersCount_currentIndexChanged(int index)
+{
+	const auto selectedPlayerCount = index + 2;
+	assert(selectedPlayerCount <= controller.map()->players.size());
+
+	for(int i = 0; i < selectedPlayerCount; ++i)
+	{
+		if(i < paramWidgets.size())
+			continue;
+
+		auto & p = controller.map()->players[i];
+		p.canComputerPlay = true;
+		paramWidgets.push_back(new PlayerParams(controller, i));
+		ui->playersLayout->addWidget(paramWidgets.back());
+	}
+
+	assert(!paramWidgets.empty());
+	for(int i = paramWidgets.size() - 1; i >= selectedPlayerCount; --i)
+	{
+		auto & p = controller.map()->players[i];
+		p.canComputerPlay = false;
+		p.canHumanPlay = false;
+		ui->playersLayout->removeWidget(paramWidgets[i]);
+		delete paramWidgets[i];
+		paramWidgets.pop_back();
+	}
+}
+
+
+void PlayerSettings::on_pushButton_clicked()
+{
+	for(auto * w : paramWidgets)
+	{
+		controller.map()->players[w->playerColor] = w->playerInfo;
+	}
+
+	controller.commitChangeWithoutRedraw();
+	close();
+}
+

+ 41 - 0
mapeditor/playersettings.h

@@ -0,0 +1,41 @@
+/*
+ * playersettings.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
+
+#include "StdInc.h"
+#include <QDialog>
+#include "playerparams.h"
+
+namespace Ui {
+class PlayerSettings;
+}
+
+class PlayerSettings : public QDialog
+{
+	Q_OBJECT
+
+public:
+	explicit PlayerSettings(MapController & controller, QWidget *parent = nullptr);
+	~PlayerSettings();
+
+private slots:
+
+	void on_playersCount_currentIndexChanged(int index);
+
+	void on_pushButton_clicked();
+
+private:
+	Ui::PlayerSettings *ui;
+
+	std::vector<PlayerParams*> paramWidgets;
+	
+	MapController & controller;
+};

+ 117 - 0
mapeditor/playersettings.ui

@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PlayerSettings</class>
+ <widget class="QDialog" name="PlayerSettings">
+  <property name="windowModality">
+   <enum>Qt::WindowModal</enum>
+  </property>
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>654</width>
+    <height>283</height>
+   </rect>
+  </property>
+  <property name="focusPolicy">
+   <enum>Qt::NoFocus</enum>
+  </property>
+  <property name="windowTitle">
+   <string>Player settings</string>
+  </property>
+  <property name="modal">
+   <bool>true</bool>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="1" column="0" colspan="8">
+    <widget class="QScrollArea" name="players">
+     <property name="widgetResizable">
+      <bool>true</bool>
+     </property>
+     <widget class="QWidget" name="playerscomon">
+      <property name="geometry">
+       <rect>
+        <x>0</x>
+        <y>0</y>
+        <width>628</width>
+        <height>187</height>
+       </rect>
+      </property>
+      <layout class="QVBoxLayout" name="verticalLayout_2">
+       <property name="leftMargin">
+        <number>0</number>
+       </property>
+       <property name="topMargin">
+        <number>0</number>
+       </property>
+       <property name="rightMargin">
+        <number>0</number>
+       </property>
+       <property name="bottomMargin">
+        <number>0</number>
+       </property>
+       <item>
+        <layout class="QVBoxLayout" name="playersLayout"/>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+   <item row="0" column="0">
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>Players</string>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="1">
+    <widget class="QComboBox" name="playersCount">
+     <item>
+      <property name="text">
+       <string>2</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>3</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>4</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>5</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>6</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>7</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>8</string>
+      </property>
+     </item>
+    </widget>
+   </item>
+   <item row="2" column="6" colspan="2">
+    <widget class="QPushButton" name="pushButton">
+     <property name="text">
+      <string>Ok</string>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 568 - 0
mapeditor/scenelayer.cpp

@@ -0,0 +1,568 @@
+/*
+ * scenelayer.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 "scenelayer.h"
+#include "mainwindow.h"
+#include "../lib/mapping/CMapEditManager.h"
+#include "inspector/inspector.h"
+#include "mapview.h"
+#include "mapcontroller.h"
+
+AbstractLayer::AbstractLayer(MapSceneBase * s): scene(s)
+{
+}
+
+void AbstractLayer::initialize(MapController & controller)
+{
+	map = controller.map();
+	handler = controller.mapHandler();
+}
+
+void AbstractLayer::show(bool show)
+{
+	if(isShown == show)
+		return;
+	
+	isShown = show;
+	
+	redraw();
+}
+
+void AbstractLayer::redraw()
+{
+	if(item)
+	{
+		if(pixmap && isShown)
+			item->setPixmap(*pixmap);
+		else
+			item->setPixmap(emptyPixmap);
+	}
+	else
+	{
+		if(pixmap && isShown)
+			item.reset(scene->addPixmap(*pixmap));
+		else
+			item.reset(scene->addPixmap(emptyPixmap));
+	}
+}
+
+GridLayer::GridLayer(MapSceneBase * s): AbstractLayer(s)
+{
+}
+
+void GridLayer::update()
+{
+	if(!map)
+		return;
+	
+	pixmap.reset(new QPixmap(map->width * 32, map->height * 32));
+	pixmap->fill(Qt::transparent);
+	QPainter painter(pixmap.get());
+	painter.setPen(QColor(0, 0, 0, 190));
+	
+	for(int j = 0; j < map->height; ++j)
+	{
+		painter.drawLine(0, j * 32, map->width * 32 - 1, j * 32);
+	}
+	for(int i = 0; i < map->width; ++i)
+	{
+		painter.drawLine(i * 32, 0, i * 32, map->height * 32 - 1);
+	}
+	
+	redraw();
+}
+
+PassabilityLayer::PassabilityLayer(MapSceneBase * s): AbstractLayer(s)
+{
+}
+
+void PassabilityLayer::update()
+{
+	if(!map)
+		return;
+	
+	pixmap.reset(new QPixmap(map->width * 32, map->height * 32));
+	pixmap->fill(Qt::transparent);
+	
+	if(scene->level == 0 || map->twoLevel)
+	{
+		QPainter painter(pixmap.get());
+		for(int j = 0; j < map->height; ++j)
+		{
+			for(int i = 0; i < map->width; ++i)
+			{
+				auto tl = map->getTile(int3(i, j, scene->level));
+				if(tl.blocked || tl.visitable)
+				{
+					painter.fillRect(i * 32, j * 32, 31, 31, tl.visitable ? QColor(200, 200, 0, 64) : QColor(255, 0, 0, 64));
+				}
+			}
+		}
+	}
+	
+	redraw();
+}
+
+SelectionTerrainLayer::SelectionTerrainLayer(MapSceneBase * s): AbstractLayer(s)
+{
+}
+
+void SelectionTerrainLayer::update()
+{
+	if(!map)
+		return;
+	
+	area.clear();
+	areaAdd.clear();
+	areaErase.clear();
+	onSelection();
+	
+	pixmap.reset(new QPixmap(map->width * 32, map->height * 32));
+	pixmap->fill(Qt::transparent);
+	
+	redraw();
+}
+
+void SelectionTerrainLayer::draw()
+{
+	if(!pixmap)
+		return;
+	
+	QPainter painter(pixmap.get());
+	painter.setCompositionMode(QPainter::CompositionMode_Source);
+	for(auto & t : areaAdd)
+	{
+		painter.fillRect(t.x * 32, t.y * 32, 31, 31, QColor(128, 128, 128, 96));
+	}
+	for(auto & t : areaErase)
+	{
+		painter.fillRect(t.x * 32, t.y * 32, 31, 31, QColor(0, 0, 0, 0));
+	}
+	
+	areaAdd.clear();
+	areaErase.clear();
+	
+	redraw();
+}
+
+void SelectionTerrainLayer::select(const int3 & tile)
+{
+	if(!map || !map->isInTheMap(tile))
+		return;
+	
+	if(!area.count(tile))
+	{
+		area.insert(tile);
+		areaAdd.insert(tile);
+		areaErase.erase(tile);
+	}
+	onSelection();
+}
+
+void SelectionTerrainLayer::erase(const int3 & tile)
+{
+	if(!map || !map->isInTheMap(tile))
+		return;
+	
+	if(area.count(tile))
+	{
+		area.erase(tile);
+		areaErase.insert(tile);
+		areaAdd.erase(tile);
+	}
+	onSelection();
+}
+
+void SelectionTerrainLayer::clear()
+{
+	areaErase = area;
+	areaAdd.clear();
+	area.clear();
+	onSelection();
+}
+
+const std::set<int3> & SelectionTerrainLayer::selection() const
+{
+	return area;
+}
+
+void SelectionTerrainLayer::onSelection()
+{
+	emit selectionMade(!area.empty());
+}
+
+
+TerrainLayer::TerrainLayer(MapSceneBase * s): AbstractLayer(s)
+{
+}
+
+void TerrainLayer::update()
+{
+	if(!map)
+		return;
+	
+	pixmap.reset(new QPixmap(map->width * 32, map->height * 32));
+	draw(false);
+}
+
+void TerrainLayer::setDirty(const int3 & tile)
+{
+	dirty.insert(tile);
+}
+
+void TerrainLayer::draw(bool onlyDirty)
+{
+	if(!pixmap)
+		return;
+	
+	if(!map)
+		return;
+	
+	QPainter painter(pixmap.get());
+	//painter.setCompositionMode(QPainter::CompositionMode_Source);
+	
+	if(onlyDirty)
+	{
+		std::set<int3> forRedrawing(dirty), neighbours;
+		for(auto & t : dirty)
+		{
+			for(auto & tt : int3::getDirs())
+			{
+				if(map->isInTheMap(t + tt))
+					neighbours.insert(t + tt);
+			}
+		}
+		for(auto & t : neighbours)
+		{
+			for(auto & tt : int3::getDirs())
+			{
+				forRedrawing.insert(t);
+				if(map->isInTheMap(t + tt))
+					forRedrawing.insert(t + tt);
+			}
+		}
+		for(auto & t : forRedrawing)
+		{
+			handler->drawTerrainTile(painter, t.x, t.y, scene->level);
+			handler->drawRiver(painter, t.x, t.y, scene->level);
+			handler->drawRoad(painter, t.x, t.y, scene->level);
+		}
+	}
+	else
+	{
+		for(int j = 0; j < map->height; ++j)
+		{
+			for(int i = 0; i < map->width; ++i)
+			{
+				handler->drawTerrainTile(painter, i, j, scene->level);
+				handler->drawRiver(painter, i, j, scene->level);
+				handler->drawRoad(painter, i, j, scene->level);
+			}
+		}
+	}
+	
+	dirty.clear();
+	redraw();
+}
+
+ObjectsLayer::ObjectsLayer(MapSceneBase * s): AbstractLayer(s)
+{
+}
+
+void ObjectsLayer::update()
+{
+	if(!map)
+		return;
+	
+	pixmap.reset(new QPixmap(map->width * 32, map->height * 32));
+	pixmap->fill(Qt::transparent);
+	draw(false);
+}
+
+void ObjectsLayer::draw(bool onlyDirty)
+{
+	if(!pixmap)
+		return;
+	
+	if(!map)
+		return;
+	
+	pixmap->fill(Qt::transparent);
+	QPainter painter(pixmap.get());
+	std::set<const CGObjectInstance *> drawen;
+	
+	
+	for(int j = 0; j < map->height; ++j)
+	{
+		for(int i = 0; i < map->width; ++i)
+		{
+			handler->drawObjects(painter, i, j, scene->level);
+			/*auto & objects = main->getMapHandler()->getObjects(i, j, scene->level);
+			 for(auto & object : objects)
+			 {
+			 if(!object.obj || drawen.count(object.obj))
+			 continue;
+			 
+			 if(!onlyDirty || dirty.count(object.obj))
+			 {
+			 main->getMapHandler()->drawObject(painter, object);
+			 drawen.insert(object.obj);
+			 }
+			 }*/
+		}
+	}
+	
+	dirty.clear();
+	redraw();
+}
+
+void ObjectsLayer::setDirty(int x, int y)
+{
+	/*auto & objects = main->getMapHandler()->getObjects(x, y, scene->level);
+	for(auto & object : objects)
+	{
+		if(object.obj)
+			dirty.insert(object.obj);
+	}*/
+}
+
+void ObjectsLayer::setDirty(const CGObjectInstance * object)
+{
+}
+
+SelectionObjectsLayer::SelectionObjectsLayer(MapSceneBase * s): AbstractLayer(s), newObject(nullptr)
+{
+}
+
+void SelectionObjectsLayer::update()
+{
+	if(!map)
+		return;
+	
+	selectedObjects.clear();
+	onSelection();
+	shift = QPoint();
+	delete newObject;
+	newObject = nullptr;
+	
+	pixmap.reset(new QPixmap(map->width * 32, map->height * 32));
+	//pixmap->fill(QColor(0, 0, 0, 0));
+	
+	draw();
+}
+
+void SelectionObjectsLayer::draw()
+{
+	if(!pixmap)
+		return;
+	
+	pixmap->fill(Qt::transparent);
+	
+	QPainter painter(pixmap.get());
+	painter.setCompositionMode(QPainter::CompositionMode_Source);
+	painter.setPen(Qt::white);
+	
+	for(auto * obj : selectedObjects)
+	{
+		if(obj != newObject)
+		{
+			QRect bbox(obj->getPosition().x, obj->getPosition().y, 1, 1);
+			for(auto & t : obj->getBlockedPos())
+			{
+				QPoint topLeft(std::min(t.x, bbox.topLeft().x()), std::min(t.y, bbox.topLeft().y()));
+				bbox.setTopLeft(topLeft);
+				QPoint bottomRight(std::max(t.x, bbox.bottomRight().x()), std::max(t.y, bbox.bottomRight().y()));
+				bbox.setBottomRight(bottomRight);
+			}
+			
+			painter.setOpacity(1.0);
+			painter.drawRect(bbox.x() * 32, bbox.y() * 32, bbox.width() * 32, bbox.height() * 32);
+		}
+		
+		//show translation
+		if(selectionMode == SelectionMode::MOVEMENT && (shift.x() || shift.y()))
+		{
+			painter.setOpacity(0.5);
+			auto newPos = QPoint(obj->getPosition().x, obj->getPosition().y) + shift;
+			handler->drawObjectAt(painter, obj, newPos.x(), newPos.y());
+		}
+	}
+	
+	redraw();
+}
+
+CGObjectInstance * SelectionObjectsLayer::selectObjectAt(int x, int y) const
+{
+	if(!map || !map->isInTheMap(int3(x, y, scene->level)))
+		return nullptr;
+	
+	auto & objects = handler->getObjects(x, y, scene->level);
+	
+	//visitable is most important
+	for(auto & object : objects)
+	{
+		if(!object.obj)
+			continue;
+		
+		if(object.obj->visitableAt(x, y))
+		{
+			return object.obj;
+		}
+	}
+	
+	//if not visitable tile - try to get blocked
+	for(auto & object : objects)
+	{
+		if(!object.obj)
+			continue;
+		
+		if(object.obj->blockingAt(x, y))
+		{
+			return object.obj;
+		}
+	}
+	
+	//finally, we can take any object
+	for(auto & object : objects)
+	{
+		if(!object.obj)
+			continue;
+		
+		if(object.obj->coveringAt(x, y))
+		{
+			return object.obj;
+		}
+	}
+	
+	return nullptr;
+}
+
+void SelectionObjectsLayer::selectObjects(int x1, int y1, int x2, int y2)
+{
+	if(!map)
+		return;
+	
+	if(x1 > x2)
+		std::swap(x1, x2);
+	
+	if(y1 > y2)
+		std::swap(y1, y2);
+	
+	for(int j = y1; j < y2; ++j)
+	{
+		for(int i = x1; i < x2; ++i)
+		{
+			for(auto & o : handler->getObjects(i, j, scene->level))
+				selectObject(o.obj, false); //do not inform about each object added
+		}
+	}
+	onSelection();
+}
+
+void SelectionObjectsLayer::selectObject(CGObjectInstance * obj, bool inform /* = true */)
+{
+	selectedObjects.insert(obj);
+	if (inform)
+	{
+		onSelection();
+	}
+}
+
+void SelectionObjectsLayer::deselectObject(CGObjectInstance * obj)
+{
+	selectedObjects.erase(obj);
+}
+
+bool SelectionObjectsLayer::isSelected(const CGObjectInstance * obj) const
+{
+	return selectedObjects.count(const_cast<CGObjectInstance*>(obj));
+}
+
+std::set<CGObjectInstance*> SelectionObjectsLayer::getSelection() const
+{
+	return selectedObjects;
+}
+
+void SelectionObjectsLayer::clear()
+{
+	selectedObjects.clear();
+	onSelection();
+	shift.setX(0);
+	shift.setY(0);
+}
+
+void SelectionObjectsLayer::onSelection()
+{
+	emit selectionMade(!selectedObjects.empty());
+}
+
+MinimapLayer::MinimapLayer(MapSceneBase * s): AbstractLayer(s)
+{
+	
+}
+
+void MinimapLayer::update()
+{
+	if(!map)
+		return;
+	
+	pixmap.reset(new QPixmap(map->width, map->height));
+	
+	QPainter painter(pixmap.get());
+	//coordinate transfomation
+	for(int j = 0; j < map->height; ++j)
+	{
+		for(int i = 0; i < map->width; ++i)
+		{
+			handler->drawMinimapTile(painter, i, j, scene->level);
+		}
+	}
+	
+	redraw();
+}
+
+MinimapViewLayer::MinimapViewLayer(MapSceneBase * s): AbstractLayer(s)
+{
+}
+
+void MinimapViewLayer::update()
+{
+	if(!map)
+		return;
+	
+	pixmap.reset(new QPixmap(map->width, map->height));
+	
+	draw();
+}
+
+void MinimapViewLayer::draw()
+{
+	if(!map)
+		return;
+	
+	pixmap->fill(Qt::transparent);
+	
+	//maybe not optimal but ok
+	QPainter painter(pixmap.get());
+	painter.setPen(Qt::white);
+	painter.drawRect(x, y, w, h);
+	
+	redraw();
+}
+
+void MinimapViewLayer::setViewport(int _x, int _y, int _w, int _h)
+{
+	x = _x;
+	y = _y;
+	w = _w;
+	h = _h;
+	draw();
+}

+ 190 - 0
mapeditor/scenelayer.h

@@ -0,0 +1,190 @@
+/*
+ * scenelayer.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
+
+#include "../lib/int3.h"
+
+class MapSceneBase;
+class MapScene;
+class CGObjectInstance;
+class MapController;
+class CMap;
+class MapHandler;
+
+class AbstractLayer : public QObject
+{
+	Q_OBJECT
+public:
+	AbstractLayer(MapSceneBase * s);
+	
+	virtual void update() = 0;
+	
+	void show(bool show);
+	void redraw();
+	void initialize(MapController & controller);
+	
+protected:
+	MapSceneBase * scene;
+	CMap * map = nullptr;
+	MapHandler * handler = nullptr;
+	bool isShown = false;
+	
+	std::unique_ptr<QPixmap> pixmap;
+	QPixmap emptyPixmap;
+	
+private:
+	std::unique_ptr<QGraphicsPixmapItem> item;
+};
+
+
+class GridLayer: public AbstractLayer
+{
+	Q_OBJECT
+public:
+	GridLayer(MapSceneBase * s);
+	
+	void update() override;
+};
+
+class PassabilityLayer: public AbstractLayer
+{
+	Q_OBJECT
+public:
+	PassabilityLayer(MapSceneBase * s);
+	
+	void update() override;
+};
+
+class SelectionTerrainLayer: public AbstractLayer
+{
+	Q_OBJECT
+public:
+	SelectionTerrainLayer(MapSceneBase* s);
+	
+	void update() override;
+	
+	void draw();
+	void select(const int3 & tile);
+	void erase(const int3 & tile);
+	void clear();
+	
+	const std::set<int3> & selection() const;
+
+signals:
+	void selectionMade(bool anythingSlected);
+
+private:
+	std::set<int3> area, areaAdd, areaErase;
+
+	void onSelection();
+};
+
+
+class TerrainLayer: public AbstractLayer
+{
+	Q_OBJECT
+public:
+	TerrainLayer(MapSceneBase * s);
+	
+	void update() override;
+	
+	void draw(bool onlyDirty = true);
+	void setDirty(const int3 & tile);
+	
+private:
+	std::set<int3> dirty;
+};
+
+
+class ObjectsLayer: public AbstractLayer
+{
+	Q_OBJECT
+public:
+	ObjectsLayer(MapSceneBase * s);
+	
+	void update() override;
+	
+	void draw(bool onlyDirty = true); //TODO: implement dirty
+	
+	void setDirty(int x, int y);
+	void setDirty(const CGObjectInstance * object);
+	
+private:
+	std::set<const CGObjectInstance *> objDirty;
+	std::set<int3> dirty;
+};
+
+
+class SelectionObjectsLayer: public AbstractLayer
+{
+	Q_OBJECT
+public:
+	enum SelectionMode
+	{
+		NOTHING, SELECTION, MOVEMENT
+	};
+	
+	SelectionObjectsLayer(MapSceneBase* s);
+	
+	void update() override;
+	
+	void draw();
+	
+	CGObjectInstance * selectObjectAt(int x, int y) const;
+	void selectObjects(int x1, int y1, int x2, int y2);
+	void selectObject(CGObjectInstance *, bool inform = true);
+	void deselectObject(CGObjectInstance *);
+	bool isSelected(const CGObjectInstance *) const;
+	std::set<CGObjectInstance*> getSelection() const;
+	void moveSelection(int x, int y);
+	void clear();
+		
+	QPoint shift;
+	CGObjectInstance * newObject;
+	//FIXME: magic number
+	SelectionMode selectionMode = SelectionMode::NOTHING;
+
+signals:
+	void selectionMade(bool anythingSlected);
+	
+private:
+	std::set<CGObjectInstance *> selectedObjects;
+
+	void onSelection();
+};
+
+class MinimapLayer: public AbstractLayer
+{
+public:
+	MinimapLayer(MapSceneBase * s);
+	
+	void update() override;
+};
+
+class MinimapViewLayer: public AbstractLayer
+{
+public:
+	MinimapViewLayer(MapSceneBase * s);
+	
+	void setViewport(int x, int y, int w, int h);
+	
+	void draw();
+	void update() override;
+	
+	int viewportX() const {return x;}
+	int viewportY() const {return y;}
+	int viewportWidth() const {return w;}
+	int viewportHeight() const {return h;}
+	
+private:
+	int x = 0, y = 0, w = 1, h = 1;
+	
+};

+ 172 - 0
mapeditor/validator.cpp

@@ -0,0 +1,172 @@
+/*
+ * validator.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 "validator.h"
+#include "ui_validator.h"
+#include "../lib/mapObjects/MapObjects.h"
+#include "../lib/CHeroHandler.h"
+
+Validator::Validator(const CMap * map, QWidget *parent) :
+	QDialog(parent),
+	ui(new Ui::Validator)
+{
+	ui->setupUi(this);
+
+	show();
+	
+	setAttribute(Qt::WA_DeleteOnClose);
+
+	std::array<QString, 2> icons{"mapeditor/icons/mod-update.png", "mapeditor/icons/mod-delete.png"};
+
+	for(auto & issue : Validator::validate(map))
+	{
+		auto * item = new QListWidgetItem(QIcon(icons[issue.critical ? 1 : 0]), issue.message);
+		ui->listWidget->addItem(item);
+	}
+}
+
+Validator::~Validator()
+{
+	delete ui;
+}
+
+std::list<Validator::Issue> Validator::validate(const CMap * map)
+{
+	std::list<Validator::Issue> issues;
+	
+	if(!map)
+	{
+		issues.emplace_back("Map is not loaded", true);
+		return issues;
+	}
+	
+	try
+	{
+		//check player settings
+		int hplayers = 0;
+		int cplayers = 0;
+		std::map<int, int> amountOfCastles;
+		for(int i = 0; i < map->players.size(); ++i)
+		{
+			auto & p = map->players[i];
+			if(p.canAnyonePlay())
+				amountOfCastles[i] = 0;
+			if(p.canComputerPlay)
+				++cplayers;
+			if(p.canHumanPlay)
+				++hplayers;
+			if(p.allowedFactions.empty())
+				issues.emplace_back(QString("No factions allowed for player %1").arg(i), true);
+		}
+		if(hplayers + cplayers == 0)
+			issues.emplace_back("No players allowed to play this map", true);
+		if(hplayers + cplayers == 1)
+			issues.emplace_back("Map is allowed for one player and cannot be started", true);
+		if(!hplayers)
+			issues.emplace_back("No human players allowed to play this map", true);
+
+		//checking all objects in the map
+		for(auto o : map->objects)
+		{
+			//owners for objects
+			if(o->getOwner() == PlayerColor::UNFLAGGABLE)
+			{
+				if(dynamic_cast<CGMine*>(o.get()) ||
+				   dynamic_cast<CGDwelling*>(o.get()) ||
+				   dynamic_cast<CGTownInstance*>(o.get()) ||
+				   dynamic_cast<CGGarrison*>(o.get()) ||
+				   dynamic_cast<CGHeroInstance*>(o.get()))
+				{
+					issues.emplace_back(QString("Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner").arg(o->instanceName.c_str()), true);
+				}
+			}
+			if(o->getOwner() != PlayerColor::NEUTRAL && o->getOwner().getNum() < map->players.size())
+			{
+				if(!map->players[o->getOwner().getNum()].canAnyonePlay())
+					issues.emplace_back(QString("Object %1 is assinged to non-playable player %2").arg(o->instanceName.c_str(), o->getOwner().getStr().c_str()), true);
+			}
+			//checking towns
+			if(auto * ins = dynamic_cast<CGTownInstance*>(o.get()))
+			{
+				bool has = amountOfCastles.count(ins->getOwner().getNum());
+				if(!has && ins->getOwner() != PlayerColor::NEUTRAL)
+					issues.emplace_back(tr("Town %1 has undefined owner %2").arg(ins->instanceName.c_str(), ins->getOwner().getStr().c_str()), true);
+				if(has)
+					++amountOfCastles[ins->getOwner().getNum()];
+			}
+			//checking heroes and prisons
+			if(auto * ins = dynamic_cast<CGHeroInstance*>(o.get()))
+			{
+				if(ins->ID == Obj::PRISON)
+				{
+					if(ins->getOwner() != PlayerColor::NEUTRAL)
+						issues.emplace_back(QString("Prison %1 must be a NEUTRAL").arg(ins->instanceName.c_str()), true);
+				}
+				else
+				{
+					bool has = amountOfCastles.count(ins->getOwner().getNum());
+					if(!has)
+						issues.emplace_back(QString("Hero %1 must have an owner").arg(ins->instanceName.c_str()), true);
+				}
+				if(ins->type)
+				{
+					if(!map->allowedHeroes[ins->type->getId().getNum()])
+						issues.emplace_back(QString("Hero %1 is prohibited by map settings").arg(ins->type->getName().c_str()), false);
+				}
+				else
+					issues.emplace_back(QString("Hero %1 has an empty type and must be removed").arg(ins->instanceName.c_str()), true);
+			}
+			
+			//checking for arts
+			if(auto * ins = dynamic_cast<CGArtifact*>(o.get()))
+			{
+				if(ins->ID == Obj::SPELL_SCROLL)
+				{
+					if(ins->storedArtifact)
+					{
+						if(!map->allowedSpell[ins->storedArtifact->id.getNum()])
+							issues.emplace_back(QString("Spell scroll %1 is prohibited by map settings").arg(ins->getObjectName().c_str()), false);
+					}
+					else
+						issues.emplace_back(QString("Spell scroll %1 doesn't have instance assigned and must be removed").arg(ins->instanceName.c_str()), true);
+				}
+				else
+				{
+					if(ins->ID == Obj::ARTIFACT && !map->allowedArtifact[ins->subID])
+					{
+						issues.emplace_back(QString("Artifact %1 is prohibited by map settings").arg(ins->getObjectName().c_str()), false);
+					}
+				}
+			}
+		}
+
+		//verification of starting towns
+		for(auto & mp : amountOfCastles)
+			if(mp.second == 0)
+				issues.emplace_back(QString("Player %1 doesn't have any starting town").arg(mp.first), false);
+
+		//verification of map name and description
+		if(map->name.empty())
+			issues.emplace_back("Map name is not specified", false);
+		if(map->description.empty())
+			issues.emplace_back("Map description is not specified", false);
+	}
+	catch(const std::exception & e)
+	{
+		issues.emplace_back(QString("Exception occurs during validation: %1").arg(e.what()), true);
+	}
+	catch(...)
+	{
+		issues.emplace_back("Unknown exception occurs during validation", true);
+	}
+	
+	return issues;
+}

+ 40 - 0
mapeditor/validator.h

@@ -0,0 +1,40 @@
+/*
+ * validator.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
+
+#include <QDialog>
+#include "../lib/mapping/CMap.h"
+
+namespace Ui {
+class Validator;
+}
+
+class Validator : public QDialog
+{
+	Q_OBJECT
+public:
+	struct Issue
+	{
+		QString message;
+		bool critical;
+		
+		Issue(const QString & m, bool c): message(m), critical(c) {}
+	};
+	
+public:
+	explicit Validator(const CMap * map, QWidget *parent = nullptr);
+	~Validator();
+	
+	static std::list<Issue> validate(const CMap * map);
+
+private:
+	Ui::Validator *ui;
+};

+ 72 - 0
mapeditor/validator.ui

@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Validator</class>
+ <widget class="QDialog" name="Validator">
+  <property name="windowModality">
+   <enum>Qt::NonModal</enum>
+  </property>
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>482</width>
+    <height>178</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Map validation results</string>
+  </property>
+  <property name="modal">
+   <bool>true</bool>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="0" column="0">
+    <widget class="QListWidget" name="listWidget">
+     <property name="font">
+      <font>
+       <pointsize>18</pointsize>
+      </font>
+     </property>
+     <property name="frameShadow">
+      <enum>QFrame::Sunken</enum>
+     </property>
+     <property name="lineWidth">
+      <number>1</number>
+     </property>
+     <property name="editTriggers">
+      <set>QAbstractItemView::NoEditTriggers</set>
+     </property>
+     <property name="selectionMode">
+      <enum>QAbstractItemView::NoSelection</enum>
+     </property>
+     <property name="iconSize">
+      <size>
+       <width>32</width>
+       <height>32</height>
+      </size>
+     </property>
+     <property name="resizeMode">
+      <enum>QListView::Adjust</enum>
+     </property>
+     <property name="gridSize">
+      <size>
+       <width>0</width>
+       <height>32</height>
+      </size>
+     </property>
+     <property name="viewMode">
+      <enum>QListView::ListMode</enum>
+     </property>
+     <property name="uniformItemSizes">
+      <bool>false</bool>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 417 - 0
mapeditor/windownewmap.cpp

@@ -0,0 +1,417 @@
+/*
+ * windownewmap.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 "../lib/mapping/CMap.h"
+#include "../lib/rmg/CRmgTemplateStorage.h"
+#include "../lib/rmg/CRmgTemplate.h"
+#include "../lib/rmg/CMapGenerator.h"
+#include "../lib/VCMI_Lib.h"
+#include "../lib/mapping/CMapEditManager.h"
+#include "../lib/CGeneralTextHandler.h"
+
+#include "windownewmap.h"
+#include "ui_windownewmap.h"
+#include "mainwindow.h"
+#include "generatorprogress.h"
+
+WindowNewMap::WindowNewMap(QWidget *parent) :
+	QDialog(parent),
+	ui(new Ui::WindowNewMap)
+{
+	ui->setupUi(this);
+
+	setAttribute(Qt::WA_DeleteOnClose);
+
+	setWindowModality(Qt::ApplicationModal);
+
+	loadUserSettings();
+
+	show();
+
+	//setup initial parameters
+	mapGenOptions.setWidth(ui->widthTxt->text().toInt());
+	mapGenOptions.setHeight(ui->heightTxt->text().toInt());
+	bool twoLevel = ui->twoLevelCheck->isChecked();
+	mapGenOptions.setHasTwoLevels(twoLevel);
+	updateTemplateList();
+}
+
+WindowNewMap::~WindowNewMap()
+{
+	saveUserSettings();
+	delete ui;
+}
+
+void WindowNewMap::loadUserSettings()
+{
+	//load window settings
+	QSettings s(Ui::teamName, Ui::appName);
+
+	auto width = s.value(newMapWidth);
+	if (width.isValid())
+	{
+		ui->widthTxt->setText(width.toString());
+	}
+	auto height = s.value(newMapHeight);
+	if (height.isValid())
+	{
+		ui->heightTxt->setText(height.toString());
+	}
+	auto twoLevel = s.value(newMapTwoLevel);
+	if (twoLevel.isValid())
+	{
+		ui->twoLevelCheck->setChecked(twoLevel.toBool());
+	}
+	auto generateRandom = s.value(newMapGenerateRandom);
+	if (generateRandom.isValid())
+	{
+		ui->randomMapCheck->setChecked(generateRandom.toBool());
+	}
+	auto players = s.value(newMapPlayers);
+	if (players.isValid())
+	{
+		ui->humanCombo->setCurrentIndex(players.toInt());
+	}
+	auto cpuPlayers = s.value(newMapCpuPlayers);
+	if (cpuPlayers.isValid())
+	{
+		ui->cpuCombo->setCurrentIndex(cpuPlayers.toInt());
+	}
+	//TODO: teams when implemented
+
+	auto waterContent = s.value(newMapWaterContent);
+	if (waterContent.isValid())
+	{
+		switch (waterContent.toInt())
+		{
+			case EWaterContent::RANDOM:
+				ui->waterOpt1->setChecked(true); break;
+			case EWaterContent::NONE:
+				ui->waterOpt2->setChecked(true); break;
+			case EWaterContent::NORMAL:
+				ui->waterOpt3->setChecked(true); break;
+			case EWaterContent::ISLANDS:
+				ui->waterOpt4->setChecked(true); break;
+		}
+
+	}
+	auto monsterStrength = s.value(newMapMonsterStrength);
+	if (monsterStrength.isValid())
+	{
+		switch (monsterStrength.toInt())
+		{
+			case EMonsterStrength::RANDOM:
+				ui->monsterOpt1->setChecked(true); break;
+			case EMonsterStrength::GLOBAL_WEAK:
+				ui->monsterOpt2->setChecked(true); break;
+			case EMonsterStrength::GLOBAL_NORMAL:
+				ui->monsterOpt3->setChecked(true); break;
+			case EMonsterStrength::GLOBAL_STRONG:
+				ui->monsterOpt4->setChecked(true); break;
+		}
+	}
+
+	auto templateName = s.value(newMapTemplate);
+	if (templateName.isValid())
+	{
+		updateTemplateList();
+		
+		auto* templ = VLC->tplh->getTemplate(templateName.toString().toStdString());
+		if (templ)
+		{
+			ui->templateCombo->setCurrentText(templateName.toString());
+			//TODO: validate inside this method
+			mapGenOptions.setMapTemplate(templ);
+		}
+		else
+		{
+			//Display problem on status bar
+		}
+	}
+}
+
+void WindowNewMap::saveUserSettings()
+{
+	QSettings s(Ui::teamName, Ui::appName);
+	s.setValue(newMapWidth, ui->widthTxt->text().toInt());
+	s.setValue(newMapHeight, ui->heightTxt->text().toInt());
+	s.setValue(newMapTwoLevel, ui->twoLevelCheck->isChecked());
+	s.setValue(newMapGenerateRandom, ui->randomMapCheck->isChecked());
+
+	s.setValue(newMapPlayers,ui->humanCombo->currentIndex());
+	s.setValue(newMapCpuPlayers,ui->cpuCombo->currentIndex());
+	//TODO: teams when implemented
+
+	EWaterContent::EWaterContent water = EWaterContent::RANDOM;
+	if(ui->waterOpt1->isChecked())
+		water = EWaterContent::RANDOM;
+	else if(ui->waterOpt2->isChecked())
+		water = EWaterContent::NONE;
+	else if(ui->waterOpt3->isChecked())
+		water = EWaterContent::NORMAL;
+	else if(ui->waterOpt4->isChecked())
+		water = EWaterContent::ISLANDS;
+	s.setValue(newMapWaterContent, static_cast<int>(water));
+
+	EMonsterStrength::EMonsterStrength monster = EMonsterStrength::RANDOM;
+	if(ui->monsterOpt1->isChecked())
+		monster = EMonsterStrength::RANDOM;
+	else if(ui->monsterOpt2->isChecked())
+		monster = EMonsterStrength::GLOBAL_WEAK;
+	else if(ui->monsterOpt3->isChecked())
+		monster = EMonsterStrength::GLOBAL_NORMAL;
+	else if(ui->monsterOpt4->isChecked())
+		monster = EMonsterStrength::GLOBAL_STRONG;
+	s.setValue(newMapMonsterStrength, static_cast<int>(monster));
+
+	auto templateName = ui->templateCombo->currentText();
+	if (templateName.size())
+	{
+		s.setValue(newMapTemplate, templateName);
+	}
+}
+
+void WindowNewMap::on_cancelButton_clicked()
+{
+	close();
+}
+
+void generateRandomMap(CMapGenerator & gen, MainWindow * window)
+{
+	window->controller.setMap(gen.generate());
+}
+
+std::unique_ptr<CMap> generateEmptyMap(CMapGenOptions & options)
+{
+	std::unique_ptr<CMap> map(new CMap);
+	map->version = EMapFormat::VCMI;
+	map->width = options.getWidth();
+	map->height = options.getHeight();
+	map->twoLevel = options.getHasTwoLevels();
+	
+	map->initTerrain();
+	map->getEditManager()->clearTerrain(&CRandomGenerator::getDefault());
+
+	return std::move(map);
+}
+
+void WindowNewMap::on_okButton_clicked()
+{
+	EWaterContent::EWaterContent water = EWaterContent::RANDOM;
+	EMonsterStrength::EMonsterStrength monster = EMonsterStrength::RANDOM;
+	if(ui->waterOpt1->isChecked())
+		water = EWaterContent::RANDOM;
+	if(ui->waterOpt2->isChecked())
+		water = EWaterContent::NONE;
+	if(ui->waterOpt3->isChecked())
+		water = EWaterContent::NORMAL;
+	if(ui->waterOpt4->isChecked())
+		water = EWaterContent::ISLANDS;
+	if(ui->monsterOpt1->isChecked())
+		monster = EMonsterStrength::RANDOM;
+	if(ui->monsterOpt2->isChecked())
+		monster = EMonsterStrength::GLOBAL_WEAK;
+	if(ui->monsterOpt3->isChecked())
+		monster = EMonsterStrength::GLOBAL_NORMAL;
+	if(ui->monsterOpt4->isChecked())
+		monster = EMonsterStrength::GLOBAL_STRONG;
+
+	mapGenOptions.setWaterContent(water);
+	mapGenOptions.setMonsterStrength(monster);
+	
+	std::unique_ptr<CMap> nmap;
+	if(ui->randomMapCheck->isChecked())
+	{
+		//verify map template
+		if(mapGenOptions.getPossibleTemplates().empty())
+		{
+			QMessageBox::warning(this, "No template", "No template for parameters scecified. Random map cannot be generated.");
+			return;
+		}
+		
+		int seed = std::time(nullptr);
+		if(ui->checkSeed->isChecked() && !ui->lineSeed->text().isEmpty())
+			seed = ui->lineSeed->text().toInt();
+			
+		CMapGenerator generator(mapGenOptions, seed);
+		auto progressBarWnd = new GeneratorProgress(generator, this);
+		progressBarWnd->show();
+	
+		try
+		{
+			auto f = std::async(std::launch::async, &CMapGenerator::generate, &generator);
+			progressBarWnd->update();
+			nmap = f.get();
+		}
+		catch(const std::exception & e)
+		{
+			QMessageBox::critical(this, "RMG failure", e.what());
+		}
+	}
+	else
+	{		
+		auto f = std::async(std::launch::async, &::generateEmptyMap, std::ref(mapGenOptions));
+		nmap = f.get();
+	}
+	
+
+	static_cast<MainWindow*>(parent())->controller.setMap(std::move(nmap));
+	static_cast<MainWindow*>(parent())->initializeMap(true);
+	close();
+}
+
+void WindowNewMap::on_sizeCombo_activated(int index)
+{
+	std::map<int, std::pair<int, int>> sizes
+	{
+		{0, {36, 36}},
+		{1, {72, 72}},
+		{2, {108, 108}},
+		{3, {144, 144}},
+	};
+
+	ui->widthTxt->setText(QString::number(sizes[index].first));
+	ui->heightTxt->setText(QString::number(sizes[index].second));
+}
+
+
+void WindowNewMap::on_twoLevelCheck_stateChanged(int arg1)
+{
+	bool twoLevel = ui->twoLevelCheck->isChecked();
+	mapGenOptions.setHasTwoLevels(twoLevel);
+	updateTemplateList();
+}
+
+
+void WindowNewMap::on_humanCombo_activated(int index)
+{
+	int humans = players.at(index);
+	if(humans > playerLimit)
+	{
+		humans = playerLimit;
+		ui->humanCombo->setCurrentIndex(humans);
+		return;
+	}
+
+	mapGenOptions.setPlayerCount(humans);
+
+	int teams = mapGenOptions.getTeamCount();
+	if(teams > humans - 1)
+	{
+		teams = humans - 1;
+		//TBD
+	}
+
+	int cpu = mapGenOptions.getCompOnlyPlayerCount();
+	if(cpu > playerLimit - humans)
+	{
+		cpu = playerLimit - humans;
+		ui->cpuCombo->setCurrentIndex(cpu + 1);
+	}
+
+	int cpuTeams = mapGenOptions.getCompOnlyTeamCount(); //comp only players - 1
+	if(cpuTeams > cpu - 1)
+	{
+		cpuTeams = cpu - 1;
+		//TBD
+	}
+
+	//void setMapTemplate(const CRmgTemplate * value);
+	updateTemplateList();
+}
+
+
+void WindowNewMap::on_cpuCombo_activated(int index)
+{
+	int humans = mapGenOptions.getPlayerCount();
+	int cpu = cpuPlayers.at(index);
+	if(cpu > playerLimit - humans)
+	{
+		cpu = playerLimit - humans;
+		ui->cpuCombo->setCurrentIndex(cpu + 1);
+		return;
+	}
+
+	mapGenOptions.setCompOnlyPlayerCount(cpu);
+	updateTemplateList();
+}
+
+
+void WindowNewMap::on_randomMapCheck_stateChanged(int arg1)
+{
+	randomMap = ui->randomMapCheck->isChecked();
+	ui->templateCombo->setEnabled(randomMap);
+	updateTemplateList();
+}
+
+
+void WindowNewMap::on_templateCombo_activated(int index)
+{
+	if(index == 0)
+	{
+		mapGenOptions.setMapTemplate(nullptr);
+		return;
+	}
+	
+	auto * templ = data_cast<const CRmgTemplate>(ui->templateCombo->currentData().toLongLong());
+	mapGenOptions.setMapTemplate(templ);
+}
+
+
+void WindowNewMap::on_widthTxt_textChanged(const QString &arg1)
+{
+	int sz = arg1.toInt();
+	if(sz > 1)
+	{
+		mapGenOptions.setWidth(arg1.toInt());
+		updateTemplateList();
+	}
+}
+
+
+void WindowNewMap::on_heightTxt_textChanged(const QString &arg1)
+{
+	int sz = arg1.toInt();
+	if(sz > 1)
+	{
+		mapGenOptions.setHeight(arg1.toInt());
+		updateTemplateList();
+	}
+}
+
+void WindowNewMap::updateTemplateList()
+{
+	ui->templateCombo->clear();
+	ui->templateCombo->setCurrentIndex(-1);
+
+	if(!randomMap)
+		return;
+
+	mapGenOptions.setMapTemplate(nullptr);
+	auto templates = mapGenOptions.getPossibleTemplates();
+	if(templates.empty())
+		return;
+
+	ui->templateCombo->addItem("[default]", 0);
+
+	for(auto * templ : templates)
+	{
+		ui->templateCombo->addItem(QString::fromStdString(templ->getName()), data_cast(templ));
+	}
+
+	ui->templateCombo->setCurrentIndex(0);
+}
+
+void WindowNewMap::on_checkSeed_toggled(bool checked)
+{
+	ui->lineSeed->setEnabled(checked);
+}
+

+ 102 - 0
mapeditor/windownewmap.h

@@ -0,0 +1,102 @@
+/*
+ * windownewmap.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
+
+#include <QDialog>
+#include "../lib/rmg/CMapGenOptions.h"
+
+namespace Ui
+{
+	class WindowNewMap;
+}
+
+class WindowNewMap : public QDialog
+{
+	Q_OBJECT
+
+	const QString newMapWidth = "NewMapWindow/Width";
+	const QString newMapHeight = "NewMapWindow/Height";
+	const QString newMapTwoLevel = "NewMapWindow/TwoLevel";
+	const QString newMapGenerateRandom = "NewMapWindow/GenerateRandom";
+	const QString newMapPlayers = "NewMapWindow/Players";		//map index
+	const QString newMapCpuPlayers = "NewMapWindow/CpuPlayers"; //map index
+	const QString newMapWaterContent = "NewMapWindow/WaterContent";
+	const QString newMapMonsterStrength = "NewMapWindow/MonsterStrength";
+	const QString newMapTemplate = "NewMapWindow/Template";
+
+	const int playerLimit = 8;
+
+	const std::map<int, int> players
+	{
+		{0, CMapGenOptions::RANDOM_SIZE},
+		{1, 1},
+		{2, 2},
+		{3, 3},
+		{4, 4},
+		{5, 5},
+		{6, 6},
+		{7, 7},
+		{8, 8}
+	};
+
+	const std::map<int, int> cpuPlayers
+	{
+		{0, CMapGenOptions::RANDOM_SIZE},
+		{1, 0},
+		{2, 1},
+		{3, 2},
+		{4, 3},
+		{5, 4},
+		{6, 5},
+		{7, 6},
+		{8, 7}
+	};
+
+public:
+	explicit WindowNewMap(QWidget *parent = nullptr);
+	~WindowNewMap();
+
+private slots:
+	void on_cancelButton_clicked();
+
+	void on_okButton_clicked();
+
+	void on_sizeCombo_activated(int index);
+
+	void on_twoLevelCheck_stateChanged(int arg1);
+
+	void on_humanCombo_activated(int index);
+
+	void on_cpuCombo_activated(int index);
+
+	void on_randomMapCheck_stateChanged(int arg1);
+
+	void on_templateCombo_activated(int index);
+
+	void on_widthTxt_textChanged(const QString &arg1);
+
+	void on_heightTxt_textChanged(const QString &arg1);
+
+	void on_checkSeed_toggled(bool checked);
+
+private:
+
+	void updateTemplateList();
+
+	void loadUserSettings();
+	void saveUserSettings();
+
+private:
+	Ui::WindowNewMap *ui;
+
+	CMapGenOptions mapGenOptions;
+	bool randomMap = false;
+};

+ 816 - 0
mapeditor/windownewmap.ui

@@ -0,0 +1,816 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>WindowNewMap</class>
+ <widget class="QDialog" name="WindowNewMap">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>444</width>
+    <height>445</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>390</width>
+    <height>351</height>
+   </size>
+  </property>
+  <property name="maximumSize">
+   <size>
+    <width>999</width>
+    <height>999</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Create new map</string>
+  </property>
+  <property name="modal">
+   <bool>false</bool>
+  </property>
+  <widget class="QGroupBox" name="groupBox">
+   <property name="geometry">
+    <rect>
+     <x>10</x>
+     <y>20</y>
+     <width>291</width>
+     <height>91</height>
+    </rect>
+   </property>
+   <property name="title">
+    <string>Map size</string>
+   </property>
+   <widget class="QWidget" name="layoutWidget">
+    <property name="geometry">
+     <rect>
+      <x>0</x>
+      <y>20</y>
+      <width>261</width>
+      <height>68</height>
+     </rect>
+    </property>
+    <layout class="QGridLayout" name="gridLayout_2" columnstretch="1,0,0">
+     <item row="1" column="0">
+      <widget class="QCheckBox" name="twoLevelCheck">
+       <property name="text">
+        <string>Two level map</string>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="2">
+      <widget class="QLineEdit" name="widthTxt">
+       <property name="inputMethodHints">
+        <set>Qt::ImhDigitsOnly</set>
+       </property>
+       <property name="text">
+        <string>36</string>
+       </property>
+       <property name="maxLength">
+        <number>3</number>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="1">
+      <widget class="QLabel" name="label_2">
+       <property name="text">
+        <string>Height</string>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="2">
+      <widget class="QLineEdit" name="heightTxt">
+       <property name="inputMethodHints">
+        <set>Qt::ImhDigitsOnly</set>
+       </property>
+       <property name="text">
+        <string>36</string>
+       </property>
+       <property name="maxLength">
+        <number>3</number>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="1">
+      <widget class="QLabel" name="label">
+       <property name="minimumSize">
+        <size>
+         <width>48</width>
+         <height>0</height>
+        </size>
+       </property>
+       <property name="maximumSize">
+        <size>
+         <width>96</width>
+         <height>16777215</height>
+        </size>
+       </property>
+       <property name="text">
+        <string>Width</string>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="0">
+      <layout class="QHBoxLayout" name="horizontalLayout_4">
+       <item>
+        <spacer name="horizontalSpacer_5">
+         <property name="orientation">
+          <enum>Qt::Horizontal</enum>
+         </property>
+         <property name="sizeType">
+          <enum>QSizePolicy::Fixed</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>8</width>
+           <height>20</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+       <item>
+        <widget class="QComboBox" name="sizeCombo">
+         <property name="minimumSize">
+          <size>
+           <width>96</width>
+           <height>0</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>120</width>
+           <height>16777215</height>
+          </size>
+         </property>
+         <item>
+          <property name="text">
+           <string>S (36x36)</string>
+          </property>
+         </item>
+         <item>
+          <property name="text">
+           <string>M (72x72)</string>
+          </property>
+         </item>
+         <item>
+          <property name="text">
+           <string>L (108x108)</string>
+          </property>
+         </item>
+         <item>
+          <property name="text">
+           <string>XL (144x144)</string>
+          </property>
+         </item>
+        </widget>
+       </item>
+      </layout>
+     </item>
+    </layout>
+   </widget>
+  </widget>
+  <widget class="QGroupBox" name="groupBox_5">
+   <property name="geometry">
+    <rect>
+     <x>10</x>
+     <y>140</y>
+     <width>431</width>
+     <height>301</height>
+    </rect>
+   </property>
+   <property name="sizePolicy">
+    <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+     <horstretch>0</horstretch>
+     <verstretch>0</verstretch>
+    </sizepolicy>
+   </property>
+   <property name="title">
+    <string>Random map</string>
+   </property>
+   <widget class="QGroupBox" name="groupBox_2">
+    <property name="geometry">
+     <rect>
+      <x>10</x>
+      <y>20</y>
+      <width>411</width>
+      <height>91</height>
+     </rect>
+    </property>
+    <property name="title">
+     <string>Players</string>
+    </property>
+    <widget class="QWidget" name="layoutWidget">
+     <property name="geometry">
+      <rect>
+       <x>10</x>
+       <y>20</y>
+       <width>391</width>
+       <height>68</height>
+      </rect>
+     </property>
+     <layout class="QGridLayout" name="gridLayout" columnstretch="0,0,0,0,1">
+      <item row="0" column="3">
+       <widget class="QComboBox" name="humanTeamsCombo">
+        <property name="minimumSize">
+         <size>
+          <width>48</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>64</width>
+          <height>16777215</height>
+         </size>
+        </property>
+        <item>
+         <property name="text">
+          <string>0</string>
+         </property>
+        </item>
+       </widget>
+      </item>
+      <item row="1" column="3">
+       <widget class="QComboBox" name="cpuTeamsCombo">
+        <item>
+         <property name="text">
+          <string>0</string>
+         </property>
+        </item>
+       </widget>
+      </item>
+      <item row="0" column="0">
+       <widget class="QLabel" name="label_3">
+        <property name="minimumSize">
+         <size>
+          <width>96</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>120</width>
+          <height>16777215</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Human/Computer</string>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="1">
+       <widget class="QComboBox" name="humanCombo">
+        <property name="minimumSize">
+         <size>
+          <width>96</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>120</width>
+          <height>16777215</height>
+         </size>
+        </property>
+        <item>
+         <property name="text">
+          <string>Random</string>
+         </property>
+        </item>
+        <item>
+         <property name="text">
+          <string>1</string>
+         </property>
+        </item>
+        <item>
+         <property name="text">
+          <string>2</string>
+         </property>
+        </item>
+        <item>
+         <property name="text">
+          <string>3</string>
+         </property>
+        </item>
+        <item>
+         <property name="text">
+          <string>4</string>
+         </property>
+        </item>
+        <item>
+         <property name="text">
+          <string>5</string>
+         </property>
+        </item>
+        <item>
+         <property name="text">
+          <string>6</string>
+         </property>
+        </item>
+        <item>
+         <property name="text">
+          <string>7</string>
+         </property>
+        </item>
+        <item>
+         <property name="text">
+          <string>8</string>
+         </property>
+        </item>
+       </widget>
+      </item>
+      <item row="1" column="0">
+       <widget class="QLabel" name="label_4">
+        <property name="text">
+         <string>Computer only</string>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="2">
+       <spacer name="horizontalSpacer_3">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>40</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item row="1" column="1">
+       <widget class="QComboBox" name="cpuCombo">
+        <item>
+         <property name="text">
+          <string>Random</string>
+         </property>
+        </item>
+        <item>
+         <property name="text">
+          <string>0</string>
+         </property>
+        </item>
+        <item>
+         <property name="text">
+          <string>1</string>
+         </property>
+        </item>
+        <item>
+         <property name="text">
+          <string>2</string>
+         </property>
+        </item>
+        <item>
+         <property name="text">
+          <string>3</string>
+         </property>
+        </item>
+        <item>
+         <property name="text">
+          <string>4</string>
+         </property>
+        </item>
+        <item>
+         <property name="text">
+          <string>5</string>
+         </property>
+        </item>
+        <item>
+         <property name="text">
+          <string>6</string>
+         </property>
+        </item>
+        <item>
+         <property name="text">
+          <string>7</string>
+         </property>
+        </item>
+       </widget>
+      </item>
+      <item row="1" column="4">
+       <spacer name="horizontalSpacer_4">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>40</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+     </layout>
+    </widget>
+   </widget>
+   <widget class="QGroupBox" name="groupBox_4">
+    <property name="geometry">
+     <rect>
+      <x>10</x>
+      <y>170</y>
+      <width>411</width>
+      <height>51</height>
+     </rect>
+    </property>
+    <property name="title">
+     <string>Monster strength</string>
+    </property>
+    <widget class="QWidget" name="layoutWidget">
+     <property name="geometry">
+      <rect>
+       <x>0</x>
+       <y>20</y>
+       <width>411</width>
+       <height>26</height>
+      </rect>
+     </property>
+     <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,1,1,1,1">
+      <item>
+       <widget class="QRadioButton" name="monsterOpt1">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>16777215</width>
+          <height>16777215</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Random</string>
+        </property>
+        <property name="checked">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QRadioButton" name="monsterOpt2">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>120</width>
+          <height>16777215</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Weak</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QRadioButton" name="monsterOpt3">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>120</width>
+          <height>16777215</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Normal</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QRadioButton" name="monsterOpt4">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>120</width>
+          <height>16777215</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Strong</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <spacer name="horizontalSpacer_2">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>16777215</width>
+          <height>16777215</height>
+         </size>
+        </property>
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>40</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+     </layout>
+    </widget>
+   </widget>
+   <widget class="QGroupBox" name="groupBox_3">
+    <property name="geometry">
+     <rect>
+      <x>10</x>
+      <y>120</y>
+      <width>411</width>
+      <height>51</height>
+     </rect>
+    </property>
+    <property name="sizePolicy">
+     <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+      <horstretch>0</horstretch>
+      <verstretch>0</verstretch>
+     </sizepolicy>
+    </property>
+    <property name="maximumSize">
+     <size>
+      <width>480</width>
+      <height>96</height>
+     </size>
+    </property>
+    <property name="title">
+     <string>Water content</string>
+    </property>
+    <widget class="QWidget" name="layoutWidget">
+     <property name="geometry">
+      <rect>
+       <x>0</x>
+       <y>20</y>
+       <width>411</width>
+       <height>26</height>
+      </rect>
+     </property>
+     <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,1,1,1,1">
+      <item>
+       <widget class="QRadioButton" name="waterOpt1">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>144</width>
+          <height>96</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Random</string>
+        </property>
+        <property name="checked">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QRadioButton" name="waterOpt2">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>144</width>
+          <height>96</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>None</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QRadioButton" name="waterOpt3">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>144</width>
+          <height>96</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Normal</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QRadioButton" name="waterOpt4">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>144</width>
+          <height>96</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Islands</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <spacer name="horizontalSpacer">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>40</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+     </layout>
+    </widget>
+   </widget>
+   <widget class="QWidget" name="layoutWidget">
+    <property name="geometry">
+     <rect>
+      <x>10</x>
+      <y>230</y>
+      <width>411</width>
+      <height>32</height>
+     </rect>
+    </property>
+    <layout class="QHBoxLayout" name="horizontalLayout_3">
+     <item>
+      <widget class="QLabel" name="label_5">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="maximumSize">
+        <size>
+         <width>120</width>
+         <height>16777215</height>
+        </size>
+       </property>
+       <property name="text">
+        <string>Template</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QComboBox" name="templateCombo">
+       <property name="enabled">
+        <bool>false</bool>
+       </property>
+       <property name="currentIndex">
+        <number>-1</number>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </widget>
+   <widget class="QLineEdit" name="lineSeed">
+    <property name="enabled">
+     <bool>false</bool>
+    </property>
+    <property name="geometry">
+     <rect>
+      <x>280</x>
+      <y>270</y>
+      <width>131</width>
+      <height>21</height>
+     </rect>
+    </property>
+    <property name="inputMethodHints">
+     <set>Qt::ImhDigitsOnly</set>
+    </property>
+    <property name="text">
+     <string>0</string>
+    </property>
+   </widget>
+   <widget class="QCheckBox" name="checkSeed">
+    <property name="geometry">
+     <rect>
+      <x>110</x>
+      <y>270</y>
+      <width>161</width>
+      <height>20</height>
+     </rect>
+    </property>
+    <property name="text">
+     <string>Custom seed</string>
+    </property>
+   </widget>
+  </widget>
+  <widget class="QCheckBox" name="randomMapCheck">
+   <property name="geometry">
+    <rect>
+     <x>10</x>
+     <y>120</y>
+     <width>291</width>
+     <height>20</height>
+    </rect>
+   </property>
+   <property name="text">
+    <string>Generate random map</string>
+   </property>
+  </widget>
+  <widget class="QWidget" name="layoutWidget">
+   <property name="geometry">
+    <rect>
+     <x>310</x>
+     <y>20</y>
+     <width>111</width>
+     <height>101</height>
+    </rect>
+   </property>
+   <layout class="QVBoxLayout" name="verticalLayout">
+    <item>
+     <widget class="QPushButton" name="okButton">
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Minimum" vsizetype="Expanding">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <property name="minimumSize">
+       <size>
+        <width>0</width>
+        <height>36</height>
+       </size>
+      </property>
+      <property name="maximumSize">
+       <size>
+        <width>16777215</width>
+        <height>36</height>
+       </size>
+      </property>
+      <property name="text">
+       <string>Ok</string>
+      </property>
+     </widget>
+    </item>
+    <item>
+     <widget class="QPushButton" name="cancelButton">
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Minimum" vsizetype="Expanding">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <property name="maximumSize">
+       <size>
+        <width>16777215</width>
+        <height>36</height>
+       </size>
+      </property>
+      <property name="text">
+       <string>Cancel</string>
+      </property>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>