Sfoglia il codice sorgente

Merge pull request #1977 from vcmi/beta

Merge 1.2 release into master branch
Ivan Savenko 2 anni fa
parent
commit
c125e040c3
100 ha cambiato i file con 1890 aggiunte e 1056 eliminazioni
  1. 40 0
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 14 0
      .github/ISSUE_TEMPLATE/feature_request.md
  3. 54 49
      .github/workflows/github.yml
  4. 3 0
      .gitignore
  5. 21 18
      AI/BattleAI/AttackPossibility.cpp
  6. 59 28
      AI/BattleAI/BattleAI.cpp
  7. 3 2
      AI/BattleAI/BattleAI.h
  8. 15 8
      AI/BattleAI/BattleExchangeVariant.cpp
  9. 1 1
      AI/BattleAI/BattleExchangeVariant.h
  10. 8 8
      AI/BattleAI/CMakeLists.txt
  11. 4 6
      AI/BattleAI/PotentialTargets.cpp
  12. 4 4
      AI/BattleAI/StackWithBonuses.cpp
  13. 2 2
      AI/BattleAI/StackWithBonuses.h
  14. 17 4
      AI/CMakeLists.txt
  15. 1 1
      AI/EmptyAI/CEmptyAI.cpp
  16. 1 1
      AI/EmptyAI/CEmptyAI.h
  17. 10 6
      AI/EmptyAI/CMakeLists.txt
  18. 3 1
      AI/EmptyAI/StdInc.h
  19. 1 1
      AI/EmptyAI/main.cpp
  20. 58 39
      AI/Nullkiller/AIGateway.cpp
  21. 3 3
      AI/Nullkiller/AIGateway.h
  22. 9 6
      AI/Nullkiller/AIUtility.cpp
  23. 1 1
      AI/Nullkiller/AIUtility.h
  24. 5 3
      AI/Nullkiller/Analyzers/ArmyManager.cpp
  25. 2 2
      AI/Nullkiller/Analyzers/BuildAnalyzer.cpp
  26. 6 1
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp
  27. 9 0
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h
  28. 21 61
      AI/Nullkiller/Analyzers/HeroManager.cpp
  29. 2 2
      AI/Nullkiller/Analyzers/ObjectClusterizer.cpp
  30. 15 8
      AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp
  31. 70 29
      AI/Nullkiller/Behaviors/DefenceBehavior.cpp
  32. 68 19
      AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp
  33. 6 11
      AI/Nullkiller/Behaviors/StartupBehavior.cpp
  34. 9 12
      AI/Nullkiller/CMakeLists.txt
  35. 9 6
      AI/Nullkiller/Engine/AIMemory.cpp
  36. 1 1
      AI/Nullkiller/Engine/FuzzyEngines.cpp
  37. 17 5
      AI/Nullkiller/Engine/FuzzyHelper.cpp
  38. 24 23
      AI/Nullkiller/Engine/Nullkiller.cpp
  39. 6 5
      AI/Nullkiller/Engine/Nullkiller.h
  40. 118 34
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  41. 2 2
      AI/Nullkiller/Engine/PriorityEvaluator.h
  42. 4 4
      AI/Nullkiller/Goals/AbstractGoal.cpp
  43. 6 6
      AI/Nullkiller/Goals/AdventureSpellCast.cpp
  44. 2 2
      AI/Nullkiller/Goals/BuildThis.cpp
  45. 1 1
      AI/Nullkiller/Goals/BuyArmy.cpp
  46. 8 5
      AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp
  47. 3 0
      AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h
  48. 12 11
      AI/Nullkiller/Goals/ExecuteHeroChain.cpp
  49. 1 1
      AI/Nullkiller/Goals/GatherArmy.cpp
  50. 3 3
      AI/Nullkiller/Goals/RecruitHero.cpp
  51. 15 5
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  52. 1 1
      AI/Nullkiller/Pathfinding/Actions/BuyArmyAction.cpp
  53. 1 1
      AI/Nullkiller/Pathfinding/Actions/TownPortalAction.cpp
  54. 19 14
      AI/Nullkiller/Pathfinding/Actors.cpp
  55. 4 2
      AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp
  56. 10 6
      AI/StupidAI/CMakeLists.txt
  57. 8 6
      AI/StupidAI/StupidAI.cpp
  58. 3 3
      AI/StupidAI/StupidAI.h
  59. 1 1
      AI/VCAI/AIUtility.cpp
  60. 9 11
      AI/VCAI/CMakeLists.txt
  61. 1 1
      AI/VCAI/FuzzyEngines.cpp
  62. 5 5
      AI/VCAI/Goals/AbstractGoal.cpp
  63. 7 7
      AI/VCAI/Goals/AdventureSpellCast.cpp
  64. 1 1
      AI/VCAI/Goals/BuyArmy.cpp
  65. 2 2
      AI/VCAI/Goals/CompleteQuest.cpp
  66. 3 3
      AI/VCAI/Goals/Explore.cpp
  67. 1 1
      AI/VCAI/Goals/GatherArmy.cpp
  68. 1 1
      AI/VCAI/Goals/GatherTroops.cpp
  69. 1 1
      AI/VCAI/Goals/VisitHero.cpp
  70. 1 1
      AI/VCAI/Goals/VisitObj.cpp
  71. 1 1
      AI/VCAI/Goals/VisitTile.cpp
  72. 0 4
      AI/VCAI/MapObjectsEvaluator.cpp
  73. 3 3
      AI/VCAI/Pathfinding/AINodeStorage.cpp
  74. 1 1
      AI/VCAI/Pathfinding/AIPathfinder.cpp
  75. 52 47
      AI/VCAI/VCAI.cpp
  76. 3 3
      AI/VCAI/VCAI.h
  77. 1 8
      CCallback.cpp
  78. 5 2
      CCallback.h
  79. 4 0
      CI/android-32/before_install.sh
  80. 4 0
      CI/android-64/before_install.sh
  81. 8 0
      CI/android/before_install.sh
  82. 4 0
      CI/android/signing.properties
  83. BIN
      CI/android/vcmi-travis.jks
  84. 5 0
      CI/conan/android-32
  85. 5 0
      CI/conan/android-64
  86. 6 0
      CI/conan/base/android
  87. 20 0
      CI/conan/base/cross-macro.j2
  88. 10 0
      CI/conan/base/cross-windows
  89. 15 0
      CI/conan/mingw32-linux.jinja
  90. 13 0
      CI/conan/mingw64-linux.jinja
  91. 11 0
      CI/linux-qt6/before_install.sh
  92. 1 0
      CI/linux-qt6/upload_package.sh
  93. 1 1
      CI/linux/before_install.sh
  94. 16 0
      CI/mingw-ubuntu/before_install.sh
  95. 7 7
      CI/msvc/before_install.sh
  96. 0 44
      CI/mxe/before_install.sh
  97. 231 75
      CMakeLists.txt
  98. 42 0
      CMakePresets.json
  99. 501 265
      ChangeLog.md
  100. 64 76
      Global.h

+ 40 - 0
.github/ISSUE_TEMPLATE/bug_report.md

@@ -0,0 +1,40 @@
+---
+name: Bug report
+about: Report an issue to help us improve
+title: ''
+labels: ["bug"]
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**Game logs**
+Please attach game logs: `VCMI_client.txt`, `VCMI_server.txt` etc.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Actual behavior**
+A clear description what is currently happening 
+
+**Did it work earlier?**
+If this something which worked well some time ago, please let us know about version where it works or at date when it worked.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Version**
+ - OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS]
+ - Version: [VCMI version]
+
+**Additional context**
+Add any other context about the problem here.

+ 14 - 0
.github/ISSUE_TEMPLATE/feature_request.md

@@ -0,0 +1,14 @@
+---
+name: Feature request
+about: Suggest an improvement
+title: ''
+labels: ["enhancement"]
+assignees: ''
+
+---
+
+**Describe your proposal**
+Give us as many as possible details about your idea.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.

+ 54 - 49
.github/workflows/github.yml

@@ -68,8 +68,8 @@ jobs:
     strategy:
       matrix:
         include:
-          - platform: linux
-            os: ubuntu-20.04
+          - platform: linux-qt6
+            os: ubuntu-22.04
             test: 0
             preset: linux-clang-release
           - platform: linux
@@ -83,6 +83,7 @@ jobs:
             extension: dmg
             preset: macos-conan-ninja-release
             conan_profile: macos-intel
+            conan_options: --options with_apple_system_libs=True
             artifact_platform: intel
           - platform: mac-arm
             os: macos-12
@@ -91,6 +92,7 @@ jobs:
             extension: dmg
             preset: macos-arm-conan-ninja-release
             conan_profile: macos-arm
+            conan_options: --options with_apple_system_libs=True
             artifact_platform: arm
           - platform: ios
             os: macos-12
@@ -99,20 +101,36 @@ jobs:
             extension: ipa
             preset: ios-release-conan
             conan_profile: ios-arm64
-          - platform: mxe
-            os: ubuntu-20.04
-            mxe: i686-w64-mingw32.shared
-            test: 0
-            pack: 1
-            cpack_args: -D CPACK_NSIS_EXECUTABLE=`which makensis`
-            extension: exe
-            cmake_args: -G Ninja
+            conan_options: --options with_apple_system_libs=True
           - platform: msvc
             os: windows-latest
             test: 0
             pack: 1
             extension: exe
             preset: windows-msvc-release
+          - platform: mingw-ubuntu
+            os: ubuntu-22.04
+            test: 0
+            pack: 1
+            extension: exe
+            cpack_args: -D CPACK_NSIS_EXECUTABLE=`which makensis`
+            cmake_args: -G Ninja
+            preset: windows-mingw-conan-linux
+            conan_profile: mingw64-linux.jinja
+          - platform: android-32
+            os: ubuntu-22.04
+            extension: apk
+            preset: android-conan-ninja-release
+            conan_profile: android-32
+            conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT
+            artifact_platform: armeabi-v7a
+          - platform: android-64
+            os: ubuntu-22.04
+            extension: apk
+            preset: android-conan-ninja-release
+            conan_profile: android-64
+            conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT
+            artifact_platform: aarch64-v8a
     runs-on: ${{ matrix.os }}
     defaults:
       run:
@@ -126,7 +144,6 @@ jobs:
     - name: Dependencies
       run: source '${{github.workspace}}/CI/${{matrix.platform}}/before_install.sh'
       env:
-        MXE_TARGET: ${{ matrix.mxe }}
         VCMI_BUILD_PLATFORM: x64
 
     - uses: actions/setup-python@v4
@@ -136,7 +153,7 @@ jobs:
     - name: Conan setup
       if: "${{ matrix.conan_profile != '' }}"
       run: |
-        pip3 install conan
+        pip3 install 'conan<2.0'
         conan profile new default --detect
         conan install . \
           --install-folder=conan-generated \
@@ -144,7 +161,7 @@ jobs:
           --build=never \
           --profile:build=default \
           --profile:host=CI/conan/${{ matrix.conan_profile }} \
-          --options with_apple_system_libs=True
+          ${{ matrix.conan_options }}
       env:
         GENERATE_ONLY_BUILT_CONFIG: 1
 
@@ -164,39 +181,16 @@ jobs:
       env:
         PULL_REQUEST: ${{ github.event.pull_request.number }}
 
-    - name: Configure CMake
-      if: "${{ matrix.preset == '' }}"
-      run: |
-        mkdir -p '${{github.workspace}}/out/build/${{matrix.preset}}'
-        cd '${{github.workspace}}/out/build/${{matrix.preset}}'
-        cmake \
-            ../.. -GNinja \
-            ${{matrix.cmake_args}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \
-            -DENABLE_TEST=${{matrix.test}} \
-            -DPACKAGE_NAME_SUFFIX:STRING="$VCMI_PACKAGE_NAME_SUFFIX" \
-            -DPACKAGE_FILE_NAME:STRING="$VCMI_PACKAGE_FILE_NAME" \
-            -DENABLE_GITVERSION="$VCMI_PACKAGE_GITVERSION"
-      env:
-        CC: ${{ matrix.cc }}
-        CXX: ${{ matrix.cxx }}
-
     - name: CMake Preset
-      if: "${{ matrix.preset != '' }}"
       run: |
         cmake --preset ${{ matrix.preset }}
 
-    - name: Build
-      if: "${{ matrix.preset == '' }}"
-      run: |
-        cmake --build '${{github.workspace}}/out/build/${{matrix.preset}}'
-
     - name: Build Preset
-      if: "${{ matrix.preset != '' }}"
       run: |
         cmake --build --preset ${{matrix.preset}}
 
     - name: Test
-      if: ${{ matrix.test == 1 &&  matrix.preset != ''}}
+      if: ${{ matrix.test == 1 }}
       run: |
         ctest --preset ${{matrix.preset}}
 
@@ -211,8 +205,15 @@ jobs:
           && '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' '${{github.workspace}}' "$(ls '${{ env.VCMI_PACKAGE_FILE_NAME }}'.*)"
         rm -rf _CPack_Packages
 
+    - name: Create android package
+      if: ${{ startsWith(matrix.platform, 'android') }}
+      run: |
+        cd android
+        ./gradlew assembleDaily --info
+        echo ANDROID_APK_PATH="$(ls ${{ github.workspace }}/android/vcmi-app/build/outputs/apk/daily/*.${{ matrix.extension }})" >> $GITHUB_ENV
+
     - name: Additional logs
-      if: ${{ failure() && steps.cpack.outcome == 'failure' && matrix.platform == 'mxe' }}
+      if: ${{ failure() && steps.cpack.outcome == 'failure' && matrix.platform == 'msvc' }}
       run: |
         cat '${{github.workspace}}/out/build/${{matrix.preset}}/_CPack_Packages/win32/NSIS/project.nsi'
         cat '${{github.workspace}}/out/build/${{matrix.preset}}/_CPack_Packages/win32/NSIS/NSISOutput.log'
@@ -223,12 +224,24 @@ jobs:
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
         path: |
-          ${{github.workspace}}/**/${{ env.VCMI_PACKAGE_FILE_NAME }}.${{ matrix.extension }}
+          ${{github.workspace}}/out/build/${{matrix.preset}}/${{ env.VCMI_PACKAGE_FILE_NAME }}.${{ matrix.extension }}
+    - name: Android artifacts
+      if: ${{ startsWith(matrix.platform, 'android') }}
+      uses: actions/upload-artifact@v3
+      with:
+        name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
+        path: |
+          ${{ env.ANDROID_APK_PATH }}
 
     - name: Upload build
-      if: ${{ matrix.pack == 1 && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/features/')) && matrix.platform != 'msvc' }}
+      if: ${{ (matrix.pack == 1 || startsWith(matrix.platform, 'android')) && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/features/')) && matrix.platform != 'msvc' }}
+      continue-on-error: true
       run: |
-        cd '${{github.workspace}}/out/build/${{matrix.preset}}'
+        if cd '${{github.workspace}}/android/vcmi-app/build/outputs/apk/daily' ; then
+          mv '${{ env.ANDROID_APK_PATH }}' "$VCMI_PACKAGE_FILE_NAME.${{ matrix.extension }}"
+        else
+          cd '${{github.workspace}}/out/build/${{matrix.preset}}'
+        fi
         source '${{github.workspace}}/CI/upload_package.sh'
       env:
         DEPLOY_RSA: ${{ secrets.DEPLOY_RSA }}
@@ -241,11 +254,3 @@ jobs:
       env:
         SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
       if: always()
-
-    - name: Trigger Android
-      uses: peter-evans/repository-dispatch@v1
-      if: ${{ (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/master') && matrix.platform == 'mxe' }}
-      with:
-        token: ${{ secrets.VCMI_ANDROID_ACCESS_TOKEN }}
-        repository: vcmi/vcmi-android
-        event-type: vcmi

+ 3 - 0
.gitignore

@@ -60,3 +60,6 @@ CMakeUserPresets.json
 /AI/FuzzyLite.lib
 /deps
 .vs/
+
+# CLion
+.idea/

+ 21 - 18
AI/BattleAI/AttackPossibility.cpp

@@ -13,9 +13,9 @@
                               // 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)
+uint64_t averageDmg(const DamageRange & range)
 {
-	return (range.first + range.second) / 2;
+	return (range.min + range.max) / 2;
 }
 
 AttackPossibility::AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack)
@@ -49,9 +49,10 @@ int64_t AttackPossibility::calculateDamageReduce(
 
 	vstd::amin(damageDealt, defender->getAvailableHealth());
 
-	auto enemyDamageBeforeAttack = cb.battleEstimateDamage(BattleAttackInfo(defender, attacker, defender->canShoot()));
+	// FIXME: provide distance info for Jousting bonus
+	auto enemyDamageBeforeAttack = cb.battleEstimateDamage(defender, attacker, 0);
 	auto enemiesKilled = damageDealt / defender->MaxHealth() + (damageDealt % defender->MaxHealth() >= defender->getFirstHPleft() ? 1 : 0);
-	auto enemyDamage = averageDmg(enemyDamageBeforeAttack);
+	auto enemyDamage = averageDmg(enemyDamageBeforeAttack.damage);
 	auto damagePerEnemy = enemyDamage / (double)defender->getCount();
 
 	return (int64_t)(damagePerEnemy * (enemiesKilled * KILL_BOUNTY + damageDealt * HEALTH_BOUNTY / (double)defender->MaxHealth()));
@@ -74,16 +75,17 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & a
 		if(!state.battleCanShoot(st))
 			continue;
 
-		BattleAttackInfo rangeAttackInfo(st, attacker, true);
+		// FIXME: provide distance info for Jousting bonus
+		BattleAttackInfo rangeAttackInfo(st, attacker, 0, true);
 		rangeAttackInfo.defenderPos = hex;
 
-		BattleAttackInfo meleeAttackInfo(st, attacker, false);
+		BattleAttackInfo meleeAttackInfo(st, attacker, 0, false);
 		meleeAttackInfo.defenderPos = hex;
 
 		auto rangeDmg = state.battleEstimateDamage(rangeAttackInfo);
 		auto meleeDmg = state.battleEstimateDamage(meleeAttackInfo);
 
-		int64_t gain = averageDmg(rangeDmg) - averageDmg(meleeDmg) + 1;
+		int64_t gain = averageDmg(rangeDmg.damage) - averageDmg(meleeDmg.damage) + 1;
 		res += gain;
 	}
 
@@ -154,17 +156,16 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf
 			{
 				int64_t damageDealt, damageReceived, defenderDamageReduce, attackerDamageReduce;
 
-				TDmgRange retaliation(0, 0);
+				DamageEstimation 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());
+				vstd::amin(attackDmg.damage.min, defenderState->getAvailableHealth());
+				vstd::amin(attackDmg.damage.max, defenderState->getAvailableHealth());
 
-				vstd::amin(retaliation.first, ap.attackerState->getAvailableHealth());
-				vstd::amin(retaliation.second, ap.attackerState->getAvailableHealth());
+				vstd::amin(retaliation.damage.min, ap.attackerState->getAvailableHealth());
+				vstd::amin(retaliation.damage.max, ap.attackerState->getAvailableHealth());
 
-				damageDealt = averageDmg(attackDmg);
+				damageDealt = averageDmg(attackDmg.damage);
 				defenderDamageReduce = calculateDamageReduce(attacker, defender, damageDealt, state);
 				ap.attackerState->afterAttack(attackInfo.shooting, false);
 
@@ -174,7 +175,7 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf
 
 				if (!attackInfo.shooting && defenderState->ableToRetaliate() && !counterAttacksBlocked)
 				{
-					damageReceived = averageDmg(retaliation);
+					damageReceived = averageDmg(retaliation.damage);
 					attackerDamageReduce = calculateDamageReduce(defender, attacker, damageReceived, state);
 					defenderState->afterAttack(attackInfo.shooting, true);
 				}
@@ -211,11 +212,13 @@ 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: d:%lld a:%lld c:%lld s:%lld",
-		attackInfo.attacker->unitType()->identifier,
-		attackInfo.defender->unitType()->identifier,
+#if BATTLE_TRACE_LEVEL>=1
+	logAi->trace("BattleAI best AP: %s -> %s at %d from %d, affects %d units: d:%lld a:%lld c:%lld s:%lld",
+		attackInfo.attacker->unitType()->getJsonKey(),
+		attackInfo.defender->unitType()->getJsonKey(),
 		(int)bestAp.dest, (int)bestAp.from, (int)bestAp.affectedUnits.size(),
 		bestAp.defenderDamageReduce, bestAp.attackerDamageReduce, bestAp.collateralDamageReduce, bestAp.shootersBlockedDmg);
+#endif
 
 	//TODO other damage related to attack (eg. fire shield and other abilities)
 	return bestAp;

+ 59 - 28
AI/BattleAI/BattleAI.cpp

@@ -46,14 +46,14 @@ std::vector<BattleHex> CBattleAI::getBrokenWallMoatHexes() const
 {
 	std::vector<BattleHex> result;
 
-	for(int wallPart = EWallPart::BOTTOM_WALL; wallPart < EWallPart::UPPER_WALL; wallPart++)
+	for(EWallPart wallPart : { EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL })
 	{
 		auto state = cb->battleGetWallState(wallPart);
 
 		if(state != EWallState::DESTROYED)
 			continue;
 
-		auto wallHex = cb->wallPartToBattleHex((EWallPart::EWallPart)wallPart);
+		auto wallHex = cb->wallPartToBattleHex((EWallPart)wallPart);
 		auto moatHex = wallHex.cloneInDirection(BattleHex::LEFT);
 
 		result.push_back(moatHex);
@@ -79,7 +79,7 @@ CBattleAI::~CBattleAI()
 	}
 }
 
-void CBattleAI::init(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
+void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
 {
 	setCbc(CB);
 	env = ENV;
@@ -89,6 +89,7 @@ void CBattleAI::init(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCa
 	wasUnlockingGs = CB->unlockGsWhenWaiting;
 	CB->waitTillRealize = true;
 	CB->unlockGsWhenWaiting = false;
+	movesSkippedByDefense = 0;
 }
 
 BattleAction CBattleAI::activeStack( const CStack * stack )
@@ -117,9 +118,9 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 
 		attemptCastingSpell();
 
-		if(auto ret = cb->battleIsFinished())
+		if(cb->battleIsFinished() || !stack->alive())
 		{
-			//spellcast may finish battle
+			//spellcast may finish battle or kill active stack
 			//send special preudo-action
 			BattleAction cancel;
 			cancel.actionType = EActionType::CANCEL;
@@ -181,12 +182,12 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 			if(bestSpellcast.is_initialized() && bestSpellcast->value > bestAttack.damageDiff())
 			{
 				// return because spellcast value is damage dealt and score is dps reduce
+				movesSkippedByDefense = 0;
 				return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id);
 			}
 
 			if(evaluationResult.score > score)
 			{
-				auto & target = bestAttack;
 				score = evaluationResult.score;
 				std::string action;
 
@@ -197,27 +198,29 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 				}
 				else if(bestAttack.attack.shooting)
 				{
-
 					result = BattleAction::makeShotAttack(stack, bestAttack.attack.defender);
 					action = "shot";
+					movesSkippedByDefense = 0;
 				}
 				else
 				{
 					result = BattleAction::makeMeleeAttack(stack, bestAttack.attack.defender->getPosition(), bestAttack.from);
 					action = "melee";
+					movesSkippedByDefense = 0;
 				}
 
 				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,
+					bestAttack.attackerState->unitType()->getJsonKey(),
+					bestAttack.affectedUnits[0]->unitType()->getJsonKey(),
 					(int)bestAttack.affectedUnits[0]->getCount(), action, (int)bestAttack.from, (int)bestAttack.attack.attacker->getPosition().hex,
-					bestAttack.attack.chargedFields, bestAttack.attack.attacker->Speed(0, true),
+					bestAttack.attack.chargeDistance, bestAttack.attack.attacker->Speed(0, true),
 					bestAttack.defenderDamageReduce, bestAttack.attackerDamageReduce, bestAttack.attackValue()
 				);
 			}
 		}
 		else if(bestSpellcast.is_initialized())
 		{
+			movesSkippedByDefense = 0;
 			return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id);
 		}
 
@@ -236,12 +239,8 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 			}
 		}
 
-		if(score > EvaluationResult::INEFFECTIVE_SCORE)
-		{
-			return result;
-		}
-
-		if(!stack->hasBonusOfType(Bonus::FLYING)
+		if(score <= EvaluationResult::INEFFECTIVE_SCORE
+			&& !stack->hasBonusOfType(Bonus::FLYING)
 			&& stack->unitSide() == BattleSide::ATTACKER
 			&& cb->battleGetSiegeLevel() >= CGTownInstance::CITADEL)
 		{
@@ -249,10 +248,12 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 
 			if(brokenWallMoat.size())
 			{
+				movesSkippedByDefense = 0;
+
 				if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition()))
-					return BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT));
+					result = BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT));
 				else
-					return goTowardsNearest(stack, brokenWallMoat);
+					result = goTowardsNearest(stack, brokenWallMoat);
 			}
 		}
 	}
@@ -265,6 +266,15 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 		logAi->error("Exception occurred in %s %s",__FUNCTION__, e.what());
 	}
 
+	if(result.actionType == EActionType::DEFEND)
+	{
+		movesSkippedByDefense++;
+	}
+	else if(result.actionType != EActionType::WAIT)
+	{
+		movesSkippedByDefense = 0;
+	}
+
 	return result;
 }
 
