Browse Source

Merge remote-tracking branch 'upstream/develop' into lobby

nordsoft 3 years ago
parent
commit
1c84cc4521
100 changed files with 10312 additions and 327 deletions
  1. 6 2
      .github/workflows/github.yml
  2. 1 1
      .gitignore
  3. 1 1
      .travis.yml
  4. 56 22
      AI/BattleAI/AttackPossibility.cpp
  5. 17 5
      AI/BattleAI/AttackPossibility.h
  6. 103 64
      AI/BattleAI/BattleAI.cpp
  7. 689 0
      AI/BattleAI/BattleExchangeVariant.cpp
  8. 107 0
      AI/BattleAI/BattleExchangeVariant.h
  9. 2 0
      AI/BattleAI/CMakeLists.txt
  10. 17 22
      AI/BattleAI/PotentialTargets.cpp
  11. 2 2
      AI/BattleAI/PotentialTargets.h
  12. 15 1
      AI/BattleAI/StackWithBonuses.cpp
  13. 1 0
      AI/BattleAI/StackWithBonuses.h
  14. 6 1
      AI/Nullkiller/AIGateway.cpp
  15. 1 1
      AI/Nullkiller/CMakeLists.txt
  16. 5 4
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  17. 14 0
      CI/conan/ios-arm64
  18. 14 0
      CI/conan/ios-armv7
  19. 2 0
      CI/conan/macos-arm
  20. 2 0
      CI/conan/macos-intel
  21. 2 2
      CI/ios/before_install.sh
  22. 1 1
      CI/mac/before_install.sh
  23. 21 20
      CMakeLists.txt
  24. 54 12
      CMakePresets.json
  25. 1 0
      README.md
  26. 2 0
      client/CMakeLists.txt
  27. 13 0
      cmake_modules/VCMIUtils.cmake
  28. 1 1
      cmake_modules/VCMI_lib.cmake
  29. 13 0
      cmake_modules/VersionDefinition.cmake
  30. 165 145
      conanfile.py
  31. 109 0
      config/schemas/terrain.json
  32. 147 0
      docs/conan.md
  33. 1 0
      ios/CMakeLists.txt
  34. 7 0
      ios/rpath_remove_symlinks.sh
  35. 12 0
      launcher/CMakeLists.txt
  36. 1 0
      launcher/eu.vcmi.VCMI.metainfo.xml
  37. 7 0
      launcher/main.cpp
  38. 1 1
      launcher/settingsView/csettingsview_moc.cpp
  39. 6 1
      lib/battle/ReachabilityInfo.cpp
  40. 1 1
      lib/mapping/MapFormatH3M.cpp
  41. 2 12
      lib/rmg/CRmgTemplateStorage.cpp
  42. 2 4
      lib/rmg/CRmgTemplateStorage.h
  43. 1 1
      lib/spells/ISpellMechanics.cpp
  44. 777 0
      mapeditor/Animation.cpp
  45. 94 0
      mapeditor/Animation.h
  46. 160 0
      mapeditor/BitmapHandler.cpp
  47. 23 0
      mapeditor/BitmapHandler.h
  48. 141 0
      mapeditor/CMakeLists.txt
  49. 1 0
      mapeditor/StdInc.cpp
  50. 55 0
      mapeditor/StdInc.h
  51. 47 0
      mapeditor/generatorprogress.cpp
  52. 32 0
      mapeditor/generatorprogress.h
  53. 46 0
      mapeditor/generatorprogress.ui
  54. 345 0
      mapeditor/graphics.cpp
  55. 85 0
      mapeditor/graphics.h
  56. BIN
      mapeditor/icons/mapeditor.128x128.png
  57. BIN
      mapeditor/icons/mapeditor.256x256.png
  58. BIN
      mapeditor/icons/mapeditor.32x32.png
  59. BIN
      mapeditor/icons/mapeditor.48x48.png
  60. BIN
      mapeditor/icons/mapeditor.64x64.png
  61. BIN
      mapeditor/icons/menu-game.png
  62. BIN
      mapeditor/icons/menu-mods.png
  63. BIN
      mapeditor/icons/menu-settings.png
  64. BIN
      mapeditor/icons/mod-delete.png
  65. BIN
      mapeditor/icons/mod-disabled.png
  66. BIN
      mapeditor/icons/mod-download.png
  67. BIN
      mapeditor/icons/mod-enabled.png
  68. BIN
      mapeditor/icons/mod-update.png
  69. 152 0
      mapeditor/inspector/armywidget.cpp
  70. 57 0
      mapeditor/inspector/armywidget.h
  71. 298 0
      mapeditor/inspector/armywidget.ui
  72. 849 0
      mapeditor/inspector/inspector.cpp
  73. 164 0
      mapeditor/inspector/inspector.h
  74. 63 0
      mapeditor/inspector/messagewidget.cpp
  75. 44 0
      mapeditor/inspector/messagewidget.h
  76. 33 0
      mapeditor/inspector/messagewidget.ui
  77. 193 0
      mapeditor/inspector/questwidget.cpp
  78. 52 0
      mapeditor/inspector/questwidget.h
  79. 50 0
      mapeditor/inspector/questwidget.ui
  80. 427 0
      mapeditor/inspector/rewardswidget.cpp
  81. 98 0
      mapeditor/inspector/rewardswidget.h
  82. 83 0
      mapeditor/inspector/rewardswidget.ui
  83. 250 0
      mapeditor/inspector/townbulidingswidget.cpp
  84. 63 0
      mapeditor/inspector/townbulidingswidget.h
  85. 49 0
      mapeditor/inspector/townbulidingswidget.ui
  86. 125 0
      mapeditor/jsonutils.cpp
  87. 22 0
      mapeditor/jsonutils.h
  88. 36 0
      mapeditor/launcherdirs.cpp
  89. 22 0
      mapeditor/launcherdirs.h
  90. 19 0
      mapeditor/main.cpp
  91. 1110 0
      mapeditor/mainwindow.cpp
  92. 145 0
      mapeditor/mainwindow.h
  93. 1120 0
      mapeditor/mainwindow.ui
  94. 508 0
      mapeditor/mapcontroller.cpp
  95. 69 0
      mapeditor/mapcontroller.h
  96. BIN
      mapeditor/mapeditor.ico
  97. 1 0
      mapeditor/mapeditor.rc
  98. 546 0
      mapeditor/maphandler.cpp
  99. 115 0
      mapeditor/maphandler.h
  100. 112 0
      mapeditor/mapsettings.cpp

+ 6 - 2
.github/workflows/github.yml

@@ -95,7 +95,8 @@ jobs:
             test: 0
             pack: 1
             extension: ipa
-            preset: ios-release
+            preset: ios-release-conan
+            conan_profile: ios-arm64
           - platform: mxe
             os: ubuntu-20.04
             mxe: i686-w64-mingw32.shared
@@ -136,7 +137,10 @@ jobs:
           --no-imports \
           --build=never \
           --profile:build=default \
-          --profile:host=CI/conan/${{ matrix.conan_profile }}
+          --profile:host=CI/conan/${{ matrix.conan_profile }} \
+          --options with_apple_system_libs=True
+      env:
+        GENERATE_ONLY_BUILT_CONFIG: 1
 
     - name: Git branch name
       id: git-branch-name

+ 1 - 1
.gitignore

@@ -2,7 +2,7 @@
 /server/vcmiserver
 /launcher/vcmilauncher
 /launcher/vcmilauncher_automoc.cpp
-/conan-generated
+/conan-*
 
 build/
 .cache/*

+ 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
 

+ 56 - 22
AI/BattleAI/AttackPossibility.cpp

@@ -13,6 +13,11 @@
                               // Eventually only IBattleInfoCallback and battle::Unit should be used, 
                               // CUnitState should be private and CStack should be removed completely
 
+uint64_t averageDmg(const TDmgRange & range)
+{
+	return (range.first + range.second) / 2;
+}
+
 AttackPossibility::AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack)
 	: from(from), dest(dest), attack(attack)
 {
@@ -20,7 +25,7 @@ AttackPossibility::AttackPossibility(BattleHex from, BattleHex dest, const Battl
 
 int64_t AttackPossibility::damageDiff() const
 {
-	return damageDealt - damageReceived - collateralDamage + shootersBlockedDmg;
+	return defenderDamageReduce - attackerDamageReduce - collateralDamageReduce + shootersBlockedDmg;
 }
 
 int64_t AttackPossibility::attackValue() const
@@ -28,7 +33,31 @@ int64_t AttackPossibility::attackValue() const
 	return damageDiff();
 }
 
-int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle * state)
+/// <summary>
+/// How enemy damage will be reduced by this attack
+/// Half bounty for kill, half for making damage equal to enemy health
+/// Bounty - the killed creature average damage calculated against attacker
+/// </summary>
+int64_t AttackPossibility::calculateDamageReduce(
+	const battle::Unit * attacker,
+	const battle::Unit * defender,
+	uint64_t damageDealt,
+	const CBattleInfoCallback & cb)
+{
+	const float HEALTH_BOUNTY = 0.5;
+	const float KILL_BOUNTY = 1.0 - HEALTH_BOUNTY;
+
+	vstd::amin(damageDealt, defender->getAvailableHealth());
+
+	auto enemyDamageBeforeAttack = cb.battleEstimateDamage(BattleAttackInfo(defender, attacker, defender->canShoot()));
+	auto enemiesKilled = damageDealt / defender->MaxHealth() + (damageDealt % defender->MaxHealth() >= defender->getFirstHPleft() ? 1 : 0);
+	auto enemyDamage = averageDmg(enemyDamageBeforeAttack);
+	auto damagePerEnemy = enemyDamage / (double)defender->getCount();
+
+	return (int64_t)(damagePerEnemy * (enemiesKilled * KILL_BOUNTY + damageDealt * HEALTH_BOUNTY / (double)defender->MaxHealth()));
+}
+
+int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle & state)
 {
 	int64_t res = 0;
 
@@ -39,10 +68,10 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & a
 	auto hexes = attacker->getSurroundingHexes(hex);
 	for(BattleHex tile : hexes)
 	{
-		auto st = state->battleGetUnitByPos(tile, true);
-		if(!st || !state->battleMatchOwner(st, attacker))
+		auto st = state.battleGetUnitByPos(tile, true);
+		if(!st || !state.battleMatchOwner(st, attacker))
 			continue;
-		if(!state->battleCanShoot(st))
+		if(!state.battleCanShoot(st))
 			continue;
 
 		BattleAttackInfo rangeAttackInfo(st, attacker, true);
@@ -51,23 +80,23 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & a
 		BattleAttackInfo meleeAttackInfo(st, attacker, false);
 		meleeAttackInfo.defenderPos = hex;
 
-		auto rangeDmg = getCbc()->battleEstimateDamage(rangeAttackInfo);
-		auto meleeDmg = getCbc()->battleEstimateDamage(meleeAttackInfo);
+		auto rangeDmg = state.battleEstimateDamage(rangeAttackInfo);
+		auto meleeDmg = state.battleEstimateDamage(meleeAttackInfo);
 
-		int64_t gain = (rangeDmg.first + rangeDmg.second - meleeDmg.first - meleeDmg.second) / 2 + 1;
+		int64_t gain = averageDmg(rangeDmg) - averageDmg(meleeDmg) + 1;
 		res += gain;
 	}
 
 	return res;
 }
 
-AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle * state)
+AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle & state)
 {
 	auto attacker = attackInfo.attacker;
 	auto defender = attackInfo.defender;
 	const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION";
 	static const auto selectorBlocksRetaliation = Selector::type()(Bonus::BLOCKS_RETALIATION);
-	const auto attackerSide = getCbc()->playerToSide(getCbc()->battleGetOwner(attacker));
+	const auto attackerSide = state.playerToSide(state.battleGetOwner(attacker));
 	const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation);
 
 	AttackPossibility bestAp(hex, BattleHex::INVALID, attackInfo);
@@ -95,9 +124,9 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf
 		std::vector<const battle::Unit*> units;
 
 		if (attackInfo.shooting)
-			units = state->getAttackedBattleUnits(attacker, defHex, true, BattleHex::INVALID);
+			units = state.getAttackedBattleUnits(attacker, defHex, true, BattleHex::INVALID);
 		else
-			units = state->getAttackedBattleUnits(attacker, defHex, false, hex);
+			units = state.getAttackedBattleUnits(attacker, defHex, false, hex);
 
 		// ensure the defender is also affected
 		bool addDefender = true;
@@ -123,10 +152,11 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf
 
 			for(int i = 0; i < totalAttacks; i++)
 			{
-				si64 damageDealt, damageReceived;
+				int64_t damageDealt, damageReceived, defenderDamageReduce, attackerDamageReduce;
 
 				TDmgRange retaliation(0, 0);
-				auto attackDmg = getCbc()->battleEstimateDamage(ap.attack, &retaliation);
+				auto attackDmg = state.battleEstimateDamage(ap.attack, &retaliation);
+				TDmgRange defenderDamageBeforeAttack = state.battleEstimateDamage(BattleAttackInfo(u, attacker, u->canShoot()));
 
 				vstd::amin(attackDmg.first, defenderState->getAvailableHealth());
 				vstd::amin(attackDmg.second, defenderState->getAvailableHealth());
@@ -134,32 +164,36 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf
 				vstd::amin(retaliation.first, ap.attackerState->getAvailableHealth());
 				vstd::amin(retaliation.second, ap.attackerState->getAvailableHealth());
 
-				damageDealt = (attackDmg.first + attackDmg.second) / 2;
+				damageDealt = averageDmg(attackDmg);
+				defenderDamageReduce = calculateDamageReduce(attacker, defender, damageDealt, state);
 				ap.attackerState->afterAttack(attackInfo.shooting, false);
 
 				//FIXME: use ranged retaliation
 				damageReceived = 0;
+				attackerDamageReduce = 0;
+
 				if (!attackInfo.shooting && defenderState->ableToRetaliate() && !counterAttacksBlocked)
 				{
-					damageReceived = (retaliation.first + retaliation.second) / 2;
+					damageReceived = averageDmg(retaliation);
+					attackerDamageReduce = calculateDamageReduce(defender, attacker, damageReceived, state);
 					defenderState->afterAttack(attackInfo.shooting, true);
 				}
 
-				bool isEnemy = state->battleMatchOwner(attacker, u);
+				bool isEnemy = state.battleMatchOwner(attacker, u);
 
 				// this includes enemy units as well as attacker units under enemy's mind control
 				if(isEnemy)
-					ap.damageDealt += damageDealt;
+					ap.defenderDamageReduce += defenderDamageReduce;
 
 				// damaging attacker's units (even those under enemy's mind control) is considered friendly fire
 				if(attackerSide == u->unitSide())
-					ap.collateralDamage += damageDealt;
+					ap.collateralDamageReduce += defenderDamageReduce;
 
 				if(u->unitId() == defender->unitId() || 
 					(!attackInfo.shooting && CStack::isMeleeAttackPossible(u, attacker, hex)))
 				{
 					//FIXME: handle RANGED_RETALIATION ?
-					ap.damageReceived += damageReceived;
+					ap.attackerDamageReduce += attackerDamageReduce;
 				}
 
 				ap.attackerState->damage(damageReceived);
@@ -177,11 +211,11 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf
 	// check how much damage we gain from blocking enemy shooters on this hex
 	bestAp.shootersBlockedDmg = evaluateBlockedShootersDmg(attackInfo, hex, state);
 
-	logAi->debug("BattleAI best AP: %s -> %s at %d from %d, affects %d units: %lld %lld %lld %lld",
+	logAi->debug("BattleAI best AP: %s -> %s at %d from %d, affects %d units: d:%lld a:%lld c:%lld s:%lld",
 		attackInfo.attacker->unitType()->identifier,
 		attackInfo.defender->unitType()->identifier,
 		(int)bestAp.dest, (int)bestAp.from, (int)bestAp.affectedUnits.size(),
-		bestAp.damageDealt, bestAp.damageReceived, bestAp.collateralDamage, bestAp.shootersBlockedDmg);
+		bestAp.defenderDamageReduce, bestAp.attackerDamageReduce, bestAp.collateralDamageReduce, bestAp.shootersBlockedDmg);
 
 	//TODO other damage related to attack (eg. fire shield and other abilities)
 	return bestAp;

+ 17 - 5
AI/BattleAI/AttackPossibility.h

@@ -13,6 +13,12 @@
 #include "common.h"
 #include "StackWithBonuses.h"
 
+#define BATTLE_TRACE_LEVEL 0
+
+/// <summary>
+/// Evaluate attack value of one particular attack taking into account various effects like
+/// retaliation, 2-hex breath, collateral damage, shooters blocked damage
+/// </summary>
 class AttackPossibility
 {
 public:
@@ -24,9 +30,9 @@ public:
 
 	std::vector<std::shared_ptr<battle::CUnitState>> affectedUnits;
 
-	int64_t damageDealt = 0;
-	int64_t damageReceived = 0; //usually by counter-attack
-	int64_t collateralDamage = 0; // friendly fire (usually by two-hex attacks)
+	int64_t defenderDamageReduce = 0;
+	int64_t attackerDamageReduce = 0; //usually by counter-attack
+	int64_t collateralDamageReduce = 0; // friendly fire (usually by two-hex attacks)
 	int64_t shootersBlockedDmg = 0;
 
 	AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack_);
@@ -34,8 +40,14 @@ public:
 	int64_t damageDiff() const;
 	int64_t attackValue() const;
 
-	static AttackPossibility evaluate(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle * state);
+	static AttackPossibility evaluate(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle & state);
+
+	static int64_t calculateDamageReduce(
+		const battle::Unit * attacker,
+		const battle::Unit * defender,
+		uint64_t damageDealt,
+		const CBattleInfoCallback & cb);
 
 private:
-	static int64_t evaluateBlockedShootersDmg(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle * state);
+	static int64_t evaluateBlockedShootersDmg(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle & state);
 };

+ 103 - 64
AI/BattleAI/BattleAI.cpp

@@ -9,6 +9,7 @@
  */
 #include "StdInc.h"
 #include "BattleAI.h"
+#include "BattleExchangeVariant.h"
 
 #include "StackWithBonuses.h"
 #include "EnemyInfo.h"
@@ -92,8 +93,11 @@ void CBattleAI::init(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCa
 
 BattleAction CBattleAI::activeStack( const CStack * stack )
 {
-	LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName())	;
+	LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName());
+
+	BattleAction result = BattleAction::makeDefend(stack);
 	setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too)
+
 	try
 	{
 		if(stack->type->idNumber == CreatureID::CATAPULT)
@@ -157,72 +161,86 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 		}
 
 		HypotheticBattle hb(env.get(), cb);
+		
+		PotentialTargets targets(stack, hb);
+		BattleExchangeEvaluator scoreEvaluator(cb, env);
+		auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, targets, hb);
 
-		PotentialTargets targets(stack, &hb);
+		int64_t score = EvaluationResult::INEFFECTIVE_SCORE;
 
 		if(!targets.possibleAttacks.empty())
 		{
-			AttackPossibility bestAttack = targets.bestAction();
+#if BATTLE_TRACE_LEVEL>=1
+			logAi->trace("Evaluating attack for %s", stack->getDescription());
+#endif
+
+			auto evaluationResult = scoreEvaluator.findBestTarget(stack, targets, hb);
+			auto & bestAttack = evaluationResult.bestAttack;
 
 			//TODO: consider more complex spellcast evaluation, f.e. because "re-retaliation" during enemy move in same turn for melee attack etc.
 			if(bestSpellcast.is_initialized() && bestSpellcast->value > bestAttack.damageDiff())
-				return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id);
-			else if(bestAttack.attack.shooting)
 			{
-				auto &target = bestAttack;
-				logAi->debug("BattleAI: %s -> %s x %d, shot, from %d curpos %d dist %d speed %d: %lld %lld %lld",
-					target.attackerState->unitType()->identifier,
-					target.affectedUnits[0]->unitType()->identifier,
-					(int)target.affectedUnits.size(), (int)target.from, (int)bestAttack.attack.attacker->getPosition().hex,
-					bestAttack.attack.chargedFields, bestAttack.attack.attacker->Speed(0, true),
-					target.damageDealt, target.damageReceived, target.attackValue()
-				);
-
-				return BattleAction::makeShotAttack(stack, bestAttack.attack.defender);
+				// return because spellcast value is damage dealt and score is dps reduce
+				return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id);
 			}
-			else
+
+			if(evaluationResult.score > score)
 			{
-				auto &target = bestAttack;
-				logAi->debug("BattleAI: %s -> %s x %d, mellee, from %d curpos %d dist %d speed %d: %lld %lld %lld",
-					target.attackerState->unitType()->identifier,
-					target.affectedUnits[0]->unitType()->identifier,
-					(int)target.affectedUnits.size(), (int)target.from, (int)bestAttack.attack.attacker->getPosition().hex,
+				auto & target = bestAttack;
+				score = evaluationResult.score;
+				std::string action;
+
+				if(evaluationResult.wait)
+				{
+					result = BattleAction::makeWait(stack);
+					action = "wait";
+				}
+				else if(bestAttack.attack.shooting)
+				{
+
+					result = BattleAction::makeShotAttack(stack, bestAttack.attack.defender);
+					action = "shot";
+				}
+				else
+				{
+					result = BattleAction::makeMeleeAttack(stack, bestAttack.attack.defender->getPosition(), bestAttack.from);
+					action = "melee";
+				}
+
+				logAi->debug("BattleAI: %s -> %s x %d, %s, from %d curpos %d dist %d speed %d: +%lld -%lld = %lld",
+					bestAttack.attackerState->unitType()->identifier,
+					bestAttack.affectedUnits[0]->unitType()->identifier,
+					(int)bestAttack.affectedUnits[0]->getCount(), action, (int)bestAttack.from, (int)bestAttack.attack.attacker->getPosition().hex,
 					bestAttack.attack.chargedFields, bestAttack.attack.attacker->Speed(0, true),
-					target.damageDealt, target.damageReceived, target.attackValue()
+					bestAttack.defenderDamageReduce, bestAttack.attackerDamageReduce, bestAttack.attackValue()
 				);
-
-				return BattleAction::makeMeleeAttack(stack,	bestAttack.attack.defender->getPosition(), bestAttack.from);
-		}
+			}
 		}
 		else if(bestSpellcast.is_initialized())
 		{
 			return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id);
 		}
-		else
+
+			//ThreatMap threatsToUs(stack); // These lines may be usefull but they are't used in the code.
+		if(moveTarget.score > score)
 		{
+			score = moveTarget.score;
+
 			if(stack->waited())
 			{
-				//ThreatMap threatsToUs(stack); // These lines may be usefull but they are't used in the code.
-				auto dists = cb->getReachability(stack);
-				if(!targets.unreachableEnemies.empty())
-				{
-					auto closestEnemy = vstd::minElementByFun(targets.unreachableEnemies, [&](const battle::Unit * enemy) -> int
-					{
-						return dists.distToNearestNeighbour(stack, enemy);
-					});
-
-					if(dists.distToNearestNeighbour(stack, *closestEnemy) < GameConstants::BFIELD_SIZE)
-					{
-						return goTowardsNearest(stack, (*closestEnemy)->getAttackableHexes(stack));
-					}
-				}
+				result = goTowardsNearest(stack, moveTarget.positions);
 			}
 			else
 			{
-				return BattleAction::makeWait(stack);
+				result = BattleAction::makeWait(stack);
 			}
 		}
 
+		if(score > EvaluationResult::INEFFECTIVE_SCORE)
+		{
+			return result;
+		}
+
 		if(!stack->hasBonusOfType(Bonus::FLYING)
 			&& stack->unitSide() == BattleSide::ATTACKER
 			&& cb->battleGetSiegeLevel() >= CGTownInstance::CITADEL)
@@ -235,7 +253,7 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 					return BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT));
 				else
 					return goTowardsNearest(stack, brokenWallMoat);
-	}
+			}
 		}
 	}
 	catch(boost::thread_interrupted &)
@@ -247,7 +265,7 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 		logAi->error("Exception occurred in %s %s",__FUNCTION__, e.what());
 	}
 
-	return BattleAction::makeDefend(stack);
+	return result;
 }
 
 BattleAction CBattleAI::goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes) const
@@ -272,10 +290,10 @@ BattleAction CBattleAI::goTowardsNearest(const CStack * stack, std::vector<Battl
 
 		if(stack->coversPos(hex))
 		{
-		logAi->warn("Warning: already standing on neighbouring tile!");
-		//We shouldn't even be here...
-		return BattleAction::makeDefend(stack);
-	}
+			logAi->warn("Warning: already standing on neighbouring tile!");
+			//We shouldn't even be here...
+			return BattleAction::makeDefend(stack);
+		}
 	}
 
 	BattleHex bestNeighbor = hexes.front();
@@ -285,13 +303,34 @@ BattleAction CBattleAI::goTowardsNearest(const CStack * stack, std::vector<Battl
 		return BattleAction::makeDefend(stack);
 	}
 