@@ -286,7 +296,9 @@ BattleAction CBattleAI::goTowardsNearest(const CStack * stack, std::vector<Battl
 	for(auto hex : hexes)
 	{
 		if(vstd::contains(avHexes, hex))
+		{
 			return BattleAction::makeMove(stack, hex);
+		}
 
 		if(stack->coversPos(hex))
 		{
@@ -365,7 +377,7 @@ BattleAction CBattleAI::useCatapult(const CStack * stack)
 	}
 	else
 	{
-		EWallPart::EWallPart wallParts[] = {
+		EWallPart wallParts[] = {
 			EWallPart::KEEP,
 			EWallPart::BOTTOM_TOWER,
 			EWallPart::UPPER_TOWER,
@@ -379,10 +391,9 @@ BattleAction CBattleAI::useCatapult(const CStack * stack)
 		{
 			auto wallState = cb->battleGetWallState(wallPart);
 
-			if(wallState == EWallState::INTACT || wallState == EWallState::DAMAGED)
+			if(wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED)
 			{
 				targetHex = cb->wallPartToBattleHex(wallPart);
-
 				break;
 			}
 		}
@@ -398,6 +409,8 @@ BattleAction CBattleAI::useCatapult(const CStack * stack)
 	attack.side = side;
 	attack.stackNumber = stack->ID;
 
+	movesSkippedByDefense = 0;
+
 	return attack;
 }
 
@@ -598,9 +611,18 @@ void CBattleAI::attemptCastingSpell()
 
 		size_t ourUnits = 0;
 
-		for(auto unit : all)
+		std::set<uint32_t> unitIds;
+
+		state.battleGetUnitsIf([&](const battle::Unit * u)->bool
+		{
+			if(!u->isGhost() && !u->isTurret())
+				unitIds.insert(u->unitId());
+
+			return false;
+		});
+
+		for(auto unitId : unitIds)
 		{
-			auto unitId = unit->unitId();
 			auto localUnit = state.battleGetUnitByID(unitId);
 
 			newHealthOfStack[unitId] = localUnit->getAvailableHealth();
@@ -622,9 +644,8 @@ void CBattleAI::attemptCastingSpell()
 		{
 			int64_t totalGain = 0;
 
-			for(auto unit : all)
+			for(auto unitId : unitIds)
 			{
-				auto unitId = unit->unitId();
 				auto localUnit = state.battleGetUnitByID(unitId);
 
 				auto newValue = getValOr(newValueOfStack, unitId, 0);
@@ -689,7 +710,7 @@ void CBattleAI::attemptCastingSpell()
 
 	if(castToPerform.value > 0)
 	{
-		LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->name % castToPerform.value);
+		LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->getNameTranslated() % castToPerform.value);
 		BattleAction spellcast;
 		spellcast.actionType = EActionType::HERO_SPELL;
 		spellcast.actionSubtype = castToPerform.spell->id;
@@ -697,10 +718,11 @@ void CBattleAI::attemptCastingSpell()
 		spellcast.side = side;
 		spellcast.stackNumber = (!side) ? -1 : -2;
 		cb->battleMakeAction(&spellcast);
+		movesSkippedByDefense = 0;
 	}
 	else
 	{
-		LOGFL("Best spell is %s. But it is actually useless (value %d).", castToPerform.spell->name % castToPerform.value);
+		LOGFL("Best spell is %s. But it is actually useless (value %d).", castToPerform.spell->getNameTranslated() % castToPerform.value);
 	}
 }
 
@@ -790,12 +812,21 @@ boost::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering()
 		}
 	}
 
+	bs.turnsSkippedByDefense = movesSkippedByDefense / bs.ourStacks.size();
+
 	if(!bs.canFlee || !bs.canSurrender)
 	{
 		return boost::none;
 	}
 
-	return cb->makeSurrenderRetreatDecision(bs);
+	auto result = cb->makeSurrenderRetreatDecision(bs);
+
+	if(!result && bs.canFlee && bs.turnsSkippedByDefense > 30)
+	{
+		return BattleAction::makeRetreat(bs.ourSide);
+	}
+
+	return result;
 }
 
 

+ 3 - 2
AI/BattleAI/BattleAI.h

@@ -60,12 +60,13 @@ class CBattleAI : public CBattleGameInterface
 
 	//Previous setting of cb
 	bool wasWaitingForRealize, wasUnlockingGs;
+	int movesSkippedByDefense;
 
 public:
 	CBattleAI();
 	~CBattleAI();
 
-	void init(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB) override;
+	void initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB) override;
 	void attemptCastingSpell();
 
 	void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only
@@ -80,7 +81,7 @@ public:
 	//void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero
 	//void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero
 	//void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack
-	//void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa) override; //called when stack receives damage (after battleAttack())
+	//void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) override; //called when stack receives damage (after battleAttack())
 	//void battleEnd(const BattleResult *br) override;
 	//void battleResultsApplied() override; //called when all effects of last battle are applied
 	//void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied;

+ 15 - 8
AI/BattleAI/BattleExchangeVariant.cpp

@@ -68,8 +68,9 @@ int64_t BattleExchangeVariant::trackAttack(
 	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);
+	DamageEstimation retaliation;
+	// FIXME: provide distance info for Jousting bonus
+	BattleAttackInfo bai(attacker.get(), defender.get(), 0, shooting);
 
 	if(shooting)
 	{
@@ -77,7 +78,7 @@ int64_t BattleExchangeVariant::trackAttack(
 	}
 
 	auto attack = cb.battleEstimateDamage(bai, &retaliation);
-	int64_t attackDamage = (attack.first + attack.second) / 2;
+	int64_t attackDamage = (attack.damage.min + attack.damage.max) / 2;
 	int64_t defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), defender.get(), attackDamage, cb);
 	int64_t attackerDamageReduce = 0;
 
@@ -107,9 +108,9 @@ int64_t BattleExchangeVariant::trackAttack(
 
 	if(defender->alive() && defender->ableToRetaliate() && !counterAttacksBlocked && !shooting)
 	{
-		if(retaliation.second != 0)
+		if(retaliation.damage.max != 0)
 		{
-			auto retaliationDamage = (retaliation.first + retaliation.second) / 2;
+			auto retaliationDamage = (retaliation.damage.min + retaliation.damage.max) / 2;
 			attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), attacker.get(), retaliationDamage, cb);
 
 			if(!evaluateOnly)
@@ -215,8 +216,6 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(const battle::Uni
 
 	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
 			{
@@ -236,7 +235,8 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(const battle::Uni
 
 		for(auto hex : hexes)
 		{
-			auto bai = BattleAttackInfo(activeStack, closestStack, cb->battleCanShoot(activeStack));
+			// FIXME: provide distance info for Jousting bonus
+			auto bai = BattleAttackInfo(activeStack, closestStack, 0, 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
@@ -355,6 +355,13 @@ int64_t BattleExchangeEvaluator::calculateExchange(
 	logAi->trace("Battle exchange at %lld", ap.attack.shooting ? ap.dest : ap.from);
 #endif
 
+	if(cb->battleGetMySide() == BattlePerspective::LEFT_SIDE
+		&& cb->battleGetGateState() == EGateState::BLOCKED
+		&& ap.attack.defender->coversPos(ESiegeHex::GATE_BRIDGE))
+	{
+		return EvaluationResult::INEFFECTIVE_SCORE;
+	}
+
 	std::vector<const battle::Unit *> ourStacks;
 	std::vector<const battle::Unit *> enemyStacks;
 

+ 1 - 1
AI/BattleAI/BattleExchangeVariant.h

@@ -42,7 +42,7 @@ struct EvaluationResult
 	bool defend;
 
 	EvaluationResult(const AttackPossibility & ap)
-		:wait(false), score(0), bestAttack(ap), defend(false)
+		:wait(false), score(INEFFECTIVE_SCORE), bestAttack(ap), defend(false)
 	{
 	}
 };

+ 8 - 8
AI/BattleAI/CMakeLists.txt

@@ -1,11 +1,8 @@
 set(battleAI_SRCS
-		StdInc.cpp
-
 		AttackPossibility.cpp
 		BattleAI.cpp
 		common.cpp
 		EnemyInfo.cpp
-		main.cpp
 		PossibleSpellcast.cpp
 		PotentialTargets.cpp
 		StackWithBonuses.cpp
@@ -27,17 +24,20 @@ set(battleAI_HEADERS
 		BattleExchangeVariant.h
 )
 
+if(NOT ENABLE_STATIC_AI_LIBS)
+	list(APPEND battleAI_SRCS main.cpp StdInc.cpp)
+endif()
 assign_source_group(${battleAI_SRCS} ${battleAI_HEADERS})
 
-if(ANDROID) # android compiles ai libs into main lib directly, so we skip this library and just reuse sources list
-	return()
+if(ENABLE_STATIC_AI_LIBS)
+	add_library(BattleAI STATIC ${battleAI_SRCS} ${battleAI_HEADERS})
+else()
+	add_library(BattleAI SHARED ${battleAI_SRCS} ${battleAI_HEADERS})
+	install(TARGETS BattleAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR})
 endif()
 
-add_library(BattleAI SHARED ${battleAI_SRCS} ${battleAI_HEADERS})
 target_include_directories(BattleAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
 target_link_libraries(BattleAI PRIVATE ${VCMI_LIB_TARGET})
 
 vcmi_set_output_dir(BattleAI "AI")
 enable_pch(BattleAI)
-
-install(TARGETS BattleAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR})

+ 4 - 6
AI/BattleAI/PotentialTargets.cpp

@@ -46,10 +46,8 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet
 
 		auto GenerateAttackInfo = [&](bool shooting, BattleHex hex) -> AttackPossibility
 		{
-			auto bai = BattleAttackInfo(attackerInfo, defender, shooting);
-
-			if(hex.isValid() && !shooting)
-				bai.chargedFields = reachability.distances[hex];
+			int distance = hex.isValid() ? reachability.distances[hex] : 0;
+			auto bai = BattleAttackInfo(attackerInfo, defender, distance, shooting);
 
 			return AttackPossibility::evaluate(bai, hex, state);
 		};
@@ -92,8 +90,8 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet
 		auto & bestAp = possibleAttacks[0];
 
 		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,
+			bestAp.attack.attacker->unitType()->getJsonKey(),
+			state.battleGetUnitByPos(bestAp.dest)->unitType()->getJsonKey(),
 			(int)bestAp.dest, (int)bestAp.from, (int)bestAp.affectedUnits.size(),
 			bestAp.defenderDamageReduce, bestAp.attackerDamageReduce, bestAp.collateralDamageReduce, bestAp.shootersBlockedDmg);
 	}

+ 4 - 4
AI/BattleAI/StackWithBonuses.cpp

@@ -205,7 +205,7 @@ std::string StackWithBonuses::getDescription() const
 	oss << unitOwner().getStr();
 	oss << " battle stack [" << unitId() << "]: " << getCount() << " of ";
 	if(type)
-		oss << type->namePl;
+		oss << type->getJsonKey();
 	else
 		oss << "[UNDEFINED TYPE]";
 
@@ -403,7 +403,7 @@ void HypotheticBattle::removeUnitBonus(uint32_t id, const std::vector<Bonus> & b
 	bonusTreeVersion++;
 }
 
-void HypotheticBattle::setWallState(int partOfWall, si8 state)
+void HypotheticBattle::setWallState(EWallPart partOfWall, EWallState state)
 {
 	//TODO:HypotheticBattle::setWallState
 }
@@ -428,9 +428,9 @@ uint32_t HypotheticBattle::nextUnitId() const
 	return nextId++;
 }
 
-int64_t HypotheticBattle::getActualDamage(const TDmgRange & damage, int32_t attackerCount, vstd::RNG & rng) const
+int64_t HypotheticBattle::getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const
 {
-	return (damage.first + damage.second) / 2;
+	return (damage.min + damage.max) / 2;
 }
 
 int64_t HypotheticBattle::getTreeVersion() const

+ 2 - 2
AI/BattleAI/StackWithBonuses.h

@@ -130,7 +130,7 @@ public:
 	void updateUnitBonus(uint32_t id, const std::vector<Bonus> & bonus) override;
 	void removeUnitBonus(uint32_t id, const std::vector<Bonus> & bonus) override;
 
-	void setWallState(int partOfWall, si8 state) override;
+	void setWallState(EWallPart partOfWall, EWallState state) override;
 
 	void addObstacle(const ObstacleChanges & changes) override;
 	void updateObstacle(const ObstacleChanges& changes) override;
@@ -138,7 +138,7 @@ public:
 
 	uint32_t nextUnitId() const override;
 
-	int64_t getActualDamage(const TDmgRange & damage, int32_t attackerCount, vstd::RNG & rng) const override;
+	int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override;
 
 	int64_t getTreeVersion() const;
 

+ 17 - 4
AI/CMakeLists.txt

@@ -12,6 +12,10 @@ if(TBB_FOUND AND MSVC)
 	   install_vcpkg_imported_tgt(TBB::tbb)
 endif()
 
+#FuzzyLite uses MSVC pragmas in headers, so, we need to disable -Wunknown-pragmas
+if(MINGW)
+    add_compile_options(-Wno-unknown-pragmas)
+endif()
 
 if(NOT FORCE_BUNDLED_FL)
 	find_package(fuzzylite)
@@ -24,9 +28,16 @@ if(TARGET fuzzylite::fuzzylite AND MSVC)
 endif()
 
 if(NOT fuzzylite_FOUND)
-    set(FL_BUILD_BINARY OFF CACHE BOOL "")
-    set(FL_BUILD_SHARED OFF CACHE BOOL "")
+	set(FL_BUILD_BINARY OFF CACHE BOOL "")
+	set(FL_BUILD_SHARED OFF CACHE BOOL "")
 	set(FL_BUILD_TESTS OFF CACHE BOOL "")
+	if(ANDROID)
+		set(FL_BACKTRACE OFF CACHE BOOL "" FORCE)
+	endif()
+	#It is for compiling FuzzyLite, it will not compile without it on GCC
+	if("x${CMAKE_CXX_COMPILER_FRONTEND_VARIANT}" STREQUAL "xGNU" OR ${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU")
+		add_compile_options(-Wno-error=deprecated-declarations)
+	endif()
 	add_subdirectory(FuzzyLite/fuzzylite EXCLUDE_FROM_ALL)
 	add_library(fuzzylite::fuzzylite ALIAS fl-static)
 	target_include_directories(fl-static PUBLIC ${CMAKE_HOME_DIRECTORY}/AI/FuzzyLite/fuzzylite)
@@ -37,7 +48,9 @@ endif()
 #######################################
 
 add_subdirectory(BattleAI)
+add_subdirectory(VCAI)
 add_subdirectory(StupidAI)
 add_subdirectory(EmptyAI)
-add_subdirectory(VCAI)
-add_subdirectory(Nullkiller)
+if(ENABLE_NULLKILLER_AI)
+	add_subdirectory(Nullkiller)
+endif()

+ 1 - 1
AI/EmptyAI/CEmptyAI.cpp

@@ -20,7 +20,7 @@ void CEmptyAI::loadGame(BinaryDeserializer & h, const int version)
 {
 }
 
-void CEmptyAI::init(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
+void CEmptyAI::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
 {
 	cb = CB;
 	env = ENV;

+ 1 - 1
AI/EmptyAI/CEmptyAI.h

@@ -22,7 +22,7 @@ public:
 	virtual void saveGame(BinarySerializer & h, const int version) override;
 	virtual void loadGame(BinaryDeserializer & h, const int version) override;
 
-	void init(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
+	void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
 	void yourTurn() override;
 	void heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID) override;
 	void commanderGotLevel (const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID) override;

+ 10 - 6
AI/EmptyAI/CMakeLists.txt

@@ -1,8 +1,5 @@
 set(emptyAI_SRCS
-		StdInc.cpp
-
 		CEmptyAI.cpp
-		exp_funcs.cpp
 )
 
 set(emptyAI_HEADERS
@@ -11,13 +8,20 @@ set(emptyAI_HEADERS
 		CEmptyAI.h
 )
 
+if(NOT ENABLE_STATIC_AI_LIBS)
+	list(APPEND emptyAI_SRCS main.cpp StdInc.cpp)
+endif()
 assign_source_group(${emptyAI_SRCS} ${emptyAI_HEADERS})
 
-add_library(EmptyAI SHARED ${emptyAI_SRCS} ${emptyAI_HEADERS})
+if(ENABLE_STATIC_AI_LIBS)
+	add_library(EmptyAI STATIC ${emptyAI_SRCS} ${emptyAI_HEADERS})
+else()
+	add_library(EmptyAI SHARED ${emptyAI_SRCS} ${emptyAI_HEADERS})
+	install(TARGETS EmptyAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR})
+endif()
+
 target_include_directories(EmptyAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
 target_link_libraries(EmptyAI PRIVATE ${VCMI_LIB_TARGET})
 
 vcmi_set_output_dir(EmptyAI "AI")
 enable_pch(EmptyAI)
-
-install(TARGETS EmptyAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR} OPTIONAL)

+ 3 - 1
AI/EmptyAI/StdInc.h

@@ -4,4 +4,6 @@
 
 // This header should be treated as a pre compiled header file(PCH) in the compiler building settings.
 
-// Here you can add specific libraries and macros which are specific to this project.
+// Here you can add specific libraries and macros which are specific to this project.
+
+VCMI_LIB_USING_NAMESPACE

+ 1 - 1
AI/EmptyAI/exp_funcs.cpp → AI/EmptyAI/main.cpp

@@ -1,5 +1,5 @@
 /*
- * exp_funcs.cpp, part of VCMI engine
+ * main.cpp, part of VCMI engine
  *
  * Authors: listed in file AUTHORS in main folder
  *

+ 58 - 39
AI/Nullkiller/AIGateway.cpp

@@ -13,7 +13,7 @@
 #include "../../lib/mapObjects/MapObjects.h"
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CHeroHandler.h"
-#include "../../lib/CModHandler.h"
+#include "../../lib/GameSettings.h"
 #include "../../lib/CGameState.h"
 #include "../../lib/NetPacks.h"
 #include "../../lib/serializer/CTypeList.h"
@@ -28,8 +28,8 @@ namespace NKAI
 {
 
 // our to enemy strength ratio constants
-const float SAFE_ATTACK_CONSTANT = 1.2;
-const float RETREAT_THRESHOLD = 0.3;
+const float SAFE_ATTACK_CONSTANT = 1.2f;
+const float RETREAT_THRESHOLD = 0.3f;
 const double RETREAT_ABSOLUTE_THRESHOLD = 10000.;
 
 //one thread may be turn of AI and another will be handling a side effect for AI2
@@ -92,8 +92,9 @@ void AIGateway::heroMoved(const TryMoveHero & details, bool verbose)
 	validateObject(details.id); //enemy hero may have left visible area
 	auto hero = cb->getHero(details.id);
 
-	const int3 from = CGHeroInstance::convertPosition(details.start, false);
-	const int3 to = CGHeroInstance::convertPosition(details.end, false);
+	const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));;
+	const int3 to   = hero ? hero->convertToVisitablePos(details.end)   : (details.end   - int3(0,1,0));
+
 	const CGObjectInstance * o1 = vstd::frontOrNull(cb->getVisitableObjs(from, verbose));
 	const CGObjectInstance * o2 = vstd::frontOrNull(cb->getVisitableObjs(to, verbose));
 
@@ -282,7 +283,7 @@ void AIGateway::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID her
 	auto firstHero = cb->getHero(hero1);
 	auto secondHero = cb->getHero(hero2);
 
-	status.addQuery(query, boost::str(boost::format("Exchange between heroes %s (%d) and %s (%d)") % firstHero->name % firstHero->tempOwner % secondHero->name % secondHero->tempOwner));
+	status.addQuery(query, boost::str(boost::format("Exchange between heroes %s (%d) and %s (%d)") % firstHero->getNameTranslated() % firstHero->tempOwner % secondHero->getNameTranslated() % secondHero->tempOwner));
 
 	requestActionASAP([=]()
 	{
@@ -358,6 +359,11 @@ void AIGateway::objectRemoved(const CGObjectInstance * obj)
 	{
 		lostHero(cb->getHero(obj->id)); //we can promote, since objectRemoved is called just before actual deletion
 	}
+
+	if(obj->ID == Obj::HERO && cb->getPlayerRelations(obj->tempOwner, playerID) == PlayerRelations::ENEMIES)
+	{
+		nullkiller->dangerHitMap->reset();
+	}
 }
 
 void AIGateway::showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor)
@@ -384,7 +390,7 @@ void AIGateway::advmapSpellCast(const CGHeroInstance * caster, int spellID)
 	NET_EVENT_HANDLER;
 }
 
-void AIGateway::showInfoDialog(const std::string & text, const std::vector<Component> & components, int soundID)
+void AIGateway::showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector<Component> & components, int soundID)
 {
 	LOG_TRACE_PARAMS(logAi, "soundID '%i'", soundID);
 	NET_EVENT_HANDLER;
@@ -488,7 +494,7 @@ void AIGateway::showMarketWindow(const IMarket * market, const CGHeroInstance *
 	NET_EVENT_HANDLER;
 }
 
-void AIGateway::showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions)
+void AIGateway::showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain)
 {
 	//TODO: AI support for ViewXXX spell
 	LOG_TRACE(logAi);
@@ -514,7 +520,7 @@ boost::optional<BattleAction> AIGateway::makeSurrenderRetreatDecision(
 }
 
 
-void AIGateway::init(std::shared_ptr<Environment> env, std::shared_ptr<CCallback> CB)
+void AIGateway::initGameInterface(std::shared_ptr<Environment> env, std::shared_ptr<CCallback> CB)
 {
 	LOG_TRACE(logAi);
 	myCb = CB;
@@ -535,8 +541,7 @@ void AIGateway::yourTurn()
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
 	status.startedTurn();
-
-	makingTurn = make_unique<boost::thread>(&AIGateway::makeTurn, this);
+	makingTurn = std::make_unique<boost::thread>(&AIGateway::makeTurn, this);
 }
 
 void AIGateway::heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector<SecondarySkill> & skills, QueryID queryID)
@@ -544,7 +549,7 @@ void AIGateway::heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimaryS
 	LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID);
 	NET_EVENT_HANDLER;
 
-	status.addQuery(queryID, boost::str(boost::format("Hero %s got level %d") % hero->name % hero->level));
+	status.addQuery(queryID, boost::str(boost::format("Hero %s got level %d") % hero->getNameTranslated() % hero->level));
 	HeroPtr hPtr = hero;
 
 	requestActionASAP([=]()
@@ -580,27 +585,38 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
 		requestActionASAP([=]()
 		{
 			//yes&no -> always answer yes, we are a brave AI :)
-			auto answer = 1;
+			bool answer = true;
 			auto objects = cb->getVisitableObjs(target);
 
 			if(hero.validAndSet() && target.valid() && objects.size())
 			{
-				auto objType = objects.front()->ID;
+				auto topObj = objects.front()->id == hero->id ? objects.back() : objects.front();
+				auto objType = topObj->ID; // top object should be our hero
+				auto goalObjectID = nullkiller->getTargetObject();
+				auto ratio = (float)nullkiller->dangerEvaluator->evaluateDanger(target, hero.get()) / (float)hero->getTotalStrength();
+
+				answer = topObj->id == goalObjectID; // no if we do not aim to visit this object
+				logAi->trace("Query hook: %s(%s) by %s danger ratio %f", target.toString(), topObj->getObjectName(), hero.name, ratio);
 
-				if(objType == Obj::ARTIFACT || objType == Obj::RESOURCE)
+				if(cb->getObj(goalObjectID, false))
+				{
+					logAi->trace("AI expected %s", cb->getObj(goalObjectID, false)->getObjectName());
+				}
+
+				if(objType == Obj::BORDERGUARD || objType == Obj::QUEST_GUARD)
+				{
+					answer = true;
+				}
+				else if(objType == Obj::ARTIFACT || objType == Obj::RESOURCE)
 				{
-					auto ratio = (float)nullkiller->dangerEvaluator->evaluateDanger(target, hero.get()) / (float)hero->getTotalStrength();
 					bool dangerUnknown = ratio == 0;
 					bool dangerTooHigh = ratio > (1 / SAFE_ATTACK_CONSTANT);
 
-					logAi->trace("Guarded object query hook: %s by %s danger ratio %f", target.toString(), hero.name, ratio);
-
-					if(text.find("guarded") >= 0 && (dangerUnknown || dangerTooHigh))
-						answer = 0; // no
+					answer = !dangerUnknown && !dangerTooHigh;
 				}
 			}
 
-			answerQuery(askID, answer);
+			answerQuery(askID, answer ? 1 : 0);
 		});
 
 		return;
@@ -616,7 +632,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
 		// TODO: Find better way to understand it is Chest of Treasures
 		if(hero.validAndSet()
 			&& components.size() == 2
-			&& components.front().id == Component::RESOURCE
+			&& components.front().id == Component::EComponentType::RESOURCE
 			&& (nullkiller->heroManager->getHeroRole(hero) != HeroRole::MAIN
 			|| nullkiller->buildAnalyzer->getGoldPreasure() > MAX_GOLD_PEASURE))
 		{
@@ -732,12 +748,12 @@ bool AIGateway::makePossibleUpgrades(const CArmedInstance * obj)
 		if(const CStackInstance * s = obj->getStackPtr(SlotID(i)))
 		{
 			UpgradeInfo ui;
-			myCb->getUpgradeInfo(obj, SlotID(i), ui);
+			myCb->fillUpgradeInfo(obj, SlotID(i), ui);
 			if(ui.oldID >= 0 && nullkiller->getFreeResources().canAfford(ui.cost[0] * s->count))
 			{
 				myCb->upgradeCreature(obj, SlotID(i), ui.newID[0]);
 				upgraded = true;
-				logAi->debug("Upgraded %d %s to %s", s->count, ui.oldID.toCreature()->namePl, ui.newID[0].toCreature()->namePl);
+				logAi->debug("Upgraded %d %s to %s", s->count, ui.oldID.toCreature()->getNamePluralTranslated(), ui.newID[0].toCreature()->getNamePluralTranslated());
 			}
 		}
 	}
@@ -787,7 +803,7 @@ void AIGateway::makeTurn()
 		for (auto h : cb->getHeroesInfo())
 		{
 			if (h->movement)
-				logAi->warn("Hero %s has %d MP left", h->name, h->movement);
+				logAi->warn("Hero %s has %d MP left", h->getNameTranslated(), h->movement);
 		}
 #if NKAI_TRACE_LEVEL == 0
 	}
@@ -808,7 +824,7 @@ void AIGateway::makeTurn()
 
 void AIGateway::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h)
 {
-	LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->name % obj->getObjectName() % obj->pos.toString());
+	LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->pos.toString());
 	switch(obj->ID)
 	{
 	case Obj::CREATURE_GENERATOR1:
@@ -1056,7 +1072,7 @@ bool AIGateway::canRecruitAnyHero(const CGTownInstance * t) const
 		return false;
 	if(cb->getHeroesInfo().size() >= ALLOWED_ROAMING_HEROES)
 		return false;
-	if(cb->getHeroesInfo().size() >= VLC->modh->settings.MAX_HEROES_ON_MAP_PER_PLAYER)
+	if(cb->getHeroesInfo().size() >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
 		return false;
 	if(!cb->getAvailableHeroes(t).size())
 		return false;
@@ -1070,7 +1086,7 @@ void AIGateway::battleStart(const CCreatureSet * army1, const CCreatureSet * arm
 	assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE);
 	status.setBattle(ONGOING_BATTLE);
 	const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit
-	battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->name : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString());
+	battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString());
 	CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side);
 }
 
@@ -1172,14 +1188,14 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 		}
 	};
 
-	logAi->debug("Moving hero %s to tile %s", h->name, dst.toString());
+	logAi->debug("Moving hero %s to tile %s", h->getNameTranslated(), dst.toString());
 	int3 startHpos = h->visitablePos();
 	bool ret = false;
 	if(startHpos == dst)
 	{
 		//FIXME: this assertion fails also if AI moves onto defeated guarded object
 		assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object
-		cb->moveHero(*h, CGHeroInstance::convertPosition(dst, true));
+		cb->moveHero(*h, h->convertFromVisitablePos(dst));
 		afterMovementCheck(); // TODO: is it feasible to hero get killed there if game work properly?
 		// If revisiting, teleport probing is never done, and so the entries into the list would remain unused and uncleared
 		teleportChannelProbingList.clear();
@@ -1192,7 +1208,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 		cb->getPathsInfo(h.get())->getPath(path, dst);
 		if(path.nodes.empty())
 		{
-			logAi->error("Hero %s cannot reach %s.", h->name, dst.toString());
+			logAi->error("Hero %s cannot reach %s.", h->getNameTranslated(), dst.toString());
 			return true;
 		}
 		int i = (int)path.nodes.size() - 1;
@@ -1233,14 +1249,14 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 
 		auto doMovement = [&](int3 dst, bool transit)
 		{
-			cb->moveHero(*h, CGHeroInstance::convertPosition(dst, true), transit);
+			cb->moveHero(*h, h->convertFromVisitablePos(dst), transit);
 		};
 
 		auto doTeleportMovement = [&](ObjectInstanceID exitId, int3 exitPos)
 		{
 			destinationTeleport = exitId;
 			if(exitPos.valid())
-				destinationTeleportPos = CGHeroInstance::convertPosition(exitPos, true);
+				destinationTeleportPos = h->convertFromVisitablePos(exitPos);
 			cb->moveHero(*h, h->pos);
 			destinationTeleport = ObjectInstanceID();
 			destinationTeleportPos = int3(-1);
@@ -1249,7 +1265,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 
 		auto doChannelProbing = [&]() -> void
 		{
-			auto currentPos = CGHeroInstance::convertPosition(h->pos, false);
+			auto currentPos = h->visitablePos();
 			auto currentExit = getObj(currentPos, true)->id;
 
 			status.setChannelProbing(true);
@@ -1266,7 +1282,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 			int3 currentCoord = path.nodes[i].coord;
 			int3 nextCoord = path.nodes[i - 1].coord;
 
-			auto currentObject = getObj(currentCoord, currentCoord == CGHeroInstance::convertPosition(h->pos, false));
+			auto currentObject = getObj(currentCoord, currentCoord == h->visitablePos());
 			auto nextObjectTop = getObj(nextCoord, false);
 			auto nextObject = getObj(nextCoord, true);
 			auto destTeleportObj = getDestTeleportObj(currentObject, nextObjectTop, nextObject);
@@ -1285,7 +1301,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 			if(path.nodes[i - 1].turns)
 			{
 				//blockedHeroes.insert(h); //to avoid attempts of moving heroes with very little MPs
-				break;
+				return false;
 			}
 
 			int3 endpos = path.nodes[i - 1].coord;
@@ -1332,7 +1348,10 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 		if(auto visitedObject = vstd::frontOrNull(cb->getVisitableObjs(h->visitablePos()))) //we stand on something interesting
 		{
 			if(visitedObject != *h)
+			{
 				performObjectInteraction(visitedObject, h);
+				ret = true;
+			}
 		}
 	}
 	if(h) //we could have lost hero after last move
@@ -1344,15 +1363,15 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 			throw cannotFulfillGoalException("Invalid path found!");
 		}
 
-		logAi->debug("Hero %s moved from %s to %s. Returning %d.", h->name, startHpos.toString(), h->visitablePos().toString(), ret);
+		logAi->debug("Hero %s moved from %s to %s. Returning %d.", h->getNameTranslated(), startHpos.toString(), h->visitablePos().toString(), ret);
 	}
 	return ret;
 }
 
 void AIGateway::buildStructure(const CGTownInstance * t, BuildingID building)
 {
-	auto name = t->town->buildings.at(building)->Name();
-	logAi->debug("Player %d will build %s in town of %s at %s", ai->playerID, name, t->name, t->pos.toString());
+	auto name = t->town->buildings.at(building)->getNameTranslated();
+	logAi->debug("Player %d will build %s in town of %s at %s", ai->playerID, name, t->getNameTranslated(), t->pos.toString());
 	cb->buildBuilding(t, building); //just do this;
 }
 

+ 3 - 3
AI/Nullkiller/AIGateway.h

@@ -110,7 +110,7 @@ public:
 
 	std::string getBattleAIName() const override;
 
-	void init(std::shared_ptr<Environment> env, std::shared_ptr<CCallback> CB) override;
+	void initGameInterface(std::shared_ptr<Environment> env, std::shared_ptr<CCallback> CB) override;
 	void yourTurn() override;
 
 	void heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector<SecondarySkill> & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id
@@ -153,7 +153,7 @@ public:
 	void playerBonusChanged(const Bonus & bonus, bool gain) override;
 	void heroCreated(const CGHeroInstance *) override;
 	void advmapSpellCast(const CGHeroInstance * caster, int spellID) override;
-	void showInfoDialog(const std::string & text, const std::vector<Component> & components, int soundID) override;
+	void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector<Component> & components, int soundID) override;
 	void requestRealized(PackageApplied * pa) override;
 	void receivedResource() override;
 	void objectRemoved(const CGObjectInstance * obj) override;
@@ -165,7 +165,7 @@ public:
 	void buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) override;
 	void heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) override;
 	void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override;
-	void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions) override;
+	void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
 	boost::optional<BattleAction> makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override;
 
 	void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) override;

+ 9 - 6
AI/Nullkiller/AIUtility.cpp

@@ -18,7 +18,7 @@
 #include "../../lib/mapObjects/MapObjects.h"
 #include "../../lib/mapping/CMapDefines.h"
 
-#include "../../lib/CModHandler.h"
+#include "../../lib/GameSettings.h"
 
 namespace NKAI
 {
@@ -69,7 +69,7 @@ HeroPtr::HeroPtr(const CGHeroInstance * H)
 	}
 
 	h = H;
-	name = h->name;
+	name = h->getNameTranslated();
 	hid = H->id;
 //	infosCount[ai->playerID][hid]++;
 }
@@ -109,7 +109,9 @@ const CGHeroInstance * HeroPtr::get(bool doWeExpectNull) const
 		}
 		else
 		{
-			assert(obj);
+			if (!obj)
+				logAi->error("Accessing no longer accessible hero %s!", h->getNameTranslated());
+			//assert(obj);
 			//assert(owned);
 		}
 	}
@@ -314,8 +316,9 @@ bool isWeeklyRevisitable(const CGObjectInstance * obj)
 		return false;
 
 	//TODO: allow polling of remaining creatures in dwelling
-	if(dynamic_cast<const CGVisitableOPW *>(obj)) // ensures future compatibility, unlike IDs
-		return true;
+	if(const auto * rewardable = dynamic_cast<const CRewardableObject *>(obj))
+		return rewardable->getResetDuration() == 7;
+
 	if(dynamic_cast<const CGDwelling *>(obj))
 		return true;
 	if(dynamic_cast<const CBank *>(obj)) //banks tend to respawn often in mods
@@ -442,7 +445,7 @@ bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObject
 	case Obj::MAGIC_WELL:
 		return h->mana < h->manaLimit();
 	case Obj::PRISON:
-		return ai->cb->getHeroesInfo().size() < VLC->modh->settings.MAX_HEROES_ON_MAP_PER_PLAYER;
+		return ai->cb->getHeroesInfo().size() < VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP);
 	case Obj::TAVERN:
 	case Obj::EYE_OF_MAGI:
 	case Obj::BOAT:

+ 1 - 1
AI/Nullkiller/AIUtility.h

@@ -329,7 +329,7 @@ public:
 
 		if(!poolIsEmpty) pool.pop_back();
 
-		return std::move(tmp);
+		return tmp;
 	}
 
 	bool empty() const

+ 5 - 3
AI/Nullkiller/Analyzers/ArmyManager.cpp

@@ -58,7 +58,7 @@ std::vector<SlotInfo> ArmyManager::getSortedSlots(const CCreatureSet * target, c
 		}
 	}
 
-	for(auto pair : creToPower)
+	for(auto & pair : creToPower)
 		resultingArmy.push_back(pair.second);
 
 	boost::sort(resultingArmy, [](const SlotInfo & left, const SlotInfo & right) -> bool
@@ -112,7 +112,7 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
 	for(auto bonus : *bonusModifiers)
 	{
 		// army bonuses will change and object bonuses are temporary
-		if(bonus->source != Bonus::ARMY || bonus->source != Bonus::OBJECT)
+		if(bonus->source != Bonus::ARMY && bonus->source != Bonus::OBJECT)
 		{
 			newArmyInstance.addNewBonus(std::make_shared<Bonus>(*bonus));
 		}
@@ -182,8 +182,10 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
 	{
 		auto weakest = getWeakestCreature(resultingArmy);
 
-		if(weakest->count == 1)
+		if(weakest->count == 1) 
 		{
+			assert(resultingArmy.size() > 1);
+
 			resultingArmy.erase(weakest);
 		}
 		else

+ 2 - 2
AI/Nullkiller/Analyzers/BuildAnalyzer.cpp

@@ -130,7 +130,7 @@ void BuildAnalyzer::update()
 
 	for(const CGTownInstance* town : towns)
 	{
-		logAi->trace("Checking town %s", town->name);
+		logAi->trace("Checking town %s", town->getNameTranslated());
 
 		developmentInfos.push_back(TownDevelopmentInfo(town));
 		TownDevelopmentInfo & developmentInfo = developmentInfos.back();
@@ -351,7 +351,7 @@ BuildingInfo::BuildingInfo(
 	dailyIncome = building->produce;
 	exists = town->hasBuilt(id);
 	prerequisitesCount = 1;
-	name = building->Name();
+	name = building->getNameTranslated();
 
 	if(creature)
 	{

+ 6 - 1
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp

@@ -14,6 +14,8 @@
 namespace NKAI
 {
 
+HitMapInfo HitMapInfo::NoTreat;
+
 void DangerHitMapAnalyzer::updateHitMap()
 {
 	if(upToDate)
@@ -47,6 +49,9 @@ void DangerHitMapAnalyzer::updateHitMap()
 
 	for(auto pair : heroes)
 	{
+		if(!pair.first.isValidPlayer())
+			continue;
+
 		if(ai->cb->getPlayerRelations(ai->playerID, pair.first) != PlayerRelations::ENEMIES)
 			continue;
 
@@ -65,7 +70,7 @@ void DangerHitMapAnalyzer::updateHitMap()
 				auto turn = path.turn();
 				auto & node = hitMap[pos.x][pos.y][pos.z];
 
-				if(tileDanger > node.maximumDanger.danger
+				if(tileDanger / (turn / 3 + 1) > node.maximumDanger.danger / (node.maximumDanger.turn / 3 + 1)
 					|| (tileDanger == node.maximumDanger.danger && node.maximumDanger.turn > turn))
 				{
 					node.maximumDanger.danger = tileDanger;

+ 9 - 0
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h

@@ -16,10 +16,17 @@ namespace NKAI
 
 struct HitMapInfo
 {
+	static HitMapInfo NoTreat;
+
 	uint64_t danger;
 	uint8_t turn;
 	HeroPtr hero;
 
+	HitMapInfo()
+	{
+		reset();
+	}
+
 	void reset()
 	{
 		danger = 0;
@@ -33,6 +40,8 @@ struct HitMapNode
 	HitMapInfo maximumDanger;
 	HitMapInfo fastestDanger;
 
+	HitMapNode() = default;
+
 	void reset()
 	{
 		maximumDanger.reset();

+ 21 - 61
AI/Nullkiller/Analyzers/HeroManager.cpp

@@ -70,18 +70,24 @@ float HeroManager::evaluateSecSkill(SecondarySkill skill, const CGHeroInstance *
 
 float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const
 {
-	auto heroSpecial = Selector::source(Bonus::HERO_SPECIAL, hero->type->ID.getNum());
-	auto secondarySkillBonus = Selector::type()(Bonus::SECONDARY_SKILL_PREMY);
+	auto heroSpecial = Selector::source(Bonus::HERO_SPECIAL, hero->type->getIndex());
+	auto secondarySkillBonus = Selector::targetSourceType()(Bonus::SECONDARY_SKILL);
 	auto specialSecondarySkillBonuses = hero->getBonuses(heroSpecial.And(secondarySkillBonus));
+	auto secondarySkillBonuses = hero->getBonuses(Selector::sourceTypeSel(Bonus::SECONDARY_SKILL));
 	float specialityScore = 0.0f;
 
-	for(auto bonus : *specialSecondarySkillBonuses)
+	for(auto bonus : *secondarySkillBonuses)
 	{
-		SecondarySkill bonusSkill = SecondarySkill(bonus->subtype);
-		float bonusScore = wariorSkillsScores.evaluateSecSkill(hero, bonusSkill);
+		auto hasBonus = !!specialSecondarySkillBonuses->getFirst(Selector::typeSubtype(bonus->type, bonus->subtype));
 
-		if(bonusScore > 0)
-			specialityScore += bonusScore * bonusScore * bonusScore;
+		if(hasBonus)
+		{
+			SecondarySkill bonusSkill = SecondarySkill(bonus->sid);
+			float bonusScore = wariorSkillsScores.evaluateSecSkill(hero, bonusSkill);
+
+			if(bonusScore > 0)
+				specialityScore += bonusScore * bonusScore * bonusScore;
+		}
 	}
 
 	return specialityScore;
@@ -92,37 +98,6 @@ float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const
 	return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->level * 1.5f;
 }
 
-std::vector<std::vector<const CGHeroInstance *>> clusterizeHeroes(CCallback * cb, std::vector<const CGHeroInstance *> heroes)
-{
-	std::vector<std::vector<const CGHeroInstance *>> clusters;
-
-	for(auto hero : heroes)
-	{
-		auto paths = cb->getPathsInfo(hero);
-		std::vector<const CGHeroInstance *> newCluster = {hero};
-
-		for(auto cluster = clusters.begin(); cluster != clusters.end();)
-		{
-			auto hero = std::find_if(cluster->begin(), cluster->end(), [&](const CGHeroInstance * h) -> bool
-			{
-				return paths->getNode(h->visitablePos())->turns < SCOUT_TURN_DISTANCE_LIMIT;
-			});
-
-			if(hero != cluster->end())
-			{
-				vstd::concatenate(newCluster, *cluster);
-				clusters.erase(cluster);
-			}
-			else
-				cluster++;
-		}
-
-		clusters.push_back(newCluster);
-	}
-
-	return clusters;
-}
-
 void HeroManager::update()
 {
 	logAi->trace("Start analysing our heroes");
@@ -140,7 +115,13 @@ void HeroManager::update()
 		return scores.at(h1) > scores.at(h2);
 	};
 
-	int globalMainCount = std::min(((int)myHeroes.size() + 2) / 3, cb->getMapSize().x / 100 + 1);
+	int globalMainCount = std::min(((int)myHeroes.size() + 2) / 3, cb->getMapSize().x / 50 + 1);
+
+	//vstd::amin(globalMainCount, 1 + (cb->getTownsInfo().size() / 3));
+	if(cb->getTownsInfo().size() < 4 && globalMainCount > 2)
+	{
+		globalMainCount = 2;
+	}
 
 	std::sort(myHeroes.begin(), myHeroes.end(), scoreSort);
 
@@ -149,30 +130,9 @@ void HeroManager::update()
 		heroRoles[hero] = (globalMainCount--) > 0 ? HeroRole::MAIN : HeroRole::SCOUT;
 	}
 
-	for(auto cluster : clusterizeHeroes(cb, myHeroes))
-	{
-		std::sort(cluster.begin(), cluster.end(), scoreSort);
-
-		auto localMainCountMax = (cluster.size() + 2) / 3;
-
-		for(auto hero : cluster)
-		{
-			if(heroRoles[hero] != HeroRole::MAIN)
-			{
-				heroRoles[hero] = HeroRole::MAIN;
-				break;
-			}
-			
-			localMainCountMax--;
-
-			if(localMainCountMax == 0)
-				break;
-		}
-	}
-
 	for(auto hero : myHeroes)
 	{
-		logAi->trace("Hero %s has role %s", hero->name, heroRoles[hero] == HeroRole::MAIN ? "main" : "scout");
+		logAi->trace("Hero %s has role %s", hero->getNameTranslated(), heroRoles[hero] == HeroRole::MAIN ? "main" : "scout");
 	}
 }
 

+ 2 - 2
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

@@ -271,7 +271,7 @@ void ObjectClusterizer::clusterize()
 				if(!shouldVisit(ai, path.targetHero, obj))
 				{
 #if NKAI_TRACE_LEVEL >= 2
-					logAi->trace("Hero %s does not need to visit %s", path.targetHero->name, obj->getObjectName());
+					logAi->trace("Hero %s does not need to visit %s", path.targetHero->getObjectName(), obj->getObjectName());
 #endif
 					continue;
 				}
@@ -285,7 +285,7 @@ void ObjectClusterizer::clusterize()
 						if(vstd::contains(heroesProcessed, path.targetHero))
 						{
 #if NKAI_TRACE_LEVEL >= 2
-							logAi->trace("Hero %s is already processed.", path.targetHero->name);
+							logAi->trace("Hero %s is already processed.", path.targetHero->getObjectName());
 #endif
 							continue;
 						}

+ 15 - 8
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp

@@ -70,7 +70,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 		if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
 		{
 #if NKAI_TRACE_LEVEL >= 2
-			logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
+			logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.getHeroStrength());
 #endif
 			continue;
 		}
@@ -81,7 +81,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 		auto hero = path.targetHero;
 		auto danger = path.getTotalDanger();
 
-		if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT && danger == 0 && path.exchangeCount > 1)
+		if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT && path.exchangeCount > 1)
 			continue;
 
 		auto firstBlockedAction = path.getFirstBlockedAction();
@@ -113,7 +113,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 			"It is %s to visit %s by %s with army %lld, danger %lld and army loss %lld",
 			isSafe ? "safe" : "not safe",
 			objToVisit ? objToVisit->getObjectName() : path.targetTile().toString(),
-			hero->name,
+			hero->getObjectName(),
 			path.getHeroStrength(),
 			danger,
 			path.getTotalArmyLoss());
@@ -126,8 +126,13 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 
 			sharedPtr.reset(newWay);
 
-			if(!closestWay || closestWay->movementCost() > path.movementCost())
+			auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero);
+
+			if(heroRole == HeroRole::SCOUT
+				&& (!closestWay || closestWay->movementCost() > path.movementCost()))
+			{
 				closestWay = &path;
+			}
 
 			if(!ai->nullkiller->arePathHeroesLocked(path))
 			{
@@ -137,11 +142,13 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 		}
 	}
 
-	assert(closestWay || waysToVisitObj.empty());
-	for(auto way : waysToVisitObj)
+	if(closestWay)
 	{
-		way->closestWayRatio
-			= closestWay->movementCost() / way->getPath().movementCost();
+		for(auto way : waysToVisitObj)
+		{
+			way->closestWayRatio
+				= closestWay->movementCost() / way->getPath().movementCost();
+		}
 	}
 
 	return tasks;

+ 70 - 29
AI/Nullkiller/Behaviors/DefenceBehavior.cpp

@@ -18,6 +18,7 @@
 #include "../Goals/RecruitHero.h"
 #include "../Goals/DismissHero.h"
 #include "../Goals/Composition.h"
+#include "../Goals/CaptureObject.h"
 #include "../Markers/DefendTown.h"
 #include "../Goals/ExchangeSwapTownHeroes.h"
 #include "lib/mapping/CMap.h" //for victory conditions
@@ -29,6 +30,8 @@ namespace NKAI
 extern boost::thread_specific_ptr<CCallback> cb;
 extern boost::thread_specific_ptr<AIGateway> ai;
 
+const float TREAT_IGNORE_RATIO = 2;
+
 using namespace Goals;
 
 std::string DefenceBehavior::toString() const
@@ -50,14 +53,14 @@ Goals::TGoalVec DefenceBehavior::decompose() const
 
 void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const
 {
-	logAi->trace("Evaluating defence for %s", town->name);
+	logAi->trace("Evaluating defence for %s", town->getNameTranslated());
 
 	auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town);
-	auto treats = { treatNode.fastestDanger, treatNode.maximumDanger };
+	auto treats = { treatNode.maximumDanger, treatNode.fastestDanger };
 
 	if(!treatNode.fastestDanger.hero)
 	{
-		logAi->trace("No treat found for town %s", town->name);
+		logAi->trace("No treat found for town %s", town->getNameTranslated());
 
 		return;
 	}
@@ -68,18 +71,23 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 	{
 		if(!ai->nullkiller->isHeroLocked(town->garrisonHero.get()))
 		{
-			if(!town->visitingHero && cb->getHeroesInfo().size() < GameConstants::MAX_HEROES_PER_PLAYER)
+			if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER)
 			{
+				logAi->trace(
+					"Extracting hero %s from garrison of town %s",
+					town->garrisonHero->getNameTranslated(),
+					town->getNameTranslated());
+
 				tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
-			}
 
-			return;
+				return;
+			}
 		}
 
 		logAi->trace(
 			"Hero %s in garrison of town %s is suposed to defend the town",
-			town->garrisonHero->name,
-			town->name);
+			town->garrisonHero->getNameTranslated(),
+			town->getNameTranslated());
 
 		return;
 	}
@@ -88,7 +96,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 
 	if(reinforcement)
 	{
-		logAi->trace("Town %s can buy defence army %lld", town->name, reinforcement);
+		logAi->trace("Town %s can buy defence army %lld", town->getNameTranslated(), reinforcement);
 		tasks.push_back(Goals::sptr(Goals::BuyArmy(town, reinforcement).setpriority(0.5f)));
 	}
 
@@ -98,10 +106,10 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 	{
 		logAi->trace(
 			"Town %s has treat %lld in %s turns, hero: %s",
-			town->name,
+			town->getNameTranslated(),
 			treat.danger,
 			std::to_string(treat.turn),
-			treat.hero->name);
+			treat.hero->getNameTranslated());
 
 		bool treatIsUnderControl = false;
 
@@ -113,22 +121,37 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 			if(town->visitingHero && path.getHeroStrength() < town->visitingHero->getHeroStrength())
 				continue;
 
-			if(path.getHeroStrength() > treat.danger)
+			if(treat.hero.validAndSet()
+				&& treat.turn <= 1
+				&& (treat.danger == treatNode.maximumDanger.danger || treat.turn < treatNode.maximumDanger.turn)
+				&& isSafeToVisit(path.targetHero, path.heroArmy, treat.danger))
 			{
-				if((path.turn() <= treat.turn && dayOfWeek + treat.turn < 6 && isSafeToVisit(path.targetHero, path.heroArmy, treat.danger))
-					|| (path.exchangeCount == 1 && path.turn() < treat.turn)
+				Composition composition;
+
+				composition.addNext(DefendTown(town, treat, path)).addNext(CaptureObject(treat.hero.get()));
+
+				tasks.push_back(Goals::sptr(composition));
+			}
+
+			bool treatIsWeak = path.getHeroStrength() / (float)treat.danger > TREAT_IGNORE_RATIO;
+			bool needToSaveGrowth = treat.turn == 0 && dayOfWeek == 7;
+
+			if(treatIsWeak && !needToSaveGrowth)
+			{
+				if((path.exchangeCount == 1 && path.turn() < treat.turn)
 					|| path.turn() < treat.turn - 1
 					|| (path.turn() < treat.turn && treat.turn >= 2))
 				{
 #if NKAI_TRACE_LEVEL >= 1
 					logAi->trace(
 						"Hero %s can eliminate danger for town %s using path %s.",
-						path.targetHero->name,
-						town->name,
+						path.targetHero->getObjectName(),
+						town->getObjectName(),
 						path.toString());
 #endif
 
 					treatIsUnderControl = true;
+
 					break;
 				}
 			}
@@ -152,7 +175,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 					if(cb->getHeroesInfo().size() < ALLOWED_ROAMING_HEROES)
 					{
 #if NKAI_TRACE_LEVEL >= 1
-						logAi->trace("Hero %s can be recruited to defend %s", hero->name, town->name);
+						logAi->trace("Hero %s can be recruited to defend %s", hero->getObjectName(), town->getObjectName());
 #endif
 						tasks.push_back(Goals::sptr(Goals::RecruitHero(town, hero).setpriority(1)));
 						continue;
@@ -187,7 +210,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 
 		if(paths.empty())
 		{
-			logAi->trace("No ways to defend town %s", town->name);
+			logAi->trace("No ways to defend town %s", town->getNameTranslated());
 
 			continue;
 		}
@@ -202,7 +225,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 #if NKAI_TRACE_LEVEL >= 1
 			logAi->trace(
 				"Hero %s can defend town with force %lld in %s turns, cost: %f, path: %s",
-				path.targetHero->name,
+				path.targetHero->getObjectName(),
 				path.getHeroStrength(),
 				std::to_string(path.turn()),
 				path.movementCost(),
@@ -211,9 +234,9 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 			if(path.turn() <= treat.turn - 2)
 			{
 #if NKAI_TRACE_LEVEL >= 1
-				logAi->trace("Deffer defence of %s by %s because he has enough time to rich the town next trun",
-					town->name,
-					path.targetHero->name);
+				logAi->trace("Defer defence of %s by %s because he has enough time to reach the town next trun",
+					town->getObjectName(),
+					path.targetHero->getObjectName());
 #endif
 
 				defferedPaths[path.targetHero].push_back(i);
@@ -221,12 +244,12 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				continue;
 			}
 
-			if(path.targetHero == town->visitingHero && path.exchangeCount == 1)
+			if(path.targetHero == town->visitingHero.get() && path.exchangeCount == 1)
 			{
 #if NKAI_TRACE_LEVEL >= 1
 				logAi->trace("Put %s to garrison of town %s",
-					path.targetHero->name,
-					town->name);
+					path.targetHero->getObjectName(),
+					town->getObjectName());
 #endif
 
 				// dismiss creatures we are not able to pick to be able to hide in garrison
@@ -242,6 +265,24 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 
 				continue;
 			}
+
+			// main without army and visiting scout with army, very specific case
+			if(town->visitingHero && town->getUpperArmy()->stacksCount() == 0
+				&& path.targetHero != town->visitingHero.get() && path.exchangeCount == 1 && path.turn() == 0
+				&& ai->nullkiller->heroManager->evaluateHero(path.targetHero) > ai->nullkiller->heroManager->evaluateHero(town->visitingHero.get())
+				&& 10 * path.targetHero->getTotalStrength() < town->visitingHero->getTotalStrength())
+			{
+				path.heroArmy = town->visitingHero.get();
+
+				tasks.push_back(
+					Goals::sptr(Composition()
+						.addNext(DefendTown(town, treat, path))
+						.addNext(ExchangeSwapTownHeroes(town, town->visitingHero.get()))
+						.addNext(ExecuteHeroChain(path, town))
+						.addNext(ExchangeSwapTownHeroes(town, path.targetHero, HeroLockedReason::DEFENCE))));
+
+				continue;
+			}
 				
 			if(treat.turn == 0 || (path.turn() <= treat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= treat.danger))
 			{
@@ -249,8 +290,8 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				{
 #if NKAI_TRACE_LEVEL >= 1
 					logAi->trace("Can not move %s to defend town %s. Path is locked.",
-						path.targetHero->name,
-						town->name);
+						path.targetHero->getObjectName(),
+						town->getObjectName());
 
 #endif
 					continue;
@@ -277,8 +318,8 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 
 #if NKAI_TRACE_LEVEL >= 1
 			logAi->trace("Move %s to defend town %s",
-				path.targetHero->name,
-				town->name);
+				path.targetHero->getObjectName(),
+				town->getObjectName());
 #endif
 			Composition composition;
 

+ 68 - 19
AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp

@@ -45,8 +45,7 @@ Goals::TGoalVec GatherArmyBehavior::decompose() const
 
 	for(const CGHeroInstance * hero : heroes)
 	{
-		if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::MAIN
-			&& hero->getArmyStrength() >= 300)
+		if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::MAIN)
 		{
 			vstd::concatenate(tasks, deliverArmyToHero(hero));
 		}
@@ -66,17 +65,11 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 {
 	Goals::TGoalVec tasks;
 	const int3 pos = hero->visitablePos();
+	auto targetHeroScore = ai->nullkiller->heroManager->evaluateHero(hero);
 
 #if NKAI_TRACE_LEVEL >= 1
 	logAi->trace("Checking ways to gaher army for hero %s, %s", hero->getObjectName(), pos.toString());
 #endif
-	if(ai->nullkiller->isHeroLocked(hero))
-	{
-#if NKAI_TRACE_LEVEL >= 1
-		logAi->trace("Skipping locked hero %s, %s", hero->getObjectName(), pos.toString());
-#endif
-		return tasks;
-	}
 
 	auto paths = ai->nullkiller->pathfinder->getPathInfo(pos);
 
@@ -92,6 +85,14 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 		
 		if(path.containsHero(hero)) continue;
 
+		if(path.turn() == 0 && hero->inTownGarrison)
+		{
+#if NKAI_TRACE_LEVEL >= 1
+			logAi->trace("Skipping garnisoned hero %s, %s", hero->getObjectName(), pos.toString());
+#endif
+			continue;
+		}
+
 		if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
 		{
 #if NKAI_TRACE_LEVEL >= 2
@@ -113,7 +114,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 		float armyValue = (float)heroExchange.getReinforcementArmyStrength() / hero->getArmyStrength();
 
 		// avoid transferring very small amount of army
-		if(armyValue < 0.1f)
+		if(armyValue < 0.1f && armyValue < 20000)
 		{
 #if NKAI_TRACE_LEVEL >= 2
 			logAi->trace("Army value is too small.");
@@ -122,7 +123,28 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 		}
 
 		// avoid trying to move bigger army to the weaker one.
-		if(armyValue > 1)
+		bool hasOtherMainInPath = false;
+
+		for(auto node : path.nodes)
+		{
+			if(!node.targetHero) continue;
+
+			auto heroRole = ai->nullkiller->heroManager->getHeroRole(node.targetHero);
+
+			if(heroRole == HeroRole::MAIN)
+			{
+				auto score = ai->nullkiller->heroManager->evaluateHero(node.targetHero);
+
+				if(score >= targetHeroScore)
+				{
+					hasOtherMainInPath = true;
+
+					break;
+				}
+			}
+		}
+
+		if(hasOtherMainInPath)
 		{
 #if NKAI_TRACE_LEVEL >= 2
 			logAi->trace("Army value is too large.");
@@ -131,15 +153,14 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 		}
 
 		auto danger = path.getTotalDanger();
-
 		auto isSafe = isSafeToVisit(hero, path.heroArmy, danger);
 
 #if NKAI_TRACE_LEVEL >= 2
 		logAi->trace(
 			"It is %s to visit %s by %s with army %lld, danger %lld and army loss %lld",
 			isSafe ? "safe" : "not safe",
-			hero->name,
-			path.targetHero->name,
+			hero->getObjectName(),
+			path.targetHero->getObjectName(),
 			path.getHeroStrength(),
 			danger,
 			path.getTotalArmyLoss());
@@ -162,7 +183,17 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 	#if NKAI_TRACE_LEVEL >= 2
 				logAi->trace("Action is blocked. Considering decomposition.");
 	#endif
-				composition.addNext(blockedAction->decompose(path.targetHero));
+				auto subGoal = blockedAction->decompose(path.targetHero);
+
+				if(subGoal->invalid())
+				{
+#if NKAI_TRACE_LEVEL >= 1
+					logAi->trace("Path is invalid. Skipping");
+#endif
+					continue;
+				}
+
+				composition.addNext(subGoal);
 			}
 			
 			tasks.push_back(sptr(composition));
@@ -194,7 +225,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 #if NKAI_TRACE_LEVEL >= 2
 		logAi->trace("Path found %s", path.toString());
 #endif
-		if(upgrader->visitingHero != path.targetHero)
+		if(upgrader->visitingHero && upgrader->visitingHero.get() != path.targetHero)
 		{
 #if NKAI_TRACE_LEVEL >= 2
 			logAi->trace("Ignore path. Town has visiting hero.");
@@ -219,7 +250,10 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 			continue;
 		}
 
-		if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
+		auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero);
+
+		if(heroRole == HeroRole::SCOUT
+			&& ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
 		{
 #if NKAI_TRACE_LEVEL >= 2
 			logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
@@ -228,10 +262,25 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 		}
 
 		auto upgrade = ai->nullkiller->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources);
+
+		if(!upgrader->garrisonHero && ai->nullkiller->heroManager->getHeroRole(path.targetHero) == HeroRole::MAIN)
+		{
+			upgrade.upgradeValue +=	
+				ai->nullkiller->armyManager->howManyReinforcementsCanGet(
+					path.targetHero,
+					path.heroArmy,
+					upgrader->getUpperArmy());	
+		}
+
 		auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength();
 
-		if(armyValue < 0.1f || upgrade.upgradeValue < 300) // avoid small upgrades
+		if((armyValue < 0.1f && armyValue < 20000) || upgrade.upgradeValue < 300) // avoid small upgrades
+		{
+#if NKAI_TRACE_LEVEL >= 2
+			logAi->trace("Ignore path. Army value is too small (%f)", armyValue);
+#endif
 			continue;
+		}
 
 		auto danger = path.getTotalDanger();
 
@@ -242,7 +291,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 			"It is %s to visit %s by %s with army %lld, danger %lld and army loss %lld",
 			isSafe ? "safe" : "not safe",
 			upgrader->getObjectName(),
-			path.targetHero->name,
+			path.targetHero->getObjectName(),
 			path.getHeroStrength(),
 			danger,
 			path.getTotalArmyLoss());

+ 6 - 11
AI/Nullkiller/Behaviors/StartupBehavior.cpp

@@ -70,10 +70,7 @@ bool needToRecruitHero(const CGTownInstance * startupTown)
 		return false;
 
 	if(!startupTown->garrisonHero && !startupTown->visitingHero)
-		return false;
-
-	auto heroToCheck = startupTown->garrisonHero ? startupTown->garrisonHero.get() : startupTown->visitingHero.get();
-	auto paths = cb->getPathsInfo(heroToCheck);
+		return true;
 
 	int treasureSourcesCount = 0;
 
@@ -84,18 +81,16 @@ bool needToRecruitHero(const CGTownInstance * startupTown)
 			|| obj->ID == Obj::CAMPFIRE
 			|| obj->ID == Obj::WATER_WHEEL)
 		{
-			auto path = paths->getPathInfo(obj->visitablePos());
-			if((path->accessible == CGPathNode::BLOCKVIS || path->accessible == CGPathNode::VISITABLE) 
-				&& path->reachable())
-			{
-				treasureSourcesCount++;
-			}
+			treasureSourcesCount++;
 		}
 	}
 
 	auto basicCount = cb->getTownsInfo().size() + 2;
-	auto boost = (int)std::floor(std::pow(treasureSourcesCount / 2.0, 2));
+	auto boost = std::min(
+		(int)std::floor(std::pow(1 + (cb->getMapSize().x / 50), 2)),
+		treasureSourcesCount / 2);
 
+	logAi->trace("Treasure sources found %d", treasureSourcesCount);
 	logAi->trace("Startup allows %d+%d heroes", basicCount, boost);
 
 	return cb->getHeroCount(ai->playerID, true) < basicCount + boost;

+ 9 - 12
AI/Nullkiller/CMakeLists.txt

@@ -1,6 +1,4 @@
 set(Nullkiller_SRCS
-		StdInc.cpp
-
 		Pathfinding/AIPathfinderConfig.cpp
 		Pathfinding/AIPathfinder.cpp
 		Pathfinding/AINodeStorage.cpp
@@ -54,7 +52,6 @@ set(Nullkiller_SRCS
 		Behaviors/BuildingBehavior.cpp
 		Behaviors/GatherArmyBehavior.cpp
 		Behaviors/ClusterBehavior.cpp
-		main.cpp
 		AIGateway.cpp
 )
 
@@ -120,24 +117,24 @@ set(Nullkiller_HEADERS
 		AIGateway.h
 )
 
+if(NOT ENABLE_STATIC_AI_LIBS)
+	list(APPEND Nullkiller_SRCS main.cpp StdInc.cpp)
+endif()
 assign_source_group(${Nullkiller_SRCS} ${Nullkiller_HEADERS})
 
-if(ANDROID) # android compiles ai libs into main lib directly, so we skip this library and just reuse sources list
-	return()
+if(ENABLE_STATIC_AI_LIBS)
+	add_library(Nullkiller STATIC ${Nullkiller_SRCS} ${Nullkiller_HEADERS})
+else()
+	add_library(Nullkiller SHARED ${Nullkiller_SRCS} ${Nullkiller_HEADERS})
+	install(TARGETS Nullkiller RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR})
 endif()
 
-add_library(Nullkiller SHARED ${Nullkiller_SRCS} ${Nullkiller_HEADERS})
-
 target_include_directories(Nullkiller PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
-
-target_link_libraries(Nullkiller PRIVATE ${VCMI_LIB_TARGET} fuzzylite::fuzzylite)
-
-target_link_libraries(Nullkiller PRIVATE TBB::tbb)
+target_link_libraries(Nullkiller PUBLIC ${VCMI_LIB_TARGET} fuzzylite::fuzzylite TBB::tbb)
 
 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 AND NOT USING_CONAN)
 	install(IMPORTED_RUNTIME_ARTIFACTS TBB::tbb LIBRARY DESTINATION ${LIB_DIR}) # CMake 3.21+
 endif()

+ 9 - 6
AI/Nullkiller/Engine/AIMemory.cpp

@@ -70,12 +70,15 @@ void AIMemory::markObjectVisited(const CGObjectInstance * obj)
 		return;
 	
 	// TODO: maybe this logic belongs to CaptureObjects::shouldVisit
-	if(dynamic_cast<const CGVisitableOPH *>(obj)) //we may want to visit it with another hero
-		return;
-	
-	if(dynamic_cast<const CGBonusingObject *>(obj)) //or another time
-		return;
-	
+	if(const auto * rewardable = dynamic_cast<const CRewardableObject *>(obj))
+	{
+		if (rewardable->getVisitMode() == CRewardableObject::VISIT_HERO) //we may want to visit it with another hero
+			return;
+
+		if (rewardable->getVisitMode() == CRewardableObject::VISIT_BONUS) //or another time
+			return;
+	}
+
 	if(obj->ID == Obj::MONSTER)
 		return;
 

+ 1 - 1
AI/Nullkiller/Engine/FuzzyEngines.cpp

@@ -31,7 +31,7 @@ engineBase::engineBase()
 void engineBase::configure()
 {
 	engine.configure("Minimum", "Maximum", "Minimum", "AlgebraicSum", "Centroid", "Proportional");
-	logAi->info(engine.toString());
+	logAi->trace(engine.toString());
 }
 
 void engineBase::addRule(const std::string & txt)

+ 17 - 5
AI/Nullkiller/Engine/FuzzyHelper.cpp

@@ -115,17 +115,28 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
 	{
 	case Obj::TOWN:
 	{
-		const CGTownInstance * cre = dynamic_cast<const CGTownInstance *>(obj);
-		return cre->getUpperArmy()->getArmyStrength();
+		const CGTownInstance * town = dynamic_cast<const CGTownInstance *>(obj);
+		auto danger = town->getUpperArmy()->getArmyStrength();
+
+		if(danger || town->visitingHero)
+		{
+			auto fortLevel = town->fortLevel();
+
+			if(fortLevel == CGTownInstance::EFortLevel::CASTLE)
+				danger += 10000;
+			else if(fortLevel == CGTownInstance::EFortLevel::CITADEL)
+				danger += 4000;
+		}
+
+		return danger;
 	}
+
 	case Obj::ARTIFACT:
 	case Obj::RESOURCE:
 	{
 		if(!vstd::contains(ai->memory->alreadyVisited, obj))
-		{
 			return 0;
-		}
-		// passthrough
+		FALLTHROUGH;
 	}
 	case Obj::MONSTER:
 	case Obj::HERO:
@@ -135,6 +146,7 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
 	case Obj::CREATURE_GENERATOR4:
 	case Obj::MINE:
 	case Obj::ABANDONED_MINE:
+	case Obj::PANDORAS_BOX:
 	{
 		const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj);
 		return a->getArmyStrength();

+ 24 - 23
AI/Nullkiller/Engine/Nullkiller.cpp

@@ -50,7 +50,7 @@ void Nullkiller::init(std::shared_ptr<CCallback> cb, PlayerColor playerID)
 		new SharedPool<PriorityEvaluator>(
 			[&]()->std::unique_ptr<PriorityEvaluator>
 			{
-				return make_unique<PriorityEvaluator>(this);
+				return std::make_unique<PriorityEvaluator>(this);
 			}));
 
 	dangerHitMap.reset(new DangerHitMapAnalyzer(this));
@@ -131,6 +131,7 @@ void Nullkiller::updateAiState(int pass, bool fast)
 	auto start = std::chrono::high_resolution_clock::now();
 
 	activeHero = nullptr;
+	setTargetObject(-1);
 
 	if(!fast)
 	{
@@ -188,7 +189,7 @@ bool Nullkiller::arePathHeroesLocked(const AIPath & path) const
 	if(getHeroLockedReason(path.targetHero) == HeroLockedReason::STARTUP)
 	{
 #if NKAI_TRACE_LEVEL >= 1
-		logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->name, path.toString());
+		logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->getObjectName(), path.toString());
 #endif
 		return true;
 	}
@@ -200,7 +201,7 @@ bool Nullkiller::arePathHeroesLocked(const AIPath & path) const
 		if(lockReason != HeroLockedReason::NOT_LOCKED)
 		{
 #if NKAI_TRACE_LEVEL >= 1
-			logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->name, path.toString());
+			logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->getObjectName(), path.toString());
 #endif
 			return true;
 		}
@@ -221,6 +222,7 @@ void Nullkiller::makeTurn()
 	boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker);
 
 	const int MAX_DEPTH = 10;
+	const float FAST_TASK_MINIMAL_PRIORITY = 0.7;
 
 	resetAiState();
 
@@ -234,21 +236,21 @@ void Nullkiller::makeTurn()
 		{
 			Goals::TTaskVec fastTasks = {
 				choseBestTask(sptr(BuyArmyBehavior()), 1),
-				choseBestTask(sptr(RecruitHeroBehavior()), 1),
 				choseBestTask(sptr(BuildingBehavior()), 1)
 			};
 
 			bestTask = choseBestTask(fastTasks);
 
-			if(bestTask->priority >= 1)
+			if(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY)
 			{
 				executeTask(bestTask);
 				updateAiState(i, true);
 			}
-		} while(bestTask->priority >= 1);
+		} while(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY);
 
 		Goals::TTaskVec bestTasks = {
 			bestTask,
+			choseBestTask(sptr(RecruitHeroBehavior()), 1),
 			choseBestTask(sptr(CaptureObjectsBehavior()), 1),
 			choseBestTask(sptr(ClusterBehavior()), MAX_DEPTH),
 			choseBestTask(sptr(DefenceBehavior()), MAX_DEPTH),
@@ -272,21 +274,16 @@ void Nullkiller::makeTurn()
 		if(heroRole != HeroRole::MAIN || bestTask->getHeroExchangeCount() <= 1)
 			useHeroChain = false;
 
-		if(bestTask->priority < NEXT_SCAN_MIN_PRIORITY
-			&& scanDepth != ScanDepth::FULL)
+		if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY)
+			&& scanDepth == ScanDepth::FULL)
 		{
-			if(heroRole == HeroRole::MAIN || bestTask->priority < MIN_PRIORITY)
-			{
-				useHeroChain = false;
-
-				logAi->trace(
-					"Goal %s has too low priority %f so increasing scan depth",
-					bestTask->toString(),
-					bestTask->priority);
-				scanDepth = (ScanDepth)((int)scanDepth + 1);
+			useHeroChain = false;
+			scanDepth = ScanDepth::SMALL;
 
-				continue;
-			}
+			logAi->trace(
+				"Goal %s has too low priority %f so increasing scan depth",
+				bestTask->toString(),
+				bestTask->priority);
 		}
 
 		if(bestTask->priority < MIN_PRIORITY)
@@ -302,6 +299,7 @@ void Nullkiller::makeTurn()
 
 void Nullkiller::executeTask(Goals::TTask task)
 {
+	auto start = std::chrono::high_resolution_clock::now();
 	std::string taskDescr = task->toString();
 
 	boost::this_thread::interruption_point();
@@ -310,17 +308,20 @@ void Nullkiller::executeTask(Goals::TTask task)
 	try
 	{
 		task->accept(ai.get());
+		logAi->trace("Task %s completed in %lld", taskDescr, timeElapsed(start));
 	}
 	catch(goalFulfilledException &)
 	{
-		logAi->trace("Task %s completed", task->toString());
+		logAi->trace("Task %s completed in %lld", taskDescr, timeElapsed(start));
 	}
 	catch(cannotFulfillGoalException & e)
 	{
-		logAi->debug("Failed to realize subgoal of type %s, I will stop.", taskDescr);
-		logAi->debug("The error message was: %s", e.what());
+		logAi->error("Failed to realize subgoal of type %s, I will stop.", taskDescr);
+		logAi->error("The error message was: %s", e.what());
 
-		throw;
+#if NKAI_TRACE_LEVEL == 0
+		throw; // will be recatched and AI turn ended
+#endif
 	}
 }
 

+ 6 - 5
AI/Nullkiller/Engine/Nullkiller.h

@@ -24,7 +24,7 @@ namespace NKAI
 
 const float MAX_GOLD_PEASURE = 0.3f;
 const float MIN_PRIORITY = 0.01f;
-const float NEXT_SCAN_MIN_PRIORITY = 0.4f;
+const float SMALL_SCAN_MIN_PRIORITY = 0.4f;
 
 enum class HeroLockedReason
 {
@@ -39,11 +39,9 @@ enum class HeroLockedReason
 
 enum class ScanDepth
 {
-	SMALL = 0,
+	FULL = 0,
 
-	MEDIUM = 1,
-
-	FULL = 2
+	SMALL = 1
 };
 
 class Nullkiller
@@ -51,6 +49,7 @@ class Nullkiller
 private:
 	const CGHeroInstance * activeHero;
 	int3 targetTile;
+	ObjectInstanceID targetObject;
 	std::map<const CGHeroInstance *, HeroLockedReason> lockedHeroes;
 	ScanDepth scanDepth;
 	TResources lockedResources;
@@ -79,6 +78,8 @@ public:
 	HeroPtr getActiveHero() { return activeHero; }
 	HeroLockedReason getHeroLockedReason(const CGHeroInstance * hero) const;
 	int3 getTargetTile() const { return targetTile; }
+	ObjectInstanceID getTargetObject() const { return targetObject; }
+	void setTargetObject(int objid) { targetObject = ObjectInstanceID(objid); }
 	void setActive(const CGHeroInstance * hero, int3 tile) { activeHero = hero; targetTile = tile; }
 	void lockHero(const CGHeroInstance * hero, HeroLockedReason lockReason) { lockedHeroes[hero] = lockReason; }
 	void unlockHero(const CGHeroInstance * hero) { lockedHeroes.erase(hero); }

+ 118 - 34
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -17,10 +17,12 @@
 #include "../../../lib/CPathfinder.h"
 #include "../../../lib/CGameStateFwd.h"
 #include "../../../lib/VCMI_Lib.h"
+#include "../../../lib/StartInfo.h"
 #include "../../../CCallback.h"
 #include "../../../lib/filesystem/Filesystem.h"
 #include "../Goals/ExecuteHeroChain.h"
 #include "../Goals/BuildThis.h"
+#include "../Goals/ExchangeSwapTownHeroes.h"
 #include "../Markers/UnlockCluster.h"
 #include "../Markers/HeroExchange.h"
 #include "../Markers/ArmyUpgrade.h"
@@ -79,6 +81,12 @@ void PriorityEvaluator::initVisitTile()
 	value = engine->getOutputVariable("Value");
 }
 
+bool isAnotherAi(const CGObjectInstance * obj, const CPlayerSpecificInfoCallback & cb)
+{
+	return obj->getOwner().isValidPlayer()
+		&& cb.getStartInfo()->getIthPlayersSettings(obj->getOwner()).isControlledByAI();
+}
+
 int32_t estimateTownIncome(CCallback * cb, const CGObjectInstance * target, const CGHeroInstance * hero)
 {
 	auto relations = cb->getPlayerRelations(hero->tempOwner, target->tempOwner);
@@ -86,11 +94,17 @@ int32_t estimateTownIncome(CCallback * cb, const CGObjectInstance * target, cons
 	if(relations != PlayerRelations::ENEMIES)
 		return 0; // if we already own it, no additional reward will be received by just visiting it
 
+	auto booster = isAnotherAi(target, *cb) ? 1 : 2;
+
 	auto town = cb->getTown(target->id);
-	auto isNeutral = target->tempOwner == PlayerColor::NEUTRAL;
-	auto isProbablyDeveloped = !isNeutral && town->hasFort();
+	auto fortLevel = town->fortLevel();
 
-	return isProbablyDeveloped ? 1500 : 500;
+	if(town->hasCapitol()) return booster * 2000;
+
+	// probably well developed town will have city hall
+	if(fortLevel == CGTownInstance::CASTLE) return booster * 750;
+	
+	return booster * (town->hasFort() && town->tempOwner != PlayerColor::NEUTRAL  ? booster * 500 : 250);
 }
 
 TResources getCreatureBankResources(const CGObjectInstance * target, const CGHeroInstance * hero)
@@ -191,11 +205,11 @@ int getDwellingArmyCost(const CGObjectInstance * target)
 
 uint64_t evaluateArtifactArmyValue(CArtifactInstance * art)
 {
-	if(art->artType->id == ArtifactID::SPELL_SCROLL)
+	if(art->artType->getId() == ArtifactID::SPELL_SCROLL)
 		return 1500;
 
 	auto statsValue =
-		10 * art->valOfBonuses(Bonus::LAND_MOVEMENT)
+		10 * art->valOfBonuses(Bonus::MOVEMENT, 1)
 		+ 1200 * art->valOfBonuses(Bonus::STACKS_SPEED)
 		+ 700 * art->valOfBonuses(Bonus::MORALE)
 		+ 700 * art->getAttack(false)
@@ -238,7 +252,17 @@ uint64_t RewardEvaluator::getArmyReward(
 	switch(target->ID)
 	{
 	case Obj::TOWN:
-		return target->tempOwner == PlayerColor::NEUTRAL ? 1000 : 10000;
+	{
+		auto town = dynamic_cast<const CGTownInstance *>(target);
+		auto fortLevel = town->fortLevel();
+		auto booster = isAnotherAi(town, *ai->cb) ? 1 : 2;
+
+		if(fortLevel < CGTownInstance::CITADEL)
+			return town->hasFort() ? booster * 500 : 0;
+		else
+			return booster * (fortLevel == CGTownInstance::CASTLE ? 5000 : 2000);
+	}
+
 	case Obj::HILL_FORT:
 		return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeValue;
 	case Obj::CREATURE_BANK:
@@ -337,8 +361,8 @@ float RewardEvaluator::getTotalResourceRequirementStrength(int resType) const
 		return 0;
 
 	float ratio = dailyIncome[resType] == 0
-		? requiredResources[resType] / 50
-		: (float)requiredResources[resType] / dailyIncome[resType] / 50;
+		? (float)requiredResources[resType] / 50.0f
+		: (float)requiredResources[resType] / dailyIncome[resType] / 50.0f;
 
 	return std::min(ratio, 1.0f);
 }
@@ -353,10 +377,12 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons
 	case Obj::MINE:
 		return target->subID == Res::GOLD 
 			? 0.5f 
-			: 0.02f * getTotalResourceRequirementStrength(target->subID) + 0.02f * getResourceRequirementStrength(target->subID);
+			: 0.4f * getTotalResourceRequirementStrength(target->subID) + 0.1f * getResourceRequirementStrength(target->subID);
 
 	case Obj::RESOURCE:
-		return target->subID == Res::GOLD ? 0 : 0.1f * getResourceRequirementStrength(target->subID);
+		return target->subID == Res::GOLD
+			? 0
+			: 0.2f * getTotalResourceRequirementStrength(target->subID) + 0.4f * getResourceRequirementStrength(target->subID);
 
 	case Obj::CREATURE_BANK:
 	{
@@ -374,12 +400,21 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons
 	}
 
 	case Obj::TOWN:
+	{
 		if(ai->buildAnalyzer->getDevelopmentInfo().empty())
 			return 1;
 
-		return dynamic_cast<const CGTownInstance *>(target)->hasFort()
-			? (target->tempOwner == PlayerColor::NEUTRAL ? 0.8f : 1.0f)
-			: 0.7f;
+		auto town = dynamic_cast<const CGTownInstance *>(target);
+		auto fortLevel = town->fortLevel();
+		auto booster = isAnotherAi(town, *ai->cb) ? 0.3 : 1;
+
+		if(town->hasCapitol()) return 1;
+
+		if(fortLevel < CGTownInstance::CITADEL)
+			return booster * (town->hasFort() ? 0.6 : 0.4);
+		else
+			return booster * (fortLevel == CGTownInstance::CASTLE ? 0.9 : 0.8);
+	}
 
 	case Obj::HERO:
 		return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
@@ -448,22 +483,17 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH
 	}
 }
 
-uint64_t RewardEvaluator::getEnemyHeroDanger(const int3 & tile, uint8_t turn) const
+const HitMapInfo & RewardEvaluator::getEnemyHeroDanger(const int3 & tile, uint8_t turn) const
 {
 	auto & treatNode = ai->dangerHitMap->getTileTreat(tile);
 
 	if(treatNode.maximumDanger.danger == 0)
-		return 0;
+		return HitMapInfo::NoTreat;
 
 	if(treatNode.maximumDanger.turn <= turn)
-		return treatNode.maximumDanger.danger;
+		return treatNode.maximumDanger;
 
-	return treatNode.fastestDanger.turn <= turn ? treatNode.fastestDanger.danger : 0;
-}
-
-uint64_t RewardEvaluator::getEnemyHeroDanger(const AIPath & path) const
-{
-	return getEnemyHeroDanger(path.targetTile(), path.turn());
+	return treatNode.fastestDanger.turn <= turn ? treatNode.fastestDanger : HitMapInfo::NoTreat;
 }
 
 int32_t getArmyCost(const CArmedInstance * army)
@@ -519,7 +549,7 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
 	case Obj::SEA_CHEST:
 		return 1500;
 	case Obj::PANDORAS_BOX:
-		return 5000;
+		return 2500;
 	case Obj::PRISON:
 		//Objectively saves us 2500 to hire hero
 		return GameConstants::HERO_GOLD_COST;
@@ -561,9 +591,29 @@ public:
 		uint64_t upgradeValue = armyUpgrade.getUpgradeValue();
 
 		evaluationContext.armyReward += upgradeValue;
+		evaluationContext.strategicalValue += upgradeValue / (float)armyUpgrade.hero->getArmyStrength();
 	}
 };
 
+void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uint8_t turn, uint64_t ourStrength)
+{
+	HitMapInfo enemyDanger = evaluationContext.evaluator.getEnemyHeroDanger(tile, turn);
+
+	if(enemyDanger.danger)
+	{
+		auto dangerRatio = enemyDanger.danger / (double)ourStrength;
+		auto enemyHero = evaluationContext.evaluator.ai->cb->getObj(enemyDanger.hero.hid, false);
+		bool isAI = enemyHero && isAnotherAi(enemyHero, *evaluationContext.evaluator.ai->cb);
+
+		if(isAI)
+		{
+			dangerRatio *= 1.5; // lets make AI bit more afraid of other AI.
+		}
+
+		vstd::amax(evaluationContext.enemyHeroDangerRatio, dangerRatio);
+	}
+}
+
 class DefendTownEvaluator : public IEvaluationContextBuilder
 {
 private:
@@ -577,7 +627,7 @@ private:
 				continue;
 
 			auto creature = creatureInfo.second.back().toCreature();
-			result += creature->AIValue * town->getGrowthInfo(creature->level).totalGrowth();
+			result += creature->AIValue * town->getGrowthInfo(creature->getLevel() - 1).totalGrowth();
 		}
 
 		return result;
@@ -596,7 +646,10 @@ public:
 		auto armyIncome = townArmyIncome(town);
 		auto dailyIncome = town->dailyIncome()[Res::GOLD];
 
-		auto strategicalValue = std::sqrt(armyIncome / 20000.0f) + dailyIncome / 10000.0f;
+		auto strategicalValue = std::sqrt(armyIncome / 20000.0f) + dailyIncome / 3000.0f;
+
+		if(evaluationContext.evaluator.ai->buildAnalyzer->getDevelopmentInfo().size() == 1)
+			strategicalValue = 1;
 
 		float multiplier = 1;
 
@@ -607,9 +660,7 @@ public:
 		evaluationContext.goldReward += dailyIncome * 5 * multiplier;
 		evaluationContext.strategicalValue += strategicalValue * multiplier;
 		vstd::amax(evaluationContext.danger, defendTown.getTreat().danger);
-
-		auto enemyDanger = evaluationContext.evaluator.getEnemyHeroDanger(town->visitablePos(), defendTown.getTurn());
-		vstd::amax(evaluationContext.enemyHeroDangerRatio, enemyDanger / (double)defendTown.getDefenceStrength());
+		addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
 	}
 };
 
@@ -665,7 +716,7 @@ public:
 
 		vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength());
 		evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr);
-		vstd::amax(evaluationContext.enemyHeroDangerRatio, evaluationContext.evaluator.getEnemyHeroDanger(path) / (double)path.getHeroStrength());
+		addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
 		vstd::amax(evaluationContext.turn, path.turn());
 	}
 };
@@ -719,6 +770,29 @@ public:
 	}
 };
 
+class ExchangeSwapTownHeroesContextBuilder : public IEvaluationContextBuilder
+{
+public:
+	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	{
+		if(task->goalType != Goals::EXCHANGE_SWAP_TOWN_HEROES)
+			return;
+
+		Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast<Goals::ExchangeSwapTownHeroes &>(*task);
+		const CGHeroInstance * garrisonHero = swapCommand.getGarrisonHero();
+
+		if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE)
+		{
+			auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero);
+			auto mpLeft = garrisonHero->movement / (float)garrisonHero->maxMovePoints(true);
+
+			evaluationContext.movementCost += mpLeft;
+			evaluationContext.movementCostByRole[defenderRole] += mpLeft;
+			evaluationContext.heroRole = defenderRole;
+		}
+	}
+};
+
 class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder
 {
 public:
@@ -733,21 +807,23 @@ public:
 		evaluationContext.goldReward += 7 * bi.dailyIncome[Res::GOLD] / 2; // 7 day income but half we already have
 		evaluationContext.heroRole = HeroRole::MAIN;
 		evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount;
-		evaluationContext.strategicalValue += buildThis.townInfo.armyStrength / 50000.0;
 		evaluationContext.goldCost += bi.buildCostWithPrerequisits[Res::GOLD];
 
 		if(bi.creatureID != CreatureID::NONE)
 		{
+			evaluationContext.strategicalValue += buildThis.townInfo.armyStrength / 50000.0;
+
 			if(bi.baseCreatureID == bi.creatureID)
 			{
-				evaluationContext.strategicalValue += 0.5f + 0.1f * bi.creatureLevel / (float)bi.prerequisitesCount;
+				evaluationContext.strategicalValue += (0.5f + 0.1f * bi.creatureLevel) / (float)bi.prerequisitesCount;
 				evaluationContext.armyReward += bi.armyStrength;
 			}
 			else
 			{
 				auto potentialUpgradeValue = evaluationContext.evaluator.getUpgradeArmyReward(buildThis.town, bi);
-				//evaluationContext.strategicalValue += 0.05f * bi.creatureLevel / (float)bi.prerequisitesCount;
-				evaluationContext.armyReward += 0.3f * potentialUpgradeValue / (float)bi.prerequisitesCount;
+				
+				evaluationContext.strategicalValue += potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount;
+				evaluationContext.armyReward += potentialUpgradeValue / (float)bi.prerequisitesCount;
 			}
 		}
 		else if(bi.id == BuildingID::CITADEL || bi.id == BuildingID::CASTLE)
@@ -757,7 +833,14 @@ public:
 		}
 		else
 		{
-			evaluationContext.strategicalValue += evaluationContext.evaluator.ai->buildAnalyzer->getGoldPreasure() * evaluationContext.goldReward / 2200.0f;
+			auto goldPreasure = evaluationContext.evaluator.ai->buildAnalyzer->getGoldPreasure();
+
+			evaluationContext.strategicalValue += evaluationContext.goldReward * goldPreasure / 3500.0f / bi.prerequisitesCount;
+		}
+
+		if(bi.notEnoughRes && bi.prerequisitesCount == 1)
+		{
+			evaluationContext.strategicalValue /= 2;
 		}
 	}
 };
@@ -783,6 +866,7 @@ PriorityEvaluator::PriorityEvaluator(const Nullkiller * ai)
 	evaluationContextBuilders.push_back(std::make_shared<HeroExchangeEvaluator>());
 	evaluationContextBuilders.push_back(std::make_shared<ArmyUpgradeEvaluator>());
 	evaluationContextBuilders.push_back(std::make_shared<DefendTownEvaluator>());
+	evaluationContextBuilders.push_back(std::make_shared<ExchangeSwapTownHeroesContextBuilder>());
 }
 
 EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const

+ 2 - 2
AI/Nullkiller/Engine/PriorityEvaluator.h

@@ -23,6 +23,7 @@ namespace NKAI
 
 class BuildingInfo;
 class Nullkiller;
+struct HitMapInfo;
 
 class RewardEvaluator
 {
@@ -41,8 +42,7 @@ public:
 	float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * hero, HeroRole role) const;
 	int32_t getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const;
 	uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const;
-	uint64_t getEnemyHeroDanger(const AIPath & path) const;
-	uint64_t getEnemyHeroDanger(const int3 & tile, uint8_t turn) const;
+	const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const;
 };
 
 struct DLL_EXPORT EvaluationContext

+ 4 - 4
AI/Nullkiller/Goals/AbstractGoal.cpp

@@ -47,7 +47,7 @@ std::string AbstractGoal::toString() const //TODO: virtualize
 	switch(goalType)
 	{
 	case COLLECT_RES:
-		desc = "COLLECT RESOURCE " + GameConstants::RESOURCE_NAMES[resID] + " (" + boost::lexical_cast<std::string>(value) + ")";
+		desc = "COLLECT RESOURCE " + GameConstants::RESOURCE_NAMES[resID] + " (" + std::to_string(value) + ")";
 		break;
 	case TRADE:
 	{
@@ -60,16 +60,16 @@ std::string AbstractGoal::toString() const //TODO: virtualize
 		desc = "GATHER TROOPS";
 		break;
 	case GET_ART_TYPE:
-		desc = "GET ARTIFACT OF TYPE " + VLC->arth->objects[aid]->getName();
+		desc = "GET ARTIFACT OF TYPE " + VLC->arth->objects[aid]->getNameTranslated();
 		break;
 	case DIG_AT_TILE:
 		desc = "DIG AT TILE " + tile.toString();
 		break;
 	default:
-		return boost::lexical_cast<std::string>(goalType);
+		return std::to_string(goalType);
 	}
 	if(hero.get(true)) //FIXME: used to crash when we lost hero and failed goal
-		desc += " (" + hero->name + ")";
+		desc += " (" + hero->getNameTranslated() + ")";
 	return desc;
 }
 

+ 6 - 6
AI/Nullkiller/Goals/AdventureSpellCast.cpp

@@ -33,19 +33,19 @@ void AdventureSpellCast::accept(AIGateway * ai)
 
 	auto spell = getSpell();
 
-	logAi->trace("Decomposing adventure spell cast of %s for hero %s", spell->name, hero->name);
+	logAi->trace("Decomposing adventure spell cast of %s for hero %s", spell->getNameTranslated(), hero->getNameTranslated());
 
 	if(!spell->isAdventure())
-		throw cannotFulfillGoalException(spell->name + " is not an adventure spell.");
+		throw cannotFulfillGoalException(spell->getNameTranslated() + " is not an adventure spell.");
 
 	if(!hero->canCastThisSpell(spell))
-		throw cannotFulfillGoalException("Hero can not cast " + spell->name);
+		throw cannotFulfillGoalException("Hero can not cast " + spell->getNameTranslated());
 
 	if(hero->mana < hero->getSpellCost(spell))
-		throw cannotFulfillGoalException("Hero has not enough mana to cast " + spell->name);
+		throw cannotFulfillGoalException("Hero has not enough mana to cast " + spell->getNameTranslated());
 
 	if(spellID == SpellID::TOWN_PORTAL && town && town->visitingHero)
-		throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->name);
+		throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->getNameTranslated());
 
 	if(town && spellID == SpellID::TOWN_PORTAL)
 	{
@@ -70,7 +70,7 @@ void AdventureSpellCast::accept(AIGateway * ai)
 
 std::string AdventureSpellCast::toString() const
 {
-	return "AdventureSpellCast " + spellID.toSpell()->name;
+	return "AdventureSpellCast " + spellID.toSpell()->getNameTranslated();
 }
 
 }

+ 2 - 2
AI/Nullkiller/Goals/BuildThis.cpp

@@ -46,7 +46,7 @@ bool BuildThis::operator==(const BuildThis & other) const
 
 std::string BuildThis::toString() const
 {
-	return "Build " + buildingInfo.name + " in " + town->name;
+	return "Build " + buildingInfo.name + " in " + town->getNameTranslated();
 }
 
 void BuildThis::accept(AIGateway * ai)
@@ -58,7 +58,7 @@ void BuildThis::accept(AIGateway * ai)
 		if(cb->canBuildStructure(town, b) == EBuildingState::ALLOWED)
 		{
 			logAi->debug("Player %d will build %s in town of %s at %s",
-				ai->playerID, town->town->buildings.at(b)->Name(), town->name, town->pos.toString());
+				ai->playerID, town->town->buildings.at(b)->getNameTranslated(), town->getNameTranslated(), town->pos.toString());
 			cb->buildBuilding(town, b);
 
 			return;

+ 1 - 1
AI/Nullkiller/Goals/BuyArmy.cpp

@@ -29,7 +29,7 @@ bool BuyArmy::operator==(const BuyArmy & other) const
 
 std::string BuyArmy::toString() const
 {
-	return "Buy army at " + town->name;
+	return "Buy army at " + town->getNameTranslated();
 }
 
 void BuyArmy::accept(AIGateway * ai)

+ 8 - 5
AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp

@@ -33,7 +33,7 @@ ExchangeSwapTownHeroes::ExchangeSwapTownHeroes(
 
 std::string ExchangeSwapTownHeroes::toString() const
 {
-	return "Exchange and swap heroes of " + town->name;
+	return "Exchange and swap heroes of " + town->getNameTranslated();
 }
 
 bool ExchangeSwapTownHeroes::operator==(const ExchangeSwapTownHeroes & other) const
@@ -54,13 +54,13 @@ void ExchangeSwapTownHeroes::accept(AIGateway * ai)
 
 		if(currentGarrisonHero.get() != town->visitingHero.get())
 		{
-			logAi->error("VisitingHero is empty, expected %s", currentGarrisonHero->name);
+			logAi->error("VisitingHero is empty, expected %s", currentGarrisonHero->getNameTranslated());
 			return;
 		}
 
 		ai->buildArmyIn(town);
 		ai->nullkiller->unlockHero(currentGarrisonHero.get());
-		logAi->debug("Extracted hero %s from garrison of %s", currentGarrisonHero->name, town->name);
+		logAi->debug("Extracted hero %s from garrison of %s", currentGarrisonHero->getNameTranslated(), town->getNameTranslated());
 
 		return;
 	}
@@ -83,7 +83,10 @@ void ExchangeSwapTownHeroes::accept(AIGateway * ai)
 	
 	cb->swapGarrisonHero(town);
 
-	ai->nullkiller->lockHero(garrisonHero, lockingReason);
+	if(lockingReason != HeroLockedReason::NOT_LOCKED)
+	{
+		ai->nullkiller->lockHero(garrisonHero, lockingReason);
+	}
 
 	if(town->visitingHero && town->visitingHero != garrisonHero)
 	{
@@ -91,7 +94,7 @@ void ExchangeSwapTownHeroes::accept(AIGateway * ai)
 		ai->makePossibleUpgrades(town->visitingHero);
 	}
 
-	logAi->debug("Put hero %s to garrison of %s", garrisonHero->name, town->name);
+	logAi->debug("Put hero %s to garrison of %s", garrisonHero->getNameTranslated(), town->getNameTranslated());
 }
 
 }

+ 3 - 0
AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h

@@ -32,6 +32,9 @@ namespace Goals
 		void accept(AIGateway * ai) override;
 		std::string toString() const override;
 		virtual bool operator==(const ExchangeSwapTownHeroes & other) const override;
+
+		const CGHeroInstance * getGarrisonHero() const { return garrisonHero; }
+		HeroLockedReason getLockingReason() const { return lockingReason; }
 	};
 }
 

+ 12 - 11
AI/Nullkiller/Goals/ExecuteHeroChain.cpp

@@ -52,6 +52,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 	logAi->debug("Executing hero chain towards %s. Path %s", targetName, chainPath.toString());
 
 	ai->nullkiller->setActive(chainPath.targetHero, tile);
+	ai->nullkiller->setTargetObject(objid);
 
 	std::set<int> blockedIndexes;
 
@@ -75,7 +76,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 			continue;
 		}
 
-		logAi->debug("Executing chain node %d. Moving hero %s to %s", i, hero->name, node.coord.toString());
+		logAi->debug("Executing chain node %d. Moving hero %s to %s", i, hero->getNameTranslated(), node.coord.toString());
 
 		try
 		{
@@ -111,7 +112,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 					{
 						logAi->error(
 							"Unable to complete chain. Expected hero %s to arrive to %s in 0 turns but he cannot do this",
-							hero->name,
+							hero->getNameTranslated(),
 							node.coord.toString());
 
 						return;
@@ -127,7 +128,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 							continue;
 						}
 					}
-					catch(cannotFulfillGoalException)
+					catch(const cannotFulfillGoalException &)
 					{
 						if(!heroPtr.validAndSet())
 						{
@@ -143,7 +144,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 
 							if(isOk && path.nodes.back().turns > 0)
 							{
-								logAi->warn("Hero %s has %d mp which is not enough to continue his way towards %s.", hero->name, hero->movement, node.coord.toString());
+								logAi->warn("Hero %s has %d mp which is not enough to continue his way towards %s.", hero->getNameTranslated(), hero->movement, node.coord.toString());
 
 								ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
 								return;
@@ -161,23 +162,23 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 			if(node.turns == 0)
 			{
 				logAi->error(
-					"Enable to complete chain. Expected hero %s to arive to %s but he is at %s", 
-					hero->name, 
+					"Unable to complete chain. Expected hero %s to arive to %s but he is at %s",
+					hero->getNameTranslated(),
 					node.coord.toString(),
 					hero->visitablePos().toString());
 
 				return;
 			}
 			
-			// no exception means we were not able to rich the tile
+			// no exception means we were not able to reach the tile
 			ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
 			blockedIndexes.insert(node.parentIndex);
 		}
-		catch(goalFulfilledException)
+		catch(const goalFulfilledException &)
 		{
 			if(!heroPtr.validAndSet())
 			{
-				logAi->debug("Hero %s was killed while attempting to rich %s", heroPtr.name, node.coord.toString());
+				logAi->debug("Hero %s was killed while attempting to reach %s", heroPtr.name, node.coord.toString());
 
 				return;
 			}
@@ -187,14 +188,14 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 
 std::string ExecuteHeroChain::toString() const
 {
-	return "ExecuteHeroChain " + targetName + " by " + chainPath.targetHero->name;
+	return "ExecuteHeroChain " + targetName + " by " + chainPath.targetHero->getNameTranslated();
 }
 
 bool ExecuteHeroChain::moveHeroToTile(const CGHeroInstance * hero, const int3 & tile)
 {
 	if(tile == hero->visitablePos() && cb->getVisitableObjs(hero->visitablePos()).size() < 2)
 	{
-		logAi->warn("Why do I want to move hero %s to tile %s? Already standing on that tile! ", hero->name, tile.toString());
+		logAi->warn("Why do I want to move hero %s to tile %s? Already standing on that tile! ", hero->getNameTranslated(), tile.toString());
 
 		return true;
 	}

+ 1 - 1
AI/Nullkiller/Goals/GatherArmy.cpp

@@ -36,7 +36,7 @@ bool GatherArmy::operator==(const GatherArmy & other) const
 
 std::string GatherArmy::completeMessage() const
 {
-	return "Hero " + hero.get()->name + " gathered army of value " + boost::lexical_cast<std::string>(value);
+	return "Hero " + hero.get()->name + " gathered army of value " + std::to_string(value);
 }
 
 TSubgoal GatherArmy::whatToDoToAchieve()

+ 3 - 3
AI/Nullkiller/Goals/RecruitHero.cpp

@@ -26,7 +26,7 @@ using namespace Goals;
 
 std::string RecruitHero::toString() const
 {
-	return "Recruit hero at " + town->name;
+	return "Recruit hero at " + town->getNameTranslated();
 }
 
 void RecruitHero::accept(AIGateway * ai)
@@ -40,7 +40,7 @@ void RecruitHero::accept(AIGateway * ai)
 		throw cannotFulfillGoalException("No town to recruit hero!");
 	}
 
-	logAi->debug("Trying to recruit a hero in %s at %s", t->name, t->visitablePos().toString());
+	logAi->debug("Trying to recruit a hero in %s at %s", t->getNameTranslated(), t->visitablePos().toString());
 
 	auto heroes = cb->getAvailableHeroes(t);
 
@@ -78,4 +78,4 @@ void RecruitHero::accept(AIGateway * ai)
 		ai->moveHeroToTile(t->visitablePos(), t->visitingHero.get());
 }
 
-}
+}

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

@@ -285,7 +285,7 @@ std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
 
 	for(auto & neighbour : accessibleNeighbourTiles)
 	{
-		for(EPathfindingLayer i = EPathfindingLayer::LAND; i <= EPathfindingLayer::AIR; i.advance(1))
+		for(EPathfindingLayer i = EPathfindingLayer::LAND; i < EPathfindingLayer::NUM_LAYERS; i.advance(1))
 		{
 			auto nextNode = getOrCreateNode(neighbour, i, srcNode->actor);
 
@@ -401,6 +401,9 @@ public:
 
 	void execute(const blocked_range<size_t>& r)
 	{
+		std::random_device randomDevice;
+		std::mt19937 randomEngine(randomDevice());
+
 		for(int i = r.begin(); i != r.end(); i++)
 		{
 			auto & pos = tiles[i];
@@ -422,7 +425,7 @@ public:
 						existingChains.push_back(&node);
 				}
 
-				std::random_shuffle(existingChains.begin(), existingChains.end());
+				std::shuffle(existingChains.begin(), existingChains.end(), randomEngine);
 
 				for(AIPathNode * node : existingChains)
 				{
@@ -480,6 +483,9 @@ public:
 
 bool AINodeStorage::calculateHeroChain()
 {
+	std::random_device randomDevice;
+	std::mt19937 randomEngine(randomDevice());
+
 	heroChainPass = EHeroChainPass::CHAIN;
 	heroChain.clear();
 
@@ -489,7 +495,7 @@ bool AINodeStorage::calculateHeroChain()
 	{
 		boost::mutex resultMutex;
 
-		std::random_shuffle(data.begin(), data.end());
+		std::shuffle(data.begin(), data.end(), randomEngine);
 
 		parallel_for(blocked_range<size_t>(0, data.size()), [&](const blocked_range<size_t>& r)
 		{
@@ -849,6 +855,10 @@ void AINodeStorage::setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes)
 
 	for(auto & hero : heroes)
 	{
+		// do not allow our own heroes in garrison to act on map
+		if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison)
+			continue;
+
 		uint64_t mask = FirstActorMask << actors.size();
 		auto actor = std::make_shared<HeroActor>(hero.first, hero.second, mask, ai);
 
@@ -1431,10 +1441,10 @@ std::string AIPath::toString() const
 {
 	std::stringstream str;
 
-	str << targetHero->name << "[" << std::hex << chainMask << std::dec << "]" << ", turn " << (int)(turn()) << ": ";
+	str << targetHero->getNameTranslated() << "[" << std::hex << chainMask << std::dec << "]" << ", turn " << (int)(turn()) << ": ";
 
 	for(auto node : nodes)
-		str << node.targetHero->name << "[" << std::hex << node.chainMask << std::dec << "]" << "->" << node.coord.toString() << "; ";
+		str << node.targetHero->getNameTranslated() << "[" << std::hex << node.chainMask << std::dec << "]" << "->" << node.coord.toString() << "; ";
 
 	return str.str();
 }

+ 1 - 1
AI/Nullkiller/Pathfinding/Actions/BuyArmyAction.cpp

@@ -27,7 +27,7 @@ namespace AIPathfinding
 		if(!hero->visitedTown)
 		{
 			throw cannotFulfillGoalException(
-				hero->name + " being at " + hero->visitablePos().toString() + " has no town to recruit creatures.");
+				hero->getNameTranslated() + " being at " + hero->visitablePos().toString() + " has no town to recruit creatures.");
 		}
 
 		ai->recruitCreatures(hero->visitedTown, hero);

+ 1 - 1
AI/Nullkiller/Pathfinding/Actions/TownPortalAction.cpp

@@ -34,7 +34,7 @@ void TownPortalAction::execute(const CGHeroInstance * hero) const
 
 std::string TownPortalAction::toString() const
 {
-	return "Town Portal to " + target->name;
+	return "Town Portal to " + target->getNameTranslated();
 }
 /*
 bool TownPortalAction::canAct(const CGHeroInstance * hero, const AIPathNode * source) const

+ 19 - 14
AI/Nullkiller/Pathfinding/Actors.cpp

@@ -80,7 +80,7 @@ int ChainActor::maxMovePoints(CGPathNode::ELayer layer)
 
 std::string ChainActor::toString() const
 {
-	return hero->name;
+	return hero->getNameTranslated();
 }
 
 ObjectActor::ObjectActor(const CGObjectInstance * obj, const CCreatureSet * army, uint64_t chainMask, int initialTurn)
@@ -274,12 +274,12 @@ ExchangeResult HeroExchangeMap::tryExchangeNoLock(const ChainActor * other)
 			return result;
 		}
 
-	if(other->isMovable && other->armyValue <= actor->armyValue / 10 && other->armyValue < MIN_ARMY_STRENGTH_FOR_CHAIN)
-		return result;
+		if(other->isMovable && other->armyValue <= actor->armyValue / 10 && other->armyValue < MIN_ARMY_STRENGTH_FOR_CHAIN)
+			return result;
 
-	TResources availableResources = resources - actor->armyCost - other->armyCost;
-	HeroExchangeArmy * upgradedInitialArmy = tryUpgrade(actor->creatureSet, other->getActorObject(), availableResources);
-	HeroExchangeArmy * newArmy;
+		TResources availableResources = resources - actor->armyCost - other->armyCost;
+		HeroExchangeArmy * upgradedInitialArmy = tryUpgrade(actor->creatureSet, other->getActorObject(), availableResources);
+		HeroExchangeArmy * newArmy;
 
 		if(other->creatureSet->Slots().size())
 		{
@@ -303,20 +303,25 @@ ExchangeResult HeroExchangeMap::tryExchangeNoLock(const ChainActor * other)
 
 		if(!newArmy) return result;
 
-		auto reinforcement = newArmy->getArmyStrength() - actor->creatureSet->getArmyStrength();
+		auto newArmyStrength = newArmy->getArmyStrength();
+		auto oldArmyStrength = actor->creatureSet->getArmyStrength();
 
-#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
+		if(newArmyStrength <= oldArmyStrength) return result;
+
+		auto reinforcement = newArmyStrength - oldArmyStrength;
+
+	#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
 		logAi->trace(
 			"Exchange %s->%s reinforcement: %d, %f%%",
 			actor->toString(),
 			other->toString(),
 			reinforcement,
 			100.0f * reinforcement / actor->armyValue);
-#endif
+	#endif
 
-	if(reinforcement <= actor->armyValue / 10 && reinforcement < MIN_ARMY_STRENGTH_FOR_CHAIN)
-	{
-		delete newArmy;
+		if(reinforcement <= actor->armyValue / 10 && reinforcement < MIN_ARMY_STRENGTH_FOR_CHAIN)
+		{
+			delete newArmy;
 
 			return result;
 		}
@@ -365,7 +370,7 @@ HeroExchangeArmy * HeroExchangeMap::tryUpgrade(
 	{
 		auto buyArmy = ai->armyManager->getArmyAvailableToBuy(target, ai->cb->getTown(upgrader->id), resources);
 
-		for(auto creatureToBuy : buyArmy)
+		for(auto & creatureToBuy : buyArmy)
 		{
 			auto targetSlot = target->getSlotFor(creatureToBuy.cre);
 
@@ -463,5 +468,5 @@ TownGarrisonActor::TownGarrisonActor(const CGTownInstance * town, uint64_t chain
 
 std::string TownGarrisonActor::toString() const
 {
-	return town->name;
+	return town->getNameTranslated();
 }

+ 4 - 2
AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp

@@ -53,13 +53,15 @@ namespace AIPathfinding
 
 		for(const CGTownInstance * t : cb->getTownsInfo())
 		{
-			if(t->hasBuilt(BuildingID::SHIPYARD))
+			// do not allow ally shipyards because of bug
+			if(t->hasBuilt(BuildingID::SHIPYARD) && t->getOwner() == ai->playerID)
 				shipyards.push_back(t);
 		}
 
 		for(const CGObjectInstance * obj : ai->memory->visitableObjs)
 		{
-			if(obj->ID != Obj::TOWN) //towns were handled in the previous loop
+			// do not allow ally shipyards because of bug
+			if(obj->ID != Obj::TOWN && obj->getOwner() == ai->playerID) //towns were handled in the previous loop
 			{
 				if(const IShipyard * shipyard = IShipyard::castFrom(obj))
 					shipyards.push_back(shipyard);

+ 10 - 6
AI/StupidAI/CMakeLists.txt

@@ -1,7 +1,4 @@
 set(stupidAI_SRCS
-		StdInc.cpp
-
-		main.cpp
 		StupidAI.cpp
 )
 
@@ -11,13 +8,20 @@ set(stupidAI_HEADERS
 		StupidAI.h
 )
 
+if(NOT ENABLE_STATIC_AI_LIBS)
+	list(APPEND stupidAI_SRCS main.cpp StdInc.cpp)
+endif()
 assign_source_group(${stupidAI_SRCS} ${stupidAI_HEADERS})
 
-add_library(StupidAI SHARED ${stupidAI_SRCS} ${stupidAI_HEADERS})
+if(ENABLE_STATIC_AI_LIBS)
+	add_library(StupidAI STATIC ${stupidAI_SRCS} ${stupidAI_HEADERS})
+else()
+	add_library(StupidAI SHARED ${stupidAI_SRCS} ${stupidAI_HEADERS})
+	install(TARGETS StupidAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR})
+endif()
+
 target_link_libraries(StupidAI PRIVATE ${VCMI_LIB_TARGET})
 target_include_directories(StupidAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
 
 vcmi_set_output_dir(StupidAI "AI")
 enable_pch(StupidAI)
-
-install(TARGETS StupidAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR})

+ 8 - 6
AI/StupidAI/StupidAI.cpp

@@ -28,7 +28,7 @@ CStupidAI::~CStupidAI()
 	print("destroyed");
 }
 
-void CStupidAI::init(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
+void CStupidAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
 {
 	print("init called, saving ptr to IBattleCallback");
 	env = ENV;
@@ -55,9 +55,11 @@ public:
 	{}
 	void calcDmg(const CStack * ourStack)
 	{
-		TDmgRange retal, dmg = cbc->battleEstimateDamage(ourStack, s, &retal);
-		adi = static_cast<int>((dmg.first + dmg.second) / 2);
-		adr = static_cast<int>((retal.first + retal.second) / 2);
+		// FIXME: provide distance info for Jousting bonus
+		DamageEstimation retal;
+		DamageEstimation dmg = cbc->battleEstimateDamage(ourStack, s, 0, &retal);
+		adi = static_cast<int>((dmg.damage.min + dmg.damage.max) / 2);
+		adr = static_cast<int>((retal.damage.min + retal.damage.max) / 2);
 	}
 
 	bool operator==(const EnemyInfo& ei) const
@@ -177,7 +179,7 @@ void CStupidAI::battleAttack(const BattleAttack *ba)
 	print("battleAttack called");
 }
 
-void CStupidAI::battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa)
+void CStupidAI::battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged)
 {
 	print("battleStacksAttacked called");
 }
@@ -202,7 +204,7 @@ void CStupidAI::battleNewRound(int round)
 	print("battleNewRound called");
 }
 
-void CStupidAI::battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance)
+void CStupidAI::battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport)
 {
 	print("battleStackMoved called");
 }

+ 3 - 3
AI/StupidAI/StupidAI.h

@@ -25,18 +25,18 @@ public:
 	CStupidAI();
 	~CStupidAI();
 
-	void init(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB) override;
+	void initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB) override;
 	void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero
 	void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero
 	BattleAction activeStack(const CStack * stack) override; //called when it's turn of that stack
 
 	void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack
-	void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa) override; //called when stack receives damage (after battleAttack())
+	void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) override; //called when stack receives damage (after battleAttack())
 	void battleEnd(const BattleResult *br) override;
 	//void battleResultsApplied() override; //called when all effects of last battle are applied
 	void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied;
 	void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
-	void battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance) override;
+	void battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport) override;
 	void battleSpellCast(const BattleSpellCast *sc) override;
 	void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks
 	//void battleTriggerEffect(const BattleTriggerEffect & bte) override;

+ 1 - 1
AI/VCAI/AIUtility.cpp

@@ -69,7 +69,7 @@ HeroPtr::HeroPtr(const CGHeroInstance * H)
 	}
 
 	h = H;
-	name = h->name;
+	name = h->getNameTranslated();
 	hid = H->id;
 //	infosCount[ai->playerID][hid]++;
 }

+ 9 - 11
AI/VCAI/CMakeLists.txt

@@ -1,6 +1,4 @@
 set(VCAI_SRCS
-		StdInc.cpp
-
 		Pathfinding/AIPathfinderConfig.cpp
 		Pathfinding/AIPathfinder.cpp
 		Pathfinding/AINodeStorage.cpp
@@ -42,7 +40,6 @@ set(VCAI_SRCS
 		Goals/GetArtOfType.cpp
 		Goals/FindObj.cpp
 		Goals/CompleteQuest.cpp
-		main.cpp
 		VCAI.cpp
 )
 
@@ -97,19 +94,20 @@ set(VCAI_HEADERS
 		VCAI.h
 )
 
+if(NOT ENABLE_STATIC_AI_LIBS)
+	list(APPEND VCAI_SRCS main.cpp StdInc.cpp)
+endif()
 assign_source_group(${VCAI_SRCS} ${VCAI_HEADERS})
 
-if(ANDROID) # android compiles ai libs into main lib directly, so we skip this library and just reuse sources list
-	return()
+if(ENABLE_STATIC_AI_LIBS)
+	add_library(VCAI STATIC ${VCAI_SRCS} ${VCAI_HEADERS})
+else()
+	add_library(VCAI SHARED ${VCAI_SRCS} ${VCAI_HEADERS})
+	install(TARGETS VCAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR})
 endif()
 
-add_library(VCAI SHARED ${VCAI_SRCS} ${VCAI_HEADERS})
-
 target_include_directories(VCAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
-
-target_link_libraries(VCAI PRIVATE ${VCMI_LIB_TARGET} fuzzylite::fuzzylite)
+target_link_libraries(VCAI PUBLIC ${VCMI_LIB_TARGET} fuzzylite::fuzzylite)
 
 vcmi_set_output_dir(VCAI "AI")
 enable_pch(VCAI)
-
-install(TARGETS VCAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR})

+ 1 - 1
AI/VCAI/FuzzyEngines.cpp

@@ -30,7 +30,7 @@ engineBase::engineBase()
 void engineBase::configure()
 {
 	engine.configure("Minimum", "Maximum", "Minimum", "AlgebraicSum", "Centroid", "Proportional");
-	logAi->info(engine.toString());
+	logAi->trace(engine.toString());
 }
 
 void engineBase::addRule(const std::string & txt)

+ 5 - 5
AI/VCAI/Goals/AbstractGoal.cpp

@@ -61,7 +61,7 @@ std::string AbstractGoal::name() const //TODO: virtualize
 	case BUILD_STRUCTURE:
 		return "BUILD STRUCTURE";
 	case COLLECT_RES:
-		desc = "COLLECT RESOURCE " + GameConstants::RESOURCE_NAMES[resID] + " (" + boost::lexical_cast<std::string>(value) + ")";
+		desc = "COLLECT RESOURCE " + GameConstants::RESOURCE_NAMES[resID] + " (" + std::to_string(value) + ")";
 		break;
 	case TRADE:
 	{
@@ -81,7 +81,7 @@ std::string AbstractGoal::name() const //TODO: virtualize
 	}
 	break;
 	case FIND_OBJ:
-		desc = "FIND OBJ " + boost::lexical_cast<std::string>(objid);
+		desc = "FIND OBJ " + std::to_string(objid);
 		break;
 	case VISIT_HERO:
 	{
@@ -91,7 +91,7 @@ std::string AbstractGoal::name() const //TODO: virtualize
 	}
 	break;
 	case GET_ART_TYPE:
-		desc = "GET ARTIFACT OF TYPE " + VLC->artifacts()->getByIndex(aid)->getName();
+		desc = "GET ARTIFACT OF TYPE " + VLC->artifacts()->getByIndex(aid)->getNameTranslated();
 		break;
 	case VISIT_TILE:
 		desc = "VISIT TILE " + tile.toString();
@@ -103,10 +103,10 @@ std::string AbstractGoal::name() const //TODO: virtualize
 		desc = "DIG AT TILE " + tile.toString();
 		break;
 	default:
-		return boost::lexical_cast<std::string>(goalType);
+		return std::to_string(goalType);
 	}
 	if(hero.get(true)) //FIXME: used to crash when we lost hero and failed goal
-		desc += " (" + hero->name + ")";
+		desc += " (" + hero->getNameTranslated() + ")";
 	return desc;
 }
 

+ 7 - 7
AI/VCAI/Goals/AdventureSpellCast.cpp

@@ -33,19 +33,19 @@ TSubgoal AdventureSpellCast::whatToDoToAchieve()
 
 	auto spell = getSpell();
 
-	logAi->trace("Decomposing adventure spell cast of %s for hero %s", spell->getName(), hero->name);
+	logAi->trace("Decomposing adventure spell cast of %s for hero %s", spell->getNameTranslated(), hero->getNameTranslated());
 
 	if(!spell->isAdventure())
-		throw cannotFulfillGoalException(spell->getName() + " is not an adventure spell.");
+		throw cannotFulfillGoalException(spell->getNameTranslated() + " is not an adventure spell.");
 
 	if(!hero->canCastThisSpell(spell))
-		throw cannotFulfillGoalException("Hero can not cast " + spell->getName());
+		throw cannotFulfillGoalException("Hero can not cast " + spell->getNameTranslated());
 
 	if(hero->mana < hero->getSpellCost(spell))
-		throw cannotFulfillGoalException("Hero has not enough mana to cast " + spell->getName());
+		throw cannotFulfillGoalException("Hero has not enough mana to cast " + spell->getNameTranslated());
 
 	if(spellID == SpellID::TOWN_PORTAL && town && town->visitingHero)
-		throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->name);
+		throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->getNameTranslated());
 
 	return iAmElementar();
 }
@@ -75,10 +75,10 @@ void AdventureSpellCast::accept(VCAI * ai)
 
 std::string AdventureSpellCast::name() const
 {
-	return "AdventureSpellCast " + getSpell()->getName();
+	return "AdventureSpellCast " + getSpell()->getNameTranslated();
 }
 
 std::string AdventureSpellCast::completeMessage() const
 {
-	return "Spell cast successfully " + getSpell()->getName();
+	return "Spell cast successfully " + getSpell()->getNameTranslated();
 }

+ 1 - 1
AI/VCAI/Goals/BuyArmy.cpp

@@ -41,5 +41,5 @@ TSubgoal BuyArmy::whatToDoToAchieve()
 
 std::string BuyArmy::completeMessage() const
 {
-	return boost::format("Bought army of value %d in town of %s") % value, town->name;
+	return boost::format("Bought army of value %d in town of %s") % value, town->getNameTranslated();
 }

+ 2 - 2
AI/VCAI/Goals/CompleteQuest.cpp

@@ -92,7 +92,7 @@ TSubgoal CompleteQuest::whatToDoToAchieve()
 		result->name(),
 		result->tile.toString(),
 		result->objid,
-		result->hero.validAndSet() ? result->hero->name : "not specified");
+		result->hero.validAndSet() ? result->hero->getNameTranslated() : "not specified");
 
 	return result;
 }
@@ -273,4 +273,4 @@ TGoalVec CompleteQuest::missionDestroyObj() const
 	}
 
 	return solutions;
-}
+}

+ 3 - 3
AI/VCAI/Goals/Explore.cpp

@@ -50,7 +50,7 @@ namespace Goals
 			sightRadius = hero->getSightRadius();
 			bestGoal = sptr(Goals::Invalid());
 			bestValue = 0;
-			ourPos = h->convertPosition(h->pos, false);
+			ourPos = h->visitablePos();
 			allowDeadEndCancellation = true;
 			allowGatherArmy = gatherArmy;
 		}
@@ -240,7 +240,7 @@ bool Explore::operator==(const Explore & other) const
 
 std::string Explore::completeMessage() const
 {
-	return "Hero " + hero.get()->name + " completed exploration";
+	return "Hero " + hero.get()->getNameTranslated() + " completed exploration";
 }
 
 TSubgoal Explore::whatToDoToAchieve()
@@ -339,7 +339,7 @@ TGoalVec Explore::getAllPossibleSubgoals()
 	{
 		for(auto h : heroes)
 		{
-			logAi->trace("Exploration searching for a new point for hero %s", h->name);
+			logAi->trace("Exploration searching for a new point for hero %s", h->getNameTranslated());
 
 			TSubgoal goal = explorationNewPoint(h);
 

+ 1 - 1
AI/VCAI/Goals/GatherArmy.cpp

@@ -33,7 +33,7 @@ bool GatherArmy::operator==(const GatherArmy & other) const
 
 std::string GatherArmy::completeMessage() const
 {
-	return "Hero " + hero.get()->name + " gathered army of value " + boost::lexical_cast<std::string>(value);
+	return "Hero " + hero.get()->getNameTranslated() + " gathered army of value " + std::to_string(value);
 }
 
 TSubgoal GatherArmy::whatToDoToAchieve()

+ 1 - 1
AI/VCAI/Goals/GatherTroops.cpp

@@ -56,7 +56,7 @@ TSubgoal GatherTroops::whatToDoToAchieve()
 	{
 		if(getCreaturesCount(hero) >= this->value)
 		{
-			logAi->trace("Completing GATHER_TROOPS by hero %s", hero->name);
+			logAi->trace("Completing GATHER_TROOPS by hero %s", hero->getNameTranslated());
 
 			throw goalFulfilledException(sptr(*this));
 		}

+ 1 - 1
AI/VCAI/Goals/VisitHero.cpp

@@ -33,7 +33,7 @@ bool VisitHero::operator==(const VisitHero & other) const
 
 std::string VisitHero::completeMessage() const
 {
-	return "hero " + hero.get()->name + " visited hero " + boost::lexical_cast<std::string>(objid);
+	return "hero " + hero.get()->getNameTranslated() + " visited hero " + std::to_string(objid);
 }
 
 TSubgoal VisitHero::whatToDoToAchieve()

+ 1 - 1
AI/VCAI/Goals/VisitObj.cpp

@@ -33,7 +33,7 @@ bool VisitObj::operator==(const VisitObj & other) const
 
 std::string VisitObj::completeMessage() const
 {
-	return "hero " + hero.get()->name + " captured Object ID = " + boost::lexical_cast<std::string>(objid);
+	return "hero " + hero.get()->getNameTranslated() + " captured Object ID = " + std::to_string(objid);
 }
 
 TGoalVec VisitObj::getAllPossibleSubgoals()

+ 1 - 1
AI/VCAI/Goals/VisitTile.cpp

@@ -33,7 +33,7 @@ bool VisitTile::operator==(const VisitTile & other) const
 
 std::string VisitTile::completeMessage() const
 {
-	return "Hero " + hero.get()->name + " visited tile " + tile.toString();
+	return "Hero " + hero.get()->getNameTranslated() + " visited tile " + tile.toString();
 }
 
 TSubgoal VisitTile::whatToDoToAchieve()

+ 0 - 4
AI/VCAI/MapObjectsEvaluator.cpp

@@ -32,10 +32,6 @@ MapObjectsEvaluator::MapObjectsEvaluator()
 				{
 					objectDatabase[CompoundMapObjectID(primaryID, secondaryID)] = handler->getAiValue().get();
 				}
-				else if(VLC->objtypeh->getObjGroupAiValue(primaryID) != boost::none) //if value is not initialized - fallback to default value for this object family if it exists
-				{
-					objectDatabase[CompoundMapObjectID(primaryID, secondaryID)] = VLC->objtypeh->getObjGroupAiValue(primaryID).get();
-				}
 				else //some default handling when aiValue not found, objects that require advanced properties (unavailable from handler) get their value calculated in getObjectValue
 				{
 					objectDatabase[CompoundMapObjectID(primaryID, secondaryID)] = 0;

+ 3 - 3
AI/VCAI/Pathfinding/AINodeStorage.cpp

@@ -107,7 +107,7 @@ boost::optional<AIPathNode *> AINodeStorage::getOrCreateNode(const int3 & pos, c
 
 std::vector<CGPathNode *> AINodeStorage::getInitialNodes()
 {
-	auto hpos = hero->getPosition(false);
+	auto hpos = hero->visitablePos();
 	auto initialNode =
 		getOrCreateNode(hpos, hero->boat ? EPathfindingLayer::SAIL : EPathfindingLayer::LAND, NORMAL_CHAIN)
 		.get();
@@ -167,7 +167,7 @@ std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
 
 	for(auto & neighbour : accessibleNeighbourTiles)
 	{
-		for(EPathfindingLayer i = EPathfindingLayer::LAND; i <= EPathfindingLayer::AIR; i.advance(1))
+		for(EPathfindingLayer i = EPathfindingLayer::LAND; i < EPathfindingLayer::NUM_LAYERS; i.advance(1))
 		{
 			auto nextNode = getOrCreateNode(neighbour, i, srcNode->chainMask);
 
@@ -211,7 +211,7 @@ std::vector<CGPathNode *> AINodeStorage::calculateTeleportations(
 		}
 	}
 
-	if(hero->getPosition(false) == source.coord)
+	if(hero->visitablePos() == source.coord)
 	{
 		calculateTownPortalTeleportations(source, neighbours);
 	}

+ 1 - 1
AI/VCAI/Pathfinding/AIPathfinder.cpp

@@ -55,7 +55,7 @@ void AIPathfinder::updatePaths(std::vector<HeroPtr> heroes)
 
 	auto calculatePaths = [&](const CGHeroInstance * hero, std::shared_ptr<AIPathfinding::AIPathfinderConfig> config)
 	{
-		logAi->debug("Recalculate paths for %s", hero->name);
+		logAi->debug("Recalculate paths for %s", hero->getNameTranslated());
 		
 		cb->calculatePaths(config);
 	};

+ 52 - 47
AI/VCAI/VCAI.cpp

@@ -18,8 +18,9 @@
 #include "../../lib/mapObjects/MapObjects.h"
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CHeroHandler.h"
-#include "../../lib/CModHandler.h"
+#include "../../lib/GameSettings.h"
 #include "../../lib/CGameState.h"
+#include "../../lib/NetPacksBase.h"
 #include "../../lib/NetPacks.h"
 #include "../../lib/serializer/CTypeList.h"
 #include "../../lib/serializer/BinarySerializer.h"
@@ -29,12 +30,6 @@
 
 extern FuzzyHelper * fh;
 
-VCMI_LIB_NAMESPACE_BEGIN
-
-class CGVisitableOPW;
-
-VCMI_LIB_NAMESPACE_END
-
 const double SAFE_ATTACK_CONSTANT = 1.5;
 
 //one thread may be turn of AI and another will be handling a side effect for AI2
@@ -98,11 +93,13 @@ void VCAI::heroMoved(const TryMoveHero & details, bool verbose)
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
 
-	validateObject(details.id); //enemy hero may have left visible area
+	//enemy hero may have left visible area
+	validateObject(details.id);
 	auto hero = cb->getHero(details.id);
 
-	const int3 from = CGHeroInstance::convertPosition(details.start, false);
-	const int3 to = CGHeroInstance::convertPosition(details.end, false);
+	const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));;
+	const int3 to   = hero ? hero->convertToVisitablePos(details.end)   : (details.end   - int3(0,1,0));
+
 	const CGObjectInstance * o1 = vstd::frontOrNull(cb->getVisitableObjs(from, verbose));
 	const CGObjectInstance * o2 = vstd::frontOrNull(cb->getVisitableObjs(to, verbose));
 
@@ -301,7 +298,7 @@ void VCAI::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, Q
 	auto firstHero = cb->getHero(hero1);
 	auto secondHero = cb->getHero(hero2);
 
-	status.addQuery(query, boost::str(boost::format("Exchange between heroes %s (%d) and %s (%d)") % firstHero->name % firstHero->tempOwner % secondHero->name % secondHero->tempOwner));
+	status.addQuery(query, boost::str(boost::format("Exchange between heroes %s (%d) and %s (%d)") % firstHero->getNameTranslated() % firstHero->tempOwner % secondHero->getNameTranslated() % secondHero->tempOwner));
 
 	requestActionASAP([=]()
 	{
@@ -479,7 +476,7 @@ void VCAI::advmapSpellCast(const CGHeroInstance * caster, int spellID)
 	NET_EVENT_HANDLER;
 }
 
-void VCAI::showInfoDialog(const std::string & text, const std::vector<Component> & components, int soundID)
+void VCAI::showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector<Component> & components, int soundID)
 {
 	LOG_TRACE_PARAMS(logAi, "soundID '%i'", soundID);
 	NET_EVENT_HANDLER;
@@ -576,14 +573,14 @@ void VCAI::showMarketWindow(const IMarket * market, const CGHeroInstance * visit
 	NET_EVENT_HANDLER;
 }
 
-void VCAI::showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions)
+void VCAI::showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain)
 {
 	//TODO: AI support for ViewXXX spell
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
 }
 
-void VCAI::init(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
+void VCAI::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
 {
 	LOG_TRACE(logAi);
 	env = ENV;
@@ -608,14 +605,14 @@ void VCAI::yourTurn()
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
 	status.startedTurn();
-	makingTurn = make_unique<boost::thread>(&VCAI::makeTurn, this);
+	makingTurn = std::make_unique<boost::thread>(&VCAI::makeTurn, this);
 }
 
 void VCAI::heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector<SecondarySkill> & skills, QueryID queryID)
 {
 	LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID);
 	NET_EVENT_HANDLER;
-	status.addQuery(queryID, boost::str(boost::format("Hero %s got level %d") % hero->name % hero->level));
+	status.addQuery(queryID, boost::str(boost::format("Hero %s got level %d") % hero->getNameTranslated() % hero->level));
 	requestActionASAP([=](){ answerQuery(queryID, 0); });
 }
 
@@ -756,7 +753,7 @@ void makePossibleUpgrades(const CArmedInstance * obj)
 		if(const CStackInstance * s = obj->getStackPtr(SlotID(i)))
 		{
 			UpgradeInfo ui;
-			cb->getUpgradeInfo(obj, SlotID(i), ui);
+			cb->fillUpgradeInfo(obj, SlotID(i), ui);
 			if(ui.oldID >= 0 && cb->getResourceAmount().canAfford(ui.cost[0] * s->count))
 			{
 				cb->upgradeCreature(obj, SlotID(i), ui.newID[0]);
@@ -812,7 +809,7 @@ void VCAI::makeTurn()
 		for (auto h : cb->getHeroesInfo())
 		{
 			if (h->movement)
-				logAi->warn("Hero %s has %d MP left", h->name, h->movement);
+				logAi->warn("Hero %s has %d MP left", h->getNameTranslated(), h->movement);
 		}
 	}
 	catch (boost::thread_interrupted & e)
@@ -866,7 +863,7 @@ void VCAI::mainLoop()
 
 	invalidPathHeroes.clear();
 
-	while (basicGoals.size())
+	for (int pass = 0; pass< 30 && basicGoals.size(); pass++)
 	{
 		vstd::removeDuplicates(basicGoals); //TODO: container which does this automagically without has would be nice
 		goalsToAdd.clear();
@@ -1032,7 +1029,7 @@ void VCAI::mainLoop()
 
 void VCAI::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h)
 {
-	LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->name % obj->getObjectName() % obj->pos.toString());
+	LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->pos.toString());
 	switch(obj->ID)
 	{
 	case Obj::CREATURE_GENERATOR1:
@@ -1321,7 +1318,7 @@ bool VCAI::canRecruitAnyHero(const CGTownInstance * t) const
 		return false;
 	if(cb->getHeroesInfo().size() >= ALLOWED_ROAMING_HEROES)
 		return false;
-	if(cb->getHeroesInfo().size() >= VLC->modh->settings.MAX_HEROES_ON_MAP_PER_PLAYER)
+	if(cb->getHeroesInfo().size() >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
 		return false;
 	if(!cb->getAvailableHeroes(t).size())
 		return false;
@@ -1425,7 +1422,7 @@ void VCAI::wander(HeroPtr h)
 			{
 				//TODO pick the truly best
 				const CGTownInstance * t = *boost::max_element(townsNotReachable, compareReinforcements);
-				logAi->debug("%s can't reach any town, we'll try to make our way to %s at %s", h->name, t->name, t->visitablePos().toString());
+				logAi->debug("%s can't reach any town, we'll try to make our way to %s at %s", h->getNameTranslated(), t->getNameTranslated(), t->visitablePos().toString());
 				int3 pos1 = h->pos;
 				striveToGoal(sptr(Goals::ClearWayTo(t->visitablePos()).sethero(h))); //TODO: drop "strive", add to mainLoop
 				//if out hero is stuck, we may need to request another hero to clear the way we see
@@ -1579,7 +1576,7 @@ void VCAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, i
 	assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE);
 	status.setBattle(ONGOING_BATTLE);
 	const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit
-	battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->name : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString());
+	battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString());
 	CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side);
 }
 
@@ -1604,12 +1601,19 @@ void VCAI::markObjectVisited(const CGObjectInstance * obj)
 {
 	if(!obj)
 		return;
-	if(dynamic_cast<const CGVisitableOPH *>(obj)) //we may want to visit it with another hero
-		return;
-	if(dynamic_cast<const CGBonusingObject *>(obj)) //or another time
-		return;
+
+	if(const auto * rewardable = dynamic_cast<const CRewardableObject *>(obj)) //we may want to visit it with another hero
+	{
+		if (rewardable->getVisitMode() == CRewardableObject::VISIT_HERO) //we may want to visit it with another hero
+			return;
+
+		if (rewardable->getVisitMode() == CRewardableObject::VISIT_BONUS) //or another time
+			return;
+	}
+
 	if(obj->ID == Obj::MONSTER)
 		return;
+
 	alreadyVisited.insert(obj);
 }
 
@@ -1665,7 +1669,7 @@ void VCAI::validateVisitableObjs()
 	});
 	for(auto & p : reservedHeroesMap)
 	{
-		errorMsg = " shouldn't be on list for hero " + p.first->name + "!";
+		errorMsg = " shouldn't be on list for hero " + p.first->getNameTranslated() + "!";
 		vstd::erase_if(p.second, shouldBeErased);
 	}
 
@@ -1806,14 +1810,14 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
 		}
 	};
 
-	logAi->debug("Moving hero %s to tile %s", h->name, dst.toString());
+	logAi->debug("Moving hero %s to tile %s", h->getNameTranslated(), dst.toString());
 	int3 startHpos = h->visitablePos();
 	bool ret = false;
 	if(startHpos == dst)
 	{
 		//FIXME: this assertion fails also if AI moves onto defeated guarded object
 		assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object
-		cb->moveHero(*h, CGHeroInstance::convertPosition(dst, true));
+		cb->moveHero(*h, h->convertFromVisitablePos(dst));
 		afterMovementCheck(); // TODO: is it feasible to hero get killed there if game work properly?
 		// If revisiting, teleport probing is never done, and so the entries into the list would remain unused and uncleared
 		teleportChannelProbingList.clear();
@@ -1826,7 +1830,7 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
 		cb->getPathsInfo(h.get())->getPath(path, dst);
 		if(path.nodes.empty())
 		{
-			logAi->error("Hero %s cannot reach %s.", h->name, dst.toString());
+			logAi->error("Hero %s cannot reach %s.", h->getNameTranslated(), dst.toString());
 			throw goalFulfilledException(sptr(Goals::VisitTile(dst).sethero(h)));
 		}
 		int i = (int)path.nodes.size() - 1;
@@ -1867,14 +1871,14 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
 
 		auto doMovement = [&](int3 dst, bool transit)
 		{
-			cb->moveHero(*h, CGHeroInstance::convertPosition(dst, true), transit);
+			cb->moveHero(*h, h->convertFromVisitablePos(dst), transit);
 		};
 
 		auto doTeleportMovement = [&](ObjectInstanceID exitId, int3 exitPos)
 		{
 			destinationTeleport = exitId;
 			if(exitPos.valid())
-				destinationTeleportPos = CGHeroInstance::convertPosition(exitPos, true);
+				destinationTeleportPos = h->convertFromVisitablePos(exitPos);
 			cb->moveHero(*h, h->pos);
 			destinationTeleport = ObjectInstanceID();
 			destinationTeleportPos = int3(-1);
@@ -1883,7 +1887,7 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
 
 		auto doChannelProbing = [&]() -> void
 		{
-			auto currentPos = CGHeroInstance::convertPosition(h->pos, false);
+			auto currentPos = h->visitablePos();
 			auto currentExit = getObj(currentPos, true)->id;
 
 			status.setChannelProbing(true);
@@ -1900,7 +1904,7 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
 			int3 currentCoord = path.nodes[i].coord;
 			int3 nextCoord = path.nodes[i - 1].coord;
 
-			auto currentObject = getObj(currentCoord, currentCoord == CGHeroInstance::convertPosition(h->pos, false));
+			auto currentObject = getObj(currentCoord, currentCoord == h->visitablePos());
 			auto nextObjectTop = getObj(nextCoord, false);
 			auto nextObject = getObj(nextCoord, true);
 			auto destTeleportObj = getDestTeleportObj(currentObject, nextObjectTop, nextObject);
@@ -1988,15 +1992,15 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
 			throw cannotFulfillGoalException("Invalid path found!");
 		}
 		evaluateGoal(h); //new hero position means new game situation
-		logAi->debug("Hero %s moved from %s to %s. Returning %d.", h->name, startHpos.toString(), h->visitablePos().toString(), ret);
+		logAi->debug("Hero %s moved from %s to %s. Returning %d.", h->getNameTranslated(), startHpos.toString(), h->visitablePos().toString(), ret);
 	}
 	return ret;
 }
 
 void VCAI::buildStructure(const CGTownInstance * t, BuildingID building)
 {
-	auto name = t->town->buildings.at(building)->Name();
-	logAi->debug("Player %d will build %s in town of %s at %s", ai->playerID, name, t->name, t->pos.toString());
+	auto name = t->town->buildings.at(building)->getNameTranslated();
+	logAi->debug("Player %d will build %s in town of %s at %s", ai->playerID, name, t->getNameTranslated(), t->pos.toString());
 	cb->buildBuilding(t, building); //just do this;
 }
 
@@ -2025,7 +2029,7 @@ void VCAI::tryRealize(Goals::VisitTile & g)
 		throw cannotFulfillGoalException("Cannot visit tile: hero is out of MPs!");
 	if(g.tile == g.hero->visitablePos() && cb->getVisitableObjs(g.hero->visitablePos()).size() < 2)
 	{
-		logAi->warn("Why do I want to move hero %s to tile %s? Already standing on that tile! ", g.hero->name, g.tile.toString());
+		logAi->warn("Why do I want to move hero %s to tile %s? Already standing on that tile! ", g.hero->getNameTranslated(), g.tile.toString());
 		throw goalFulfilledException(sptr(g));
 	}
 	if(ai->moveHeroToTile(g.tile, g.hero.get()))
@@ -2041,7 +2045,7 @@ void VCAI::tryRealize(Goals::VisitObj & g)
 		throw cannotFulfillGoalException("Cannot visit object: hero is out of MPs!");
 	if(position == g.hero->visitablePos() && cb->getVisitableObjs(g.hero->visitablePos()).size() < 2)
 	{
-		logAi->warn("Why do I want to move hero %s to tile %s? Already standing on that tile! ", g.hero->name, g.tile.toString());
+		logAi->warn("Why do I want to move hero %s to tile %s? Already standing on that tile! ", g.hero->getNameTranslated(), g.tile.toString());
 		throw goalFulfilledException(sptr(g));
 	}
 	if(ai->moveHeroToTile(position, g.hero.get()))
@@ -2079,7 +2083,7 @@ void VCAI::tryRealize(Goals::BuildThis & g)
 		if (cb->canBuildStructure(t, b) == EBuildingState::ALLOWED)
 		{
 			logAi->debug("Player %d will build %s in town of %s at %s",
-				playerID, t->town->buildings.at(b)->Name(), t->name, t->pos.toString());
+				playerID, t->town->buildings.at(b)->getNameTranslated(), t->getNameTranslated(), t->pos.toString());
 			cb->buildBuilding(t, b);
 			throw goalFulfilledException(sptr(g));
 		}
@@ -2402,7 +2406,7 @@ void VCAI::performTypicalActions()
 		if(!h) //hero might be lost. getUnblockedHeroes() called once on start of turn
 			continue;
 
-		logAi->debug("Hero %s started wandering, MP=%d", h->name.c_str(), h->movement);
+		logAi->debug("Hero %s started wandering, MP=%d", h->getNameTranslated(), h->movement);
 		makePossibleUpgrades(*h);
 		pickBestArtifacts(*h);
 		try
@@ -2437,7 +2441,7 @@ void VCAI::checkHeroArmy(HeroPtr h)
 
 void VCAI::recruitHero(const CGTownInstance * t, bool throwing)
 {
-	logAi->debug("Trying to recruit a hero in %s at %s", t->name, t->visitablePos().toString());
+	logAi->debug("Trying to recruit a hero in %s at %s", t->getNameTranslated(), t->visitablePos().toString());
 
 	auto heroes = cb->getAvailableHeroes(t);
 	if(heroes.size())
@@ -2740,8 +2744,9 @@ bool AIStatus::channelProbing()
 bool isWeeklyRevisitable(const CGObjectInstance * obj)
 {
 	//TODO: allow polling of remaining creatures in dwelling
-	if(dynamic_cast<const CGVisitableOPW *>(obj)) // ensures future compatibility, unlike IDs
-		return true;
+	if(const auto * rewardable = dynamic_cast<const CRewardableObject *>(obj))
+		return rewardable->getResetDuration() == 7;
+
 	if(dynamic_cast<const CGDwelling *>(obj))
 		return true;
 	if(dynamic_cast<const CBank *>(obj)) //banks tend to respawn often in mods
@@ -2846,12 +2851,12 @@ bool shouldVisit(HeroPtr h, const CGObjectInstance * obj)
 	case Obj::MAGIC_WELL:
 		return h->mana < h->manaLimit();
 	case Obj::PRISON:
-		return ai->myCb->getHeroesInfo().size() < VLC->modh->settings.MAX_HEROES_ON_MAP_PER_PLAYER;
+		return ai->myCb->getHeroesInfo().size() < VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP);
 	case Obj::TAVERN:
 	{
 		//TODO: make AI actually recruit heroes
 		//TODO: only on request
-		if(ai->myCb->getHeroesInfo().size() >= VLC->modh->settings.MAX_HEROES_ON_MAP_PER_PLAYER)
+		if(ai->myCb->getHeroesInfo().size() >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
 			return false;
 		else if(ai->ah->freeGold() < GameConstants::HERO_GOLD_COST)
 			return false;

+ 3 - 3
AI/VCAI/VCAI.h

@@ -143,7 +143,7 @@ public:
 
 	std::string getBattleAIName() const override;
 
-	void init(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
+	void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
 	void yourTurn() override;
 
 	void heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector<SecondarySkill> & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id
@@ -186,7 +186,7 @@ public:
 	void playerBonusChanged(const Bonus & bonus, bool gain) override;
 	void heroCreated(const CGHeroInstance *) override;
 	void advmapSpellCast(const CGHeroInstance * caster, int spellID) override;
-	void showInfoDialog(const std::string & text, const std::vector<Component> & components, int soundID) override;
+	void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector<Component> & components, int soundID) override;
 	void requestRealized(PackageApplied * pa) override;
 	void receivedResource() override;
 	void objectRemoved(const CGObjectInstance * obj) override;
@@ -198,7 +198,7 @@ public:
 	void buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) override;
 	void heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) override;
 	void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override;
-	void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions) override;
+	void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
 
 	void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) override;
 	void battleEnd(const BattleResult * br) override;

+ 1 - 8
CCallback.cpp

@@ -11,7 +11,6 @@
 #include "CCallback.h"
 
 #include "lib/CCreatureHandler.h"
-#include "client/CGameInfo.h"
 #include "lib/CGameState.h"
 #include "client/CPlayerInterface.h"
 #include "client/Client.h"
@@ -21,7 +20,6 @@
 #include "lib/CGeneralTextHandler.h"
 #include "lib/CHeroHandler.h"
 #include "lib/NetPacks.h"
-#include "client/mapHandler.h"
 #include "lib/CArtHandler.h"
 #include "lib/GameConstants.h"
 #include "lib/CPlayerState.h"
@@ -335,11 +333,6 @@ int3 CCallback::getGuardingCreaturePosition(int3 tile)
 	return gs->map->guardingCreaturePositions[tile.z][tile.x][tile.y];
 }
 
-void CCallback::calculatePaths( const CGHeroInstance *hero, CPathsInfo &out)
-{
-	gs->calculatePaths(hero, out);
-}
-
 void CCallback::dig( const CGObjectInstance *hero )
 {
 	DigWithHero dwh;
@@ -400,4 +393,4 @@ boost::optional<BattleAction> CBattleCallback::makeSurrenderRetreatDecision(
 	const BattleStateInfoForRetreat & battleState)
 {
 	return cl->playerint[getPlayerID().get()]->makeSurrenderRetreatDecision(battleState);
-}
+}

+ 5 - 2
CCallback.h

@@ -36,6 +36,11 @@ class BattleStateInfoForRetreat;
 
 VCMI_LIB_NAMESPACE_END
 
+// in static AI build this file gets included into libvcmi
+#ifdef STATIC_AI
+VCMI_LIB_USING_NAMESPACE
+#endif
+
 class CClient;
 struct lua_State;
 
@@ -133,8 +138,6 @@ public:
 	virtual int3 getGuardingCreaturePosition(int3 tile);
 	virtual std::shared_ptr<const CPathsInfo> getPathsInfo(const CGHeroInstance * h);
 
-	virtual void calculatePaths(const CGHeroInstance *hero, CPathsInfo &out);
-
 	//Set of metrhods that allows adding more interfaces for this player that'll receive game event call-ins.
 	void registerBattleInterface(std::shared_ptr<IBattleEventsReceiver> battleEvents);
 	void unregisterBattleInterface(std::shared_ptr<IBattleEventsReceiver> battleEvents);

+ 4 - 0
CI/android-32/before_install.sh

@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+
+DEPS_FILENAME=armeabi-v7a
+. CI/android/before_install.sh

+ 4 - 0
CI/android-64/before_install.sh

@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+
+DEPS_FILENAME=aarch64-v8a
+. CI/android/before_install.sh

+ 8 - 0
CI/android/before_install.sh

@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+sudo apt-get update
+sudo apt-get install ninja-build
+
+mkdir ~/.conan ; cd ~/.conan
+curl -L "https://github.com/vcmi/vcmi-dependencies/releases/download/android-1.0/$DEPS_FILENAME.txz" \
+	| tar -xf - --xz

+ 4 - 0
CI/android/signing.properties

@@ -0,0 +1,4 @@
+STORE_FILE=vcmi-travis.jks
+STORE_PASSWORD=traviskey
+KEY_ALIAS=vcmitraviskey
+KEY_PASSWORD=traviskey

BIN
CI/android/vcmi-travis.jks


+ 5 - 0
CI/conan/android-32

@@ -0,0 +1,5 @@
+include(base/android)
+
+[settings]
+arch=armv7
+os.api_level=19

+ 5 - 0
CI/conan/android-64

@@ -0,0 +1,5 @@
+include(base/android)
+
+[settings]
+arch=armv8
+os.api_level=21

+ 6 - 0
CI/conan/base/android

@@ -0,0 +1,6 @@
+[settings]
+build_type=Release
+compiler=clang
+compiler.libcxx=c++_shared
+compiler.version=14
+os=Android

+ 20 - 0
CI/conan/base/cross-macro.j2

@@ -0,0 +1,20 @@
+{% macro generate_env(target_host) -%}
+CONAN_CROSS_COMPILE={{ target_host }}-
+CHOST={{ target_host }}
+AR={{ target_host }}-ar
+AS={{ target_host }}-as
+CC={{ target_host }}-gcc
+CXX={{ target_host }}-g++
+RANLIB={{ target_host }}-ranlib
+STRIP={{ target_host }}-strip
+{%- endmacro -%}
+
+{% macro generate_env_win32(target_host) -%}
+CONAN_SYSTEM_LIBRARY_LOCATION=/usr/lib/gcc/{{ target_host }}/10-posix/
+RC={{ target_host }}-windres
+{%- endmacro -%}
+
+{% macro generate_conf(target_host) -%}
+tools.build:compiler_executables = {"c": "{{ target_host }}-gcc", "cpp": "{{ target_host }}-g++"}
+tools.build:sysroot = /usr/{{ target_host }}
+{%- endmacro -%}

+ 10 - 0
CI/conan/base/cross-windows

@@ -0,0 +1,10 @@
+[settings]
+os=Windows
+compiler=gcc
+compiler.libcxx=libstdc++11
+compiler.version=10
+compiler.cppstd=11
+build_type=Release
+
+[conf]
+tools.cmake.cmaketoolchain:generator = Ninja

+ 15 - 0
CI/conan/mingw32-linux.jinja

@@ -0,0 +1,15 @@
+{% import 'base/cross-macro.j2' as cross -%}
+include(base/cross-windows)
+{% set target_host="i686-w64-mingw32" %}
+
+[settings]
+arch=x86
+
+[conf]
+{{ cross.generate_conf(target_host)}}
+tools.build:cflags = ["-msse2"]
+tools.build:cxxflags = ["-msse2"]
+
+[env]
+{{ cross.generate_env(target_host) }}
+{{ cross.generate_env_win32(target_host) }}

+ 13 - 0
CI/conan/mingw64-linux.jinja

@@ -0,0 +1,13 @@
+{% import 'base/cross-macro.j2' as cross -%}
+include(base/cross-windows)
+{% set target_host="x86_64-w64-mingw32" %}
+
+[settings]
+arch=x86_64
+
+[conf]
+{{ cross.generate_conf(target_host)}}
+
+[env]
+{{ cross.generate_env(target_host) }}
+{{ cross.generate_env_win32(target_host) }}

+ 11 - 0
CI/linux-qt6/before_install.sh

@@ -0,0 +1,11 @@
+#!/bin/sh
+
+sudo apt-get update
+
+# Dependencies
+sudo apt-get install libboost-all-dev
+sudo apt-get install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev
+sudo apt-get install qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools
+sudo apt-get install ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev
+# Optional dependencies
+sudo apt-get install libminizip-dev libfuzzylite-dev

+ 1 - 0
CI/linux-qt6/upload_package.sh

@@ -0,0 +1 @@
+#!/bin/sh

+ 1 - 1
CI/linux/before_install.sh

@@ -8,4 +8,4 @@ sudo apt-get install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf
 sudo apt-get install qtbase5-dev
 sudo apt-get install ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev
 # Optional dependencies
-sudo apt-get install libminizip-dev libfuzzylite-dev
+sudo apt-get install libminizip-dev libfuzzylite-dev qttools5-dev

+ 16 - 0
CI/mingw-ubuntu/before_install.sh

@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+sudo apt-get update
+sudo apt-get install ninja-build mingw-w64 nsis
+sudo update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix
+
+# Workaround for getting new MinGW headers on Ubuntu 22.04.
+# Remove it once MinGW headers version in repository will be 10.0 at least
+curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-common_10.0.0-2_all.deb \
+  && sudo dpkg -i mingw-w64-common_10.0.0-2_all.deb;
+curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-x86-64-dev_10.0.0-2_all.deb \
+  && sudo dpkg -i mingw-w64-x86-64-dev_10.0.0-2_all.deb;
+
+mkdir ~/.conan ; cd ~/.conan
+curl -L "https://github.com/vcmi/vcmi-deps-windows-conan/releases/download/1.0/vcmi-deps-windows-conan-w64.tgz" \
+	| tar -xzf -

+ 7 - 7
CI/msvc/before_install.sh

@@ -1,10 +1,10 @@
-curl -LfsS -o "vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v140.7z" \
-	"https://github.com/vcmi/vcmi-deps-windows/releases/download/v1.5/vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v140.7z"
-7z x "vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v140.7z"
+curl -LfsS -o "vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z" \
+	"https://github.com/vcmi/vcmi-deps-windows/releases/download/v1.6/vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z"
+7z x "vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z"
 
-rm -r -f vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug
-mkdir -p vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug/bin
-cp vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/bin/* vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug/bin
+#rm -r -f vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug
+#mkdir -p vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug/bin
+#cp vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/bin/* vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug/bin
 
 DUMPBIN_DIR=$(vswhere -latest -find **/dumpbin.exe | head -n 1)
-dirname "$DUMPBIN_DIR" > $GITHUB_PATH
+dirname "$DUMPBIN_DIR" > $GITHUB_PATH

+ 0 - 44
CI/mxe/before_install.sh

@@ -1,44 +0,0 @@
-#!/bin/sh
-
-# Install nsis for installer creation
-sudo add-apt-repository 'deb http://security.ubuntu.com/ubuntu bionic-security main'
-sudo apt-get install -qq nsis ninja-build libssl1.0.0
-
-# MXE repository was too slow for Travis far too often
-wget -nv https://github.com/vcmi/vcmi-deps-mxe/releases/download/2021-02-20/mxe-i686-w64-mingw32.shared-2021-01-22.tar
-tar -xvf mxe-i686-w64-mingw32.shared-2021-01-22.tar
-sudo dpkg -i mxe-*.deb
-sudo apt-get install -f --yes
-
-if false; then
-	# Add MXE repository and key
-	echo "deb http://pkg.mxe.cc/repos/apt/debian wheezy main" \
-		| sudo tee /etc/apt/sources.list.d/mxeapt.list
-
-	sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys D43A795B73B16ABE9643FE1AFD8FFF16DB45C6AB
-
-	# Install needed packages
-	sudo apt-get update -qq
-
-	sudo apt-get install -q --yes \
-	mxe-$MXE_TARGET-gcc \
-	mxe-$MXE_TARGET-boost \
-	mxe-$MXE_TARGET-zlib \
-	mxe-$MXE_TARGET-sdl2 \
-	mxe-$MXE_TARGET-sdl2-gfx \
-	mxe-$MXE_TARGET-sdl2-image \
-	mxe-$MXE_TARGET-sdl2-mixer \
-	mxe-$MXE_TARGET-sdl2-ttf \
-	mxe-$MXE_TARGET-ffmpeg \
-	mxe-$MXE_TARGET-qt \
-	mxe-$MXE_TARGET-qtbase \
-	mxe-$MXE_TARGET-intel-tbb \
-	mxe-i686-w64-mingw32.static-luajit
-
-fi # Disable
-
-# alias for CMake
-
-CMAKE_LOCATION=$(which cmake)
-sudo mv $CMAKE_LOCATION $CMAKE_LOCATION.orig
-sudo ln -s /usr/lib/mxe/usr/bin/$MXE_TARGET-cmake $CMAKE_LOCATION

+ 231 - 75
CMakeLists.txt

@@ -1,7 +1,7 @@
 # Minimum required version greatly affect CMake behavior
 # So cmake_minimum_required must be called before the project()
-# 3.10.0 is used since it's minimal in MXE dependencies for now
-cmake_minimum_required(VERSION 3.10.0)
+# 3.16.0 is used since it's used by our currently oldest suppored system: Ubuntu-20.04
+cmake_minimum_required(VERSION 3.16.0)
 
 project(VCMI)
 # TODO
@@ -10,9 +10,6 @@ project(VCMI)
 # Cmake put them after all install code of main CMakelists in cmake_install.cmake
 # Currently I just added extra add_subdirectory and CMakeLists.txt in osx directory to bypass that.
 #
-# MXE:
-# - Try to implement MXE support into BundleUtilities so we can deploy deps automatically
-#
 # Vckpg:
 # - Improve install code once there is better way to deploy DLLs and Qt plugins
 #
@@ -33,10 +30,6 @@ if(APPLE)
 	endif()
 endif()
 
-if(APPLE_IOS)
-	set(BUILD_SINGLE_APP 1)
-endif()
-
 ############################################
 #        User-provided options             #
 ############################################
@@ -48,25 +41,41 @@ if(NOT CMAKE_BUILD_TYPE)
 	set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS Debug Release RelWithDebInfo)
 endif()
 
+set(singleProcess OFF)
+set(staticAI OFF)
+if(ANDROID)
+	set(staticAI ON)
+	set(singleProcess ON)
+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(NOT ANDROID)
+	option(ENABLE_LAUNCHER "Enable compilation of launcher" ON)
+	option(ENABLE_EDITOR "Enable compilation of map editor" ON)
+endif()
+option(ENABLE_TRANSLATIONS "Enable generation of translations for launcher and editor" ON)
+option(ENABLE_NULLKILLER_AI "Enable compilation of Nullkiller AI library" 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")
+	set(ENABLE_SINGLE_APP_BUILD ON)
 else()
 	option(ENABLE_TEST "Enable compilation of unit tests" OFF)
+	option(ENABLE_SINGLE_APP_BUILD "Builds client and server as single executable" ${singleProcess})
 endif()
-if(NOT ${CMAKE_VERSION} VERSION_LESS "3.16.0")
-	option(ENABLE_PCH "Enable compilation using precompiled headers" ON)
-endif(NOT ${CMAKE_VERSION} VERSION_LESS "3.16.0")
+
+option(ENABLE_PCH "Enable compilation using precompiled headers" ON)
 option(ENABLE_GITVERSION "Enable Version.cpp with Git commit hash" ON)
 option(ENABLE_DEBUG_CONSOLE "Enable debug console for Windows builds" ON)
+option(ENABLE_STRICT_COMPILATION "Treat all compiler warnings as errors" OFF)
 option(ENABLE_MULTI_PROCESS_BUILDS "Enable /MP flag for MSVS solution" ON)
+option(COPY_CONFIG_ON_BUILD "Copies config folder into output directory at building phase" ON)
+option(ENABLE_STATIC_AI_LIBS "Add AI code into VCMI lib directly" ${staticAI})
 
 # Used for Snap packages and also useful for debugging
-if(NOT APPLE_IOS)
+if(NOT APPLE_IOS AND NOT ANDROID)
 	option(ENABLE_MONOLITHIC_INSTALL "Install everything in single directory on Linux and Mac" OFF)
 endif()
 
@@ -79,6 +88,11 @@ if(ENABLE_ERM AND NOT ENABLE_LUA)
 	set(ENABLE_LUA ON)
 endif()
 
+# We don't want to deploy assets into build directory for android/iOS build
+if((APPLE_IOS OR ANDROID) AND COPY_CONFIG_ON_BUILD)
+	set(COPY_CONFIG_ON_BUILD OFF)
+endif()
+
 ############################################
 #        Miscellaneous options             #
 ############################################
@@ -88,6 +102,10 @@ set(CMAKE_MODULE_PATH ${CMAKE_HOME_DIRECTORY}/cmake_modules ${PROJECT_SOURCE_DIR
 
 include(VCMIUtils)
 include(VersionDefinition)
+if(ANDROID)
+	set(VCMI_VERSION "${APP_SHORT_VERSION}")
+	configure_file("android/GeneratedVersion.java.in" "${CMAKE_SOURCE_DIR}/android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/GeneratedVersion.java" @ONLY)
+endif()
 
 vcmi_print_important_variables()
 
@@ -118,14 +136,18 @@ else()
 endif(ENABLE_GITVERSION)
 
 # Precompiled header configuration
-if(ENABLE_PCH AND NOT ${CMAKE_VERSION} VERSION_LESS "3.16.0")
+if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 6.0 )
+	set(ENABLE_PCH OFF) # broken
+endif()
+
+if(ENABLE_PCH)
 	macro(enable_pch name)
 		target_precompile_headers(${name} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:<StdInc.h$<ANGLE-R>>)
 	endmacro(enable_pch)
-else(ENABLE_PCH AND NOT ${CMAKE_VERSION} VERSION_LESS "3.16.0")
+else()
 	macro(enable_pch ignore)
 	endmacro(enable_pch)
-endif(ENABLE_PCH AND NOT ${CMAKE_VERSION} VERSION_LESS "3.16.0")
+endif()
 
 ############################################
 #        Documentation section             #
@@ -144,12 +166,12 @@ set(CMAKE_CXX_VISIBILITY_PRESET hidden)
 set(CMAKE_VISIBILITY_INLINES_HIDDEN 1)
 
 #Global fallback mapping
-# RelWithDebInfo falls back to Release, then MinSizeRel
-set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO RelWithDebInfo Release MinSizeRel "")
-# MinSizeRel falls back to Release, then RelWithDebInfo
-set(CMAKE_MAP_IMPORTED_CONFIG_MINSIZEREL MinSizeRel Release RelWithDebInfo "")
-# Release falls back to RelWithDebInfo, then MinSizeRel
-set(CMAKE_MAP_IMPORTED_CONFIG_RELEASE Release RelWithDebInfo MinSizeRel "")
+# RelWithDebInfo falls back to Release, then MinSizeRel, and then to None (tbb in 22.04 requires it)
+set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO RelWithDebInfo Release MinSizeRel None "")
+# MinSizeRel falls back to Release, then RelWithDebInfo, and then to None (tbb in 22.04 requires it)
+set(CMAKE_MAP_IMPORTED_CONFIG_MINSIZEREL MinSizeRel Release RelWithDebInfo None "")
+# Release falls back to RelWithDebInfo, then MinSizeRel, and then to None (tbb in 22.04 requires it)
+set(CMAKE_MAP_IMPORTED_CONFIG_RELEASE Release RelWithDebInfo MinSizeRel None "")
 
 set(CMAKE_XCODE_ATTRIBUTE_APP_DISPLAY_NAME ${APP_DISPLAY_NAME})
 set(CMAKE_XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC YES)
@@ -162,8 +184,28 @@ set(CMAKE_XCODE_ATTRIBUTE_MARKETING_VERSION ${APP_SHORT_VERSION})
 set(CMAKE_XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH NO)
 set(CMAKE_XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH[variant=Debug] YES)
 
-if(BUILD_SINGLE_APP)
-	add_compile_definitions(SINGLE_PROCESS_APP=1)
+#Check for endian
+if(${CMAKE_VERSION} VERSION_LESS "3.20.0") 
+	include(TestBigEndian)
+	test_big_endian(VCMI_ENDIAN_BIG)
+	if(VCMI_ENDIAN_BIG)
+		add_definitions(-DVCMI_ENDIAN_BIG)
+	endif()
+elseif(${CMAKE_CXX_BYTE_ORDER} EQUAL "BIG_ENDIAN")
+	add_definitions(-DVCMI_ENDIAN_BIG)
+endif()
+
+
+if(ENABLE_LAUNCHER)
+	add_definitions(-DENABLE_LAUNCHER)
+endif()
+
+if(ENABLE_EDITOR)
+	add_definitions(-DENABLE_EDITOR)
+endif()
+
+if(ENABLE_SINGLE_APP_BUILD)
+	add_definitions(-DSINGLE_PROCESS_APP=1)
 endif()
 
 if(APPLE_IOS)
@@ -203,10 +245,18 @@ if(MINGW OR MSVC)
 		# Suppress warnings
 		add_definitions(-D_CRT_SECURE_NO_WARNINGS)
 		add_definitions(-D_SCL_SECURE_NO_WARNINGS)
-		# 4250: 'class1' : inherits 'class2::member' via dominance
-		# 4251: class 'xxx' needs to have dll-interface to be used by clients of class 'yyy'
-		# 4275: non dll-interface class 'xxx' used as base for dll-interface class
-		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj /wd4250 /wd4251 /wd4275")
+
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj")
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4250") # 4250: 'class1' : inherits 'class2::member' via dominance
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4251") # 4251: class 'xxx' needs to have dll-interface to be used by clients of class 'yyy'
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4244") # 4244: conversion from 'xxx' to 'yyy', possible loss of data
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4267") # 4267: conversion from 'xxx' to 'yyy', possible loss of data
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4275") # 4275: non dll-interface class 'xxx' used as base for dll-interface class
+		#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4800") # 4800: implicit conversion from 'xxx' to bool. Possible information loss
+
+		if(ENABLE_STRICT_COMPILATION)
+			set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /WX") # Treats all compiler warnings as errors
+		endif()
 
 		if(ENABLE_MULTI_PROCESS_BUILDS)
 			set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP")
@@ -241,15 +291,39 @@ if(MINGW OR MSVC)
 	endif(MINGW)
 endif(MINGW OR MSVC)
 
-if(CMAKE_COMPILER_IS_GNUCXX OR NOT WIN32) #so far all *nix compilers support such parameters
-	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpointer-arith -Wuninitialized")
-	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-strict-aliasing -Wno-switch -Wno-sign-compare -Wno-unused-local-typedefs")
-	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-parameter -Wno-overloaded-virtual -Wno-type-limits -Wno-unknown-pragmas")
-	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-reorder")
-	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-varargs") # fuzzylite - Operation.h
+if(ANDROID)
+	if(ANDROID_NDK_MAJOR LESS 23 AND ANDROID_ABI MATCHES "^armeabi")
+		# libunwind must come before other shared libs:
+		# https://android.googlesource.com/platform/ndk/+/master/docs/BuildSystemMaintainers.md#Unwinding
+		list(APPEND SYSTEM_LIBS unwind)
+	endif()
+	list(APPEND SYSTEM_LIBS log)
+endif()
+
+if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR NOT WIN32)
+	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")
+	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wpointer-arith")
+	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wuninitialized")
+
+	if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 10.0 OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang" )
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wmismatched-tags")
+	endif()
+
+	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-parameter")   # low chance of valid reports, a lot of emitted warnings
+	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-switch")             # large number of false-positives, disabled
+	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-reorder")            # large number of noise, low chance of any significant issues
+	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-sign-compare")       # low chance of any significant issues
+	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-varargs")            # emitted in fuzzylite headers, disabled
 
-	if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
-		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-mismatched-tags -Wno-unknown-warning-option -Wno-missing-braces")
+	if(ENABLE_STRICT_COMPILATION)
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror")
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-error=array-bounds") # false positives in boost::multiarray during release build, keep as warning-only
+	endif()
+
+	# Fix string inspection with lldb
+	# https://stackoverflow.com/questions/58578615/cannot-inspect-a-stdstring-variable-in-lldb
+	if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
+		set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fstandalone-debug")
 	endif()
 
 	if(UNIX)
@@ -270,7 +344,28 @@ if(NOT WIN32 AND NOT APPLE_IOS)
 endif()
 
 if(ENABLE_LUA)
-	add_compile_definitions(SCRIPTING_ENABLED=1)
+	add_definitions(-DSCRIPTING_ENABLED=1)
+endif()
+
+if(USING_CONAN AND (MINGW AND CMAKE_HOST_UNIX))
+	# Hack for workaround https://github.com/conan-io/conan-center-index/issues/15405
+	# Remove once it will be fixed
+	execute_process(COMMAND
+		bash -c "grep -rl Mf ${CONAN_INSTALL_FOLDER} | xargs sed -i 's/Mf/mf/g'"
+	)
+	# Hack for workaround ffmpeg broken linking (conan ffmpeg forgots to link to ws2_32)
+	# Remove once it will be fixed
+	execute_process(COMMAND
+		bash -c "grep -rl secur32 ${CONAN_INSTALL_FOLDER} | xargs sed -i 's/secur32)/secur32 ws2_32)/g'"
+	)
+	execute_process(COMMAND
+		bash -c "grep -rl secur32 ${CONAN_INSTALL_FOLDER} | xargs sed -i 's/secur32 mfplat/secur32 ws2_32 mfplat/g'"
+	)
+	# Fixup tbb for cross-compiling on Conan
+	# Remove once it will be fixed
+	execute_process(COMMAND
+		bash -c "grep -rl tbb12 ${CONAN_INSTALL_FOLDER} | xargs sed -i 's/tbb tbb12/tbb12/g'"
+	)
 endif()
 
 ############################################
@@ -312,24 +407,25 @@ find_package(SDL2_ttf REQUIRED)
 if(TARGET SDL2_ttf::SDL2_ttf)
 	add_library(SDL2::TTF ALIAS SDL2_ttf::SDL2_ttf)
 endif()
-find_package(TBB REQUIRED)
 
 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)
+
+	if(ENABLE_TRANSLATIONS)
+		find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS LinguistTools)
+		add_definitions(-DENABLE_QT_TRANSLATIONS)
+	endif()
+endif()
+
+if(ENABLE_NULLKILLER_AI)
+	find_package(TBB REQUIRED)
 endif()
 
 if(ENABLE_LUA)
 	find_package(luajit)
-	# MXE paths hardcoded for current dependencies pack - tried and could not make it work another way
-	if((MINGW) AND (${CMAKE_CROSSCOMPILING}) AND (DEFINED MSYS) AND (NOT TARGET luajit::luajit))
-		add_library(luajit::luajit STATIC IMPORTED)
-		set_target_properties(luajit::luajit PROPERTIES
-			INTERFACE_INCLUDE_DIRECTORIES "/usr/lib/mxe/usr/i686-w64-mingw32.static/include/luajit-2.0")
-		set_target_properties(luajit::luajit PROPERTIES
-			IMPORTED_LOCATION "/usr/lib/mxe/usr/i686-w64-mingw32.static/lib/libluajit-5.1.a")
-	endif()
+
 	if(TARGET luajit::luajit)
 		message(STATUS "Using LuaJIT provided by system")
 	else()
@@ -377,6 +473,10 @@ elseif(APPLE)
 			set(DATA_DIR ".")
 		endif()
 	endif()
+elseif(ANDROID)
+	include(GNUInstallDirs)
+	set(LIB_DIR "jniLibs/${ANDROID_ABI}")
+	set(DATA_DIR "assets")
 else()
 	# includes lib path which determines where to install shared libraries (either /lib or /lib64)
 	include(GNUInstallDirs)
@@ -385,6 +485,12 @@ else()
 		set(BIN_DIR "." CACHE STRING "Where to install binaries")
 		set(LIB_DIR "." CACHE STRING "Where to install main library")
 		set(DATA_DIR "." CACHE STRING "Where to install data files")
+
+		# following constants only used for platforms using XDG (Linux, BSD, etc)
+		add_definitions(-DM_DATA_DIR="${DATA_DIR}")
+		add_definitions(-DM_BIN_DIR="${BIN_DIR}")
+		add_definitions(-DM_LIB_DIR="${LIB_DIR}")
+
 		set(CMAKE_INSTALL_RPATH "$ORIGIN/")
 	else()
 		if(NOT BIN_DIR)
@@ -396,18 +502,18 @@ else()
 		if(NOT DATA_DIR)
 			set(DATA_DIR "${CMAKE_INSTALL_DATAROOTDIR}/vcmi" CACHE STRING "Where to install data files")
 		endif()
-		set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${LIB_DIR}")
-	endif()
 
+		# following constants only used for platforms using XDG (Linux, BSD, etc)
+		add_definitions(-DM_DATA_DIR="${CMAKE_INSTALL_PREFIX}/${DATA_DIR}")
+		add_definitions(-DM_BIN_DIR="${CMAKE_INSTALL_PREFIX}/${BIN_DIR}")
+		add_definitions(-DM_LIB_DIR="${CMAKE_INSTALL_PREFIX}/${LIB_DIR}")
 
-	# following constants only used for platforms using XDG (Linux, BSD, etc)
-	add_definitions(-DM_DATA_DIR="${CMAKE_INSTALL_PREFIX}/${DATA_DIR}")
-	add_definitions(-DM_BIN_DIR="${CMAKE_INSTALL_PREFIX}/${BIN_DIR}")
-	add_definitions(-DM_LIB_DIR="${CMAKE_INSTALL_PREFIX}/${LIB_DIR}")
+		set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${LIB_DIR}")
+	endif()
 endif()
 
-# iOS has flat libs directory structure
-if(APPLE_IOS)
+# iOS/Android have flat libs directory structure
+if(APPLE_IOS OR ANDROID)
 	set(AI_LIB_DIR "${LIB_DIR}")
 	set(SCRIPTING_LIB_DIR "${LIB_DIR}")
 else()
@@ -423,14 +529,13 @@ if(APPLE_IOS)
 	add_subdirectory(ios)
 endif()
 
+set(VCMI_LIB_TARGET vcmi)
+add_subdirectory_with_folder("AI" AI)
+
 include(VCMI_lib)
-if(BUILD_SINGLE_APP)
-	add_subdirectory(lib_client)
+add_subdirectory(lib)
+if(ENABLE_SINGLE_APP_BUILD)
 	add_subdirectory(lib_server)
-	set(VCMI_LIB_TARGET vcmi_lib_client)
-else()
-	add_subdirectory(lib)
-	set(VCMI_LIB_TARGET vcmi)
 endif()
 
 if(ENABLE_ERM)
@@ -452,7 +557,6 @@ if(ENABLE_EDITOR)
 endif()
 add_subdirectory(client)
 add_subdirectory(server)
-add_subdirectory_with_folder("AI" AI)
 if(ENABLE_TEST)
 	enable_testing()
 	add_subdirectory(test)
@@ -462,14 +566,48 @@ endif()
 #        Installation section         #
 #######################################
 
-install(DIRECTORY config DESTINATION ${DATA_DIR})
-install(DIRECTORY Mods DESTINATION ${DATA_DIR})
+if(ANDROID)
+	if(ANDROID_STL MATCHES "_shared$")
+		set(stlLibName "${CMAKE_SHARED_LIBRARY_PREFIX}${ANDROID_STL}${CMAKE_SHARED_LIBRARY_SUFFIX}")
+		install(FILES "${CMAKE_SYSROOT}/usr/lib/${ANDROID_SYSROOT_LIB_SUBDIR}/${stlLibName}"
+			DESTINATION ${LIB_DIR}
+		)
+	endif()
+
+	install(FILES AUTHORS
+		DESTINATION res/raw
+		RENAME authors.txt
+	)
+
+	# zip internal assets - 'config' and 'Mods' dirs, save md5 of the zip
+	install(CODE "
+		cmake_path(ABSOLUTE_PATH CMAKE_INSTALL_PREFIX
+			OUTPUT_VARIABLE absolute_install_prefix
+		)
+		set(absolute_data_dir \"\${absolute_install_prefix}/${DATA_DIR}\")
+		file(MAKE_DIRECTORY \"\${absolute_data_dir}\")
+
+		set(internal_data_zip \"\${absolute_data_dir}/internalData.zip\")
+		execute_process(COMMAND
+			\"${CMAKE_COMMAND}\" -E tar c \"\${internal_data_zip}\" --format=zip -- config Mods
+			WORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}\"
+		)
+
+		file(MD5 \"\${internal_data_zip}\" internal_data_zip_md5)
+		file(WRITE \"\${absolute_data_dir}/internalDataHash.txt\"
+			\${internal_data_zip_md5}
+		)
+	")
+else()
+	install(DIRECTORY config DESTINATION ${DATA_DIR})
+	install(DIRECTORY Mods DESTINATION ${DATA_DIR})
+endif()
 if(ENABLE_LUA)
 	install(DIRECTORY scripts DESTINATION ${DATA_DIR})
 endif()
 
-# that script is useless for Windows and iOS
-if(NOT WIN32 AND NOT APPLE_IOS)
+# that script is useless for Windows / iOS / Android
+if(NOT WIN32 AND NOT APPLE_IOS AND NOT ANDROID)
 	install(FILES vcmibuilder DESTINATION ${BIN_DIR} PERMISSIONS
 		OWNER_WRITE OWNER_READ OWNER_EXECUTE
 					GROUP_READ GROUP_EXECUTE
@@ -478,13 +616,21 @@ endif()
 
 
 if(WIN32)
-	file(GLOB dep_files
-		${dep_files}
-		"${CMAKE_FIND_ROOT_PATH}/bin/*.dll")
+	if(USING_CONAN)
+		#Conan imports enabled
+		vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}")
+		file(GLOB dep_files
+				${dep_files}
+				"${CMAKE_SYSROOT}/bin/*.dll" 
+				"${CMAKE_SYSROOT}/lib/*.dll" 
+				"${CONAN_SYSTEM_LIBRARY_LOCATION}/*.dll")
+	else()
+		file(GLOB dep_files
+				${dep_files}
+				"${CMAKE_FIND_ROOT_PATH}/bin/*.dll")
+	endif()
 
-	if((${CMAKE_CROSSCOMPILING}) AND (DEFINED MSYS))
-		message(STATUS "Detected MXE build")
-	elseif(CMAKE_BUILD_TYPE MATCHES Debug)
+	if(CMAKE_BUILD_TYPE MATCHES Debug)
 		# Copy debug versions of libraries if build type is debug
 		set(debug_postfix d)
 	endif()
@@ -551,7 +697,11 @@ if(WIN32)
 	else()
 		set(CPACK_NSIS_PACKAGE_NAME "VCMI ${CPACK_PACKAGE_VERSION} ${PACKAGE_NAME_SUFFIX} ")
 	endif()
-	set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES")
+	if(CMAKE_SYSTEM_PROCESSOR MATCHES ".*64")
+		set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES64")
+	else()
+		set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES")
+	endif()
 	if(ENABLE_LAUNCHER)
 		set(CPACK_PACKAGE_EXECUTABLES "VCMI_launcher;VCMI")
 		set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS " CreateShortCut \\\"$DESKTOP\\\\VCMI.lnk\\\" \\\"$INSTDIR\\\\VCMI_launcher.exe\\\"")
@@ -561,6 +711,12 @@ if(WIN32)
 	endif()
 	set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS " Delete \\\"$DESKTOP\\\\VCMI.lnk\\\" ")
 
+	# Strip MinGW CPack target if build configuration without debug info
+	if(MINGW)
+		if(NOT (CMAKE_BUILD_TYPE STREQUAL "Debug") OR (CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo"))
+			set(CPACK_STRIP_FILES ON)
+		endif()
+	endif()
 	# set the install/unistall icon used for the installer itself
 	# There is a bug in NSI that does not handle full unix paths properly.
 	set(CPACK_NSIS_MUI_ICON "${CMAKE_CURRENT_SOURCE_DIR}/client\\\\vcmi.ico")
@@ -577,7 +733,7 @@ if(WIN32)
 	set(CPACK_NSIS_URL_INFO_ABOUT "http://vcmi.eu/")
 	set(CPACK_NSIS_CONTACT @CPACK_PACKAGE_CONTACT@)
 	set(CPACK_NSIS_EXECUTABLES_DIRECTORY ".")
-	# Use BundleUtilities to fix build when Vcpkg is used and disable it for MXE
+	# Use BundleUtilities to fix build when Vcpkg is used and disable it for mingw
 	if(NOT (${CMAKE_CROSSCOMPILING}))
 		add_subdirectory(win)
 	endif()

+ 42 - 0
CMakePresets.json

@@ -24,6 +24,7 @@
                 "PACKAGE_NAME_SUFFIX" : "$env{VCMI_PACKAGE_NAME_SUFFIX}",
                 "CMAKE_BUILD_TYPE": "RelWithDebInfo",
                 "ENABLE_TEST": "OFF",
+                "ENABLE_STRICT_COMPILATION": "ON",
                 "ENABLE_GITVERSION": "$env{VCMI_PACKAGE_GITVERSION}"
             }
         },
@@ -80,6 +81,19 @@
                 "FORCE_BUNDLED_MINIZIP": "ON"
             }
         },
+        {
+            "name": "windows-mingw-conan-linux",
+            "displayName": "Ninja+Conan release",
+            "description": "VCMI Windows Ninja using Conan on Linux",
+            "inherits": [
+                "build-with-conan",
+                "default-release"
+            ],
+            "cacheVariables": {
+                "CMAKE_BUILD_TYPE": "Release",
+                "FORCE_BUNDLED_FL": "ON"
+            }
+        },
         {
             "name": "macos-ninja-release",
             "displayName": "Ninja release",
@@ -172,6 +186,18 @@
             "cacheVariables": {
                 "CMAKE_PREFIX_PATH": "${sourceDir}/build/iphoneos"
             }
+        },
+        {
+            "name": "android-conan-ninja-release",
+            "displayName": "Android release",
+            "description": "VCMI Android Ninja using Conan",
+            "inherits": [
+                "build-with-conan",
+                "default-release"
+            ],
+            "cacheVariables": {
+                "CMAKE_BUILD_TYPE": "Release"
+            }
         }
     ],
     "buildPresets": [
@@ -221,6 +247,12 @@
             "configurePreset": "windows-msvc-release",
             "inherits": "default-release"
         },
+        {
+            "name": "windows-mingw-conan-linux",
+            "configurePreset": "windows-mingw-conan-linux",
+            "inherits": "default-release",
+            "configuration": "Release"
+        },
         {
             "name": "ios-release-conan",
             "configurePreset": "ios-release-conan",
@@ -235,6 +267,11 @@
             "name": "ios-release-legacy",
             "configurePreset": "ios-release-legacy",
             "inherits": "ios-release-conan"
+        },
+        {
+            "name": "android-conan-ninja-release",
+            "configurePreset": "android-conan-ninja-release",
+            "inherits": "default-release"
         }
     ],
     "testPresets": [
@@ -270,6 +307,11 @@
             "name": "windows-msvc-release",
             "configurePreset": "windows-msvc-release",
             "inherits": "default-release"
+        },
+        {
+            "name": "windows-mingw-conan-linux",
+            "configurePreset": "windows-mingw-conan-linux",
+            "inherits": "default-release"
         }
     ]
 }