+	BattleExchangeEvaluator scoreEvaluator(cb, env);
+	HypotheticBattle hb(env.get(), cb);
+
+	scoreEvaluator.updateReachabilityMap(hb);
+
 	if(stack->hasBonusOfType(Bonus::FLYING))
 	{
+		std::set<BattleHex> moatHexes;
+
+		if(hb.battleGetSiegeLevel() >= BuildingID::CITADEL)
+		{
+			auto townMoat = hb.getDefendedTown()->town->moatHexes;
+
+			moatHexes = std::set<BattleHex>(townMoat.begin(), townMoat.end());
+		}
 		// Flying stack doesn't go hex by hex, so we can't backtrack using predecessors.
 		// We just check all available hexes and pick the one closest to the target.
 		auto nearestAvailableHex = vstd::minElementByFun(avHexes, [&](BattleHex hex) -> int
 		{
-			return BattleHex::getDistance(bestNeighbor, hex);
+			const int MOAT_PENALTY = 100; // avoid landing on moat
+			const int BLOCKED_STACK_PENALTY = 100; // avoid landing on moat
+
+			auto distance = BattleHex::getDistance(bestNeighbor, hex);
+
+			if(vstd::contains(moatHexes, hex))
+				distance += MOAT_PENALTY;
+
+			return scoreEvaluator.checkPositionBlocksOurStacks(hb, stack, hex) ? BLOCKED_STACK_PENALTY + distance : distance;
 		});
 
 		return BattleAction::makeMove(stack, *nearestAvailableHex);
@@ -303,11 +342,11 @@ BattleAction CBattleAI::goTowardsNearest(const CStack * stack, std::vector<Battl
 		{
 			if(!currentDest.isValid())
 			{
-				logAi->error("CBattleAI::goTowards: internal error");
 				return BattleAction::makeDefend(stack);
 			}
 
-			if(vstd::contains(avHexes, currentDest))
+			if(vstd::contains(avHexes, currentDest)
+				&& !scoreEvaluator.checkPositionBlocksOurStacks(hb, stack, currentDest))
 				return BattleAction::makeMove(stack, currentDest);
 
 			currentDest = reachability.predecessors[currentDest];
@@ -407,7 +446,7 @@ void CBattleAI::attemptCastingSpell()
 
 	using ValueMap = PossibleSpellcast::ValueMap;
 
-	auto evaluateQueue = [&](ValueMap & values, const std::vector<battle::Units> & queue, HypotheticBattle * state, size_t minTurnSpan, bool * enemyHadTurnOut) -> bool
+	auto evaluateQueue = [&](ValueMap & values, const std::vector<battle::Units> & queue, HypotheticBattle & state, size_t minTurnSpan, bool * enemyHadTurnOut) -> bool
 	{
 		bool firstRound = true;
 		bool enemyHadTurn = false;
@@ -418,7 +457,7 @@ void CBattleAI::attemptCastingSpell()
 		for(auto & round : queue)
 		{
 			if(!firstRound)
-				state->nextRound(0);//todo: set actual value?
+				state.nextRound(0);//todo: set actual value?
 			for(auto unit : round)
 			{
 				if(!vstd::contains(values, unit->unitId()))
@@ -427,11 +466,11 @@ void CBattleAI::attemptCastingSpell()
 				if(!unit->alive())
 					continue;
 
-				if(state->battleGetOwner(unit) != playerID)
+				if(state.battleGetOwner(unit) != playerID)
 				{
 					enemyHadTurn = true;
 
-					if(!firstRound || state->battleCastSpells(unit->unitSide()) == 0)
+					if(!firstRound || state.battleCastSpells(unit->unitSide()) == 0)
 					{
 						//enemy could counter our spell at this point
 						//anyway, we do not know what enemy will do
@@ -445,7 +484,7 @@ void CBattleAI::attemptCastingSpell()
 					ourTurnSpan++;
 				}
 
-				state->nextTurn(unit->unitId());
+				state.nextTurn(unit->unitId());
 
 				PotentialTargets pt(unit, state);
 
@@ -453,22 +492,22 @@ void CBattleAI::attemptCastingSpell()
 				{
 					AttackPossibility ap = pt.bestAction();
 
-					auto swb = state->getForUpdate(unit->unitId());
+					auto swb = state.getForUpdate(unit->unitId());
 					*swb = *ap.attackerState;
 
-					if(ap.damageDealt > 0)
+					if(ap.defenderDamageReduce > 0)
 						swb->removeUnitBonus(Bonus::UntilAttack);
-					if(ap.damageReceived > 0)
+					if(ap.attackerDamageReduce > 0)
 						swb->removeUnitBonus(Bonus::UntilBeingAttacked);
 
 					for(auto affected : ap.affectedUnits)
 					{
-						swb = state->getForUpdate(affected->unitId());
+						swb = state.getForUpdate(affected->unitId());
 						*swb = *affected;
 
-						if(ap.damageDealt > 0)
+						if(ap.defenderDamageReduce > 0)
 							swb->removeUnitBonus(Bonus::UntilBeingAttacked);
-						if(ap.damageReceived > 0 && ap.attack.defender->unitId() == affected->unitId())
+						if(ap.attackerDamageReduce > 0 && ap.attack.defender->unitId() == affected->unitId())
 							swb->removeUnitBonus(Bonus::UntilAttack);
 					}
 				}
@@ -476,7 +515,7 @@ void CBattleAI::attemptCastingSpell()
 				auto bav = pt.bestActionValue();
 
 				//best action is from effective owner`s point if view, we need to convert to our point if view
-				if(state->battleGetOwner(unit) != playerID)
+				if(state.battleGetOwner(unit) != playerID)
 					bav = -bav;
 				values[unit->unitId()] += bav;
 			}
@@ -529,7 +568,7 @@ void CBattleAI::attemptCastingSpell()
 
 		HypotheticBattle state(env.get(), cb);
 
-		evaluateQueue(valueOfStack, turnOrder, &state, 0, &enemyHadTurn);
+		evaluateQueue(valueOfStack, turnOrder, state, 0, &enemyHadTurn);
 
 		if(!enemyHadTurn)
 		{
@@ -577,7 +616,7 @@ void CBattleAI::attemptCastingSpell()
 
 		state.battleGetTurnOrder(newTurnOrder, amount, 2);
 
-		const bool turnSpanOK = evaluateQueue(newValueOfStack, newTurnOrder, &state, minTurnSpan, nullptr);
+		const bool turnSpanOK = evaluateQueue(newValueOfStack, newTurnOrder, state, minTurnSpan, nullptr);
 
 		if(turnSpanOK || castNow)
 		{

+ 689 - 0
AI/BattleAI/BattleExchangeVariant.cpp

@@ -0,0 +1,689 @@
+/*
+ * BattleAI.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 "BattleExchangeVariant.h"
+#include "../../lib/CStack.h"
+
+AttackerValue::AttackerValue()
+{
+	value = 0;
+	isRetalitated = false;
+}
+
+MoveTarget::MoveTarget()
+	: positions()
+{
+	score = EvaluationResult::INEFFECTIVE_SCORE;
+}
+
+int64_t BattleExchangeVariant::trackAttack(const AttackPossibility & ap, HypotheticBattle & state)
+{
+	auto affectedUnits = ap.affectedUnits;
+
+	affectedUnits.push_back(ap.attackerState);
+
+	for(auto affectedUnit : affectedUnits)
+	{
+		auto unitToUpdate = state.getForUpdate(affectedUnit->unitId());
+
+		unitToUpdate->health = affectedUnit->health;
+		unitToUpdate->shots = affectedUnit->shots;
+		unitToUpdate->counterAttacks = affectedUnit->counterAttacks;
+		unitToUpdate->movedThisRound = affectedUnit->movedThisRound;
+	}
+
+	auto attackValue = ap.attackValue();
+
+	dpsScore += attackValue;
+
+#if BATTLE_TRACE_LEVEL>=1
+	logAi->trace(
+		"%s -> %s, ap attack, %s, dps: %lld, score: %lld",
+		ap.attack.attacker->getDescription(),
+		ap.attack.defender->getDescription(),
+		ap.attack.shooting ? "shot" : "mellee",
+		ap.damageDealt,
+		attackValue);
+#endif
+
+	return attackValue;
+}
+
+int64_t BattleExchangeVariant::trackAttack(
+	std::shared_ptr<StackWithBonuses> attacker,
+	std::shared_ptr<StackWithBonuses> defender,
+	bool shooting,
+	bool isOurAttack,
+	const CBattleInfoCallback & cb,
+	bool evaluateOnly)
+{
+	const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION";
+	static const auto selectorBlocksRetaliation = Selector::type()(Bonus::BLOCKS_RETALIATION);
+	const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation);
+
+	TDmgRange retaliation;
+	BattleAttackInfo bai(attacker.get(), defender.get(), shooting);
+
+	if(shooting)
+	{
+		bai.attackerPos.setXY(8, 5);
+	}
+
+	auto attack = cb.battleEstimateDamage(bai, &retaliation);
+	int64_t attackDamage = (attack.first + attack.second) / 2;
+	int64_t defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), defender.get(), attackDamage, cb);
+	int64_t attackerDamageReduce = 0;
+
+	if(!evaluateOnly)
+	{
+#if BATTLE_TRACE_LEVEL>=1
+		logAi->trace(
+			"%s -> %s, normal attack, %s, dps: %lld, %lld",
+			attacker->getDescription(),
+			defender->getDescription(),
+			shooting ? "shot" : "mellee",
+			attackDamage,
+			defenderDamageReduce);
+#endif
+
+		if(isOurAttack)
+		{
+			dpsScore += defenderDamageReduce;
+			attackerValue[attacker->unitId()].value += defenderDamageReduce;
+		}
+		else
+			dpsScore -= defenderDamageReduce;
+
+		defender->damage(attackDamage);
+		attacker->afterAttack(shooting, false);
+	}
+
+	if(defender->alive() && defender->ableToRetaliate() && !counterAttacksBlocked && !shooting)
+	{
+		if(retaliation.second != 0)
+		{
+			auto retaliationDamage = (retaliation.first + retaliation.second) / 2;
+			attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), attacker.get(), retaliationDamage, cb);
+
+			if(!evaluateOnly)
+			{
+#if BATTLE_TRACE_LEVEL>=1
+				logAi->trace(
+					"%s -> %s, retaliation, dps: %lld, %lld",
+					defender->getDescription(),
+					attacker->getDescription(),
+					retaliationDamage,
+					attackerDamageReduce);
+#endif
+
+				if(isOurAttack)
+				{
+					dpsScore -= attackerDamageReduce;
+					attackerValue[attacker->unitId()].isRetalitated = true;
+				}
+				else
+				{
+					dpsScore += attackerDamageReduce;
+					attackerValue[defender->unitId()].value += attackerDamageReduce;
+				}
+
+				attacker->damage(retaliationDamage);
+				defender->afterAttack(false, true);
+			}
+		}
+	}
+
+	auto score = defenderDamageReduce - attackerDamageReduce;
+
+#if BATTLE_TRACE_LEVEL>=1
+	if(!score)
+	{
+		logAi->trace("Attack has zero score d:%lld a:%lld", defenderDamageReduce, attackerDamageReduce);
+	}
+#endif
+
+	return score;
+}
+
+EvaluationResult BattleExchangeEvaluator::findBestTarget(const battle::Unit * activeStack, PotentialTargets & targets, HypotheticBattle & hb)
+{
+	EvaluationResult result(targets.bestAction());
+
+	updateReachabilityMap(hb);
+
+	for(auto & ap : targets.possibleAttacks)
+	{
+		int64_t score = calculateExchange(ap, targets, hb);
+
+		if(score > result.score)
+		{
+			result.score = score;
+			result.bestAttack = ap;
+		}
+	}
+
+	if(!activeStack->waited())
+	{
+#if BATTLE_TRACE_LEVEL>=1
+		logAi->trace("Evaluating waited attack for %s", activeStack->getDescription());
+#endif
+
+		hb.getForUpdate(activeStack->unitId())->waiting = true;
+		hb.getForUpdate(activeStack->unitId())->waitedThisTurn = true;
+
+		updateReachabilityMap(hb);
+
+		for(auto & ap : targets.possibleAttacks)
+		{
+			int64_t score = calculateExchange(ap, targets, hb);
+
+			if(score > result.score)
+			{
+				result.score = score;
+				result.bestAttack = ap;
+				result.wait = true;
+			}
+		}
+	}
+
+	return result;
+}
+
+MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(const battle::Unit * activeStack, PotentialTargets & targets, HypotheticBattle & hb)
+{
+	MoveTarget result;
+	BattleExchangeVariant ev;
+
+	if(targets.unreachableEnemies.empty())
+		return result;
+
+	updateReachabilityMap(hb);
+
+	auto dists = cb->getReachability(activeStack);
+	auto speed = activeStack->Speed();
+
+	for(const battle::Unit * enemy : targets.unreachableEnemies)
+	{
+		int64_t stackScore = EvaluationResult::INEFFECTIVE_SCORE;
+
+		std::vector<const battle::Unit *> adjacentStacks = getAdjacentUnits(enemy);
+		auto closestStack = *vstd::minElementByFun(adjacentStacks, [&](const battle::Unit * u) -> int64_t
+			{
+				return dists.distToNearestNeighbour(activeStack, u) * 100000 - activeStack->getTotalHealth();
+			});
+
+		auto distance = dists.distToNearestNeighbour(activeStack, closestStack);
+
+		if(distance >= GameConstants::BFIELD_SIZE)
+			continue;
+
+		if(distance <= speed)
+			continue;
+
+		auto turnsToRich = (distance - 1) / speed + 1;
+		auto hexes = closestStack->getSurroundingHexes();
+
+		for(auto hex : hexes)
+		{
+			auto bai = BattleAttackInfo(activeStack, closestStack, cb->battleCanShoot(activeStack));
+			auto attack = AttackPossibility::evaluate(bai, hex, hb);
+
+			attack.shootersBlockedDmg = 0; // we do not want to count on it, it is not for sure
+
+			auto score = calculateExchange(attack, targets, hb) / turnsToRich;
+
+			if(result.score < score)
+			{
+				result.score = score;
+				result.positions = closestStack->getAttackableHexes(activeStack);
+			}
+		}
+	}
+
+	return result;
+}
+
+std::vector<const battle::Unit *> BattleExchangeEvaluator::getAdjacentUnits(const battle::Unit * blockerUnit)
+{
+	std::queue<const battle::Unit *> queue;
+	std::vector<const battle::Unit *> checkedStacks;
+
+	queue.push(blockerUnit);
+
+	while(!queue.empty())
+	{
+		auto stack = queue.front();
+
+		queue.pop();
+		checkedStacks.push_back(stack);
+
+		auto hexes = stack->getSurroundingHexes();
+		for(auto hex : hexes)
+		{
+			auto neighbor = cb->battleGetStackByPos(hex);
+
+			if(neighbor && neighbor->unitSide() == stack->unitSide() && !vstd::contains(checkedStacks, neighbor))
+			{
+				queue.push(neighbor);
+				checkedStacks.push_back(neighbor);
+			}
+		}
+	}
+
+	return checkedStacks;
+}
+
+std::vector<const battle::Unit *> BattleExchangeEvaluator::getExchangeUnits(
+	const AttackPossibility & ap,
+	PotentialTargets & targets,
+	HypotheticBattle & hb)
+{
+	auto hexes = ap.attack.defender->getHexes();
+
+	if(!ap.attack.shooting) hexes.push_back(ap.from);
+
+	std::vector<const battle::Unit *> exchangeUnits;
+	std::vector<const battle::Unit *> allReachableUnits;
+
+	for(auto hex : hexes)
+	{
+		vstd::concatenate(allReachableUnits, reachabilityMap[hex]);
+	}
+
+	vstd::removeDuplicates(allReachableUnits);
+
+	auto copy = allReachableUnits;
+	for(auto unit : copy)
+	{
+		for(auto adjacentUnit : getAdjacentUnits(unit))
+		{
+			auto unitWithBonuses = hb.battleGetUnitByID(adjacentUnit->unitId());
+
+			if(vstd::contains(targets.unreachableEnemies, adjacentUnit)
+				&& !vstd::contains(allReachableUnits, unitWithBonuses))
+			{
+				allReachableUnits.push_back(unitWithBonuses);
+			}
+		}
+	}
+
+	vstd::removeDuplicates(allReachableUnits);
+
+	if(!vstd::contains(allReachableUnits, ap.attack.attacker))
+	{
+		allReachableUnits.push_back(ap.attack.attacker);
+	}
+
+	if(allReachableUnits.size() < 2)
+	{
+#if BATTLE_TRACE_LEVEL>=1
+		logAi->trace("Reachability map contains only %d stacks", allReachableUnits.size());
+#endif
+
+		return exchangeUnits;
+	}
+
+	for(int turn = 0; turn < turnOrder.size(); turn++)
+	{
+		for(auto unit : turnOrder[turn])
+		{
+			if(vstd::contains(allReachableUnits, unit))
+				exchangeUnits.push_back(unit);
+		}
+	}
+
+	return exchangeUnits;
+}
+
+int64_t BattleExchangeEvaluator::calculateExchange(
+	const AttackPossibility & ap,
+	PotentialTargets & targets,
+	HypotheticBattle & hb)
+{
+#if BATTLE_TRACE_LEVEL>=1
+	logAi->trace("Battle exchange at %lld", ap.attack.shooting ? ap.dest : ap.from);
+#endif
+
+	std::vector<const battle::Unit *> ourStacks;
+	std::vector<const battle::Unit *> enemyStacks;
+
+	enemyStacks.push_back(ap.attack.defender);
+
+	std::vector<const battle::Unit *> exchangeUnits = getExchangeUnits(ap, targets, hb);
+
+	if(exchangeUnits.empty())
+	{
+		return 0;
+	}
+
+	HypotheticBattle exchangeBattle(env.get(), cb);
+	BattleExchangeVariant v;
+	auto melleeAttackers = ourStacks;
+
+	vstd::removeDuplicates(melleeAttackers);
+	vstd::erase_if(melleeAttackers, [&](const battle::Unit * u) -> bool
+		{
+			return !cb->battleCanShoot(u);
+		});
+
+	for(auto unit : exchangeUnits)
+	{
+		bool isOur = cb->battleMatchOwner(ap.attack.attacker, unit, true);
+		auto & attackerQueue = isOur ? ourStacks : enemyStacks;
+
+		if(!vstd::contains(attackerQueue, unit))
+		{
+			attackerQueue.push_back(unit);
+		}
+	}
+
+	bool canUseAp = true;
+
+	for(auto activeUnit : exchangeUnits)
+	{
+		bool isOur = cb->battleMatchOwner(ap.attack.attacker, activeUnit, true);
+		battle::Units & attackerQueue = isOur ? ourStacks : enemyStacks;
+		battle::Units & oppositeQueue = isOur ? enemyStacks : ourStacks;
+
+		auto attacker = exchangeBattle.getForUpdate(activeUnit->unitId());
+
+		if(!attacker->alive())
+		{
+#if BATTLE_TRACE_LEVEL>=1
+			logAi->trace(	"Attacker is dead");
+#endif
+
+			continue;
+		}
+
+		auto targetUnit = ap.attack.defender;
+
+		if(!isOur || !exchangeBattle.getForUpdate(targetUnit->unitId())->alive())
+		{
+			auto estimateAttack = [&](const battle::Unit * u) -> int64_t
+			{
+				auto stackWithBonuses = exchangeBattle.getForUpdate(u->unitId());
+				auto score = v.trackAttack(
+					attacker,
+					stackWithBonuses,
+					exchangeBattle.battleCanShoot(stackWithBonuses.get()),
+					isOur,
+					*cb,
+					true);
+
+#if BATTLE_TRACE_LEVEL>=1
+				logAi->trace("Best target selector %s->%s score = %lld", attacker->getDescription(), u->getDescription(), score);
+#endif
+
+				return score;
+			};
+
+			if(!oppositeQueue.empty())
+			{
+				targetUnit = *vstd::maxElementByFun(oppositeQueue, estimateAttack);
+			}
+			else
+			{
+				auto reachable = exchangeBattle.battleGetUnitsIf([&](const battle::Unit * u) -> bool
+					{
+						if(!u->alive() || u->unitSide() == attacker->unitSide())
+							return false;
+
+						return vstd::contains_if(reachabilityMap[u->getPosition()], [&](const battle::Unit * other) -> bool
+							{
+								return attacker->unitId() == other->unitId();
+							});
+					});
+
+				if(!reachable.empty())
+				{
+					targetUnit = *vstd::maxElementByFun(reachable, estimateAttack);
+				}
+				else
+				{
+#if BATTLE_TRACE_LEVEL>=1
+					logAi->trace("Battle queue is empty and no reachable enemy.");
+#endif
+
+					continue;
+				}
+			}
+		}
+
+		auto defender = exchangeBattle.getForUpdate(targetUnit->unitId());
+		auto shooting = cb->battleCanShoot(attacker.get());
+		const int totalAttacks = attacker->getTotalAttacks(shooting);
+
+		if(canUseAp && activeUnit == ap.attack.attacker && targetUnit == ap.attack.defender)
+		{
+			v.trackAttack(ap, exchangeBattle);
+		}
+		else
+		{
+			for(int i = 0; i < totalAttacks; i++)
+			{
+				v.trackAttack(attacker, defender, shooting, isOur, exchangeBattle);
+
+				if(!attacker->alive() || !defender->alive())
+					break;
+			}
+		}
+
+		canUseAp = false;
+
+		vstd::erase_if(attackerQueue, [&](const battle::Unit * u) -> bool
+			{
+				return !exchangeBattle.getForUpdate(u->unitId())->alive();
+			});
+
+		vstd::erase_if(oppositeQueue, [&](const battle::Unit * u) -> bool
+			{
+				return !exchangeBattle.getForUpdate(u->unitId())->alive();
+			});
+	}
+
+	// avoid blocking path for stronger stack by weaker stack
+	// the method checks if all stacks can be placed around enemy
+	v.adjustPositions(melleeAttackers, ap, reachabilityMap);
+
+#if BATTLE_TRACE_LEVEL>=1
+	logAi->trace("Exchange score: %lld", v.getScore());
+#endif
+
+	return v.getScore();
+}
+
+void BattleExchangeVariant::adjustPositions(
+	std::vector<const battle::Unit*> attackers,
+	const AttackPossibility & ap,
+	std::map<BattleHex, battle::Units> & reachabilityMap)
+{
+	auto hexes = ap.attack.defender->getSurroundingHexes();
+
+	boost::sort(attackers, [&](const battle::Unit * u1, const battle::Unit * u2) -> bool
+		{
+			if(attackerValue[u1->unitId()].isRetalitated && !attackerValue[u2->unitId()].isRetalitated)
+				return true;
+
+			if(attackerValue[u2->unitId()].isRetalitated && !attackerValue[u1->unitId()].isRetalitated)
+				return false;
+
+			return attackerValue[u1->unitId()].value > attackerValue[u2->unitId()].value;
+		});
+
+	if(!ap.attack.shooting)
+	{
+		vstd::erase_if_present(hexes, ap.from);
+		vstd::erase_if_present(hexes, ap.attack.attacker->occupiedHex(ap.attack.attackerPos));
+	}
+
+	int64_t notRealizedDamage = 0;
+
+	for(auto unit : attackers)
+	{
+		if(unit->unitId() == ap.attack.attacker->unitId())
+			continue;
+
+		if(!vstd::contains_if(hexes, [&](BattleHex h) -> bool
+			{
+				return vstd::contains(reachabilityMap[h], unit);
+			}))
+		{
+			notRealizedDamage += attackerValue[unit->unitId()].value;
+			continue;
+		}
+
+		auto desiredPosition = vstd::minElementByFun(hexes, [&](BattleHex h) -> int64_t
+			{
+				auto score = vstd::contains(reachabilityMap[h], unit)
+					? reachabilityMap[h].size()
+					: 0;
+
+				if(unit->doubleWide())
+				{
+					auto backHex = unit->occupiedHex(h);
+
+					if(vstd::contains(hexes, backHex))
+						score += reachabilityMap[backHex].size();
+				}
+
+				return score;
+			});
+
+		hexes.erase(desiredPosition);
+	}
+
+	if(notRealizedDamage > ap.attackValue() && notRealizedDamage > attackerValue[ap.attack.attacker->unitId()].value)
+	{
+		dpsScore = EvaluationResult::INEFFECTIVE_SCORE;
+	}
+}
+
+void BattleExchangeEvaluator::updateReachabilityMap(HypotheticBattle & hb)
+{
+	const int TURN_DEPTH = 2;
+
+	turnOrder.clear();
+	
+	hb.battleGetTurnOrder(turnOrder, std::numeric_limits<int>::max(), TURN_DEPTH);
+	reachabilityMap.clear();
+
+	for(int turn = 0; turn < turnOrder.size(); turn++)
+	{
+		auto & turnQueue = turnOrder[turn];
+		HypotheticBattle turnBattle(env.get(), cb);
+
+		for(const battle::Unit * unit : turnQueue)
+		{
+			if(turnBattle.battleCanShoot(unit))
+			{
+				for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1)
+				{
+					reachabilityMap[hex].push_back(unit);
+				}
+
+				continue;
+			}
+
+			auto unitReachability = turnBattle.getReachability(unit);
+
+			for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1)
+			{
+				bool reachable = unitReachability.distances[hex] <= unit->Speed(turn);
+
+				if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK)
+				{
+					const battle::Unit * hexStack = cb->battleGetUnitByPos(hex);
+
+					if(hexStack && cb->battleMatchOwner(unit, hexStack, false))
+					{
+						for(BattleHex neighbor : hex.neighbouringTiles())
+						{
+							reachable = unitReachability.distances[neighbor] <= unit->Speed(turn);
+
+							if(reachable) break;
+						}
+					}
+				}
+
+				if(reachable)
+				{
+					reachabilityMap[hex].push_back(unit);
+				}
+			}
+		}
+	}
+}
+
+// avoid blocking path for stronger stack by weaker stack
+bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb, const battle::Unit * activeUnit, BattleHex position)
+{
+	const int BLOCKING_THRESHOLD = 70;
+	const int BLOCKING_OWN_ATTACK_PENALTY = 100;
+	const int BLOCKING_OWN_MOVE_PENALTY = 1;
+
+	float blockingScore = 0;
+
+	auto activeUnitDamage = activeUnit->getMinDamage(hb.battleCanShoot(activeUnit)) * activeUnit->getCount();
+
+	for(int turn = 0; turn < turnOrder.size(); turn++)
+	{
+		auto & turnQueue = turnOrder[turn];
+		HypotheticBattle turnBattle(env.get(), cb);
+
+		auto unitToUpdate = turnBattle.getForUpdate(activeUnit->unitId());
+		unitToUpdate->setPosition(position);
+
+		for(const battle::Unit * unit : turnQueue)
+		{
+			if(unit->unitId() == unitToUpdate->unitId() || cb->battleMatchOwner(unit, activeUnit, false))
+				continue;
+
+			auto blockedUnitDamage = unit->getMinDamage(hb.battleCanShoot(unit)) * unit->getCount();
+			auto ratio = blockedUnitDamage / (blockedUnitDamage + activeUnitDamage);
+
+			auto unitReachability = turnBattle.getReachability(unit);
+
+			for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1)
+			{
+				bool enemyUnit = false;
+				bool reachable = unitReachability.distances[hex] <= unit->Speed(turn);
+
+				if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK)
+				{
+					const battle::Unit * hexStack = turnBattle.battleGetUnitByPos(hex);
+
+					if(hexStack && cb->battleMatchOwner(unit, hexStack, false))
+					{
+						enemyUnit = true;
+
+						for(BattleHex neighbor : hex.neighbouringTiles())
+						{
+							reachable = unitReachability.distances[neighbor] <= unit->Speed(turn);
+
+							if(reachable) break;
+						}
+					}
+				}
+
+				if(!reachable && vstd::contains(reachabilityMap[hex], unit))
+				{
+					blockingScore += ratio * (enemyUnit ? BLOCKING_OWN_ATTACK_PENALTY : BLOCKING_OWN_MOVE_PENALTY);
+				}
+			}
+		}
+	}
+
+#if BATTLE_TRACE_LEVEL>=1
+	logAi->trace("Position %d, blocking score %f", position.hex, blockingScore);
+#endif
+
+	return blockingScore > BLOCKING_THRESHOLD;
+}

+ 107 - 0
AI/BattleAI/BattleExchangeVariant.h

@@ -0,0 +1,107 @@
+/*
+ * BattleExchangeVariant.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/AI_Base.h"
+#include "../../lib/battle/ReachabilityInfo.h"
+#include "PotentialTargets.h"
+#include "StackWithBonuses.h"
+
+struct AttackerValue
+{
+	int64_t value;
+	bool isRetalitated;
+	BattleHex position;
+
+	AttackerValue();
+};
+
+struct MoveTarget
+{
+	int64_t score;
+	std::vector<BattleHex> positions;
+
+	MoveTarget();
+};
+
+struct EvaluationResult
+{
+	static const int64_t INEFFECTIVE_SCORE = -1000000;
+
+	AttackPossibility bestAttack;
+	MoveTarget bestMove;
+	bool wait;
+	int64_t score;
+	bool defend;
+
+	EvaluationResult(const AttackPossibility & ap)
+		:wait(false), score(0), bestAttack(ap), defend(false)
+	{
+	}
+};
+
+/// <summary>
+/// The class represents evaluation of attack value
+/// of exchanges between all stacks which can access particular hex
+/// starting from initial attack represented by AttackPossibility and further according turn order.
+/// Negative score value means we get more demage than deal
+/// </summary>
+class BattleExchangeVariant
+{
+public:
+	BattleExchangeVariant()
+		:dpsScore(0), attackerValue()
+	{
+	}
+
+	int64_t trackAttack(const AttackPossibility & ap, HypotheticBattle & state);
+
+	int64_t trackAttack(
+		std::shared_ptr<StackWithBonuses> attacker,
+		std::shared_ptr<StackWithBonuses> defender,
+		bool shooting,
+		bool isOurAttack,
+		const CBattleInfoCallback & cb,
+		bool evaluateOnly = false);
+
+	int64_t getScore() const { return dpsScore; }
+
+	void adjustPositions(
+		std::vector<const battle::Unit *> attackers,
+		const AttackPossibility & ap,
+		std::map<BattleHex, battle::Units> & reachabilityMap);
+
+private:
+	int64_t dpsScore;
+	std::map<uint32_t, AttackerValue> attackerValue;
+};
+
+class BattleExchangeEvaluator
+{
+private:
+	std::shared_ptr<CBattleInfoCallback> cb;
+	std::shared_ptr<Environment> env;
+	std::map<BattleHex, std::vector<const battle::Unit *>> reachabilityMap;
+	std::vector<battle::Units> turnOrder;
+
+public:
+	BattleExchangeEvaluator(std::shared_ptr<CBattleInfoCallback> cb, std::shared_ptr<Environment> env)
+		:cb(cb), reachabilityMap(), env(env), turnOrder()
+	{
+	}
+
+	EvaluationResult findBestTarget(const battle::Unit * activeStack, PotentialTargets & targets, HypotheticBattle & hb);
+	int64_t calculateExchange(const AttackPossibility & ap, PotentialTargets & targets, HypotheticBattle & hb);
+	void updateReachabilityMap(HypotheticBattle & hb);
+	std::vector<const battle::Unit *> getExchangeUnits(const AttackPossibility & ap, PotentialTargets & targets, HypotheticBattle & hb);
+	bool checkPositionBlocksOurStacks(HypotheticBattle & hb, const battle::Unit * unit, BattleHex position);
+	MoveTarget findMoveTowardsUnreachable(const battle::Unit * activeStack, PotentialTargets & targets, HypotheticBattle & hb);
+	std::vector<const battle::Unit *> getAdjacentUnits(const battle::Unit * unit);
+};

+ 2 - 0
AI/BattleAI/CMakeLists.txt

@@ -10,6 +10,7 @@ set(battleAI_SRCS
 		PotentialTargets.cpp
 		StackWithBonuses.cpp
 		ThreatMap.cpp
+		BattleExchangeVariant.cpp
 )
 
 set(battleAI_HEADERS
@@ -23,6 +24,7 @@ set(battleAI_HEADERS
 		PossibleSpellcast.h
 		StackWithBonuses.h
 		ThreatMap.h
+		BattleExchangeVariant.h
 )
 
 assign_source_group(${battleAI_SRCS} ${battleAI_HEADERS})

+ 17 - 22
AI/BattleAI/PotentialTargets.cpp

@@ -11,13 +11,11 @@
 #include "PotentialTargets.h"
 #include "../../lib/CStack.h"//todo: remove
 
-PotentialTargets::PotentialTargets(const battle::Unit * attacker, const HypotheticBattle * state)
+PotentialTargets::PotentialTargets(const battle::Unit * attacker, const HypotheticBattle & state)
 {
-	auto attIter = state->stackStates.find(attacker->unitId());
-	const battle::Unit * attackerInfo = (attIter == state->stackStates.end()) ? attacker : attIter->second.get();
-
-	auto reachability = state->getReachability(attackerInfo);
-	auto avHexes = state->battleGetAvailableHexes(reachability, attackerInfo);
+	auto attackerInfo = state.battleGetUnitByID(attacker->unitId());
+	auto reachability = state.getReachability(attackerInfo);
+	auto avHexes = state.battleGetAvailableHexes(reachability, attackerInfo);
 
 	//FIXME: this should part of battleGetAvailableHexes
 	bool forceTarget = false;
@@ -27,7 +25,7 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet
 	if(attackerInfo->hasBonusOfType(Bonus::ATTACKS_NEAREST_CREATURE))
 	{
 		forceTarget = true;
-		auto nearest = state->getNearestStack(attackerInfo);
+		auto nearest = state.getNearestStack(attackerInfo);
 
 		if(nearest.first != nullptr)
 		{
@@ -36,14 +34,14 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet
 		}
 	}
 
-	auto aliveUnits = state->battleGetUnitsIf([=](const battle::Unit * unit)
+	auto aliveUnits = state.battleGetUnitsIf([=](const battle::Unit * unit)
 	{
 		return unit->isValidTarget() && unit->unitId() != attackerInfo->unitId();
 	});
 
 	for(auto defender : aliveUnits)
 	{
-		if(!forceTarget && !state->battleMatchOwner(attackerInfo, defender))
+		if(!forceTarget && !state.battleMatchOwner(attackerInfo, defender))
 			continue;
 
 		auto GenerateAttackInfo = [&](bool shooting, BattleHex hex) -> AttackPossibility
@@ -63,7 +61,7 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet
 			else
 				unreachableEnemies.push_back(defender);
 		}
-		else if(state->battleCanShoot(attackerInfo, defender->getPosition()))
+		else if(state.battleCanShoot(attackerInfo, defender->getPosition()))
 		{
 			possibleAttacks.push_back(GenerateAttackInfo(true, BattleHex::INVALID));
 		}
@@ -86,22 +84,18 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet
 
 	boost::sort(possibleAttacks, [](const AttackPossibility & lhs, const AttackPossibility & rhs) -> bool
 	{
-		if(lhs.collateralDamage > rhs.collateralDamage)
-			return false;
-		if(lhs.collateralDamage < rhs.collateralDamage)
-			return true;
-		return (lhs.damageDealt + lhs.shootersBlockedDmg - lhs.damageReceived > rhs.damageDealt + rhs.shootersBlockedDmg - rhs.damageReceived);
+		return lhs.damageDiff() > rhs.damageDiff();
 	});
 
 	if (!possibleAttacks.empty())
 	{
-		auto &bestAp = possibleAttacks[0];
+		auto & bestAp = possibleAttacks[0];
 
-		logGlobal->info("Battle AI best: %s -> %s at %d from %d, affects %d units: %lld %lld %lld %lld",
+		logGlobal->info("Battle AI best: %s -> %s at %d from %d, affects %d units: d:%lld a:%lld c:%lld s:%lld",
 			bestAp.attack.attacker->unitType()->identifier,
-			state->battleGetUnitByPos(bestAp.dest)->unitType()->identifier,
+			state.battleGetUnitByPos(bestAp.dest)->unitType()->identifier,
 			(int)bestAp.dest, (int)bestAp.from, (int)bestAp.affectedUnits.size(),
-			bestAp.damageDealt, bestAp.damageReceived, bestAp.collateralDamage, bestAp.shootersBlockedDmg);
+			bestAp.defenderDamageReduce, bestAp.attackerDamageReduce, bestAp.collateralDamageReduce, bestAp.shootersBlockedDmg);
 	}
 }
 
@@ -109,13 +103,14 @@ int64_t PotentialTargets::bestActionValue() const
 {
 	if(possibleAttacks.empty())
 		return 0;
+
 	return bestAction().attackValue();
 }
 
-AttackPossibility PotentialTargets::bestAction() const
+const AttackPossibility & PotentialTargets::bestAction() const
 {
 	if(possibleAttacks.empty())
 		throw std::runtime_error("No best action, since we don't have any actions");
-	return possibleAttacks[0];
-	//return *vstd::maxElementByFun(possibleAttacks, [](const AttackPossibility &ap) { return ap.attackValue(); } );
+
+	return possibleAttacks.front();
 }

+ 2 - 2
AI/BattleAI/PotentialTargets.h

@@ -17,8 +17,8 @@ public:
 	std::vector<const battle::Unit *> unreachableEnemies;
 
 	PotentialTargets(){};
-	PotentialTargets(const battle::Unit * attacker, const HypotheticBattle * state);
+	PotentialTargets(const battle::Unit * attacker, const HypotheticBattle & state);
 
-	AttackPossibility bestAction() const;
+	const AttackPossibility & bestAction() const;
 	int64_t bestActionValue() const;
 };

+ 15 - 1
AI/BattleAI/StackWithBonuses.cpp

@@ -199,6 +199,21 @@ void StackWithBonuses::removeUnitBonus(const CSelector & selector)
 	vstd::erase_if(bonusesToUpdate, [&](const Bonus & b){return selector(&b);});
 }
 
+std::string StackWithBonuses::getDescription() const
+{
+	std::ostringstream oss;
+	oss << unitOwner().getStr();
+	oss << " battle stack [" << unitId() << "]: " << getCount() << " of ";
+	if(type)
+		oss << type->namePl;
+	else
+		oss << "[UNDEFINED TYPE]";
+
+	oss << " from slot " << slot;
+
+	return oss.str();
+}
+
 void StackWithBonuses::spendMana(ServerCallback * server, const int spellCost) const
 {
 	//TODO: evaluate cast use
@@ -284,7 +299,6 @@ int32_t HypotheticBattle::getActiveStackID() const
 void HypotheticBattle::nextRound(int32_t roundNr)
 {
 	//TODO:HypotheticBattle::nextRound
-
 	for(auto unit : battleAliveUnits())
 	{
 		auto forUpdate = getForUpdate(unit->unitId());

+ 1 - 0
AI/BattleAI/StackWithBonuses.h

@@ -85,6 +85,7 @@ public:
 	void removeUnitBonus(const CSelector & selector);
 
 	void spendMana(ServerCallback * server, const int spellCost) const override;
+	std::string getDescription() const override;
 
 private:
 	const IBonusBearer * origBearer;

+ 6 - 1
AI/Nullkiller/AIGateway.cpp

@@ -27,7 +27,9 @@
 namespace NKAI
 {
 
+// our to enemy strength ratio constants
 const float SAFE_ATTACK_CONSTANT = 1.2;
+const float RETREAT_THRESHOLD = 0.3;
 
 //one thread may be turn of AI and another will be handling a side effect for AI2
 boost::thread_specific_ptr<CCallback> cb;
@@ -202,6 +204,9 @@ void AIGateway::gameOver(PlayerColor player, const EVictoryLossCheckResult & vic
 			logAi->debug("AIGateway: Player %d (%s) lost. It's me. What a disappointment! :(", player, player.getStr());
 		}
 
+		// some whitespace to flush stream
+		logAi->debug(std::string(200, ' '));
+
 		finish();
 	}
 }
@@ -498,7 +503,7 @@ boost::optional<BattleAction> AIGateway::makeSurrenderRetreatDecision(
 	double fightRatio = battleState.getOurStrength() / (double)battleState.getEnemyStrength();
 
 	// if we have no towns - things are already bad, so retreat is not an option.
-	if(cb->getTownsInfo().size() && fightRatio < 0.3 && battleState.canFlee)
+	if(cb->getTownsInfo().size() && fightRatio < RETREAT_THRESHOLD && battleState.canFlee)
 	{
 		return BattleAction::makeRetreat(battleState.ourSide);
 	}

+ 1 - 1
AI/Nullkiller/CMakeLists.txt

@@ -138,6 +138,6 @@ vcmi_set_output_dir(Nullkiller "AI")
 enable_pch(Nullkiller)
 
 install(TARGETS Nullkiller RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR})
-if(APPLE_IOS)
+if(APPLE_IOS AND NOT USING_CONAN)
 	install(IMPORTED_RUNTIME_ARTIFACTS TBB::tbb LIBRARY DESTINATION ${LIB_DIR}) # CMake 3.21+
 endif()

+ 5 - 4
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -180,13 +180,14 @@ std::vector<CGPathNode *> AINodeStorage::getInitialNodes()
 	for(auto actorPtr : actors)
 	{
 		ChainActor * actor = actorPtr.get();
-		AIPathNode * initialNode =
-			getOrCreateNode(actor->initialPosition, actor->layer, actor)
-			.get();
 
-		if(!initialNode)
+		auto allocated = getOrCreateNode(actor->initialPosition, actor->layer, actor);
+
+		if(!allocated)
 			continue;
 
+		AIPathNode * initialNode = allocated.get();
+
 		initialNode->inPQ = false;
 		initialNode->pq = nullptr;
 		initialNode->turns = actor->initialTurn;

+ 14 - 0
CI/conan/ios-arm64

@@ -0,0 +1,14 @@
+[settings]
+os=iOS
+os.version=12.0
+os.sdk=iphoneos
+arch=armv8
+compiler=apple-clang
+compiler.version=13
+compiler.libcxx=libc++
+build_type=Release
+[options]
+[build_requires]
+[env]
+[conf]
+tools.cmake.cmaketoolchain:generator = Ninja

+ 14 - 0
CI/conan/ios-armv7

@@ -0,0 +1,14 @@
+[settings]
+os=iOS
+os.version=10.0
+os.sdk=iphoneos
+arch=armv7
+compiler=apple-clang
+compiler.version=13
+compiler.libcxx=libc++
+build_type=Release
+[options]
+[build_requires]
+[env]
+[conf]
+tools.cmake.cmaketoolchain:generator = Ninja

+ 2 - 0
CI/conan/macos-arm

@@ -9,3 +9,5 @@ build_type=Release
 [options]
 [build_requires]
 [env]
+[conf]
+tools.cmake.cmaketoolchain:generator = Ninja

+ 2 - 0
CI/conan/macos-intel

@@ -9,3 +9,5 @@ build_type=Release
 [options]
 [build_requires]
 [env]
+[conf]
+tools.cmake.cmaketoolchain:generator = Ninja

+ 2 - 2
CI/ios/before_install.sh

@@ -2,6 +2,6 @@
 
 echo DEVELOPER_DIR=/Applications/Xcode_13.4.1.app >> $GITHUB_ENV
 
-curl -L 'https://github.com/vcmi/vcmi-ios-deps/releases/latest/download/vcmi-ios-depends-xc13.2.1.txz' \
+mkdir ~/.conan ; cd ~/.conan
+curl -L 'https://github.com/vcmi/vcmi-ios-deps/releases/download/1.1/ios-arm64.xz' \
 	| tar -xf -
-build/fix_install_paths.command

+ 1 - 1
CI/mac/before_install.sh

@@ -5,5 +5,5 @@ echo DEVELOPER_DIR=/Applications/Xcode_13.4.1.app >> $GITHUB_ENV
 brew install ninja
 
 mkdir ~/.conan ; cd ~/.conan
-curl -L "https://github.com/vcmi/vcmi-deps-macos/releases/latest/download/$DEPS_FILENAME.txz" \
+curl -L "https://github.com/vcmi/vcmi-deps-macos/releases/download/1.1/$DEPS_FILENAME.txz" \
 	| tar -xf -

+ 21 - 20
CMakeLists.txt

@@ -48,23 +48,10 @@ if(NOT CMAKE_BUILD_TYPE)
 	set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS Debug Release RelWithDebInfo)
 endif()
 
-set(VCMI_VERSION_MAJOR 1)
-set(VCMI_VERSION_MINOR 1)
-set(VCMI_VERSION_PATCH 0)
-add_definitions(
-	-DVCMI_VERSION_MAJOR=${VCMI_VERSION_MAJOR}
-	-DVCMI_VERSION_MINOR=${VCMI_VERSION_MINOR}
-	-DVCMI_VERSION_PATCH=${VCMI_VERSION_PATCH}
-	-DVCMI_VERSION_STRING="${VCMI_VERSION_MAJOR}.${VCMI_VERSION_MINOR}.${VCMI_VERSION_PATCH}"
-)
-set(APP_SHORT_VERSION "${VCMI_VERSION_MAJOR}.${VCMI_VERSION_MINOR}")
-if(NOT VCMI_VERSION_PATCH EQUAL 0)
-	string(APPEND APP_SHORT_VERSION ".${VCMI_VERSION_PATCH}")
-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")
@@ -98,7 +85,10 @@ endif()
 
 set(CMAKE_MODULE_PATH ${CMAKE_HOME_DIRECTORY}/cmake_modules ${PROJECT_SOURCE_DIR}/CI)
 # Contains custom functions and macros, but don't altering any options
+
 include(VCMIUtils)
+include(VersionDefinition)
+
 vcmi_print_important_variables()
 
 # Options to enable folders in CMake generated projects for Visual Studio, Xcode, etc
@@ -180,9 +170,11 @@ if(APPLE_IOS)
 	set(CMAKE_MACOSX_RPATH 1)
 	set(CMAKE_OSX_DEPLOYMENT_TARGET 12.0)
 
-	list(APPEND CMAKE_FIND_ROOT_PATH "${CMAKE_PREFIX_PATH}") # required for Boost
-	set(CMAKE_FIND_USE_CMAKE_ENVIRONMENT_PATH FALSE)
-	set(CMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH FALSE)
+	if(NOT USING_CONAN)
+		list(APPEND CMAKE_FIND_ROOT_PATH "${CMAKE_PREFIX_PATH}") # required for Boost
+		set(CMAKE_FIND_USE_CMAKE_ENVIRONMENT_PATH FALSE)
+		set(CMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH FALSE)
+	endif()
 
 	set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED NO)
 	set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED_FOR_APPS YES)
@@ -294,7 +286,7 @@ if(TARGET zlib::zlib)
 endif()
 
 set(FFMPEG_COMPONENTS avutil swscale avformat avcodec)
-if(APPLE_IOS)
+if(APPLE_IOS AND NOT USING_CONAN)
 	list(APPEND FFMPEG_COMPONENTS swresample)
 endif()
 find_package(ffmpeg COMPONENTS ${FFMPEG_COMPONENTS})
@@ -309,6 +301,9 @@ endif()
 
 find_package(SDL2 REQUIRED)
 find_package(SDL2_image REQUIRED)
+if(TARGET SDL2_image::SDL2_image)
+	add_library(SDL2::Image ALIAS SDL2_image::SDL2_image)
+endif()
 find_package(SDL2_mixer REQUIRED)
 if(TARGET SDL2_mixer::SDL2_mixer)
 	add_library(SDL2::Mixer ALIAS SDL2_mixer::SDL2_mixer)
@@ -319,7 +314,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)
@@ -450,6 +445,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)
@@ -489,7 +487,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
@@ -594,9 +592,12 @@ elseif(APPLE_MACOS AND NOT ENABLE_MONOLITHIC_INSTALL)
 
 	include(GetGitRevisionDescription)
 	get_git_head_revision(GIT_REFSPEC GIT_SHA1)
+	string(TIMESTAMP CURRENT_YEAR "%Y")
 
 	set(MACOSX_BUNDLE_NAME "${CMAKE_PROJECT_NAME}")
 	set(MACOSX_BUNDLE_BUNDLE_NAME "${CMAKE_PROJECT_NAME}")
+	set(MACOSX_BUNDLE_COPYRIGHT "Copyright © 2007-${CURRENT_YEAR} VCMI team")
+	set(MACOSX_BUNDLE_GUI_IDENTIFIER "eu.vcmi.vcmi")
 	set(MACOSX_BUNDLE_BUNDLE_VERSION ${GIT_SHA1})
 	set(MACOSX_BUNDLE_SHORT_VERSION_STRING ${APP_SHORT_VERSION})
 	if(ENABLE_LAUNCHER)

+ 54 - 12
CMakePresets.json

@@ -6,6 +6,14 @@
             "hidden": true,
             "binaryDir": "${sourceDir}/out/build/${presetName}"
         },
+        {
+            "name": "build-with-conan",
+            "hidden": true,
+            "cacheVariables": {
+                "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/conan-generated/conan_toolchain.cmake",
+                "FORCE_BUNDLED_MINIZIP": "OFF"
+            }
+        },
         {
             "name": "base-release",
             "inherits": "release-binary-dir",
@@ -81,9 +89,11 @@
             "name": "macos-conan-ninja-release",
             "displayName": "Ninja+Conan release",
             "description": "VCMI MacOS Ninja using Conan",
-            "inherits": "default-release",
+            "inherits": [
+                "build-with-conan",
+                "default-release"
+            ],
             "cacheVariables": {
-                "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/conan-generated/conan_toolchain.cmake",
                 "CMAKE_BUILD_TYPE": "Release"
             }
         },
@@ -109,7 +119,8 @@
             "cacheVariables": {
                 "CMAKE_SYSTEM_NAME": "iOS",
                 "FORCE_BUNDLED_FL": "ON",
-                "FORCE_BUNDLED_MINIZIP": "ON"
+                "FORCE_BUNDLED_MINIZIP": "ON",
+                "ENABLE_EDITOR" : "OFF"
             }
         },
         {
@@ -122,16 +133,42 @@
             }
         },
         {
-            "name": "ios-release",
-            "displayName": "iOS release",
-            "description": "VCMI iOS release",
+            "name": "ios-device-conan",
+            "displayName": "Base iOS device using Conan",
+            "description": "Base VCMI preset for iOS device using Conan",
             "inherits": [
-                "base-release",
-                "ios-device",
-                "release-binary-dir"
+                "build-with-conan",
+                "ios-device"
+            ]
+        },
+        {
+            "name": "base-ios-release",
+            "displayName": "Base iOS release",
+            "description": "Base VCMI preset for iOS release",
+            "inherits": "base-release",
+            "hidden": true,
+            "cacheVariables": {
+                "BUNDLE_IDENTIFIER_PREFIX": "eu.vcmi"
+            }
+        },
+        {
+            "name": "ios-release-conan",
+            "displayName": "iOS+Conan release",
+            "description": "VCMI iOS release using Conan",
+            "inherits": [
+                "base-ios-release",
+                "ios-device-conan"
+            ]
+        },
+        {
+            "name": "ios-release-legacy",
+            "displayName": "iOS release using legacy dependencies",
+            "description": "VCMI iOS release using legacy dependencies",
+            "inherits": [
+                "base-ios-release",
+                "ios-device"
             ],
             "cacheVariables": {
-                "BUNDLE_IDENTIFIER_PREFIX": "eu.vcmi",
                 "CMAKE_PREFIX_PATH": "${sourceDir}/build/iphoneos"
             }
         }
@@ -184,14 +221,19 @@
             "inherits": "default-release"
         },
         {
-            "name": "ios-release",
-            "configurePreset": "ios-release",
+            "name": "ios-release-conan",
+            "configurePreset": "ios-release-conan",
             "inherits": "default-release",
             "configuration": "Release",
             "targets": ["vcmiclient"],
             "nativeToolOptions": [
                 "CODE_SIGNING_ALLOWED_FOR_APPS=NO"
             ]
+        },
+        {
+            "name": "ios-release-legacy",
+            "configurePreset": "ios-release-legacy",
+            "inherits": "ios-release-conan"
         }
     ],
     "testPresets": [

+ 1 - 0
README.md

@@ -26,6 +26,7 @@ To use VCMI you need to own original data files.
 
 Platform support is constantly tested by continuous integration and CMake configuration adjusted to generate nice looking projects for all major IDE. Following guides will help you to setup build environment with no effort:
 
+ * (optional) All platforms: [using Conan package manager to obtain prebuilt dependencies](docs/conan.md)
  * [On Linux](https://wiki.vcmi.eu/How_to_build_VCMI_(Linux))
  * [On Linux for Windows with MXE](https://wiki.vcmi.eu/How_to_build_VCMI_(Linux/Cmake/MXE))
  * [On macOS](https://wiki.vcmi.eu/How_to_build_VCMI_(macOS))

+ 2 - 0
client/CMakeLists.txt

@@ -268,9 +268,11 @@ vcmi_set_output_dir(vcmiclient "")
 enable_pch(vcmiclient)
 
 if(APPLE_IOS)
+	vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}")
 	add_custom_command(TARGET vcmiclient POST_BUILD
 		COMMAND ios/set_build_version.sh "$<TARGET_BUNDLE_CONTENT_DIR:vcmiclient>"
 		COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" --component "${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}" --config "$<CONFIG>" --prefix "$<TARGET_BUNDLE_CONTENT_DIR:vcmiclient>"
+		COMMAND ios/rpath_remove_symlinks.sh
 		COMMAND ios/codesign.sh
 		WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
 	)

+ 13 - 0
cmake_modules/VCMIUtils.cmake

@@ -134,3 +134,16 @@ function(install_vcpkg_imported_tgt tgt)
 	message("${tgt_name}: ${TGT_DLL}")
 	install(FILES ${TGT_DLL} DESTINATION ${BIN_DIR})
 endfunction(install_vcpkg_imported_tgt)
+
+# install dependencies from Conan, install_dir should contain \${CMAKE_INSTALL_PREFIX}
+function(vcmi_install_conan_deps install_dir)
+	if(NOT USING_CONAN)
+		return()
+	endif()
+	install(CODE "
+		execute_process(COMMAND
+			conan imports \"${CMAKE_SOURCE_DIR}\" --install-folder \"${CONAN_INSTALL_FOLDER}\" --import-folder \"${install_dir}\"
+		)
+		file(REMOVE \"${install_dir}/conan_imports_manifest.txt\")
+	")
+endfunction()

+ 1 - 1
cmake_modules/VCMI_lib.cmake

@@ -502,7 +502,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 	if("${LIBRARY_TYPE}" STREQUAL SHARED)
 		install(TARGETS ${TARGET_NAME} RUNTIME DESTINATION ${LIB_DIR} LIBRARY DESTINATION ${LIB_DIR})
 	endif()
-	if(APPLE_IOS)
+	if(APPLE_IOS AND NOT USING_CONAN)
 		get_target_property(LINKED_LIBS ${TARGET_NAME} LINK_LIBRARIES)
 		foreach(LINKED_LIB IN LISTS LINKED_LIBS)
 			if(NOT TARGET ${LINKED_LIB})

+ 13 - 0
cmake_modules/VersionDefinition.cmake

@@ -0,0 +1,13 @@
+set(VCMI_VERSION_MAJOR 1)
+set(VCMI_VERSION_MINOR 1)
+set(VCMI_VERSION_PATCH 0)
+add_definitions(
+	-DVCMI_VERSION_MAJOR=${VCMI_VERSION_MAJOR}
+	-DVCMI_VERSION_MINOR=${VCMI_VERSION_MINOR}
+	-DVCMI_VERSION_PATCH=${VCMI_VERSION_PATCH}
+	-DVCMI_VERSION_STRING="${VCMI_VERSION_MAJOR}.${VCMI_VERSION_MINOR}.${VCMI_VERSION_PATCH}"
+)
+set(APP_SHORT_VERSION "${VCMI_VERSION_MAJOR}.${VCMI_VERSION_MINOR}")
+if(NOT VCMI_VERSION_PATCH EQUAL 0)
+	string(APPEND APP_SHORT_VERSION ".${VCMI_VERSION_PATCH}")
+endif()

+ 165 - 145
conanfile.py

@@ -1,166 +1,181 @@
 from conan import ConanFile
+from conan.errors import ConanInvalidConfiguration
 from conan.tools.apple import is_apple_os
-from conan.tools.cmake import CMakeDeps
+from conan.tools.cmake import CMakeDeps, CMakeToolchain
 from conans import tools
 
-import os
+required_conan_version = ">=1.51.3"
 
 class VCMI(ConanFile):
     settings = "os", "compiler", "build_type", "arch"
-    generators = "CMakeToolchain"
     requires = [
-        "boost/1.79.0",
-        "ffmpeg/4.4",
-        "minizip/1.2.12",
-        "onetbb/2021.3.0", # Nullkiller AI
-        "qt/5.15.5", # launcher
-        "sdl/2.0.20",
-        "sdl_image/2.0.5",
-        "sdl_mixer/2.0.4",
-        "sdl_ttf/2.0.18",
+        "boost/[^1.69]",
+        "minizip/[~1.2.12]",
+        "onetbb/[^2021.3]", # Nullkiller AI
+        "qt/[~5.15.2]", # launcher
+        "sdl/[~2.24.0]",
+        "sdl_image/[~2.0.5]",
+        "sdl_mixer/[~2.0.4]",
+        "sdl_ttf/[~2.0.18]",
     ]
-
-    def _disableQtOptions(disableFlag, options):
-        return " ".join([f"-{disableFlag}-{tool}" for tool in options])
-
-    _qtOptions = [
-        _disableQtOptions("no", [
-            "gif",
-            "ico",
-        ]),
-        _disableQtOptions("no-feature", [
-            # xpm format is required for Drag'n'Drop support
-            "imageformat_bmp",
-            "imageformat_jpeg",
-            "imageformat_ppm",
-            "imageformat_xbm",
-
-            # we need only macdeployqt
-            # TODO: disabling these doesn't disable generation of CMake targets
-            # TODO: in Qt 6.3 it's a part of qtbase
-            # "assistant",
-            # "designer",
-            # "distancefieldgenerator",
-            # "kmap2qmap",
-            # "linguist",
-            # "makeqpf",
-            # "pixeltool",
-            # "qdbus",
-            # "qev",
-            # "qtattributionsscanner",
-            # "qtdiag",
-            # "qtpaths",
-            # "qtplugininfo",
-        ]),
-    ]
-
+    options = {
+        "default_options_of_requirements": [True, False],
+        "with_apple_system_libs": [True, False],
+        "with_ffmpeg": [True, False],
+        "with_luajit": [True, False],
+    }
     default_options = {
-        # shared libs
+        "default_options_of_requirements": False,
+        "with_apple_system_libs": False,
+        "with_ffmpeg": True,
+        "with_luajit": False,
+
         "boost/*:shared": True,
-        "libpng/*:shared": True, # SDL_image and Qt depend on it
         "minizip/*:shared": True,
         "onetbb/*:shared": True,
-        "qt/*:shared": True,
+    }
+
+    def configure(self):
+        # SDL_image and Qt depend on it, in iOS both are static
+        self.options["libpng"].shared = self.settings.os != "iOS"
+        # static Qt for iOS is the only viable option at the moment
+        self.options["qt"].shared = self.settings.os != "iOS"
+
+        if self.options.default_options_of_requirements:
+            return
 
         # we need only the following Boost parts:
         # date_time filesystem locale program_options system thread
         # some other parts are also enabled because they're dependents
         # see e.g. conan-center-index/recipes/boost/all/dependencies
-        "boost/*:without_context": True,
-        "boost/*:without_contract": True,
-        "boost/*:without_coroutine": True,
-        "boost/*:without_fiber": True,
-        "boost/*:without_graph": True,
-        "boost/*:without_graph_parallel": True,
-        "boost/*:without_iostreams": True,
-        "boost/*:without_json": True,
-        "boost/*:without_log": True,
-        "boost/*:without_math": True,
-        "boost/*:without_mpi": True,
-        "boost/*:without_nowide": True,
-        "boost/*:without_python": True,
-        "boost/*:without_random": True,
-        "boost/*:without_regex": True,
-        "boost/*:without_serialization": True,
-        "boost/*:without_stacktrace": True,
-        "boost/*:without_test": True,
-        "boost/*:without_timer": True,
-        "boost/*:without_type_erasure": True,
-        "boost/*:without_wave": True,
-
-        "ffmpeg/*:avdevice": False,
-        "ffmpeg/*:avfilter": False,
-        "ffmpeg/*:postproc": False,
-        "ffmpeg/*:swresample": False,
-        "ffmpeg/*:with_freetype": False,
-        "ffmpeg/*:with_libfdk_aac": False,
-        "ffmpeg/*:with_libmp3lame": False,
-        "ffmpeg/*:with_libvpx": False,
-        "ffmpeg/*:with_libwebp": False,
-        "ffmpeg/*:with_libx264": False,
-        "ffmpeg/*:with_libx265": False,
-        "ffmpeg/*:with_openh264": False,
-        "ffmpeg/*:with_openjpeg": False,
-        "ffmpeg/*:with_opus": False,
-        "ffmpeg/*:with_programs": False,
-        "ffmpeg/*:with_ssl": False,
-        "ffmpeg/*:with_vorbis": False,
-
-        "sdl/*:vulkan": False,
-
-        "sdl_image/*:imageio": True,
-        "sdl_image/*:lbm": False,
-        "sdl_image/*:pnm": False,
-        "sdl_image/*:svg": False,
-        "sdl_image/*:tga": False,
-        "sdl_image/*:with_libjpeg": False,
-        "sdl_image/*:with_libtiff": False,
-        "sdl_image/*:with_libwebp": False,
-        "sdl_image/*:xcf": False,
-        "sdl_image/*:xpm": False,
-        "sdl_image/*:xv": False,
-
-        "sdl_mixer/*:flac": False,
-        "sdl_mixer/*:mad": False,
-        "sdl_mixer/*:mikmod": False,
-        "sdl_mixer/*:modplug": False,
-        "sdl_mixer/*:nativemidi": False,
-        "sdl_mixer/*:opus": False,
-        "sdl_mixer/*:wav": False,
-
-        "qt/*:config": " ".join(_qtOptions),
-        "qt/*:openssl": False,
-        "qt/*:qttools": True,
-        "qt/*:with_freetype": False,
-        "qt/*:with_libjpeg": False,
-        "qt/*:with_md4c": False,
-        "qt/*:with_mysql": False,
-        "qt/*:with_odbc": False,
-        "qt/*:with_openal": False,
-        "qt/*:with_pq": False,
+        self.options["boost"].without_context = True
+        self.options["boost"].without_contract = True
+        self.options["boost"].without_coroutine = True
+        self.options["boost"].without_fiber = True
+        self.options["boost"].without_graph = True
+        self.options["boost"].without_graph_parallel = True
+        self.options["boost"].without_iostreams = True
+        self.options["boost"].without_json = True
+        self.options["boost"].without_log = True
+        self.options["boost"].without_math = True
+        self.options["boost"].without_mpi = True
+        self.options["boost"].without_nowide = True
+        self.options["boost"].without_python = True
+        self.options["boost"].without_random = True
+        self.options["boost"].without_regex = True
+        self.options["boost"].without_serialization = True
+        self.options["boost"].without_stacktrace = True
+        self.options["boost"].without_test = True
+        self.options["boost"].without_timer = True
+        self.options["boost"].without_type_erasure = True
+        self.options["boost"].without_wave = True
+
+        self.options["ffmpeg"].avdevice = False
+        self.options["ffmpeg"].avfilter = False
+        self.options["ffmpeg"].postproc = False
+        self.options["ffmpeg"].swresample = False
+        self.options["ffmpeg"].with_freetype = False
+        self.options["ffmpeg"].with_libfdk_aac = False
+        self.options["ffmpeg"].with_libmp3lame = False
+        self.options["ffmpeg"].with_libvpx = False
+        self.options["ffmpeg"].with_libwebp = False
+        self.options["ffmpeg"].with_libx264 = False
+        self.options["ffmpeg"].with_libx265 = False
+        self.options["ffmpeg"].with_openh264 = False
+        self.options["ffmpeg"].with_openjpeg = False
+        self.options["ffmpeg"].with_opus = False
+        self.options["ffmpeg"].with_programs = False
+        self.options["ffmpeg"].with_ssl = False
+        self.options["ffmpeg"].with_vorbis = False
+
+        self.options["sdl"].sdl2main = self.settings.os != "iOS"
+        self.options["sdl"].vulkan = False
+
+        self.options["sdl_image"].lbm = False
+        self.options["sdl_image"].pnm = False
+        self.options["sdl_image"].svg = False
+        self.options["sdl_image"].tga = False
+        self.options["sdl_image"].with_libjpeg = False
+        self.options["sdl_image"].with_libtiff = False
+        self.options["sdl_image"].with_libwebp = False
+        self.options["sdl_image"].xcf = False
+        self.options["sdl_image"].xpm = False
+        self.options["sdl_image"].xv = False
+        if is_apple_os(self):
+            self.options["sdl_image"].imageio = True
+
+        self.options["sdl_mixer"].flac = False
+        self.options["sdl_mixer"].mad = False
+        self.options["sdl_mixer"].mikmod = False
+        self.options["sdl_mixer"].modplug = False
+        self.options["sdl_mixer"].nativemidi = False
+        self.options["sdl_mixer"].opus = False
+        self.options["sdl_mixer"].wav = False
+
+        def _disableQtOptions(disableFlag, options):
+            return " ".join([f"-{disableFlag}-{tool}" for tool in options])
+
+        _qtOptions = [
+            _disableQtOptions("no", [
+                "gif",
+                "ico",
+            ]),
+            _disableQtOptions("no-feature", [
+                # xpm format is required for Drag'n'Drop support
+                "imageformat_bmp",
+                "imageformat_jpeg",
+                "imageformat_ppm",
+                "imageformat_xbm",
+
+                # we need only macdeployqt
+                # TODO: disabling these doesn't disable generation of CMake targets
+                # TODO: in Qt 6.3 it's a part of qtbase
+                # "assistant",
+                # "designer",
+                # "distancefieldgenerator",
+                # "kmap2qmap",
+                # "linguist",
+                # "makeqpf",
+                # "pixeltool",
+                # "qdbus",
+                # "qev",
+                # "qtattributionsscanner",
+                # "qtdiag",
+                # "qtpaths",
+                # "qtplugininfo",
+            ]),
+        ]
+        self.options["qt"].config = " ".join(_qtOptions)
+        self.options["qt"].qttools = True
+        self.options["qt"].with_freetype = False
+        self.options["qt"].with_libjpeg = False
+        self.options["qt"].with_md4c = False
+        self.options["qt"].with_mysql = False
+        self.options["qt"].with_odbc = False
+        self.options["qt"].with_openal = False
+        self.options["qt"].with_pq = False
+        self.options["qt"].openssl = not is_apple_os(self)
+        if self.settings.os == "iOS":
+            self.options["qt"].opengl = "es2"
 
         # transitive deps
-        "pcre2/*:build_pcre2grep": False, # doesn't link to overridden bzip2 & zlib, the tool isn't needed anyway
-    }
-
-    def configure(self):
-        # workaround: macOS deployment target isn't passed to linker when building Boost
-        # TODO: remove when https://github.com/conan-io/conan-center-index/pull/12468 is merged
-        if is_apple_os(self):
-            osVersion = self.settings.get_safe("os.version")
-            if osVersion:
-                deploymentTargetFlag = tools.apple_deployment_target_flag(
-                    self.settings.os,
-                    osVersion,
-                    self.settings.get_safe("os.sdk"),
-                    self.settings.get_safe("os.subsystem"),
-                    self.settings.get_safe("arch")
-                )
-                self.options["boost"].extra_b2_flags = f"linkflags={deploymentTargetFlag}"
+        # doesn't link to overridden bzip2 & zlib, the tool isn't needed anyway
+        self.options["pcre2"].build_pcre2grep = False
 
     def requirements(self):
+        # TODO: will no longer be needed after merging https://github.com/conan-io/conan-center-index/pull/13399
+        self.requires("libpng/[~1.6.38]", override=True) # freetype / Qt
+        if self.options.default_options_of_requirements:
+            self.requires("libjpeg/9e", override=True) # libtiff / Qt
+            self.requires("freetype/[~2.12.1]", override=True) # sdl_ttf / Qt
+            if self.options.with_ffmpeg:
+                self.requires("libwebp/[~1.2.4]", override=True) # sdl_image / ffmpeg
+
+        if self.options.with_ffmpeg:
+            self.requires("ffmpeg/[^4.4]")
+
         # use Apple system libraries instead of external ones
-        if is_apple_os(self):
+        if self.options.with_apple_system_libs and not self.options.default_options_of_requirements and is_apple_os(self):
             systemLibsOverrides = [
                 "bzip2/1.0.8",
                 "libiconv/1.17",
@@ -168,15 +183,20 @@ class VCMI(ConanFile):
                 "zlib/1.2.12",
             ]
             for lib in systemLibsOverrides:
-                self.requires(f"{lib}@kambala/apple", override=True)
+                self.requires(f"{lib}@vcmi/apple", override=True)
 
-        # TODO: the latest official release of LuaJIT (which is quite old) can't be built for arm Mac
-        if self.settings.os != "Macos" or self.settings.arch != "armv8":
-            self.requires("luajit/2.0.5")
+        # TODO: the latest official release of LuaJIT (which is quite old) can't be built for arm
+        if self.options.with_luajit and not str(self.settings.arch).startswith("arm"):
+            self.requires("luajit/[~2.0.5]")
 
     def generate(self):
+        tc = CMakeToolchain(self)
+        tc.variables["USING_CONAN"] = True
+        tc.variables["CONAN_INSTALL_FOLDER"] = self.install_folder
+        tc.generate()
+
         deps = CMakeDeps(self)
-        if os.getenv("USE_CONAN_WITH_ALL_CONFIGS", "0") == "0":
+        if tools.get_env("GENERATE_ONLY_BUILT_CONFIG", default=False):
             deps.generate()
             return
 

+ 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"
+		}
+	}
+}

+ 147 - 0
docs/conan.md

@@ -0,0 +1,147 @@
+# Using dependencies from Conan
+
+[Conan](https://conan.io/) is a package manager for C/C++. We provide prebuilt binary dependencies for some platforms that are used by our CI, but they can also be consumed by users to build VCMI. However, it's not required to use only the prebuilt binaries: you can build them from source as well.
+
+## Supported platforms
+
+The following platforms are supported and known to work, others might require changes to our [conanfile.py](../conanfile.py) or upstream recipes.
+
+- **macOS**: x86_64 (Intel) - target 10.13 (High Sierra), arm64 (Apple Silicon) - target 11.0 (Big Sur)
+- **iOS**: arm64 - target 12.0
+
+## Getting started
+
+1. [Install Conan](https://docs.conan.io/en/latest/installation.html)
+2. Execute in terminal: `conan profile new default --detect`
+
+## Download dependencies
+
+0. If your platform is not on the list of supported ones or you don't want to use our prebuilt binaries, you can still build dependencies from source or try consuming prebuilt binaries from the central Conan repository - [ConanCenter](https://conan.io/center/). In this case skip to the [next section](#generate-cmake-integration) directly.
+
+1. Check if your build environment can use the prebuilt binaries: basically, that your compiler version (or Xcode major version) matches the information below. If you're unsure, simply advance to the next step.
+
+    - macOS: libraries are built with Apple clang 13 (Xcode 13.4.1), should be consumable by Xcode and Xcode CLT 13.x
+    - iOS: libraries are built with Apple clang 13 (Xcode 13.4.1), should be consumable by Xcode 13.x
+
+2. Download the binaries archive and unpack it to `~/.conan` directory:
+
+    - [macOS](https://github.com/vcmi/vcmi-deps-macos/releases/latest): pick **intel.txz** if you have Intel Mac, otherwise - **intel-cross-arm.txz**
+    - [iOS](https://github.com/vcmi/vcmi-ios-deps/releases/latest)
+
+3. Only if you have Apple Silicon Mac and trying to build for macOS or iOS: follow [instructions how to build Qt host tools for Apple Silicon](https://github.com/vcmi/vcmi-ios-deps#note-for-arm-macs), on step 3 copy them to `~/.conan/data/qt/5.15.x/_/_/package/SOME_HASH/bin` (`5.15.x` and `SOME_HASH` are placeholders).
+
+## Generate CMake integration
+
+Conan needs to generate CMake toolchain file to make dependencies available to CMake. See `CMakeDeps` and `CMakeToolchain` [in the official documentation](https://docs.conan.io/en/latest/reference/conanfile/tools/cmake.html) for details.
+
+In terminal `cd` to the VCMI source directory and run the following command. If you want to download prebuilt binaries from ConanCenter, also read the [next section](#using-prebuilt-binaries-from-conancenter).
+
+<pre>
+conan install . \
+  --install-folder=<b><i>conan-generated</i></b> \
+  --no-imports \
+  --build=<b><i>never</i></b> \
+  --profile:build=default \
+  --profile:host=<b><i>CI/conan/PROFILE</i></b>
+</pre>
+
+The highlighted parts can be adjusted:
+
+- ***conan-generated***: directory (absolute or relative) where the generated files will appear. This value is used in CMake presets from VCMI, but you can actually use any directory and override it in your local CMake presets.
+- ***never***: use this value to avoid building any dependency from source. You can also use `missing` to build recipes, that are not present in your local cache, from source.
+- ***CI/conan/PROFILE***: if you want to consume our prebuilt binaries, ***PROFILE*** must be replaced with one of filenames from our [Conan profiles directory](../CI/conan) (determining the right file should be straight-forward). Otherwise, either select one of our profiles or replace ***CI/conan/PROFILE*** with `default` (your default profile).
+
+If you use `--build=never` and this command fails, then it means that you can't use prebuilt binaries out of the box. For example, try using `--build=missing` instead.
+
+VCMI "recipe" also has some options that you can specify. For example, if you don't care about game videos, you can disable FFmpeg dependency by passing `-o with_ffmpeg=False`. If you only want to make release build, you can use `GENERATE_ONLY_BUILT_CONFIG=1` environment variable to skip generating files for other configurations (our CI does this).
+
+_Note_: you can find full reference of this command [in the official documentation](https://docs.conan.io/en/latest/reference/commands/consumer/install.html) or by executing `conan help install`.
+
+### Using prebuilt binaries from ConanCenter
+
+First, check if binaries for [your platform](https://github.com/conan-io/conan-center-index/blob/master/docs/supported_platforms_and_configurations.md) are produced.
+
+You must adjust the above `conan install` command:
+
+1. Replace ***CI/conan/PROFILE*** with `default`.
+2. Additionally pass `-o default_options_of_requirements=True`: this disables all custom options of our `conanfile.py` to match ConanCenter builds.
+
+## Configure project for building
+
+You must pass the generated toolchain file to CMake invocation.
+
+- if using custom CMake presets, just make sure to inherit our `build-with-conan` preset. If you store Conan generated files in a non-default directory, define the path to the generated toolchain in `toolchainFile` field (or `CMAKE_TOOLCHAIN_FILE` cache variable) or include CMake presets file generated by Conan.
+- otherwise, if passing CMake options on the command line, use `--toolchain` option (available in CMake 3.21+) or `CMAKE_TOOLCHAIN_FILE` variable.
+
+## Examples
+
+In these examples only the minimum required amount of options is passed to `cmake` invocation, you can pass additional ones as needed.
+
+### Use our prebuilt binaries to build for macOS x86_64 with Xcode
+
+```
+conan install . \
+  --install-folder=conan-generated \
+  --no-imports \
+  --build=never \
+  --profile:build=default \
+  --profile:host=CI/conan/macos-intel
+
+cmake -S . -B build -G Xcode \
+  --toolchain conan-generated/conan_toolchain.cmake
+```
+
+### Try to use binaries from ConanCenter for your platform
+
+If you also want to build the missing binaries from source, use `--build=missing` instead of `--build=never`.
+
+```
+conan install . \
+  --install-folder=~/my-dir \
+  --no-imports \
+  --build=never \
+  --profile:build=default \
+  --profile:host=default \
+  -o default_options_of_requirements=True
+
+cmake -S . -B build \
+  -D CMAKE_TOOLCHAIN_FILE=~/my-dir/conan_toolchain.cmake
+```
+
+### Use our prebuilt binaries to build for iOS arm64 device with custom preset
+
+```
+conan install . \
+  --install-folder=~/my-dir \
+  --no-imports \
+  --build=never \
+  --profile:build=default \
+  --profile:host=CI/conan/ios-arm64
+
+cmake --preset ios-conan
+```
+
+`CMakeUserPresets.json` file:
+
+```json
+{
+    "version": 3,
+    "cmakeMinimumRequired": {
+        "major": 3,
+        "minor": 21,
+        "patch": 0
+    },
+    "configurePresets": [
+        {
+            "name": "ios-conan",
+            "displayName": "iOS",
+            "inherits": ["build-with-conan", "ios-device"],
+            "toolchainFile": "~/my-dir/conan_toolchain.cmake",
+            "cacheVariables": {
+                "BUNDLE_IDENTIFIER_PREFIX": "com.YOUR-NAME",
+                "CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM": "YOUR_TEAM_ID"
+            }
+        }
+    ]
+}
+```

+ 1 - 0
ios/CMakeLists.txt

@@ -8,3 +8,4 @@ target_link_libraries(iOS_utils PRIVATE
 target_include_directories(iOS_utils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
 
 vcmi_set_output_dir(iOS_utils "")
+install(TARGETS iOS_utils RUNTIME DESTINATION ${LIB_DIR} LIBRARY DESTINATION ${LIB_DIR})

+ 7 - 0
ios/rpath_remove_symlinks.sh

@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+cd "$CODESIGNING_FOLDER_PATH/Frameworks"
+tbbFilename=$(otool -L libNullkiller.dylib | egrep --only-matching 'libtbb\S+')
+if [[ -L "$tbbFilename" ]]; then
+	mv -f "$(readlink "$tbbFilename")" "$tbbFilename"
+fi

+ 12 - 0
launcher/CMakeLists.txt

@@ -122,6 +122,18 @@ enable_pch(vcmilauncher)
 
 if(APPLE_IOS)
 	set(ICONS_DESTINATION ${DATA_DIR})
+
+	# workaround https://github.com/conan-io/conan-center-index/issues/13332
+	if(USING_CONAN)
+		file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/QIOSIntegrationPlugin.h
+			"#include <QtPlugin>\nQ_IMPORT_PLUGIN(QIOSIntegrationPlugin)"
+		)
+		# target_include_directories(vcmilauncher PRIVATE ${CMAKE_BINARY_DIR})
+		target_link_libraries(vcmilauncher
+			Qt${QT_VERSION_MAJOR}::QIOSIntegrationPlugin
+			qt::QIOSIntegrationPlugin
+		)
+	endif()
 else()
 	set(ICONS_DESTINATION ${DATA_DIR}/launcher)
 

+ 1 - 0
launcher/eu.vcmi.VCMI.metainfo.xml

@@ -51,6 +51,7 @@
 	<provides>
 		<binary>vcmibuilder</binary>
 		<binary>vcmiclient</binary>
+		<binary>vcmieditor</binary>
 		<binary>vcmilauncher</binary>
 		<binary>vcmiserver</binary>
 	</provides>

+ 7 - 0
launcher/main.cpp

@@ -12,6 +12,13 @@
 
 #include <QApplication>
 
+// Conan workaround https://github.com/conan-io/conan-center-index/issues/13332
+#ifdef VCMI_IOS
+#if __has_include("QIOSIntegrationPlugin.h")
+#include "QIOSIntegrationPlugin.h"
+#endif
+#endif
+
 int main(int argc, char * argv[])
 {
 	int result;

+ 1 - 1
launcher/settingsView/csettingsview_moc.cpp

@@ -97,7 +97,7 @@ void CSettingsView::loadSettings()
 
 	ui->lineEditUserDataDir->setText(pathToQString(VCMIDirs::get().userDataPath()));
 	ui->lineEditGameDir->setText(pathToQString(VCMIDirs::get().binaryPath()));
-	ui->lineEditTempDir->setText(pathToQString(VCMIDirs::get().userCachePath()));
+	ui->lineEditTempDir->setText(pathToQString(VCMIDirs::get().userLogsPath()));
 
 	std::string encoding = settings["general"]["encoding"].String();
 	size_t encodingIndex = boost::range::find(knownEncodingsList, encoding) - knownEncodingsList;

+ 6 - 1
lib/battle/ReachabilityInfo.cpp

@@ -70,7 +70,12 @@ int ReachabilityInfo::distToNearestNeighbour(
 	const battle::Unit * defender,
 	BattleHex * chosenHex) const
 {
-	auto attackableHexes = defender->getAttackableHexes(attacker);
+	auto attackableHexes = defender->getHexes();
+
+	if(attacker->doubleWide())
+	{
+		vstd::concatenate(attackableHexes, battle::Unit::getHexes(defender->occupiedHex(), true, attacker->unitSide()));
+	}
 
 	return distToNearestNeighbour(attackableHexes, chosenHex);
 }

+ 1 - 1
lib/mapping/MapFormatH3M.cpp

@@ -400,7 +400,7 @@ void CMapLoaderH3M::readVictoryLossConditions()
 				EventExpression::OperatorAll oper;
 				EventCondition cond(EventCondition::HAVE_BUILDING);
 				cond.position = readInt3();
-				cond.objectType = BuildingID::VILLAGE_HALL + reader.readUInt8();
+				cond.objectType = BuildingID::TOWN_HALL + reader.readUInt8();
 				oper.expressions.push_back(cond);
 				cond.objectType = BuildingID::FORT + reader.readUInt8();
 				oper.expressions.push_back(cond);

+ 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;
 };
 
 

+ 1 - 1
lib/spells/ISpellMechanics.cpp

@@ -776,7 +776,7 @@ std::unique_ptr<IAdventureSpellMechanics> IAdventureSpellMechanics::createMechan
 	case SpellID::VIEW_AIR:
 		return make_unique<ViewAirMechanics>(s);
 	default:
-		return std::unique_ptr<IAdventureSpellMechanics>();
+		return s->combat ? std::unique_ptr<IAdventureSpellMechanics>() : make_unique<AdventureSpellMechanics>(s);
 	}
 }
 

+ 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);
+}
+

+ 141 - 0
mapeditor/CMakeLists.txt

@@ -0,0 +1,141 @@
+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 "vcmieditor.desktop" DESTINATION share/applications)
+	install(FILES "icons/mapeditor.32x32.png"   DESTINATION share/icons/hicolor/32x32/apps RENAME vcmieditor.png)
+	install(FILES "icons/mapeditor.48x48.png"   DESTINATION share/icons/hicolor/48x48/apps RENAME vcmieditor.png)
+	install(FILES "icons/mapeditor.64x64.png"   DESTINATION share/icons/hicolor/64x64/apps RENAME vcmieditor.png)
+	install(FILES "icons/mapeditor.128x128.png" DESTINATION share/icons/hicolor/128x128/apps RENAME vcmieditor.png)
+	install(FILES "icons/mapeditor.256x256.png" DESTINATION share/icons/hicolor/256x256/apps RENAME vcmieditor.png)
+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/mapeditor.128x128.png


BIN
mapeditor/icons/mapeditor.256x256.png


BIN
mapeditor/icons/mapeditor.32x32.png


BIN
mapeditor/icons/mapeditor.48x48.png


BIN
mapeditor/icons/mapeditor.64x64.png


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();
+}

Some files were not shown because too many files changed in this diff