File diff suppressed because it is too large
+ 501 - 265
ChangeLog.md


+ 64 - 76
Global.h

@@ -15,25 +15,6 @@
 // Fixed width bool data type is important for serialization
 static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 
-#ifdef __GNUC__
-#  define GCC_VERSION (__GNUC__ * 100 + __GNUC_MINOR__ * 10 + __GNUC_PATCHLEVEL__)
-#endif
-
-#if !defined(__clang__) && defined(__GNUC__) && (GCC_VERSION < 470)
-#  error VCMI requires at least gcc-4.7.2 for successful compilation or clang-3.1. Please update your compiler
-#endif
-
-#if defined(__GNUC__) && (GCC_VERSION == 470 || GCC_VERSION == 471)
-#  error This GCC version has buggy std::array::at version and should not be used. Please update to 4.7.2 or later
-#endif
-
-/* ---------------------------------------------------------------------------- */
-/* Suppress some compiler warnings */
-/* ---------------------------------------------------------------------------- */
-#ifdef _MSC_VER
-#  pragma warning (disable : 4800 ) /* disable conversion to bool warning -- I think it's intended in all places */
-#endif
-
 /* ---------------------------------------------------------------------------- */
 /* System detection. */
 /* ---------------------------------------------------------------------------- */
@@ -53,7 +34,7 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 #elif defined(__linux__) || defined(__gnu_linux__) || defined(linux) || defined(__linux)
 #  define VCMI_UNIX
 #  define VCMI_XDG
-#  ifdef __ANDROID__
+#  if defined(__ANDROID__) || defined(ANDROID)
 #    define VCMI_ANDROID
 #  endif
 #elif defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
@@ -79,10 +60,15 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 //#  warning "Unknown Apple target."?
 #  endif
 #else
-#  error "VCMI supports only Windows, OSX, Linux and Android targets"
+#  error "This platform isn't supported"
+#endif
+
+#if defined(VCMI_ANDROID) || defined(VCMI_IOS)
+#define VCMI_MOBILE
 #endif
 
 // Each compiler uses own way to supress fall through warning. Try to find it.
+// TODO: replace with c++17 [[fallthrough]]
 #ifdef __has_cpp_attribute
 #  if __has_cpp_attribute(fallthrough)
 #    define FALLTHROUGH [[fallthrough]];
@@ -101,9 +87,15 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 /* Commonly used C++, Boost headers */
 /* ---------------------------------------------------------------------------- */
 #ifdef VCMI_WINDOWS
-#  define WIN32_LEAN_AND_MEAN		// Exclude rarely-used stuff from Windows headers - delete this line if something is missing.
-#  define NOMINMAX					// Exclude min/max macros from <Windows.h>. Use std::[min/max] from <algorithm> instead.
-#  define _NO_W32_PSEUDO_MODIFIERS  // Exclude more macros for compiling with MinGW on Linux.
+#  ifndef WIN32_LEAN_AND_MEAN
+#    define WIN32_LEAN_AND_MEAN		 // Exclude rarely-used stuff from Windows headers - delete this line if something is missing.
+#  endif
+#  ifndef NOMINMAX
+#    define NOMINMAX				 // Exclude min/max macros from <Windows.h>. Use std::[min/max] from <algorithm> instead.
+#  endif
+#  ifndef _NO_W32_PSEUDO_MODIFIERS
+#    define _NO_W32_PSEUDO_MODIFIERS // Exclude more macros for compiling with MinGW on Linux.
+#  endif
 #endif
 
 #ifdef VCMI_ANDROID
@@ -122,38 +114,34 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 #  define STRONG_INLINE inline
 #endif
 
-#define TO_STRING_HELPER(x) #x
-#define TO_STRING(x) TO_STRING_HELPER(x)
-#define LINE_IN_FILE __FILE__ ":" TO_STRING(__LINE__)
-
 #define _USE_MATH_DEFINES
 
-#include <cstdio>
-#include <stdio.h>
-
 #include <algorithm>
 #include <array>
+#include <atomic>
+#include <bitset>
 #include <cassert>
 #include <climits>
 #include <cmath>
 #include <cstdlib>
-#include <functional>
+#include <cstdio>
 #include <fstream>
+#include <functional>
 #include <iomanip>
 #include <iostream>
 #include <map>
 #include <memory>
+#include <mutex>
 #include <numeric>
 #include <queue>
 #include <random>
 #include <set>
 #include <sstream>
 #include <string>
-#include <unordered_set>
 #include <unordered_map>
+#include <unordered_set>
 #include <utility>
 #include <vector>
-#include <atomic>
 
 //The only available version is 3, as of Boost 1.50
 #include <boost/version.hpp>
@@ -233,7 +221,10 @@ typedef boost::lock_guard<boost::recursive_mutex> TLockGuardRec;
 /* ---------------------------------------------------------------------------- */
 // Import + Export macro declarations
 #ifdef VCMI_WINDOWS
-#  ifdef __GNUC__
+#ifdef VCMI_DLL_STATIC
+#    define DLL_IMPORT
+#    define DLL_EXPORT
+#elif defined(__GNUC__)
 #    define DLL_IMPORT __attribute__((dllimport))
 #    define DLL_EXPORT __attribute__((dllexport))
 #  else
@@ -262,7 +253,8 @@ template<typename T, size_t N> char (&_ArrayCountObj(const T (&)[N]))[N];
 #define ARRAY_COUNT(arr)    (sizeof(_ArrayCountObj(arr)))
 
 // should be used for variables that becomes unused in release builds (e.g. only used for assert checks)
-#define UNUSED(VAR) ((void)VAR)
+// TODO: replace with c++17 [[maybe_unused]]
+#define MAYBE_UNUSED(VAR) ((void)VAR)
 
 // old iOS SDKs compatibility
 #ifdef VCMI_IOS
@@ -449,6 +441,20 @@ namespace vstd
 		}
 	}
 
+	// c++17: makes a to fit the range <b, c>
+	template <typename t1, typename t2, typename t3>
+	t1 clamp(const t1 &value, const t2 &low, const t3 &high)
+	{
+		if ( value > high)
+			return high;
+
+		if ( value < low)
+			return low;
+
+		return value;
+	}
+
+
 	//makes a to fit the range <b, c>
 	template <typename t1, typename t2, typename t3>
 	t1 &abetween(t1 &a, const t2 &b, const t3 &c)
@@ -503,36 +509,6 @@ namespace vstd
 		ptr = nullptr;
 	}
 
-#if _MSC_VER >= 1800
-	using std::make_unique;
-#else
-	template<typename T>
-	std::unique_ptr<T> make_unique()
-	{
-		return std::unique_ptr<T>(new T());
-	}
-	template<typename T, typename Arg1>
-	std::unique_ptr<T> make_unique(Arg1 &&arg1)
-	{
-		return std::unique_ptr<T>(new T(std::forward<Arg1>(arg1)));
-	}
-	template<typename T, typename Arg1, typename Arg2>
-	std::unique_ptr<T> make_unique(Arg1 &&arg1, Arg2 &&arg2)
-	{
-		return std::unique_ptr<T>(new T(std::forward<Arg1>(arg1), std::forward<Arg2>(arg2)));
-	}
-	template<typename T, typename Arg1, typename Arg2, typename Arg3>
-	std::unique_ptr<T> make_unique(Arg1 &&arg1, Arg2 &&arg2, Arg3 &&arg3)
-	{
-		return std::unique_ptr<T>(new T(std::forward<Arg1>(arg1), std::forward<Arg2>(arg2), std::forward<Arg3>(arg3)));
-	}
-	template<typename T, typename Arg1, typename Arg2, typename Arg3, typename Arg4>
-	std::unique_ptr<T> make_unique(Arg1 &&arg1, Arg2 &&arg2, Arg3 &&arg3, Arg4 &&arg4)
-	{
-		return std::unique_ptr<T>(new T(std::forward<Arg1>(arg1), std::forward<Arg2>(arg2), std::forward<Arg3>(arg3), std::forward<Arg4>(arg4)));
-	}
-#endif
-
 	template <typename Container>
 	typename Container::const_reference circularAt(const Container &r, size_t index)
 	{
@@ -543,6 +519,12 @@ namespace vstd
 		return *itr;
 	}
 
+	template <typename Container, typename Item>
+	void erase(Container &c, const Item &item)
+	{
+		c.erase(boost::remove(c, item), c.end());
+	}
+
 	template<typename Range, typename Predicate>
 	void erase_if(Range &vec, Predicate pred)
 	{
@@ -704,12 +686,6 @@ namespace vstd
 		return false;
 	}
 
-	template <typename Container, typename Pred>
-	void erase(Container &c, Pred pred)
-	{
-		c.erase(boost::remove_if(c, pred), c.end());
-	}
-
 	template<typename T>
 	void removeDuplicates(std::vector<T> &vec)
 	{
@@ -760,10 +736,24 @@ namespace vstd
 		return v;
 	}
 
-	using boost::math::round;
+	//c++20 feature
+	template<typename Arithmetic, typename Floating>
+	Arithmetic lerp(const Arithmetic & a, const Arithmetic & b, const Floating & f)
+	{
+		return a + (b - a) * f;
+	}
+
+
+	///compile-time version of std::abs for ints for int3, in clang++15 std::abs is constexpr
+	static constexpr int abs(int i) {
+		if(i < 0) return -i;
+		return i;
+	}
 }
 using vstd::operator-=;
-using vstd::make_unique;
+
+VCMI_LIB_NAMESPACE_END
+
 
 #ifdef NO_STD_TOSTRING
 namespace std
@@ -777,5 +767,3 @@ namespace std
 	}
 }
 #endif // NO_STD_TOSTRING
-
-VCMI_LIB_NAMESPACE_END

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