瀏覽代碼

Merge branch 'develop' into timed_events_objects_removal

Dydzio 1 年之前
父節點
當前提交
e9be46af98
共有 100 個文件被更改,包括 1860 次插入761 次删除
  1. 5 3
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 91 42
      .github/workflows/github.yml
  3. 1 1
      AI/BattleAI/AttackPossibility.cpp
  4. 27 8
      AI/BattleAI/BattleEvaluator.cpp
  5. 1 1
      AI/BattleAI/BattleExchangeVariant.cpp
  6. 15 15
      AI/BattleAI/StackWithBonuses.cpp
  7. 9 9
      AI/BattleAI/StackWithBonuses.h
  8. 14 20
      AI/Nullkiller/AIGateway.cpp
  9. 10 11
      AI/Nullkiller/AIUtility.cpp
  10. 2 7
      AI/Nullkiller/AIUtility.h
  11. 12 19
      AI/Nullkiller/Analyzers/ArmyManager.cpp
  12. 36 17
      AI/Nullkiller/Analyzers/BuildAnalyzer.cpp
  13. 3 3
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp
  14. 2 0
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h
  15. 14 13
      AI/Nullkiller/Analyzers/HeroManager.cpp
  16. 2 2
      AI/Nullkiller/Analyzers/HeroManager.h
  17. 11 6
      AI/Nullkiller/Analyzers/ObjectClusterizer.cpp
  18. 35 12
      AI/Nullkiller/Behaviors/BuildingBehavior.cpp
  19. 9 8
      AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp
  20. 4 9
      AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp
  21. 27 20
      AI/Nullkiller/Behaviors/DefenceBehavior.cpp
  22. 13 29
      AI/Nullkiller/Behaviors/ExplorationBehavior.cpp
  23. 6 31
      AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp
  24. 64 25
      AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp
  25. 1 10
      AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp
  26. 1 2
      AI/Nullkiller/Engine/FuzzyEngines.cpp
  27. 12 3
      AI/Nullkiller/Engine/FuzzyHelper.cpp
  28. 174 26
      AI/Nullkiller/Engine/Nullkiller.cpp
  29. 2 1
      AI/Nullkiller/Engine/Nullkiller.h
  30. 406 52
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  31. 30 2
      AI/Nullkiller/Engine/PriorityEvaluator.h
  32. 29 44
      AI/Nullkiller/Engine/Settings.cpp
  33. 13 1
      AI/Nullkiller/Engine/Settings.h
  34. 1 0
      AI/Nullkiller/Goals/AbstractGoal.h
  35. 3 0
      AI/Nullkiller/Goals/AdventureSpellCast.cpp
  36. 2 2
      AI/Nullkiller/Goals/BuildThis.cpp
  37. 1 1
      AI/Nullkiller/Goals/CaptureObject.h
  38. 5 2
      AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp
  39. 3 1
      AI/Nullkiller/Goals/ExecuteHeroChain.cpp
  40. 1 0
      AI/Nullkiller/Goals/RecruitHero.cpp
  41. 1 0
      AI/Nullkiller/Goals/RecruitHero.h
  42. 2 6
      AI/Nullkiller/Goals/StayAtTown.cpp
  43. 1 1
      AI/Nullkiller/Helpers/ExplorationHelper.cpp
  44. 33 13
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  45. 5 5
      AI/Nullkiller/Pathfinding/AINodeStorage.h
  46. 1 1
      AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp
  47. 3 3
      AI/Nullkiller/Pathfinding/Actors.cpp
  48. 1 1
      AI/Nullkiller/Pathfinding/Actors.h
  49. 3 4
      AI/VCAI/AIUtility.cpp
  50. 0 2
      AI/VCAI/AIUtility.h
  51. 1 1
      AI/VCAI/ArmyManager.cpp
  52. 4 4
      AI/VCAI/BuildingManager.cpp
  53. 1 1
      AI/VCAI/Goals/BuildThis.cpp
  54. 1 1
      AI/VCAI/Goals/CompleteQuest.cpp
  55. 1 1
      AI/VCAI/Goals/FindObj.cpp
  56. 4 4
      AI/VCAI/Goals/GatherTroops.cpp
  57. 3 3
      AI/VCAI/MapObjectsEvaluator.cpp
  58. 2 2
      AI/VCAI/Pathfinding/AINodeStorage.cpp
  59. 0 2
      AI/VCAI/ResourceManager.cpp
  60. 11 14
      AI/VCAI/VCAI.cpp
  61. 1 0
      AUTHORS.h
  62. 73 40
      CCallback.cpp
  63. 11 1
      CCallback.h
  64. 0 4
      CI/android-32/before_install.sh
  65. 0 4
      CI/android-64/before_install.sh
  66. 0 7
      CI/android/before_install.sh
  67. 4 0
      CI/before_install/android.sh
  68. 2 3
      CI/before_install/linux_qt5.sh
  69. 3 1
      CI/before_install/linux_qt6.sh
  70. 0 2
      CI/before_install/macos.sh
  71. 7 0
      CI/before_install/mingw.sh
  72. 17 0
      CI/before_install/msvc.sh
  73. 1 1
      CI/conan/base/cross-macro.j2
  74. 280 0
      CI/example.markdownlint-cli2.jsonc
  75. 1 1
      CI/install_conan_dependencies.sh
  76. 7 0
      CI/install_vcpkg_dependencies.sh
  77. 0 5
      CI/ios/before_install.sh
  78. 0 1
      CI/linux-qt6/upload_package.sh
  79. 0 1
      CI/linux/upload_package.sh
  80. 0 4
      CI/mac-arm/before_install.sh
  81. 0 4
      CI/mac-intel/before_install.sh
  82. 0 14
      CI/mingw-32/before_install.sh
  83. 0 14
      CI/mingw/before_install.sh
  84. 0 10
      CI/msvc/before_install.sh
  85. 0 6
      CI/msvc/build_script.bat
  86. 0 5
      CI/msvc/coverity_build_script.bat
  87. 0 17
      CI/msvc/coverity_upload_script.ps
  88. 0 0
      CI/validate_json.py
  89. 18 20
      CMakeLists.txt
  90. 21 1
      CMakePresets.json
  91. 235 67
      ChangeLog.md
  92. 4 1
      Global.h
  93. 0 0
      Mods/vcmi/Content/Data/NotoSans-Medium.ttf
  94. 0 0
      Mods/vcmi/Content/Data/NotoSerif-Black.ttf
  95. 0 0
      Mods/vcmi/Content/Data/NotoSerif-Bold.ttf
  96. 0 0
      Mods/vcmi/Content/Data/NotoSerif-Medium.ttf
  97. 0 0
      Mods/vcmi/Content/Data/s/std.verm
  98. 0 0
      Mods/vcmi/Content/Data/s/testy.erm
  99. 0 0
      Mods/vcmi/Content/Sounds/we5.wav
  100. 0 0
      Mods/vcmi/Content/Sprites/PortraitsLarge.json

+ 5 - 3
.github/ISSUE_TEMPLATE/bug_report.md

@@ -15,6 +15,7 @@ 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 '....'
@@ -24,7 +25,7 @@ Steps to reproduce the behavior:
 A clear and concise description of what you expected to happen.
 
 **Actual behavior**
-A clear description what is currently happening 
+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.
@@ -33,8 +34,9 @@ If this something which worked well some time ago, please let us know about vers
 If applicable, add screenshots to help explain your problem.
 
 **Version**
- - OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS]
- - Version: [VCMI 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.

+ 91 - 42
.github/workflows/github.yml

@@ -3,7 +3,6 @@ name: VCMI
 on:
   push:
     branches:
-      - features/*
       - beta
       - master
       - develop
@@ -22,84 +21,112 @@ jobs:
           - platform: linux-qt6
             os: ubuntu-24.04
             test: 0
+            before_install: linux_qt6.sh
             preset: linux-clang-test
           - platform: linux
             os: ubuntu-24.04
             test: 1
+            before_install: linux_qt5.sh
             preset: linux-gcc-test
           - platform: linux
             os: ubuntu-20.04
             test: 0
+            before_install: linux_qt5.sh
             preset: linux-gcc-debug
           - platform: mac-intel
             os: macos-13
             test: 0
             pack: 1
+            upload: 1
             pack_type: Release
             extension: dmg
+            before_install: macos.sh
             preset: macos-conan-ninja-release
             conan_profile: macos-intel
+            conan_prebuilts: dependencies-mac-intel
             conan_options: --options with_apple_system_libs=True
             artifact_platform: intel
           - platform: mac-arm
             os: macos-13
             test: 0
             pack: 1
+            upload: 1
             pack_type: Release
             extension: dmg
+            before_install: macos.sh
             preset: macos-arm-conan-ninja-release
             conan_profile: macos-arm
+            conan_prebuilts: dependencies-mac-arm
             conan_options: --options with_apple_system_libs=True
             artifact_platform: arm
           - platform: ios
             os: macos-13
             test: 0
             pack: 1
+            upload: 1
             pack_type: Release
             extension: ipa
+            before_install: macos.sh
             preset: ios-release-conan-ccache
             conan_profile: ios-arm64
+            conan_prebuilts: dependencies-ios
             conan_options: --options with_apple_system_libs=True
-          - platform: msvc
+          - platform: msvc-x64
             os: windows-latest
             test: 0
             pack: 1
+            upload: 1
             pack_type: RelWithDebInfo
             extension: exe
+            before_install: msvc.sh
             preset: windows-msvc-release
-          - platform: mingw
-            os: ubuntu-22.04
+          - platform: msvc-x86
+            os: windows-latest
+            test: 0
+            pack: 1
+            pack_type: RelWithDebInfo
+            extension: exe
+            before_install: msvc.sh
+            preset: windows-msvc-release-x86
+          - platform: mingw_x86_64
+            os: ubuntu-24.04
             test: 0
             pack: 1
             pack_type: Release
             extension: exe
-            cpack_args: -D CPACK_NSIS_EXECUTABLE=`which makensis`
             cmake_args: -G Ninja
+            before_install: mingw.sh
             preset: windows-mingw-conan-linux
             conan_profile: mingw64-linux.jinja
-          - platform: mingw-32
-            os: ubuntu-22.04
+            conan_prebuilts: dependencies-mingw-x86-64
+          - platform: mingw_x86
+            os: ubuntu-24.04
             test: 0
             pack: 1
             pack_type: Release
             extension: exe
-            cpack_args: -D CPACK_NSIS_EXECUTABLE=`which makensis`
             cmake_args: -G Ninja
+            before_install: mingw.sh
             preset: windows-mingw-conan-linux
             conan_profile: mingw32-linux.jinja
+            conan_prebuilts: dependencies-mingw-x86
           - platform: android-32
-            os: macos-14
+            os: ubuntu-24.04
+            upload: 1
             extension: apk
             preset: android-conan-ninja-release
-            conan_profile: android-32
-            conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT
+            before_install: android.sh
+            conan_profile: android-32-ndk
+            conan_prebuilts: dependencies-android-armeabi-v7a
             artifact_platform: armeabi-v7a
           - platform: android-64
-            os: macos-14
+            os: ubuntu-24.04
+            upload: 1
             extension: apk
             preset: android-conan-ninja-release
-            conan_profile: android-64
-            conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT
+            before_install: android.sh
+            conan_profile: android-64-ndk
+            conan_prebuilts: dependencies-android-arm64-v8a
             artifact_platform: arm64-v8a
     runs-on: ${{ matrix.os }}
     defaults:
@@ -107,15 +134,25 @@ jobs:
         shell: bash
 
     steps:
-    - uses: actions/checkout@v4
+    - name: Checkout repository
+      uses: actions/checkout@v4
       with:
         submodules: recursive
 
-    - name: Dependencies
-      run: source '${{github.workspace}}/CI/${{matrix.platform}}/before_install.sh'
+    - name: Prepare CI
+      if: "${{ matrix.before_install != '' }}"
+      run: source '${{github.workspace}}/CI/before_install/${{matrix.before_install}}'
       env:
         VCMI_BUILD_PLATFORM: x64
 
+    - name: Install Conan Dependencies
+      if: "${{ matrix.conan_prebuilts != '' }}"
+      run: source '${{github.workspace}}/CI/install_conan_dependencies.sh' '${{matrix.conan_prebuilts}}'
+
+    - name: Install vcpkg Dependencies
+      if: ${{ startsWith(matrix.platform, 'msvc') }}
+      run: source '${{github.workspace}}/CI/install_vcpkg_dependencies.sh' '${{matrix.platform}}'
+
     # ensure the ccache for each PR is separate so they don't interfere with each other
     # fall back to ccache of the vcmi/vcmi repo if no PR-specific ccache is found
     - name: ccache for PRs
@@ -157,15 +194,13 @@ jobs:
         mkdir -p ~/.local/share/vcmi/
         mv h3_assets/* ~/.local/share/vcmi/
 
-    - uses: actions/setup-python@v5
+    - name: Install Conan
       if: "${{ matrix.conan_profile != '' }}"
-      with:
-        python-version: '3.10'
+      run: pipx install 'conan<2.0'
 
-    - name: Conan setup
+    - name: Install Conan profile
       if: "${{ matrix.conan_profile != '' }}"
       run: |
-        pip3 install 'conan<2.0'
         conan profile new default --detect
         conan install . \
           --install-folder=conan-generated \
@@ -177,7 +212,13 @@ jobs:
       env:
         GENERATE_ONLY_BUILT_CONFIG: 1
 
-    - uses: actions/setup-java@v4
+    # Workaround for gradle not discovering SDK that was installed via conan
+    - name: Find Android NDK
+      if: ${{ startsWith(matrix.platform, 'android') }}
+      run: sudo ln -s -T /home/runner/.conan/data/android-ndk/r25c/_/_/package/4db1be536558d833e52e862fd84d64d75c2b3656/bin /usr/local/lib/android/sdk/ndk/25.2.9519653
+
+    - name: Install Java
+      uses: actions/setup-java@v4
       if: ${{ startsWith(matrix.platform, 'android') }}
       with:
         distribution: 'temurin'
@@ -208,11 +249,11 @@ jobs:
         elif [[ (${{matrix.preset}} == android-conan-ninja-release) && (${{github.ref}} != 'refs/heads/master') ]]
         then
             cmake -DENABLE_CCACHE:BOOL=ON -DANDROID_GRADLE_PROPERTIES="applicationIdSuffix=.daily;signingConfig=dailySigning;applicationLabel=VCMI daily" --preset ${{ matrix.preset }}
-        elif [[ ${{matrix.platform}} != msvc ]]
+        elif [[ ${{startsWith(matrix.platform, 'msvc') }} ]]
         then
-            cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }}
-        else
             cmake --preset ${{ matrix.preset }}
+        else
+            cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }}
         fi
 
     - name: Build
@@ -242,10 +283,13 @@ jobs:
       if: ${{ matrix.pack == 1 }}
       run: |
         cd '${{github.workspace}}/out/build/${{matrix.preset}}'
-        CPACK_PATH=`which -a cpack | grep -m1 -v -i chocolatey`
-        counter=0; until "$CPACK_PATH" -C ${{matrix.pack_type}} ${{ matrix.cpack_args }} || ((counter > 20)); do sleep 3; ((counter++)); done
-        test -f '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' \
-          && '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' '${{github.workspace}}' "$(ls '${{ env.VCMI_PACKAGE_FILE_NAME }}'.*)"
+        
+        # Workaround for CPack bug on macOS 13
+        counter=0
+        until cpack -C ${{matrix.pack_type}} || ((counter > 20)); do
+            sleep 3
+            ((counter++))
+        done
         rm -rf _CPack_Packages
 
     - name: Artifacts
@@ -253,6 +297,7 @@ jobs:
       uses: actions/upload-artifact@v4
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
+        compression-level: 0
         path: |
           ${{github.workspace}}/out/build/${{matrix.preset}}/${{ env.VCMI_PACKAGE_FILE_NAME }}.${{ matrix.extension }}
 
@@ -268,32 +313,35 @@ jobs:
         echo "ANDROID_APK_PATH=$ANDROID_APK_PATH" >> $GITHUB_ENV
         echo "ANDROID_AAB_PATH=$ANDROID_AAB_PATH" >> $GITHUB_ENV
 
-    - name: Android apk artifacts
+    - name: Upload android apk artifacts
       if: ${{ startsWith(matrix.platform, 'android') }}
       uses: actions/upload-artifact@v4
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
+        compression-level: 0
         path: |
           ${{ env.ANDROID_APK_PATH }}
 
-    - name: Android aab artifacts
-      if: ${{ startsWith(matrix.platform, 'android') }}
+    - name: Upload Android aab artifacts
+      if: ${{ startsWith(matrix.platform, 'android') && github.ref == 'refs/heads/master' }}
       uses: actions/upload-artifact@v4
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - aab
+        compression-level: 0
         path: |
           ${{ env.ANDROID_AAB_PATH }}
 
-    - name: Symbols
-      if: ${{ matrix.platform == 'msvc' }}
+    - name: Upload debug symbols
+      if: ${{ startsWith(matrix.platform, 'msvc') }}
       uses: actions/upload-artifact@v4
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - symbols
+        compression-level: 9
         path: |
             ${{github.workspace}}/**/*.pdb
 
     - name: Upload build
-      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' && matrix.platform != 'mingw-32' }}
+      if: ${{ (matrix.upload == 1) && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/master') }}
       continue-on-error: true
       run: |
         if [ -z '${{ env.ANDROID_APK_PATH }}' ] ; then
@@ -343,11 +391,6 @@ jobs:
     steps:
         - uses: actions/checkout@v4
 
-        - uses: actions/setup-python@v5
-          if: "${{ matrix.conan_profile != '' }}"
-          with:
-            python-version: '3.10'
-
         - name: Ensure LF line endings
           run: |
             find . -path ./.git -prune -o -path ./AI/FuzzyLite -prune -o -path ./test/googletest \
@@ -358,4 +401,10 @@ jobs:
         - name: Validate JSON
           run: |
             sudo apt install python3-jstyleson
-            python3 CI/linux-qt6/validate_json.py
+            python3 CI/validate_json.py
+
+        - name: Validate Markdown
+          uses: DavidAnson/markdownlint-cli2-action@v18
+          with:
+            config: 'CI/example.markdownlint-cli2.jsonc'
+            globs: '**/*.md'

+ 1 - 1
AI/BattleAI/AttackPossibility.cpp

@@ -58,7 +58,7 @@ void DamageCache::buildObstacleDamageCache(std::shared_ptr<HypotheticBattle> hb,
 			return u->alive() && !u->isTurret() && u->getPosition().isValid();
 		});
 
-		std::shared_ptr<HypotheticBattle> inner = std::make_shared<HypotheticBattle>(hb->env, hb);
+		auto inner = std::make_shared<HypotheticBattle>(hb->env, hb);
 
 		for(auto stack : stacks)
 		{

+ 27 - 8
AI/BattleAI/BattleEvaluator.cpp

@@ -394,7 +394,7 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
 	{
 		std::set<BattleHex> obstacleHexes;
 
-		auto insertAffected = [](const CObstacleInstance & spellObst, std::set<BattleHex> obstacleHexes) {
+		auto insertAffected = [](const CObstacleInstance & spellObst, std::set<BattleHex> & obstacleHexes) {
 			auto affectedHexes = spellObst.getAffectedTiles();
 			obstacleHexes.insert(affectedHexes.cbegin(), affectedHexes.cend());
 		};
@@ -675,7 +675,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 				spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell);
 				cast.castEval(state->getServerCallback(), ps.dest);
 
-				auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return true; });
+				auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return u->isValidTarget(); });
 
 				auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool
 					{
@@ -731,7 +731,6 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 					ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state);
 				}
-
 				for(const auto & unit : allUnits)
 				{
 					if(!unit->isValidTarget(true))
@@ -771,11 +770,31 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 							ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
 
 #if BATTLE_TRACE_LEVEL >= 1
-						logAi->trace(
-							"Spell affects %s (%d), dps: %2f",
-							unit->creatureId().toCreature()->getNameSingularTranslated(),
-							unit->getCount(),
-							dpsReduce);
+						// Ensure ps.dest is not empty before accessing the first element
+						if (!ps.dest.empty()) 
+						{
+							logAi->trace(
+								"Spell %s to %d affects %s (%d), dps: %2f oldHealth: %d newHealth: %d",
+								ps.spell->getNameTranslated(),
+								ps.dest.at(0).hexValue.hex,  // Safe to access .at(0) now
+								unit->creatureId().toCreature()->getNameSingularTranslated(),
+								unit->getCount(),
+								dpsReduce,
+								oldHealth,
+								newHealth);
+						}
+						else 
+						{
+							// Handle the case where ps.dest is empty
+							logAi->trace(
+								"Spell %s has no destination, affects %s (%d), dps: %2f oldHealth: %d newHealth: %d",
+								ps.spell->getNameTranslated(),
+								unit->creatureId().toCreature()->getNameSingularTranslated(),
+								unit->getCount(),
+								dpsReduce,
+								oldHealth,
+								newHealth);
+						}
 #endif
 					}
 				}

+ 1 - 1
AI/BattleAI/BattleExchangeVariant.cpp

@@ -906,7 +906,7 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUn
 {
 	std::vector<const battle::Unit *> result;
 
-	for(int i = 0; i < turnOrder.size(); i++)
+	for(int i = 0; i < turnOrder.size(); i++, turn++)
 	{
 		auto & turnQueue = turnOrder[i];
 		HypotheticBattle turnBattle(env.get(), cb);

+ 15 - 15
AI/BattleAI/StackWithBonuses.cpp

@@ -531,44 +531,44 @@ vstd::RNG * HypotheticBattle::HypotheticServerCallback::getRNG()
 	return &rngStub;
 }
 
-void HypotheticBattle::HypotheticServerCallback::apply(CPackForClient * pack)
+void HypotheticBattle::HypotheticServerCallback::apply(CPackForClient & pack)
 {
 	logAi->error("Package of type %s is not allowed in battle evaluation", typeid(pack).name());
 }
 
-void HypotheticBattle::HypotheticServerCallback::apply(BattleLogMessage * pack)
+void HypotheticBattle::HypotheticServerCallback::apply(BattleLogMessage & pack)
 {
-	pack->applyBattle(owner);
+	pack.applyBattle(owner);
 }
 
-void HypotheticBattle::HypotheticServerCallback::apply(BattleStackMoved * pack)
+void HypotheticBattle::HypotheticServerCallback::apply(BattleStackMoved & pack)
 {
-	pack->applyBattle(owner);
+	pack.applyBattle(owner);
 }
 
-void HypotheticBattle::HypotheticServerCallback::apply(BattleUnitsChanged * pack)
+void HypotheticBattle::HypotheticServerCallback::apply(BattleUnitsChanged & pack)
 {
-	pack->applyBattle(owner);
+	pack.applyBattle(owner);
 }
 
-void HypotheticBattle::HypotheticServerCallback::apply(SetStackEffect * pack)
+void HypotheticBattle::HypotheticServerCallback::apply(SetStackEffect & pack)
 {
-	pack->applyBattle(owner);
+	pack.applyBattle(owner);
 }
 
-void HypotheticBattle::HypotheticServerCallback::apply(StacksInjured * pack)
+void HypotheticBattle::HypotheticServerCallback::apply(StacksInjured & pack)
 {
-	pack->applyBattle(owner);
+	pack.applyBattle(owner);
 }
 
-void HypotheticBattle::HypotheticServerCallback::apply(BattleObstaclesChanged * pack)
+void HypotheticBattle::HypotheticServerCallback::apply(BattleObstaclesChanged & pack)
 {
-	pack->applyBattle(owner);
+	pack.applyBattle(owner);
 }
 
-void HypotheticBattle::HypotheticServerCallback::apply(CatapultAttack * pack)
+void HypotheticBattle::HypotheticServerCallback::apply(CatapultAttack & pack)
 {
-	pack->applyBattle(owner);
+	pack.applyBattle(owner);
 }
 
 HypotheticBattle::HypotheticEnvironment::HypotheticEnvironment(HypotheticBattle * owner_, const Environment * upperEnvironment)

+ 9 - 9
AI/BattleAI/StackWithBonuses.h

@@ -189,15 +189,15 @@ private:
 
 		vstd::RNG * getRNG() override;
 
-		void apply(CPackForClient * pack) override;
-
-		void apply(BattleLogMessage * pack) override;
-		void apply(BattleStackMoved * pack) override;
-		void apply(BattleUnitsChanged * pack) override;
-		void apply(SetStackEffect * pack) override;
-		void apply(StacksInjured * pack) override;
-		void apply(BattleObstaclesChanged * pack) override;
-		void apply(CatapultAttack * pack) override;
+		void apply(CPackForClient & pack) override;
+
+		void apply(BattleLogMessage & pack) override;
+		void apply(BattleStackMoved & pack) override;
+		void apply(BattleUnitsChanged & pack) override;
+		void apply(SetStackEffect & pack) override;
+		void apply(StacksInjured & pack) override;
+		void apply(BattleObstaclesChanged & pack) override;
+		void apply(CatapultAttack & pack) override;
 	private:
 		HypotheticBattle * owner;
 		RNGStub rngStub;

+ 14 - 20
AI/Nullkiller/AIGateway.cpp

@@ -17,7 +17,6 @@
 #include "../../lib/mapObjects/ObjectTemplate.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/CConfigHandler.h"
-#include "../../lib/CHeroHandler.h"
 #include "../../lib/IGameSettings.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/serializer/CTypeList.h"
@@ -35,11 +34,6 @@
 namespace NKAI
 {
 
-// our to enemy strength ratio constants
-const float SAFE_ATTACK_CONSTANT = 1.1f;
-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
 thread_local CCallback * cb = nullptr;
 thread_local AIGateway * ai = nullptr;
@@ -554,7 +548,7 @@ std::optional<BattleAction> AIGateway::makeSurrenderRetreatDecision(const Battle
 	double fightRatio = ourStrength / (double)battleState.getEnemyStrength();
 
 	// if we have no towns - things are already bad, so retreat is not an option.
-	if(cb->getTownsInfo().size() && ourStrength < RETREAT_ABSOLUTE_THRESHOLD && fightRatio < RETREAT_THRESHOLD && battleState.canFlee)
+	if(cb->getTownsInfo().size() && ourStrength < nullkiller->settings->getRetreatThresholdAbsolute() && fightRatio < nullkiller->settings->getRetreatThresholdRelative() && battleState.canFlee)
 	{
 		return BattleAction::makeRetreat(battleState.ourSide);
 	}
@@ -649,12 +643,12 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
 				auto danger = nullkiller->dangerEvaluator->evaluateDanger(target, hero.get());
 				auto ratio = static_cast<float>(danger) / hero->getTotalStrength();
 
-				answer = 1;
+				answer = true;
 				
 				if(topObj->id != goalObjectID && nullkiller->dangerEvaluator->evaluateDanger(topObj) > 0)
 				{
 					// no if we do not aim to visit this object
-					answer = 0;
+					answer = false;
 				}
 				
 				logAi->trace("Query hook: %s(%s) by %s danger ratio %f", target.toString(), topObj->getObjectName(), hero.name(), ratio);
@@ -671,7 +665,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
 				else if(objType == Obj::ARTIFACT || objType == Obj::RESOURCE)
 				{
 					bool dangerUnknown = danger == 0;
-					bool dangerTooHigh = ratio > (1 / SAFE_ATTACK_CONSTANT);
+					bool dangerTooHigh = ratio * nullkiller->settings->getSafeAttackRatio() > 1;
 
 					answer = !dangerUnknown && !dangerTooHigh;
 				}
@@ -764,7 +758,7 @@ void AIGateway::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstan
 	//you can't request action from action-response thread
 	requestActionASAP([=]()
 	{
-		if(removableUnits && up->tempOwner == down->tempOwner && nullkiller->settings->isGarrisonTroopsUsageAllowed() && !cb->getStartInfo()->isSteadwickFallCampaignMission())
+		if(removableUnits && up->tempOwner == down->tempOwner && nullkiller->settings->isGarrisonTroopsUsageAllowed() && !cb->getStartInfo()->isRestorationOfErathiaCampaign())
 		{
 			pickBestCreatures(down, up);
 		}
@@ -864,7 +858,7 @@ void AIGateway::makeTurn()
 
 void AIGateway::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h)
 {
-	LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->pos.toString());
+	LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->anchorPos().toString());
 	switch(obj->ID)
 	{
 	case Obj::TOWN:
@@ -1056,7 +1050,7 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance
 				//FIXME: why are the above possible to be null?
 
 				bool emptySlotFound = false;
-				for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType()))
+				for(auto slot : artifact->getType()->getPossibleSlots().at(target->bearerType()))
 				{
 					if(target->isPositionFree(slot) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move
 					{
@@ -1069,7 +1063,7 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance
 				}
 				if(!emptySlotFound) //try to put that atifact in already occupied slot
 				{
-					for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType()))
+					for(auto slot : artifact->getType()->getPossibleSlots().at(target->bearerType()))
 					{
 						auto otherSlot = target->getSlot(slot);
 						if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one
@@ -1080,8 +1074,8 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance
 							{
 								logAi->trace(
 									"Exchange artifacts %s <-> %s",
-									artifact->artType->getNameTranslated(),
-									otherSlot->artifact->artType->getNameTranslated());
+									artifact->getType()->getNameTranslated(),
+									otherSlot->artifact->getType()->getNameTranslated());
 
 								if(!otherSlot->artifact->canBePutAt(artHolder, location.slot, true))
 								{
@@ -1130,10 +1124,10 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re
 		{
 			for(auto stack : recruiter->Slots())
 			{
-				if(!stack.second->type)
+				if(!stack.second->getType())
 					continue;
 				
-				auto duplicatingSlot = recruiter->getSlotFor(stack.second->type);
+				auto duplicatingSlot = recruiter->getSlotFor(stack.second->getCreature());
 
 				if(duplicatingSlot != stack.first)
 				{
@@ -1454,8 +1448,8 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 
 void AIGateway::buildStructure(const CGTownInstance * t, BuildingID building)
 {
-	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());
+	auto name = t->getTown()->buildings.at(building)->getNameTranslated();
+	logAi->debug("Player %d will build %s in town of %s at %s", ai->playerID, name, t->getNameTranslated(), t->anchorPos().toString());
 	cb->buildBuilding(t, building); //just do this;
 }
 

+ 10 - 11
AI/Nullkiller/AIUtility.cpp

@@ -14,7 +14,6 @@
 
 #include "../../lib/UnlockGuard.h"
 #include "../../lib/CConfigHandler.h"
-#include "../../lib/CHeroHandler.h"
 #include "../../lib/mapObjects/MapObjects.h"
 #include "../../lib/mapping/CMapDefines.h"
 #include "../../lib/gameState/QuestInfo.h"
@@ -147,21 +146,21 @@ bool HeroPtr::operator==(const HeroPtr & rhs) const
 	return h == rhs.get(true);
 }
 
-bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength)
+bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength, float safeAttackRatio)
 {
-	const ui64 heroStrength = h->getFightingStrength() * heroArmy->getArmyStrength();
+	const ui64 heroStrength = h->getHeroStrength() * heroArmy->getArmyStrength();
 
 	if(dangerStrength)
 	{
-		return heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength;
+		return heroStrength > dangerStrength * safeAttackRatio;
 	}
 
 	return true; //there's no danger
 }
 
-bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength)
+bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength, float safeAttackRatio)
 {
-	return isSafeToVisit(h, h, dangerStrength);
+	return isSafeToVisit(h, h, dangerStrength, safeAttackRatio);
 }
 
 bool isObjectRemovable(const CGObjectInstance * obj)
@@ -194,7 +193,7 @@ bool canBeEmbarkmentPoint(const TerrainTile * t, bool fromWater)
 {
 	// TODO: Such information should be provided by pathfinder
 	// Tile must be free or with unoccupied boat
-	if(!t->blocked)
+	if(!t->blocked())
 	{
 		return true;
 	}
@@ -268,8 +267,8 @@ bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2)
 
 bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2)
 {
-	auto art1 = a1->artType;
-	auto art2 = a2->artType;
+	auto art1 = a1->getType();
+	auto art2 = a2->getType();
 
 	if(art1->getPrice() == art2->getPrice())
 		return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL);
@@ -313,7 +312,7 @@ int getDuplicatingSlots(const CArmedInstance * army)
 
 	for(auto stack : army->Slots())
 	{
-		if(stack.second->type && army->getSlotFor(stack.second->type) != stack.first)
+		if(stack.second->getCreature() && army->getSlotFor(stack.second->getCreature()) != stack.first)
 			duplicatingSlots++;
 	}
 
@@ -388,7 +387,7 @@ bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObject
 	{
 		for(auto slot : h->Slots())
 		{
-			if(slot.second->type->hasUpgrades())
+			if(slot.second->getType()->hasUpgrades())
 				return true; //TODO: check price?
 		}
 		return false;

+ 2 - 7
AI/Nullkiller/AIUtility.h

@@ -61,11 +61,6 @@ const int GOLD_MINE_PRODUCTION = 1000;
 const int WOOD_ORE_MINE_PRODUCTION = 2;
 const int RESOURCE_MINE_PRODUCTION = 1;
 const int ACTUAL_RESOURCE_COUNT = 7;
-const int ALLOWED_ROAMING_HEROES = 8;
-
-//implementation-dependent
-extern const float SAFE_ATTACK_CONSTANT;
-extern const int GOLD_RESERVE;
 
 extern thread_local CCallback * cb;
 
@@ -213,8 +208,8 @@ bool isBlockVisitObj(const int3 & pos);
 bool isWeeklyRevisitable(const Nullkiller * ai, const CGObjectInstance * obj);
 
 bool isObjectRemovable(const CGObjectInstance * obj); //FIXME FIXME: move logic to object property!
-bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength);
-bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet *, uint64_t dangerStrength);
+bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength, float safeAttackRatio);
+bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet *, uint64_t dangerStrength, float safeAttackRatio);
 
 bool compareHeroStrength(const CGHeroInstance * h1, const CGHeroInstance * h2);
 bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2);

+ 12 - 19
AI/Nullkiller/Analyzers/ArmyManager.cpp

@@ -13,6 +13,7 @@
 #include "../Engine/Nullkiller.h"
 #include "../../../CCallback.h"
 #include "../../../lib/mapObjects/MapObjects.h"
+#include "../../../lib/IGameSettings.h"
 #include "../../../lib/GameConstants.h"
 
 namespace NKAI
@@ -90,7 +91,7 @@ std::vector<SlotInfo> ArmyManager::getSortedSlots(const CCreatureSet * target, c
 	{
 		for(auto & i : armyPtr->Slots())
 		{
-			auto cre = dynamic_cast<const CCreature*>(i.second->type);
+			auto cre = dynamic_cast<const CCreature*>(i.second->getType());
 			auto & slotInfp = creToPower[cre];
 
 			slotInfp.creature = cre;
@@ -144,7 +145,7 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
 
 	for(auto & slot : sortedSlots)
 	{
-		alignmentMap[slot.creature->getFaction()] += slot.power;
+		alignmentMap[slot.creature->getFactionID()] += slot.power;
 	}
 
 	std::set<FactionID> allowedFactions;
@@ -152,16 +153,6 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
 	uint64_t armyValue = 0;
 
 	TemporaryArmy newArmyInstance;
-	auto bonusModifiers = armyCarrier->getBonuses(Selector::type()(BonusType::MORALE));
-
-	for(auto bonus : *bonusModifiers)
-	{
-		// army bonuses will change and object bonuses are temporary
-		if(bonus->source != BonusSource::ARMY && bonus->source != BonusSource::OBJECT_INSTANCE && bonus->source != BonusSource::OBJECT_TYPE)
-		{
-			newArmyInstance.addNewBonus(std::make_shared<Bonus>(*bonus));
-		}
-	}
 
 	while(allowedFactions.size() < alignmentMap.size())
 	{
@@ -178,7 +169,7 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
 
 		for(auto & slot : sortedSlots)
 		{
-			if(vstd::contains(allowedFactions, slot.creature->getFaction()))
+			if(vstd::contains(allowedFactions, slot.creature->getFactionID()))
 			{
 				auto slotID = newArmyInstance.getSlotFor(slot.creature->getId());
 
@@ -197,16 +188,18 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
 			auto morale = slot.second->moraleVal();
 			auto multiplier = 1.0f;
 
-			const float BadMoraleChance = 0.083f;
-			const float HighMoraleChance = 0.04f;
+			const auto & badMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_BAD_MORALE_DICE);
+			const auto & highMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE);
 
-			if(morale < 0)
+			if(morale < 0 && !badMoraleDice.empty())
 			{
-				multiplier += morale * BadMoraleChance;
+				size_t diceIndex = std::min<size_t>(badMoraleDice.size(), -morale) - 1;
+				multiplier -= 1.0 / badMoraleDice.at(diceIndex);
 			}
-			else if(morale > 0)
+			else if(morale > 0 && !highMoraleDice.empty())
 			{
-				multiplier += morale * HighMoraleChance;
+				size_t diceIndex = std::min<size_t>(highMoraleDice.size(), morale) - 1;
+				multiplier += 1.0 / highMoraleDice.at(diceIndex);
 			}
 
 			newValue += multiplier * slot.second->getPower();

+ 36 - 17
AI/Nullkiller/Analyzers/BuildAnalyzer.cpp

@@ -17,7 +17,7 @@ namespace NKAI
 
 void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
 {
-	auto townInfo = developmentInfo.town->town;
+	auto townInfo = developmentInfo.town->getTown();
 	auto creatures = townInfo->creatures;
 	auto buildings = townInfo->getAllBuildings();
 
@@ -31,7 +31,7 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
 		}
 	}
 
-	for(int level = 0; level < developmentInfo.town->town->creatures.size(); level++)
+	for(int level = 0; level < developmentInfo.town->getTown()->creatures.size(); level++)
 	{
 		logAi->trace("Checking dwelling level %d", level);
 		BuildingInfo nextToBuild = BuildingInfo();
@@ -39,7 +39,6 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
 		for(int upgradeIndex : {1, 0})
 		{
 			BuildingID building = BuildingID(BuildingID::getDwellingFromLevel(level, upgradeIndex));
-
 			if(!vstd::contains(buildings, building))
 				continue; // no such building in town
 
@@ -73,16 +72,23 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo)
 
 	if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday)
 	{
-		otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE});
 		otherBuildings.push_back({BuildingID::HORDE_1});
 		otherBuildings.push_back({BuildingID::HORDE_2});
 	}
 
+	otherBuildings.push_back({ BuildingID::CITADEL, BuildingID::CASTLE });
+	otherBuildings.push_back({ BuildingID::RESOURCE_SILO });
+	otherBuildings.push_back({ BuildingID::SPECIAL_1 });
+	otherBuildings.push_back({ BuildingID::SPECIAL_2 });
+	otherBuildings.push_back({ BuildingID::SPECIAL_3 });
+	otherBuildings.push_back({ BuildingID::SPECIAL_4 });
+	otherBuildings.push_back({ BuildingID::MARKETPLACE });
+
 	for(auto & buildingSet : otherBuildings)
 	{
 		for(auto & buildingID : buildingSet)
 		{
-			if(!developmentInfo.town->hasBuilt(buildingID) && developmentInfo.town->town->buildings.count(buildingID))
+			if(!developmentInfo.town->hasBuilt(buildingID) && developmentInfo.town->getTown()->buildings.count(buildingID))
 			{
 				developmentInfo.addBuildingToBuild(getBuildingOrPrerequisite(developmentInfo.town, buildingID));
 
@@ -141,6 +147,8 @@ void BuildAnalyzer::update()
 
 	auto towns = ai->cb->getTownsInfo();
 
+	float economyDevelopmentCost = 0;
+
 	for(const CGTownInstance* town : towns)
 	{
 		logAi->trace("Checking town %s", town->getNameTranslated());
@@ -153,6 +161,11 @@ void BuildAnalyzer::update()
 
 		requiredResources += developmentInfo.requiredResources;
 		totalDevelopmentCost += developmentInfo.townDevelopmentCost;
+		for(auto building : developmentInfo.toBuild)
+		{
+			if (building.dailyIncome[EGameResID::GOLD] > 0)
+				economyDevelopmentCost += building.buildCostWithPrerequisites[EGameResID::GOLD];
+		}
 		armyCost += developmentInfo.armyCost;
 
 		for(auto bi : developmentInfo.toBuild)
@@ -171,15 +184,7 @@ void BuildAnalyzer::update()
 
 	updateDailyIncome();
 
-	if(ai->cb->getDate(Date::DAY) == 1)
-	{
-		goldPressure = 1;
-	}
-	else
-	{
-		goldPressure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f
-			+ (float)armyCost[EGameResID::GOLD] / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
-	}
+	goldPressure = (ai->getLockedResources()[EGameResID::GOLD] + (float)armyCost[EGameResID::GOLD] + economyDevelopmentCost) / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
 
 	logAi->trace("Gold pressure: %f", goldPressure);
 }
@@ -198,7 +203,7 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 	bool excludeDwellingDependencies) const
 {
 	BuildingID building = toBuild;
-	auto townInfo = town->town;
+	auto townInfo = town->getTown();
 
 	const CBuilding * buildPtr = townInfo->buildings.at(building);
 	const CCreature * creature = nullptr;
@@ -237,6 +242,12 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 	logAi->trace("checking %s", info.name);
 	logAi->trace("buildInfo %s", info.toString());
 
+	int highestFort = 0;
+	for (auto twn : ai->cb->getTownsInfo())
+	{
+		highestFort = std::max(highestFort, (int)twn->fortLevel());
+	}
+
 	if(!town->hasBuilt(building))
 	{
 		auto canBuild = ai->cb->canBuildStructure(town, building);
@@ -281,7 +292,15 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 				prerequisite.baseCreatureID = info.baseCreatureID;
 				prerequisite.prerequisitesCount++;
 				prerequisite.armyCost = info.armyCost;
-				prerequisite.dailyIncome = info.dailyIncome;
+				bool haveSameOrBetterFort = false;
+				if (prerequisite.id == BuildingID::FORT && highestFort >= CGTownInstance::EFortLevel::FORT)
+					haveSameOrBetterFort = true;
+				if (prerequisite.id == BuildingID::CITADEL && highestFort >= CGTownInstance::EFortLevel::CITADEL)
+					haveSameOrBetterFort = true;
+				if (prerequisite.id == BuildingID::CASTLE && highestFort >= CGTownInstance::EFortLevel::CASTLE)
+					haveSameOrBetterFort = true;
+				if(!haveSameOrBetterFort)
+					prerequisite.dailyIncome = info.dailyIncome;
 
 				return prerequisite;
 			}
@@ -327,7 +346,7 @@ bool BuildAnalyzer::hasAnyBuilding(int32_t alignment, BuildingID bid) const
 {
 	for(auto tdi : developmentInfos)
 	{
-		if(tdi.town->getFaction() == alignment && tdi.town->hasBuilt(bid))
+		if(tdi.town->getFactionID() == alignment && tdi.town->hasBuilt(bid))
 			return true;
 	}
 

+ 3 - 3
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp

@@ -89,7 +89,6 @@ void DangerHitMapAnalyzer::updateHitMap()
 
 			heroes[hero->tempOwner][hero] = HeroRole::MAIN;
 		}
-
 		if(obj->ID == Obj::TOWN)
 		{
 			auto town = dynamic_cast<const CGTownInstance *>(obj);
@@ -140,6 +139,7 @@ void DangerHitMapAnalyzer::updateHitMap()
 
 				newThreat.hero = path.targetHero;
 				newThreat.turn = path.turn();
+				newThreat.threat = path.getHeroStrength() * (1 - path.movementCost() / 2.0);
 				newThreat.danger = path.getHeroStrength();
 
 				if(newThreat.value() > node.maximumDanger.value())
@@ -316,8 +316,8 @@ uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath &
 
 	const auto& info = getTileThreat(tile);
 	
-	return (info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger))
-		|| (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger));
+	return (info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger, ai->settings->getSafeAttackRatio()))
+		|| (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger, ai->settings->getSafeAttackRatio()));
 }
 
 const HitMapNode & DangerHitMapAnalyzer::getObjectThreat(const CGObjectInstance * obj) const

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

@@ -22,6 +22,7 @@ struct HitMapInfo
 
 	uint64_t danger;
 	uint8_t turn;
+	float threat;
 	HeroPtr hero;
 
 	HitMapInfo()
@@ -33,6 +34,7 @@ struct HitMapInfo
 	{
 		danger = 0;
 		turn = 255;
+		threat = 0;
 		hero = HeroPtr();
 	}
 

+ 14 - 13
AI/Nullkiller/Analyzers/HeroManager.cpp

@@ -11,7 +11,6 @@
 #include "../StdInc.h"
 #include "../Engine/Nullkiller.h"
 #include "../../../lib/mapObjects/MapObjects.h"
-#include "../../../lib/CHeroHandler.h"
 #include "../../../lib/IGameSettings.h"
 
 namespace NKAI
@@ -71,7 +70,7 @@ float HeroManager::evaluateSecSkill(SecondarySkill skill, const CGHeroInstance *
 
 float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const
 {
-	auto heroSpecial = Selector::source(BonusSource::HERO_SPECIAL, BonusSourceID(hero->type->getId()));
+	auto heroSpecial = Selector::source(BonusSource::HERO_SPECIAL, BonusSourceID(hero->getHeroTypeID()));
 	auto secondarySkillBonus = Selector::targetSourceType()(BonusSource::SECONDARY_SKILL);
 	auto specialSecondarySkillBonuses = hero->getBonuses(heroSpecial.And(secondarySkillBonus));
 	auto secondarySkillBonuses = hero->getBonuses(Selector::sourceTypeSel(BonusSource::SECONDARY_SKILL));
@@ -96,7 +95,7 @@ float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const
 
 float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const
 {
-	return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->level * 1.5f;
+	return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->getBasePrimarySkillValue(PrimarySkill::ATTACK) + hero->getBasePrimarySkillValue(PrimarySkill::DEFENSE) + hero->getBasePrimarySkillValue(PrimarySkill::SPELL_POWER) + hero->getBasePrimarySkillValue(PrimarySkill::KNOWLEDGE);
 }
 
 void HeroManager::update()
@@ -109,7 +108,7 @@ void HeroManager::update()
 	for(auto & hero : myHeroes)
 	{
 		scores[hero] = evaluateFightingStrength(hero);
-		knownFightingStrength[hero->id] = hero->getFightingStrength();
+		knownFightingStrength[hero->id] = hero->getHeroStrength();
 	}
 
 	auto scoreSort = [&](const CGHeroInstance * h1, const CGHeroInstance * h2) -> bool
@@ -148,7 +147,10 @@ void HeroManager::update()
 
 HeroRole HeroManager::getHeroRole(const HeroPtr & hero) const
 {
-	return heroRoles.at(hero);
+	if (heroRoles.find(hero) != heroRoles.end())
+		return heroRoles.at(hero);
+	else
+		return HeroRole::SCOUT;
 }
 
 const std::map<HeroPtr, HeroRole> & HeroManager::getHeroRoles() const
@@ -189,13 +191,11 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const
 	return evaluateFightingStrength(hero);
 }
 
-bool HeroManager::heroCapReached() const
+bool HeroManager::heroCapReached(bool includeGarrisoned) const
 {
-	const bool includeGarnisoned = true;
-	int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned);
+	int heroCount = cb->getHeroCount(ai->playerID, includeGarrisoned);
 
-	return heroCount >= ALLOWED_ROAMING_HEROES
-		|| heroCount >= ai->settings->getMaxRoamingHeroes()
+	return heroCount >= ai->settings->getMaxRoamingHeroes()
 		|| heroCount >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)
 		|| heroCount >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP);
 }
@@ -205,7 +205,7 @@ float HeroManager::getFightingStrengthCached(const CGHeroInstance * hero) const
 	auto cached = knownFightingStrength.find(hero->id);
 
 	//FIXME: fallback to hero->getFightingStrength() is VERY slow on higher difficulties (no object graph? map reveal?)
-	return cached != knownFightingStrength.end() ? cached->second : hero->getFightingStrength();
+	return cached != knownFightingStrength.end() ? cached->second : hero->getHeroStrength();
 }
 
 float HeroManager::getMagicStrength(const CGHeroInstance * hero) const
@@ -282,7 +282,7 @@ const CGHeroInstance * HeroManager::findHeroWithGrail() const
 	return nullptr;
 }
 
-const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) const
+const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance* townToSpare) const
 {
 	const CGHeroInstance * weakestHero = nullptr;
 	auto myHeroes = ai->cb->getHeroesInfo();
@@ -293,12 +293,13 @@ const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) co
 			|| existingHero->getArmyStrength() >armyLimit
 			|| getHeroRole(existingHero) == HeroRole::MAIN
 			|| existingHero->movementPointsRemaining()
+			|| (townToSpare != nullptr && existingHero->visitedTown == townToSpare)
 			|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
 		{
 			continue;
 		}
 
-		if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength())
+		if(!weakestHero || weakestHero->getHeroStrength() > existingHero->getHeroStrength())
 		{
 			weakestHero = existingHero;
 		}

+ 2 - 2
AI/Nullkiller/Analyzers/HeroManager.h

@@ -56,9 +56,9 @@ public:
 	float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const;
 	float evaluateHero(const CGHeroInstance * hero) const;
 	bool canRecruitHero(const CGTownInstance * t = nullptr) const;
-	bool heroCapReached() const;
+	bool heroCapReached(bool includeGarrisoned = true) const;
 	const CGHeroInstance * findHeroWithGrail() const;
-	const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const;
+	const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance * townToSpare = nullptr) const;
 	float getMagicStrength(const CGHeroInstance * hero) const;
 	float getFightingStrengthCached(const CGHeroInstance * hero) const;
 

+ 11 - 6
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

@@ -97,9 +97,10 @@ std::optional<const CGObjectInstance *> ObjectClusterizer::getBlocker(const AIPa
 	{
 		auto guardPos = ai->cb->getGuardingCreaturePosition(node.coord);
 
-		blockers = ai->cb->getVisitableObjs(node.coord);
+		if (ai->cb->isVisible(node.coord))
+			blockers = ai->cb->getVisitableObjs(node.coord);
 
-		if(guardPos.valid())
+		if(guardPos.valid() && ai->cb->isVisible(guardPos))
 		{
 			auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node.coord));
 
@@ -474,9 +475,11 @@ void ObjectClusterizer::clusterizeObject(
 
 				heroesProcessed.insert(path.targetHero);
 
-				float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
+				float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER);
 
-				if(priority < MIN_PRIORITY)
+				if(ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
+					continue;
+				else if (priority <= 0)
 					continue;
 
 				ClusterMap::accessor cluster;
@@ -495,9 +498,11 @@ void ObjectClusterizer::clusterizeObject(
 
 		heroesProcessed.insert(path.targetHero);
 
-		float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
+		float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER);
 
-		if(priority < MIN_PRIORITY)
+		if (ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
+			continue;
+		else if (priority <= 0)
 			continue;
 
 		bool interestingObject = path.turn() <= 2 || priority > 0.5f;

+ 35 - 12
AI/Nullkiller/Behaviors/BuildingBehavior.cpp

@@ -49,26 +49,49 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const
 	auto & developmentInfos = ai->buildAnalyzer->getDevelopmentInfo();
 	auto isGoldPressureLow = !ai->buildAnalyzer->isGoldPressureHigh();
 
+	ai->dangerHitMap->updateHitMap();
+
 	for(auto & developmentInfo : developmentInfos)
 	{
-		for(auto & buildingInfo : developmentInfo.toBuild)
+		bool emergencyDefense = false;
+		uint8_t closestThreat = std::numeric_limits<uint8_t>::max();
+		for (auto threat : ai->dangerHitMap->getTownThreats(developmentInfo.town))
+		{
+			closestThreat = std::min(closestThreat, threat.turn);
+		}
+		for (auto& buildingInfo : developmentInfo.toBuild)
 		{
-			if(isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0)
+			if (closestThreat <= 1 && developmentInfo.town->fortLevel() < CGTownInstance::EFortLevel::CASTLE && !buildingInfo.notEnoughRes)
 			{
-				if(buildingInfo.notEnoughRes)
+				if (buildingInfo.id == BuildingID::CITADEL || buildingInfo.id == BuildingID::CASTLE)
 				{
-					if(ai->getLockedResources().canAfford(buildingInfo.buildCost))
-						continue;
-
-					Composition composition;
+					tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
+					emergencyDefense = true;
+				}
+			}
+		}
+		if (!emergencyDefense)
+		{
+			for (auto& buildingInfo : developmentInfo.toBuild)
+			{
+				if (isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0)
+				{
+					if (buildingInfo.notEnoughRes)
+					{
+						if (ai->getLockedResources().canAfford(buildingInfo.buildCost))
+							continue;
 
-					composition.addNext(BuildThis(buildingInfo, developmentInfo));
-					composition.addNext(SaveResources(buildingInfo.buildCost));
+						Composition composition;
 
-					tasks.push_back(sptr(composition));
+						composition.addNext(BuildThis(buildingInfo, developmentInfo));
+						composition.addNext(SaveResources(buildingInfo.buildCost));
+						tasks.push_back(sptr(composition));
+					}
+					else
+					{
+						tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
+					}
 				}
-				else
-					tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
 			}
 		}
 	}

+ 9 - 8
AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp

@@ -28,9 +28,6 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
 {
 	Goals::TGoalVec tasks;
 
-	if(ai->cb->getDate(Date::DAY) == 1)
-		return tasks;
-		
 	auto heroes = cb->getHeroesInfo();
 
 	if(heroes.empty())
@@ -38,19 +35,23 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
 		return tasks;
 	}
 
+	ai->dangerHitMap->updateHitMap();
+
 	for(auto town : cb->getTownsInfo())
 	{
+		uint8_t closestThreat = ai->dangerHitMap->getTileThreat(town->visitablePos()).fastestDanger.turn;
+
+		if (closestThreat >=2 && ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL) && cb->canBuildStructure(town, BuildingID::CITY_HALL) != EBuildingState::FORBIDDEN)
+		{
+			return tasks;
+		}
+		
 		auto townArmyAvailableToBuy = ai->armyManager->getArmyAvailableToBuyAsCCreatureSet(
 			town,
 			ai->getFreeResources());
 
 		for(const CGHeroInstance * targetHero : heroes)
 		{
-			if(ai->buildAnalyzer->isGoldPressureHigh()	&& !town->hasBuilt(BuildingID::CITY_HALL))
-			{
-				continue;
-			}
-
 			if(ai->heroManager->getHeroRole(targetHero) == HeroRole::MAIN)
 			{
 				auto reinforcement = ai->armyManager->howManyReinforcementsCanGet(

+ 4 - 9
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp

@@ -68,14 +68,6 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
 		logAi->trace("Path found %s", path.toString());
 #endif
 
-		if(nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
-		{
-#if NKAI_TRACE_LEVEL >= 2
-			logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.getHeroStrength());
-#endif
-			continue;
-		}
-
 		if(objToVisit && !force && !shouldVisit(nullkiller, path.targetHero, objToVisit))
 		{
 #if NKAI_TRACE_LEVEL >= 2
@@ -87,6 +79,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
 		auto hero = path.targetHero;
 		auto danger = path.getTotalDanger();
 
+		if (hero->getOwner() != nullkiller->playerID)
+			continue;
+
 		if(nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT
 			&& (path.getTotalDanger() == 0 || path.turn() > 0)
 			&& path.exchangeCount > 1)
@@ -119,7 +114,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
 			continue;
 		}
 
-		auto isSafe = isSafeToVisit(hero, path.heroArmy, danger);
+		auto isSafe = isSafeToVisit(hero, path.heroArmy, danger, nullkiller->settings->getSafeAttackRatio());
 
 #if NKAI_TRACE_LEVEL >= 2
 		logAi->trace(

+ 27 - 20
AI/Nullkiller/Behaviors/DefenceBehavior.cpp

@@ -41,6 +41,9 @@ Goals::TGoalVec DefenceBehavior::decompose(const Nullkiller * ai) const
 	for(auto town : ai->cb->getTownsInfo())
 	{
 		evaluateDefence(tasks, town, ai);
+		//Let's do only one defence-task per pass since otherwise it can try to hire the same hero twice
+		if (!tasks.empty())
+			break;
 	}
 
 	return tasks;
@@ -130,7 +133,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
 
 			tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
 
-			return true;
+			return false;
 		}
 		else if(ai->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN)
 		{
@@ -141,7 +144,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
 			{
 				tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5)));
 
-				return true;
+				return false;
 			}
 		}
 	}
@@ -158,11 +161,10 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 	
 	threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there
 
-	if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai))
+	if (town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai))
 	{
 		return;
 	}
-
 	if(!threatNode.fastestDanger.hero)
 	{
 		logAi->trace("No threat found for town %s", town->getNameTranslated());
@@ -250,6 +252,16 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				continue;
 			}
 
+			if (!path.targetHero->canBeMergedWith(*town))
+			{
+#if NKAI_TRACE_LEVEL >= 1
+				logAi->trace("Can't merge armies of hero %s and town %s",
+					path.targetHero->getObjectName(),
+					town->getObjectName());
+#endif
+				continue;
+			}
+
 			if(path.targetHero == town->visitingHero.get() && path.exchangeCount == 1)
 			{
 #if NKAI_TRACE_LEVEL >= 1
@@ -261,6 +273,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				// dismiss creatures we are not able to pick to be able to hide in garrison
 				if(town->garrisonHero
 					|| town->getUpperArmy()->stacksCount() == 0
+					|| path.targetHero->canBeMergedWith(*town)
 					|| (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL))
 				{
 					tasks.push_back(
@@ -292,7 +305,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				continue;
 			}
 				
-			if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= threat.danger))
+			if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * ai->settings->getSafeAttackRatio() >= threat.danger))
 			{
 				if(ai->arePathHeroesLocked(path))
 				{
@@ -343,23 +356,14 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 			}
 			else if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero))
 			{
-				if(town->garrisonHero)
+				if(town->garrisonHero && town->garrisonHero != path.targetHero)
 				{
-					if(ai->heroManager->getHeroRole(town->visitingHero.get()) == HeroRole::SCOUT
-						&& town->visitingHero->getArmyStrength() < path.heroArmy->getArmyStrength() / 20)
-					{
-						if(path.turn() == 0)
-							sequence.push_back(sptr(DismissHero(town->visitingHero.get())));
-					}
-					else
-					{
 #if NKAI_TRACE_LEVEL >= 1
-						logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero",
-							path.targetHero->getObjectName(),
-							town->getObjectName());
+					logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero",
+						path.targetHero->getObjectName(),
+						town->getObjectName());
 #endif
-						continue;
-					}
+					continue;
 				}
 				else if(path.turn() == 0)
 				{
@@ -405,6 +409,9 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 
 void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town, const Nullkiller * ai) const
 {
+	if (threat.turn > 0 || town->garrisonHero || town->visitingHero)
+		return;
+	
 	if(town->hasBuilt(BuildingID::TAVERN)
 		&& ai->cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
 	{
@@ -451,7 +458,7 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM
 			}
 			else if(ai->heroManager->heroCapReached())
 			{
-				heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength());
+				heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength(), town);
 
 				if(!heroToDismiss)
 					continue;

+ 13 - 29
AI/Nullkiller/Behaviors/ExplorationBehavior.cpp

@@ -33,48 +33,32 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const
 {
 	Goals::TGoalVec tasks;
 
-	for(auto obj : ai->memory->visitableObjs)
+	for (auto obj : ai->memory->visitableObjs)
 	{
-		if(!vstd::contains(ai->memory->alreadyVisited, obj))
+		switch (obj->ID.num)
 		{
-			switch(obj->ID.num)
-			{
 			case Obj::REDWOOD_OBSERVATORY:
 			case Obj::PILLAR_OF_FIRE:
-				tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj))));
+			{
+				auto rObj = dynamic_cast<const CRewardableObject*>(obj);
+				if (!rObj->wasScouted(ai->playerID))
+					tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj))));
 				break;
+			}
 			case Obj::MONOLITH_ONE_WAY_ENTRANCE:
 			case Obj::MONOLITH_TWO_WAY:
 			case Obj::SUBTERRANEAN_GATE:
 			case Obj::WHIRLPOOL:
-				auto tObj = dynamic_cast<const CGTeleport *>(obj);
-				if(TeleportChannel::IMPASSABLE != ai->memory->knownTeleportChannels[tObj->channel]->passability)
-				{
-					tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
-				}
-				break;
-			}
-		}
-		else
-		{
-			switch(obj->ID.num)
 			{
-			case Obj::MONOLITH_TWO_WAY:
-			case Obj::SUBTERRANEAN_GATE:
-			case Obj::WHIRLPOOL:
-				auto tObj = dynamic_cast<const CGTeleport *>(obj);
-				if(TeleportChannel::IMPASSABLE == ai->memory->knownTeleportChannels[tObj->channel]->passability)
-					break;
-				for(auto exit : ai->memory->knownTeleportChannels[tObj->channel]->exits)
+				auto tObj = dynamic_cast<const CGTeleport*>(obj);
+				for (auto exit : cb->getTeleportChannelExits(tObj->channel))
 				{
-					if(!cb->getObj(exit))
-					{ 
-						// Always attempt to visit two-way teleports if one of channel exits is not visible
-						tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
-						break;
+					if (exit != tObj->id)
+					{
+						if (!cb->isVisible(cb->getObjInstance(exit)))
+							tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
 					}
 				}
-				break;
 			}
 		}
 	}

+ 6 - 31
AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp

@@ -81,6 +81,9 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
 		logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
 #endif
 		
+		if (path.targetHero->getOwner() != ai->playerID)
+			continue;
+		
 		if(path.containsHero(hero))
 		{
 #if NKAI_TRACE_LEVEL >= 2
@@ -89,14 +92,6 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
 			continue;
 		}
 
-		if(path.turn() > 0 && ai->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());
-#endif
-			continue;
-		}
-
 		if(ai->arePathHeroesLocked(path))
 		{
 #if NKAI_TRACE_LEVEL >= 2
@@ -150,7 +145,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
 		}
 
 		auto danger = path.getTotalDanger();
-		auto isSafe = isSafeToVisit(hero, path.heroArmy, danger);
+		auto isSafe = isSafeToVisit(hero, path.heroArmy, danger, ai->settings->getSafeAttackRatio());
 
 #if NKAI_TRACE_LEVEL >= 2
 		logAi->trace(
@@ -292,17 +287,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
 			continue;
 		}
 
-		auto heroRole = ai->heroManager->getHeroRole(path.targetHero);
-
-		if(heroRole == HeroRole::SCOUT
-			&& ai->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());
-#endif
-			continue;
-		}
-
 		auto upgrade = ai->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources);
 
 		if(!upgrader->garrisonHero
@@ -320,14 +304,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
 
 			armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength();
 
-			armyToGetOrBuy.addArmyToBuy(
-				ai->armyManager->toSlotInfo(
-					ai->armyManager->getArmyAvailableToBuy(
-						path.heroArmy,
-						upgrader,
-						ai->getFreeResources(),
-						path.turn())));
-
 			upgrade.upgradeValue += armyToGetOrBuy.upgradeValue;
 			upgrade.upgradeCost += armyToGetOrBuy.upgradeCost;
 			vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy);
@@ -339,8 +315,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
 			{
 				for(auto hero : cb->getAvailableHeroes(upgrader))
 				{
-					auto scoutReinforcement =  ai->armyManager->howManyReinforcementsCanBuy(hero, upgrader)
-						+ ai->armyManager->howManyReinforcementsCanGet(hero, upgrader);
+					auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanGet(hero, upgrader);
 
 					if(scoutReinforcement >= armyToGetOrBuy.upgradeValue
 						&& ai->getFreeGold() >20000
@@ -366,7 +341,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
 
 		auto danger = path.getTotalDanger();
 
-		auto isSafe = isSafeToVisit(path.targetHero, path.heroArmy, danger);
+		auto isSafe = isSafeToVisit(path.targetHero, path.heroArmy, danger, ai->settings->getSafeAttackRatio());
 
 #if NKAI_TRACE_LEVEL >= 2
 		logAi->trace(

+ 64 - 25
AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp

@@ -31,9 +31,11 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
 
 	auto ourHeroes = ai->heroManager->getHeroRoles();
 	auto minScoreToHireMain = std::numeric_limits<float>::max();
+	int currentArmyValue = 0;
 
 	for(auto hero : ourHeroes)
 	{
+		currentArmyValue += hero.first->getArmyCost();
 		if(hero.second != HeroRole::MAIN)
 			continue;
 
@@ -45,51 +47,88 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
 			minScoreToHireMain = newScore;
 		}
 	}
-
+	// If we don't have any heros we might want to lower our expectations.
+	if (ourHeroes.empty())
+		minScoreToHireMain = 0;
+
+	const CGHeroInstance* bestHeroToHire = nullptr;
+	const CGTownInstance* bestTownToHireFrom = nullptr;
+	float bestScore = 0;
+	bool haveCapitol = false;
+
+	ai->dangerHitMap->updateHitMap();
+	int treasureSourcesCount = 0;
+	
 	for(auto town : towns)
 	{
+		uint8_t closestThreat = UINT8_MAX;
+		for (auto threat : ai->dangerHitMap->getTownThreats(town))
+		{
+			closestThreat = std::min(closestThreat, threat.turn);
+		}
+		//Don't hire a hero where there already is one present
+		if (town->visitingHero && town->garrisonHero)
+			continue;
+		float visitability = 0;
+		for (auto checkHero : ourHeroes)
+		{
+			if (ai->dangerHitMap->getClosestTown(checkHero.first.get()->visitablePos()) == town)
+				visitability++;
+		}
 		if(ai->heroManager->canRecruitHero(town))
 		{
 			auto availableHeroes = ai->cb->getAvailableHeroes(town);
-
-			for(auto hero : availableHeroes)
+			
+			for (auto obj : ai->objectClusterizer->getNearbyObjects())
 			{
-				auto score = ai->heroManager->evaluateHero(hero);
-
-				if(score > minScoreToHireMain)
-				{
-					tasks.push_back(Goals::sptr(Goals::RecruitHero(town, hero).setpriority(200)));
-					break;
-				}
-			}
-
-			int treasureSourcesCount = 0;
-
-			for(auto obj : ai->objectClusterizer->getNearbyObjects())
-			{
-				if((obj->ID == Obj::RESOURCE)
+				if ((obj->ID == Obj::RESOURCE)
 					|| obj->ID == Obj::TREASURE_CHEST
 					|| obj->ID == Obj::CAMPFIRE
 					|| isWeeklyRevisitable(ai, obj)
-					|| obj->ID ==Obj::ARTIFACT)
+					|| obj->ID == Obj::ARTIFACT)
 				{
 					auto tile = obj->visitablePos();
 					auto closestTown = ai->dangerHitMap->getClosestTown(tile);
 
-					if(town == closestTown)
+					if (town == closestTown)
 						treasureSourcesCount++;
 				}
 			}
 
-			if(treasureSourcesCount < 5 && (town->garrisonHero || town->getUpperArmy()->getArmyStrength() < 10000))
-				continue;
-
-			if(ai->cb->getHeroesInfo().size() < ai->cb->getTownsInfo().size() + 1
-				|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh()))
+			for(auto hero : availableHeroes)
 			{
-				tasks.push_back(Goals::sptr(Goals::RecruitHero(town).setpriority(3)));
+				auto score = ai->heroManager->evaluateHero(hero);
+				if(score > minScoreToHireMain)
+				{
+					score *= score / minScoreToHireMain;
+				}
+				score *= (hero->getArmyCost() + currentArmyValue);
+				if (hero->getFactionID() == town->getFactionID())
+					score *= 1.5;
+				if (vstd::isAlmostZero(visitability))
+					score *= 30 * town->getTownLevel();
+				else
+					score *= town->getTownLevel() / visitability;
+				if (score > bestScore)
+				{
+					bestScore = score;
+					bestHeroToHire = hero;
+					bestTownToHireFrom = town;
+				}
 			}
 		}
+		if (town->hasCapitol())
+			haveCapitol = true;
+	}
+	if (bestHeroToHire && bestTownToHireFrom)
+	{
+		if (ai->cb->getHeroesInfo().size() == 0
+			|| treasureSourcesCount > ai->cb->getHeroesInfo().size() * 5
+			|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol)
+			|| (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh()))
+		{
+			tasks.push_back(Goals::sptr(Goals::RecruitHero(bestTownToHireFrom, bestHeroToHire).setpriority((float)3 / (ourHeroes.size() + 1))));
+		}
 	}
 
 	return tasks;

+ 1 - 10
AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp

@@ -39,9 +39,6 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const
 
 	for(auto town : towns)
 	{
-		if(!town->hasBuilt(BuildingID::MAGES_GUILD_1))
-			continue;
-
 		ai->pathfinder->calculatePathInfo(paths, town->visitablePos());
 
 		for(auto & path : paths)
@@ -49,14 +46,8 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const
 			if(town->visitingHero && town->visitingHero.get() != path.targetHero)
 				continue;
 
-			if(!path.targetHero->hasSpellbook() || path.targetHero->mana >= 0.75f * path.targetHero->manaLimit())
-				continue;
-
-			if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1)
+			if(!path.getFirstBlockedAction() && path.exchangeCount <= 1)
 			{
-				if(path.targetHero->mana == path.targetHero->manaLimit())
-					continue;
-
 				Composition stayAtTown;
 
 				stayAtTown.addNextSequence({

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

@@ -17,8 +17,7 @@
 namespace NKAI
 {
 
-#define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter
-#define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
+constexpr float MIN_AI_STRENGTH = 0.5f; //lower when combat AI gets smarter
 
 engineBase::engineBase()
 {

+ 12 - 3
AI/Nullkiller/Engine/FuzzyHelper.cpp

@@ -52,6 +52,15 @@ ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visit
 			{
 				objectDanger += evaluateDanger(hero->visitedTown.get());
 			}
+			objectDanger *= ai->heroManager->getFightingStrengthCached(hero);
+		}
+		if (objWithID<Obj::TOWN>(dangerousObject))
+		{
+			auto town = dynamic_cast<const CGTownInstance*>(dangerousObject);
+			auto hero = town->garrisonHero;
+
+			if (hero)
+				objectDanger *= ai->heroManager->getFightingStrengthCached(hero);
 		}
 
 		if(objectDanger)
@@ -117,10 +126,10 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
 		{
 			auto fortLevel = town->fortLevel();
 
-			if(fortLevel == CGTownInstance::EFortLevel::CASTLE)
-				danger += 10000;
+			if (fortLevel == CGTownInstance::EFortLevel::CASTLE)
+				danger = std::max(danger * 2, danger + 10000);
 			else if(fortLevel == CGTownInstance::EFortLevel::CITADEL)
-				danger += 4000;
+				danger = std::max(ui64(danger * 1.4), danger + 4000);
 		}
 
 		return danger;

+ 174 - 26
AI/Nullkiller/Engine/Nullkiller.cpp

@@ -34,13 +34,12 @@ using namespace Goals;
 std::unique_ptr<ObjectGraph> Nullkiller::baseGraph;
 
 Nullkiller::Nullkiller()
-	:activeHero(nullptr), scanDepth(ScanDepth::MAIN_FULL), useHeroChain(true)
+	: activeHero(nullptr)
+	, scanDepth(ScanDepth::MAIN_FULL)
+	, useHeroChain(true)
+	, memory(std::make_unique<AIMemory>())
 {
-	memory = std::make_unique<AIMemory>();
-	settings = std::make_unique<Settings>();
 
-	useObjectGraph = settings->isObjectGraphAllowed();
-	openMap = settings->isOpenMap() || useObjectGraph;
 }
 
 bool canUseOpenMap(std::shared_ptr<CCallback> cb, PlayerColor playerID)
@@ -62,17 +61,23 @@ bool canUseOpenMap(std::shared_ptr<CCallback> cb, PlayerColor playerID)
 		return false;
 	}
 
-	return cb->getStartInfo()->difficulty >= 3;
+	return true;
 }
 
 void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway)
 {
 	this->cb = cb;
 	this->gateway = gateway;
-	
-	playerID = gateway->playerID;
+	this->playerID = gateway->playerID;
 
-	if(openMap && !canUseOpenMap(cb, playerID))
+	settings = std::make_unique<Settings>(cb->getStartInfo()->difficulty);
+
+	if(canUseOpenMap(cb, playerID))
+	{
+		useObjectGraph = settings->isObjectGraphAllowed();
+		openMap = settings->isOpenMap() || useObjectGraph;
+	}
+	else
 	{
 		useObjectGraph = false;
 		openMap = false;
@@ -122,11 +127,14 @@ void TaskPlan::merge(TSubgoal task)
 {
 	TGoalVec blockers;
 
+	if (task->asTask()->priority <= 0)
+		return;
+
 	for(auto & item : tasks)
 	{
 		for(auto objid : item.affectedObjects)
 		{
-			if(task == item.task || task->asTask()->isObjectAffected(objid))
+			if(task == item.task || task->asTask()->isObjectAffected(objid) || (task->asTask()->getHero() != nullptr && task->asTask()->getHero() == item.task->asTask()->getHero()))
 			{
 				if(item.task->asTask()->priority >= task->asTask()->priority)
 					return;
@@ -166,20 +174,19 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TGoalVec & tasks) const
 	return taskptr(*bestTask);
 }
 
-Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks) const
+Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks, int priorityTier) const
 {
 	TaskPlan taskPlan;
 
-	tbb::parallel_for(tbb::blocked_range<size_t>(0, tasks.size()), [this, &tasks](const tbb::blocked_range<size_t> & r)
+	tbb::parallel_for(tbb::blocked_range<size_t>(0, tasks.size()), [this, &tasks, priorityTier](const tbb::blocked_range<size_t> & r)
 		{
 			auto evaluator = this->priorityEvaluators->acquire();
 
 			for(size_t i = r.begin(); i != r.end(); i++)
 			{
 				auto task = tasks[i];
-
-				if(task->asTask()->priority <= 0)
-					task->asTask()->priority = evaluator->evaluate(task);
+				if (task->asTask()->priority <= 0 || priorityTier != PriorityEvaluator::PriorityTier::BUILDINGS)
+					task->asTask()->priority = evaluator->evaluate(task, priorityTier);
 			}
 		});
 
@@ -326,7 +333,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->getObjectName(), path.toString());
+			logAi->trace("Hero %s is locked by %d. Discarding %s", path.targetHero->getObjectName(), (int)lockReason,  path.toString());
 #endif
 			return true;
 		}
@@ -347,12 +354,24 @@ void Nullkiller::makeTurn()
 	boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker);
 
 	const int MAX_DEPTH = 10;
-	const float FAST_TASK_MINIMAL_PRIORITY = 0.7f;
 
 	resetAiState();
 
 	Goals::TGoalVec bestTasks;
 
+#if NKAI_TRACE_LEVEL >= 1
+	float totalHeroStrength = 0;
+	int totalTownLevel = 0;
+	for (auto heroInfo : cb->getHeroesInfo())
+	{
+		totalHeroStrength += heroInfo->getTotalStrength();
+	}
+	for (auto townInfo : cb->getTownsInfo())
+	{
+		totalTownLevel += townInfo->getTownLevel();
+	}
+	logAi->info("Beginning: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString());
+#endif
 	for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++)
 	{
 		auto start = std::chrono::high_resolution_clock::now();
@@ -360,17 +379,21 @@ void Nullkiller::makeTurn()
 
 		Goals::TTask bestTask = taskptr(Goals::Invalid());
 
-		for(;i <= settings->getMaxPass(); i++)
+		while(true)
 		{
 			bestTasks.clear();
 
+			decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
 			decompose(bestTasks, sptr(BuyArmyBehavior()), 1);
 			decompose(bestTasks, sptr(BuildingBehavior()), 1);
 
 			bestTask = choseBestTask(bestTasks);
 
-			if(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY)
+			if(bestTask->priority > 0)
 			{
+#if NKAI_TRACE_LEVEL >= 1
+				logAi->info("Pass %d: Performing prio 0 task %s with prio: %d", i, bestTask->toString(), bestTask->priority);
+#endif
 				if(!executeTask(bestTask))
 					return;
 
@@ -382,7 +405,6 @@ void Nullkiller::makeTurn()
 			}
 		}
 
-		decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
 		decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1);
 		decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH);
 		decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH);
@@ -392,12 +414,24 @@ void Nullkiller::makeTurn()
 		if(!isOpenMap())
 			decompose(bestTasks, sptr(ExplorationBehavior()), MAX_DEPTH);
 
-		if(cb->getDate(Date::DAY) == 1 || heroManager->getHeroRoles().empty())
+		TTaskVec selectedTasks;
+#if NKAI_TRACE_LEVEL >= 1
+		int prioOfTask = 0;
+#endif
+		for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::DEFEND; ++prio)
 		{
-			decompose(bestTasks, sptr(StartupBehavior()), 1);
+#if NKAI_TRACE_LEVEL >= 1
+			prioOfTask = prio;
+#endif
+			selectedTasks = buildPlan(bestTasks, prio);
+			if (!selectedTasks.empty() || settings->isUseFuzzy())
+				break;
 		}
 
-		auto selectedTasks = buildPlan(bestTasks);
+		std::sort(selectedTasks.begin(), selectedTasks.end(), [](const TTask& a, const TTask& b) 
+		{
+			return a->priority > b->priority;
+		});
 
 		logAi->debug("Decision madel in %ld", timeElapsed(start));
 
@@ -438,7 +472,7 @@ void Nullkiller::makeTurn()
 					bestTask->priority);
 			}
 
-			if(bestTask->priority < MIN_PRIORITY)
+			if((settings->isUseFuzzy() && bestTask->priority < MIN_PRIORITY) || (!settings->isUseFuzzy() && bestTask->priority <= 0))
 			{
 				auto heroes = cb->getHeroesInfo();
 				auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
@@ -463,7 +497,9 @@ void Nullkiller::makeTurn()
 
 				continue;
 			}
-
+#if NKAI_TRACE_LEVEL >= 1
+			logAi->info("Pass %d: Performing prio %d task %s with prio: %d", i, prioOfTask, bestTask->toString(), bestTask->priority);
+#endif
 			if(!executeTask(bestTask))
 			{
 				if(hasAnySuccess)
@@ -471,13 +507,27 @@ void Nullkiller::makeTurn()
 				else
 					return;
 			}
-
 			hasAnySuccess = true;
 		}
 
+		hasAnySuccess |= handleTrading();
+
 		if(!hasAnySuccess)
 		{
 			logAi->trace("Nothing was done this turn. Ending turn.");
+#if NKAI_TRACE_LEVEL >= 1
+			totalHeroStrength = 0;
+			totalTownLevel = 0;
+			for (auto heroInfo : cb->getHeroesInfo())
+			{
+				totalHeroStrength += heroInfo->getTotalStrength();
+			}
+			for (auto townInfo : cb->getTownsInfo())
+			{
+				totalTownLevel += townInfo->getTownLevel();
+			}
+			logAi->info("End: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString());
+#endif
 			return;
 		}
 
@@ -554,4 +604,102 @@ void Nullkiller::lockResources(const TResources & res)
 	lockedResources += res;
 }
 
+bool Nullkiller::handleTrading()
+{
+	bool haveTraded = false;
+	bool shouldTryToTrade = true;
+	int marketId = -1;
+	for (auto town : cb->getTownsInfo())
+	{
+		if (town->hasBuiltSomeTradeBuilding())
+		{
+			marketId = town->id;
+		}
+	}
+	if (marketId == -1)
+		return false;
+	if (const CGObjectInstance* obj = cb->getObj(ObjectInstanceID(marketId), false))
+	{
+		if (const auto* m = dynamic_cast<const IMarket*>(obj))
+		{
+			while (shouldTryToTrade)
+			{
+				shouldTryToTrade = false;
+				buildAnalyzer->update();
+				TResources required = buildAnalyzer->getTotalResourcesRequired();
+				TResources income = buildAnalyzer->getDailyIncome();
+				TResources available = cb->getResourceAmount();
+#if NKAI_TRACE_LEVEL >= 2
+				logAi->debug("Available %s", available.toString());
+				logAi->debug("Required  %s", required.toString());
+#endif
+				int mostWanted = -1;
+				int mostExpendable = -1;
+				float minRatio = std::numeric_limits<float>::max();
+				float maxRatio = std::numeric_limits<float>::min();
+
+				for (int i = 0; i < required.size(); ++i)
+				{
+					if (required[i] <= 0)
+						continue;
+					float ratio = static_cast<float>(available[i]) / required[i];
+
+					if (ratio < minRatio) {
+						minRatio = ratio;
+						mostWanted = i;
+					}
+				}
+
+				for (int i = 0; i < required.size(); ++i)
+				{
+					float ratio = available[i];
+					if (required[i] > 0)
+						ratio = static_cast<float>(available[i]) / required[i];
+					else
+						ratio = available[i];
+
+					bool okToSell = false;
+
+					if (i == GameResID::GOLD)
+					{
+						if (income[i] > 0 && !buildAnalyzer->isGoldPressureHigh())
+							okToSell = true;
+					}
+					else
+					{
+						if (required[i] <= 0 && income[i] > 0)
+							okToSell = true;
+					}
+
+					if (ratio > maxRatio && okToSell) {
+						maxRatio = ratio;
+						mostExpendable = i;
+					}
+				}
+#if NKAI_TRACE_LEVEL >= 2
+				logAi->debug("mostExpendable: %d mostWanted: %d", mostExpendable, mostWanted);
+#endif
+				if (mostExpendable == mostWanted || mostWanted == -1 || mostExpendable == -1)
+					return false;
+
+				int toGive;
+				int toGet;
+				m->getOffer(mostExpendable, mostWanted, toGive, toGet, EMarketMode::RESOURCE_RESOURCE);
+				//logAi->info("Offer is: I get %d of %s for %d of %s at %s", toGet, mostWanted, toGive, mostExpendable, obj->getObjectName());
+				//TODO trade only as much as needed
+				if (toGive && toGive <= available[mostExpendable]) //don't try to sell 0 resources
+				{
+					cb->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), toGive);
+#if NKAI_TRACE_LEVEL >= 1
+					logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName());
+#endif
+					haveTraded = true;
+					shouldTryToTrade = true;
+				}
+			}
+		}
+	}
+	return haveTraded;
+}
+
 }

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

@@ -120,13 +120,14 @@ public:
 	ScanDepth getScanDepth() const { return scanDepth; }
 	bool isOpenMap() const { return openMap; }
 	bool isObjectGraphAllowed() const { return useObjectGraph; }
+	bool handleTrading();
 
 private:
 	void resetAiState();
 	void updateAiState(int pass, bool fast = false);
 	void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const;
 	Goals::TTask choseBestTask(Goals::TGoalVec & tasks) const;
-	Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks) const;
+	Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks, int priorityTier) const;
 	bool executeTask(Goals::TTask task);
 	bool areAffectedObjectsPresent(Goals::TTask task) const;
 	HeroRole getTaskRole(Goals::TTask task) const;

+ 406 - 52
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -15,6 +15,8 @@
 #include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
 #include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
 #include "../../../lib/mapObjects/MapObjects.h"
+#include "../../../lib/mapping/CMapDefines.h"
+#include "../../../lib/RoadHandler.h"
 #include "../../../lib/CCreatureHandler.h"
 #include "../../../lib/VCMI_Lib.h"
 #include "../../../lib/StartInfo.h"
@@ -33,11 +35,9 @@
 namespace NKAI
 {
 
-#define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter
-#define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
-const float MIN_CRITICAL_VALUE = 2.0f;
+constexpr float MIN_CRITICAL_VALUE = 2.0f;
 
-EvaluationContext::EvaluationContext(const Nullkiller * ai)
+EvaluationContext::EvaluationContext(const Nullkiller* ai)
 	: movementCost(0.0),
 	manaCost(0),
 	danger(0),
@@ -51,9 +51,22 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai)
 	heroRole(HeroRole::SCOUT),
 	turn(0),
 	strategicalValue(0),
+	conquestValue(0),
 	evaluator(ai),
 	enemyHeroDangerRatio(0),
-	armyGrowth(0)
+	threat(0),
+	armyGrowth(0),
+	armyInvolvement(0),
+	defenseValue(0),
+	isDefend(false),
+	threatTurns(INT_MAX),
+	involvesSailing(false),
+	isTradeBuilding(false),
+	isExchange(false),
+	isArmyUpgrade(false),
+	isHero(false),
+	isEnemy(false),
+	explorePriority(0)
 {
 }
 
@@ -155,10 +168,10 @@ uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHero
 	for (auto c : creatures)
 	{
 		//Only if hero has slot for this creature in the army
-		auto ccre = dynamic_cast<const CCreature*>(c.data.type);
+		auto ccre = dynamic_cast<const CCreature*>(c.data.getType());
 		if (hero->getSlotFor(ccre).validSlot() || duplicatingSlots > 0)
 		{
-			result += (c.data.type->getAIValue() * c.data.count) * c.chance;
+			result += (c.data.getType()->getAIValue() * c.data.count) * c.chance;
 		}
 		/*else
 		{
@@ -225,7 +238,7 @@ int getDwellingArmyCost(const CGObjectInstance * target)
 			auto creature = creLevel.second.back().toCreature();
 			auto creaturesAreFree = creature->getLevel() == 1;
 			if(!creaturesAreFree)
-				cost += creature->getRecruitCost(EGameResID::GOLD) * creLevel.first;
+				cost += creature->getFullRecruitCost().marketValue() * creLevel.first;
 		}
 	}
 
@@ -251,6 +264,8 @@ static uint64_t evaluateArtifactArmyValue(const CArtifact * art)
 
 	switch(art->aClass)
 	{
+	case CArtifact::EartClass::ART_TREASURE:
+		//FALL_THROUGH
 	case CArtifact::EartClass::ART_MINOR:
 		classValue = 1000;
 		break;
@@ -289,8 +304,10 @@ uint64_t RewardEvaluator::getArmyReward(
 	case Obj::CREATURE_GENERATOR3:
 	case Obj::CREATURE_GENERATOR4:
 		return getDwellingArmyValue(ai->cb.get(), target, checkGold);
+	case Obj::SPELL_SCROLL:
+		//FALL_THROUGH
 	case Obj::ARTIFACT:
-		return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact->artType);
+		return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact->getType());
 	case Obj::HERO:
 		return  relations == PlayerRelations::ENEMIES
 			? enemyArmyEliminationRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->getArmyStrength()
@@ -479,7 +496,7 @@ uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const
 	return result;
 }
 
-uint64_t RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const
+float RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const
 {
 	return ai->heroManager->getMagicStrength(hero) * 10000 * (1.0f - std::sqrt(static_cast<float>(hero->mana) / hero->manaLimit()));
 }
@@ -581,6 +598,54 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target, cons
 	return 0;
 }
 
+float RewardEvaluator::getConquestValue(const CGObjectInstance* target) const
+{
+	if (!target)
+		return 0;
+	if (target->getOwner() == ai->playerID)
+		return 0;
+	switch (target->ID)
+	{
+	case Obj::TOWN:
+	{
+		if (ai->buildAnalyzer->getDevelopmentInfo().empty())
+			return 10.0f;
+
+		auto town = dynamic_cast<const CGTownInstance*>(target);
+
+		if (town->getOwner() == ai->playerID)
+		{
+			auto armyIncome = townArmyGrowth(town);
+			auto dailyIncome = town->dailyIncome()[EGameResID::GOLD];
+
+			return std::min(1.0f, std::sqrt(armyIncome / 40000.0f)) + std::min(0.3f, dailyIncome / 10000.0f);
+		}
+
+		auto fortLevel = town->fortLevel();
+		auto booster = 1.0f;
+
+		if (town->hasCapitol())
+			return booster * 1.5;
+
+		if (fortLevel < CGTownInstance::CITADEL)
+			return booster * (town->hasFort() ? 1.0 : 0.8);
+		else
+			return booster * (fortLevel == CGTownInstance::CASTLE ? 1.4 : 1.2);
+	}
+
+	case Obj::HERO:
+		return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
+			? getEnemyHeroStrategicalValue(dynamic_cast<const CGHeroInstance*>(target))
+			: 0;
+
+	case Obj::KEYMASTER:
+		return 0.6f;
+
+	default:
+		return 0;
+	}
+}
+
 float RewardEvaluator::evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const
 {
 	auto rewardable = dynamic_cast<const CRewardableObject *>(hut);
@@ -705,7 +770,7 @@ int32_t getArmyCost(const CArmedInstance * army)
 
 	for(auto stack : army->Slots())
 	{
-		value += stack.second->getCreatureID().toCreature()->getRecruitCost(EGameResID::GOLD) * stack.second->count;
+		value += stack.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * stack.second->count;
 	}
 
 	return value;
@@ -786,7 +851,9 @@ public:
 		uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai);
 
 		evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength());
+		evaluationContext.conquestValue += 2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength();
 		evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero);
+		evaluationContext.isExchange = true;
 	}
 };
 
@@ -804,6 +871,7 @@ public:
 
 		evaluationContext.armyReward += upgradeValue;
 		evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength());
+		evaluationContext.isArmyUpgrade = true;
 	}
 };
 
@@ -818,22 +886,46 @@ public:
 		int tilesDiscovered = task->value;
 
 		evaluationContext.addNonCriticalStrategicalValue(0.03f * tilesDiscovered);
+		for (auto obj : evaluationContext.evaluator.ai->cb->getVisitableObjs(task->tile))
+		{
+			switch (obj->ID.num)
+			{
+			case Obj::MONOLITH_ONE_WAY_ENTRANCE:
+			case Obj::MONOLITH_TWO_WAY:
+			case Obj::SUBTERRANEAN_GATE:
+				evaluationContext.explorePriority = 1;
+				break;
+			case Obj::REDWOOD_OBSERVATORY:
+			case Obj::PILLAR_OF_FIRE:
+				evaluationContext.explorePriority = 2;
+				break;
+			}
+		}
+		if(evaluationContext.evaluator.ai->cb->getTile(task->tile)->roadType != RoadId::NO_ROAD)
+			evaluationContext.explorePriority = 1;
+		if (evaluationContext.explorePriority == 0)
+			evaluationContext.explorePriority = 3;
 	}
 };
 
 class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder
 {
 public:
-	void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	void buildEvaluationContext(EvaluationContext& evaluationContext, Goals::TSubgoal task) const override
 	{
-		if(task->goalType != Goals::STAY_AT_TOWN)
+		if (task->goalType != Goals::STAY_AT_TOWN)
 			return;
 
-		Goals::StayAtTown & stayAtTown = dynamic_cast<Goals::StayAtTown &>(*task);
+		Goals::StayAtTown& stayAtTown = dynamic_cast<Goals::StayAtTown&>(*task);
 
 		evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero());
-		evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
-		evaluationContext.movementCost += stayAtTown.getMovementWasted();
+		if (evaluationContext.armyReward == 0)
+			evaluationContext.isDefend = true;
+		else
+		{
+			evaluationContext.movementCost += stayAtTown.getMovementWasted();
+			evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
+		}
 	}
 };
 
@@ -844,15 +936,8 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin
 	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);
+		vstd::amax(evaluationContext.threat, enemyDanger.threat);
 	}
 }
 
@@ -896,6 +981,10 @@ public:
 		else
 			evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue);
 
+		evaluationContext.defenseValue = town->fortLevel();
+		evaluationContext.isDefend = true;
+		evaluationContext.threatTurns = treat.turn;
+
 		vstd::amax(evaluationContext.danger, defendTown.getTreat().danger);
 		addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
 	}
@@ -926,6 +1015,8 @@ public:
 		for(auto & node : path.nodes)
 		{
 			vstd::amax(costsPerHero[node.targetHero], node.cost);
+			if (node.layer == EPathfindingLayer::SAIL)
+				evaluationContext.involvesSailing = true;
 		}
 
 		for(auto pair : costsPerHero)
@@ -952,10 +1043,18 @@ public:
 			evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army);
 			evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole);
 			evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target));
+			evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
+			if (target->ID == Obj::HERO)
+				evaluationContext.isHero = true;
+			if (target->getOwner() != PlayerColor::NEUTRAL && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES)
+				evaluationContext.isEnemy = true;
 			evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
+			evaluationContext.armyInvolvement += army->getArmyCost();
+			if(evaluationContext.danger > 0)
+				evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength();
 		}
 
-		vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength());
+		vstd::amax(evaluationContext.armyLossPersentage, (float)path.getTotalArmyLoss() / (float)army->getArmyStrength());
 		addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
 		vstd::amax(evaluationContext.turn, path.turn());
 	}
@@ -996,6 +1095,7 @@ public:
 			evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost;
 			evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost;
 			evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost);
+			evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
 			evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost;
 			evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost;
 			evaluationContext.movementCost += objInfo.second.movementCost / boost;
@@ -1021,6 +1121,14 @@ public:
 		Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast<Goals::ExchangeSwapTownHeroes &>(*task);
 		const CGHeroInstance * garrisonHero = swapCommand.getGarrisonHero();
 
+		logAi->trace("buildEvaluationContext ExchangeSwapTownHeroesContextBuilder %s affected objects: %d", swapCommand.toString(), swapCommand.getAffectedObjects().size());
+		for (auto obj : swapCommand.getAffectedObjects())
+		{
+			logAi->trace("affected object: %s", evaluationContext.evaluator.ai->cb->getObj(obj)->getObjectName());
+		}
+		if (garrisonHero)
+			logAi->debug("with %s and %d", garrisonHero->getNameTranslated(), int(swapCommand.getLockingReason()));
+
 		if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE)
 		{
 			auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero);
@@ -1029,6 +1137,9 @@ public:
 			evaluationContext.movementCost += mpLeft;
 			evaluationContext.movementCostByRole[defenderRole] += mpLeft;
 			evaluationContext.heroRole = defenderRole;
+			evaluationContext.isDefend = true;
+			evaluationContext.armyInvolvement = garrisonHero->getArmyStrength();
+			logAi->debug("evaluationContext.isDefend: %d", evaluationContext.isDefend);
 		}
 	}
 };
@@ -1072,8 +1183,14 @@ public:
 		evaluationContext.goldReward += 7 * bi.dailyIncome[EGameResID::GOLD] / 2; // 7 day income but half we already have
 		evaluationContext.heroRole = HeroRole::MAIN;
 		evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount;
-		evaluationContext.goldCost += bi.buildCostWithPrerequisites[EGameResID::GOLD];
+		int32_t cost = bi.buildCost[EGameResID::GOLD];
+		evaluationContext.goldCost += cost;
 		evaluationContext.closestWayRatio = 1;
+		evaluationContext.buildingCost += bi.buildCostWithPrerequisites;
+		if (bi.id == BuildingID::MARKETPLACE || bi.dailyIncome[EGameResID::WOOD] > 0)
+			evaluationContext.isTradeBuilding = true;
+
+		logAi->trace("Building costs for %s : %s MarketValue: %d",bi.toString(), evaluationContext.buildingCost.toString(), evaluationContext.buildingCost.marketValue());
 
 		if(bi.creatureID != CreatureID::NONE)
 		{
@@ -1100,7 +1217,18 @@ public:
 		else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5)
 		{
 			evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1);
+			for (auto hero : evaluationContext.evaluator.ai->cb->getHeroesInfo())
+			{
+				evaluationContext.armyInvolvement += hero->getArmyCost();
+			}
 		}
+		int sameTownBonus = 0;
+		for (auto town : evaluationContext.evaluator.ai->cb->getTownsInfo())
+		{
+			if (buildThis.town->getFaction() == town->getFaction())
+				sameTownBonus += town->getTownLevel();
+		}
+		evaluationContext.armyReward *= sameTownBonus;
 		
 		if(evaluationContext.goldReward)
 		{
@@ -1120,7 +1248,7 @@ public:
 
 uint64_t RewardEvaluator::getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const
 {
-	if(ai->buildAnalyzer->hasAnyBuilding(town->getFaction(), bi.id))
+	if(ai->buildAnalyzer->hasAnyBuilding(town->getFactionID(), bi.id))
 		return 0;
 
 	auto creaturesToUpgrade = ai->armyManager->getTotalCreaturesAvailable(bi.baseCreatureID);
@@ -1162,6 +1290,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
 	for(auto subgoal : parts)
 	{
 		context.goldCost += subgoal->goldCost;
+		context.buildingCost += subgoal->buildingCost;
 
 		for(auto builder : evaluationContextBuilders)
 		{
@@ -1172,7 +1301,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
 	return context;
 }
 
-float PriorityEvaluator::evaluate(Goals::TSubgoal task)
+float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 {
 	auto evaluationContext = buildEvaluationContext(task);
 
@@ -1185,36 +1314,256 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
 	
 	double result = 0;
 
-	try
+	if (ai->settings->isUseFuzzy())
 	{
-		armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage);
-		heroRoleVariable->setValue(evaluationContext.heroRole);
-		mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
-		scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
-		goldRewardVariable->setValue(goldRewardPerTurn);
-		armyRewardVariable->setValue(evaluationContext.armyReward);
-		armyGrowthVariable->setValue(evaluationContext.armyGrowth);
-		skillRewardVariable->setValue(evaluationContext.skillReward);
-		dangerVariable->setValue(evaluationContext.danger);
-		rewardTypeVariable->setValue(rewardType);
-		closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio);
-		strategicalValueVariable->setValue(evaluationContext.strategicalValue);
-		goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure());
-		goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f));
-		turnVariable->setValue(evaluationContext.turn);
-		fearVariable->setValue(evaluationContext.enemyHeroDangerRatio);
-
-		engine->process();
-
-		result = value->getValue();
+		float fuzzyResult = 0;
+		try
+		{
+			armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage);
+			heroRoleVariable->setValue(evaluationContext.heroRole);
+			mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
+			scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
+			goldRewardVariable->setValue(goldRewardPerTurn);
+			armyRewardVariable->setValue(evaluationContext.armyReward);
+			armyGrowthVariable->setValue(evaluationContext.armyGrowth);
+			skillRewardVariable->setValue(evaluationContext.skillReward);
+			dangerVariable->setValue(evaluationContext.danger);
+			rewardTypeVariable->setValue(rewardType);
+			closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio);
+			strategicalValueVariable->setValue(evaluationContext.strategicalValue);
+			goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure());
+			goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f));
+			turnVariable->setValue(evaluationContext.turn);
+			fearVariable->setValue(evaluationContext.enemyHeroDangerRatio);
+
+			engine->process();
+
+			fuzzyResult = value->getValue();
+		}
+		catch (fl::Exception& fe)
+		{
+			logAi->error("evaluate VisitTile: %s", fe.getWhat());
+		}
+		result = fuzzyResult;
 	}
-	catch(fl::Exception & fe)
+	else
 	{
-		logAi->error("evaluate VisitTile: %s", fe.getWhat());
+		float score = 0;
+		float maxWillingToLose = ai->cb->getTownsInfo().empty() || (evaluationContext.isDefend && evaluationContext.threatTurns == 0) ? 1 : 0.25;
+
+		bool arriveNextWeek = false;
+		if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7)
+			arriveNextWeek = true;
+
+#if NKAI_TRACE_LEVEL >= 2
+		logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, explorePriority: %d isDefend: %d",
+			priorityTier,
+			task->toString(),
+			evaluationContext.armyLossPersentage,
+			(int)evaluationContext.turn,
+			evaluationContext.movementCostByRole[HeroRole::MAIN],
+			evaluationContext.movementCostByRole[HeroRole::SCOUT],
+			goldRewardPerTurn,
+			evaluationContext.goldCost,
+			evaluationContext.armyReward,
+			evaluationContext.armyGrowth,
+			evaluationContext.skillReward,
+			evaluationContext.danger,
+			evaluationContext.threatTurns,
+			evaluationContext.threat,
+			evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
+			evaluationContext.strategicalValue,
+			evaluationContext.conquestValue,
+			evaluationContext.closestWayRatio,
+			evaluationContext.enemyHeroDangerRatio,
+			evaluationContext.explorePriority,
+			evaluationContext.isDefend);
+#endif
+
+		switch (priorityTier)
+		{
+			case PriorityTier::INSTAKILL: //Take towns / kill heroes in immediate reach
+			{
+				if (evaluationContext.turn > 0)
+					return 0;
+				if(evaluationContext.conquestValue > 0)
+					score = 1000;
+				if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
+					return 0;
+				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+					return 0;
+				score *= evaluationContext.closestWayRatio;
+				if (evaluationContext.movementCost > 0)
+					score /= evaluationContext.movementCost;
+				break;
+			}
+			case PriorityTier::INSTADEFEND: //Defend immediately threatened towns
+			{
+				if (evaluationContext.isDefend && evaluationContext.threatTurns == 0 && evaluationContext.turn == 0)
+					score = evaluationContext.armyInvolvement;
+				if (evaluationContext.isEnemy && maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+					return 0;
+				score *= evaluationContext.closestWayRatio;
+				break;
+			}
+			case PriorityTier::KILL: //Take towns / kill heroes that are further away
+			{
+				if (evaluationContext.turn > 0 && evaluationContext.isHero)
+					return 0;
+				if (arriveNextWeek && evaluationContext.isEnemy)
+					return 0;
+				if (evaluationContext.conquestValue > 0)
+					score = 1000;
+				if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
+					return 0;
+				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+					return 0;
+				score *= evaluationContext.closestWayRatio;
+				if (evaluationContext.movementCost > 0)
+					score /= evaluationContext.movementCost;
+				break;
+			}
+			case PriorityTier::UPGRADE:
+			{
+				if (!evaluationContext.isArmyUpgrade)
+					return 0;
+				if (evaluationContext.enemyHeroDangerRatio > 1)
+					return 0;
+				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+					return 0;
+				score = 1000;
+				score *= evaluationContext.closestWayRatio;
+				if (evaluationContext.movementCost > 0)
+					score /= evaluationContext.movementCost;
+				break;
+			}
+			case PriorityTier::HIGH_PRIO_EXPLORE:
+			{
+				if (evaluationContext.enemyHeroDangerRatio > 1)
+					return 0;
+				if (evaluationContext.explorePriority != 1)
+					return 0;
+				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+					return 0;
+				score = 1000;
+				score *= evaluationContext.closestWayRatio;
+				if (evaluationContext.movementCost > 0)
+					score /= evaluationContext.movementCost;
+				break;
+			}
+			case PriorityTier::HUNTER_GATHER: //Collect guarded stuff
+			{
+				if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend)
+					return 0;
+				if (evaluationContext.buildingCost.marketValue() > 0)
+					return 0;
+				if (evaluationContext.isDefend && (evaluationContext.enemyHeroDangerRatio < 1 || evaluationContext.threatTurns > 0 || evaluationContext.turn > 0))
+					return 0;
+				if (evaluationContext.explorePriority == 3)
+					return 0;
+				if (evaluationContext.isArmyUpgrade)
+					return 0;
+				if ((evaluationContext.enemyHeroDangerRatio > 0 && arriveNextWeek) || evaluationContext.enemyHeroDangerRatio > 1)
+					return 0;
+				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+					return 0;
+				score += evaluationContext.strategicalValue * 1000;
+				score += evaluationContext.goldReward;
+				score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
+				score += evaluationContext.armyReward;
+				score += evaluationContext.armyGrowth;
+				score -= evaluationContext.goldCost;
+				score -= evaluationContext.armyInvolvement * evaluationContext.armyLossPersentage;
+				if (score > 0)
+				{
+					score = 1000;
+					score *= evaluationContext.closestWayRatio;
+					if (evaluationContext.movementCost > 0)
+						score /= evaluationContext.movementCost;
+				}
+				break;
+			}
+			case PriorityTier::LOW_PRIO_EXPLORE:
+			{
+				if (evaluationContext.enemyHeroDangerRatio > 1)
+					return 0;
+				if (evaluationContext.explorePriority != 3)
+					return 0;
+				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+					return 0;
+				score = 1000;
+				score *= evaluationContext.closestWayRatio;
+				if (evaluationContext.movementCost > 0)
+					score /= evaluationContext.movementCost;
+				break;
+			}
+			case PriorityTier::DEFEND: //Defend whatever if nothing else is to do
+			{
+				if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange)
+					return 0;
+				if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade)
+					score = 1000;
+				score *= evaluationContext.closestWayRatio;
+				score /= (evaluationContext.turn + 1);
+				break;
+			}
+			case PriorityTier::BUILDINGS: //For buildings and buying army
+			{
+				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+					return 0;
+				//If we already have locked resources, we don't look at other buildings
+				if (ai->getLockedResources().marketValue() > 0)
+					return 0;
+				score += evaluationContext.conquestValue * 1000;
+				score += evaluationContext.strategicalValue * 1000;
+				score += evaluationContext.goldReward;
+				score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
+				score += evaluationContext.armyReward;
+				score += evaluationContext.armyGrowth;
+				if (evaluationContext.buildingCost.marketValue() > 0)
+				{
+					if (!evaluationContext.isTradeBuilding && ai->getFreeResources()[EGameResID::WOOD] - evaluationContext.buildingCost[EGameResID::WOOD] < 5 && ai->buildAnalyzer->getDailyIncome()[EGameResID::WOOD] < 1)
+					{
+						logAi->trace("Should make sure to build market-place instead of %s", task->toString());
+						for (auto town : ai->cb->getTownsInfo())
+						{
+							if (!town->hasBuiltSomeTradeBuilding())
+								return 0;
+						}
+					}
+					score += 1000;
+					auto resourcesAvailable = evaluationContext.evaluator.ai->getFreeResources();
+					auto income = ai->buildAnalyzer->getDailyIncome();
+					if(ai->buildAnalyzer->isGoldPressureHigh())
+						score /= evaluationContext.buildingCost.marketValue();
+					if (!resourcesAvailable.canAfford(evaluationContext.buildingCost))
+					{
+						TResources needed = evaluationContext.buildingCost - resourcesAvailable;
+						needed.positive();
+						int turnsTo = needed.maxPurchasableCount(income);
+						if (turnsTo == INT_MAX)
+							return 0;
+						else
+							score /= turnsTo;
+					}
+				}
+				else
+				{
+					if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend && vstd::isAlmostZero(evaluationContext.conquestValue))
+						return 0;
+				}
+				break;
+			}
+		}
+		result = score;
+		//TODO: Figure out the root cause for why evaluationContext.closestWayRatio has become -nan(ind).
+		if (std::isnan(result))
+			return 0;
 	}
 
 #if NKAI_TRACE_LEVEL >= 2
-	logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f",
+	logAi->trace("priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, result %f",
+		priorityTier,
 		task->toString(),
 		evaluationContext.armyLossPersentage,
 		(int)evaluationContext.turn,
@@ -1223,9 +1572,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
 		goldRewardPerTurn,
 		evaluationContext.goldCost,
 		evaluationContext.armyReward,
+		evaluationContext.armyGrowth,
+		evaluationContext.skillReward,
 		evaluationContext.danger,
+		evaluationContext.threatTurns,
+		evaluationContext.threat,
 		evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
 		evaluationContext.strategicalValue,
+		evaluationContext.conquestValue,
 		evaluationContext.closestWayRatio,
 		evaluationContext.enemyHeroDangerRatio,
 		result);

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

@@ -41,6 +41,7 @@ public:
 	float getResourceRequirementStrength(int resType) const;
 	float getResourceRequirementStrength(const TResources & res) const;
 	float getStrategicalValue(const CGObjectInstance * target, const CGHeroInstance * hero = nullptr) const;
+	float getConquestValue(const CGObjectInstance* target) const;
 	float getTotalResourceRequirementStrength(int resType) const;
 	float evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const;
 	float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * hero, HeroRole role) const;
@@ -48,7 +49,7 @@ public:
 	uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const;
 	const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const;
 	uint64_t townArmyGrowth(const CGTownInstance * town) const;
-	uint64_t getManaRecoveryArmyReward(const CGHeroInstance * hero) const;
+	float getManaRecoveryArmyReward(const CGHeroInstance * hero) const;
 };
 
 struct DLL_EXPORT EvaluationContext
@@ -65,10 +66,24 @@ struct DLL_EXPORT EvaluationContext
 	int32_t goldCost;
 	float skillReward;
 	float strategicalValue;
+	float conquestValue;
 	HeroRole heroRole;
 	uint8_t turn;
 	RewardEvaluator evaluator;
 	float enemyHeroDangerRatio;
+	float threat;
+	float armyInvolvement;
+	int defenseValue;
+	bool isDefend;
+	int threatTurns;
+	TResources buildingCost;
+	bool involvesSailing;
+	bool isTradeBuilding;
+	bool isExchange;
+	bool isArmyUpgrade;
+	bool isHero;
+	bool isEnemy;
+	int explorePriority;
 
 	EvaluationContext(const Nullkiller * ai);
 
@@ -91,7 +106,20 @@ public:
 	~PriorityEvaluator();
 	void initVisitTile();
 
-	float evaluate(Goals::TSubgoal task);
+	float evaluate(Goals::TSubgoal task, int priorityTier = BUILDINGS);
+
+	enum PriorityTier : int32_t
+	{
+		BUILDINGS = 0,
+		INSTAKILL,
+		INSTADEFEND,
+		KILL,
+		UPGRADE,
+		HIGH_PRIO_EXPLORE,
+		HUNTER_GATHER,
+		LOW_PRIO_EXPLORE,
+		DEFEND
+	};
 
 private:
 	const Nullkiller * ai;

+ 29 - 44
AI/Nullkiller/Engine/Settings.cpp

@@ -11,6 +11,8 @@
 #include <limits>
 
 #include "Settings.h"
+
+#include "../../../lib/constants/StringConstants.h"
 #include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h"
 #include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
 #include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
@@ -22,56 +24,39 @@
 
 namespace NKAI
 {
-	Settings::Settings()
+	Settings::Settings(int difficultyLevel)
 		: maxRoamingHeroes(8),
 		mainHeroTurnDistanceLimit(10),
 		scoutHeroTurnDistanceLimit(5),
-		maxGoldPressure(0.3f), 
+		maxGoldPressure(0.3f),
+		retreatThresholdRelative(0.3),
+		retreatThresholdAbsolute(10000),
+		safeAttackRatio(1.1),
 		maxpass(10),
+		pathfinderBucketsCount(1),
+		pathfinderBucketSize(32),
 		allowObjectGraph(true),
 		useTroopsFromGarrisons(false),
-		openMap(true)
+		openMap(true),
+		useFuzzy(false)
 	{
-		JsonNode node = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings");
-
-		if(node.Struct()["maxRoamingHeroes"].isNumber())
-		{
-			maxRoamingHeroes = node.Struct()["maxRoamingHeroes"].Integer();
-		}
-
-		if(node.Struct()["mainHeroTurnDistanceLimit"].isNumber())
-		{
-			mainHeroTurnDistanceLimit = node.Struct()["mainHeroTurnDistanceLimit"].Integer();
-		}
-
-		if(node.Struct()["scoutHeroTurnDistanceLimit"].isNumber())
-		{
-			scoutHeroTurnDistanceLimit = node.Struct()["scoutHeroTurnDistanceLimit"].Integer();
-		}
-
-		if(node.Struct()["maxpass"].isNumber())
-		{
-			maxpass = node.Struct()["maxpass"].Integer();
-		}
-
-		if(node.Struct()["maxGoldPressure"].isNumber())
-		{
-			maxGoldPressure = node.Struct()["maxGoldPressure"].Float();
-		}
-
-		if(!node.Struct()["allowObjectGraph"].isNull())
-		{
-			allowObjectGraph = node.Struct()["allowObjectGraph"].Bool();
-		}
-
-		if(!node.Struct()["openMap"].isNull())
-		{
-			openMap = node.Struct()["openMap"].Bool();
-		}
-
-		if(!node.Struct()["useTroopsFromGarrisons"].isNull())
-		{
-			useTroopsFromGarrisons = node.Struct()["useTroopsFromGarrisons"].Bool();
-		}
+		const std::string & difficultyName = GameConstants::DIFFICULTY_NAMES[difficultyLevel];
+		const JsonNode & rootNode = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings");
+		const JsonNode & node = rootNode[difficultyName];
+
+		maxRoamingHeroes = node["maxRoamingHeroes"].Integer();
+		mainHeroTurnDistanceLimit = node["mainHeroTurnDistanceLimit"].Integer();
+		scoutHeroTurnDistanceLimit = node["scoutHeroTurnDistanceLimit"].Integer();
+		maxpass = node["maxpass"].Integer();
+		pathfinderBucketsCount = node["pathfinderBucketsCount"].Integer();
+		pathfinderBucketSize = node["pathfinderBucketSize"].Integer();
+		maxGoldPressure = node["maxGoldPressure"].Float();
+		retreatThresholdRelative = node["retreatThresholdRelative"].Float();
+		retreatThresholdAbsolute = node["retreatThresholdAbsolute"].Float();
+		safeAttackRatio = node["safeAttackRatio"].Float();
+		allowObjectGraph = node["allowObjectGraph"].Bool();
+		openMap = node["openMap"].Bool();
+		useFuzzy = node["useFuzzy"].Bool();
+		useTroopsFromGarrisons = node["useTroopsFromGarrisons"].Bool();
 	}
 }

+ 13 - 1
AI/Nullkiller/Engine/Settings.h

@@ -25,21 +25,33 @@ namespace NKAI
 		int mainHeroTurnDistanceLimit;
 		int scoutHeroTurnDistanceLimit;
 		int maxpass;
+		int pathfinderBucketsCount;
+		int pathfinderBucketSize;
 		float maxGoldPressure;
+		float retreatThresholdRelative;
+		float retreatThresholdAbsolute;
+		float safeAttackRatio;
 		bool allowObjectGraph;
 		bool useTroopsFromGarrisons;
 		bool openMap;
+		bool useFuzzy;
 
 	public:
-		Settings();
+		explicit Settings(int difficultyLevel);
 
 		int getMaxPass() const { return maxpass; }
 		float getMaxGoldPressure() const { return maxGoldPressure; }
+		float getRetreatThresholdRelative() const { return retreatThresholdRelative; }
+		float getRetreatThresholdAbsolute() const { return retreatThresholdAbsolute; }
+		float getSafeAttackRatio() const { return safeAttackRatio; }
 		int getMaxRoamingHeroes() const { return maxRoamingHeroes; }
 		int getMainHeroTurnDistanceLimit() const { return mainHeroTurnDistanceLimit; }
 		int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; }
+		int getPathfinderBucketsCount() const { return pathfinderBucketsCount; }
+		int getPathfinderBucketSize() const { return pathfinderBucketSize; }
 		bool isObjectGraphAllowed() const { return allowObjectGraph; }
 		bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; }
 		bool isOpenMap() const { return openMap; }
+		bool isUseFuzzy() const { return useFuzzy; }
 	};
 }

+ 1 - 0
AI/Nullkiller/Goals/AbstractGoal.h

@@ -104,6 +104,7 @@ namespace Goals
 		bool isAbstract; SETTER(bool, isAbstract)
 		int value; SETTER(int, value)
 		ui64 goldCost; SETTER(ui64, goldCost)
+		TResources buildingCost; SETTER(TResources, buildingCost)
 		int resID; SETTER(int, resID)
 		int objid; SETTER(int, objid)
 		int aid; SETTER(int, aid)

+ 3 - 0
AI/Nullkiller/Goals/AdventureSpellCast.cpp

@@ -53,6 +53,9 @@ void AdventureSpellCast::accept(AIGateway * ai)
 			throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->getNameTranslated());
 	}
 
+	if (hero->inTownGarrison)
+		ai->myCb->swapGarrisonHero(hero->visitedTown);
+
 	auto wait = cb->waitTillRealize;
 
 	cb->waitTillRealize = true;

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

@@ -23,7 +23,7 @@ BuildThis::BuildThis(BuildingID Bid, const CGTownInstance * tid)
 	: ElementarGoal(Goals::BUILD_STRUCTURE)
 {
 	buildingInfo = BuildingInfo(
-		tid->town->buildings.at(Bid),
+		tid->getTown()->buildings.at(Bid),
 		nullptr,
 		CreatureID::NONE,
 		tid,
@@ -52,7 +52,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)->getNameTranslated(), town->getNameTranslated(), town->pos.toString());
+				ai->playerID, town->getTown()->buildings.at(b)->getNameTranslated(), town->getNameTranslated(), town->anchorPos().toString());
 			cb->buildBuilding(town, b);
 
 			return;

+ 1 - 1
AI/Nullkiller/Goals/CaptureObject.h

@@ -31,7 +31,7 @@ namespace Goals
 		{
 			objid = obj->id.getNum();
 			tile = obj->visitablePos();
-			name = obj->typeName;
+			name = obj->getTypeName();
 		}
 
 		bool operator==(const CaptureObject & other) const override;

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

@@ -90,9 +90,12 @@ void ExchangeSwapTownHeroes::accept(AIGateway * ai)
 	
 	if(!town->garrisonHero)
 	{
-		while(upperArmy->stacksCount() != 0)
+		if (!garrisonHero->canBeMergedWith(*town))
 		{
-			cb->dismissCreature(upperArmy, upperArmy->Slots().begin()->first);
+			while (upperArmy->stacksCount() != 0)
+			{
+				cb->dismissCreature(upperArmy, upperArmy->Slots().begin()->first);
+			}
 		}
 	}
 	

+ 3 - 1
AI/Nullkiller/Goals/ExecuteHeroChain.cpp

@@ -22,6 +22,7 @@ ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance *
 {
 	hero = path.targetHero;
 	tile = path.targetTile();
+	closestWayRatio = 1;
 
 	if(obj)
 	{
@@ -30,7 +31,7 @@ ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance *
 #if NKAI_TRACE_LEVEL >= 1
 		targetName = obj->getObjectName() + tile.toString();
 #else
-		targetName = obj->typeName + tile.toString();
+		targetName = obj->getTypeName() + tile.toString();
 #endif
 	}
 	else
@@ -85,6 +86,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 
 	ai->nullkiller->setActive(chainPath.targetHero, tile);
 	ai->nullkiller->setTargetObject(objid);
+	ai->nullkiller->objectClusterizer->reset();
 
 	auto targetObject = ai->myCb->getObj(static_cast<ObjectInstanceID>(objid), false);
 

+ 1 - 0
AI/Nullkiller/Goals/RecruitHero.cpp

@@ -73,6 +73,7 @@ void RecruitHero::accept(AIGateway * ai)
 		std::unique_lock lockGuard(ai->nullkiller->aiStateMutex);
 
 		ai->nullkiller->heroManager->update();
+		ai->nullkiller->objectClusterizer->reset();
 	}
 }
 

+ 1 - 0
AI/Nullkiller/Goals/RecruitHero.h

@@ -44,6 +44,7 @@ namespace Goals
 		}
 
 		std::string toString() const override;
+		const CGHeroInstance* getHero() const override { return heroToBuy; }
 		void accept(AIGateway * ai) override;
 	};
 }

+ 2 - 6
AI/Nullkiller/Goals/StayAtTown.cpp

@@ -36,16 +36,12 @@ std::string StayAtTown::toString() const
 {
 	return "Stay at town " + town->getNameTranslated()
 		+ " hero " + hero->getNameTranslated()
-		+ ", mana: " + std::to_string(hero->mana);
+		+ ", mana: " + std::to_string(hero->mana)
+		+ " / " + std::to_string(hero->manaLimit());
 }
 
 void StayAtTown::accept(AIGateway * ai)
 {
-	if(hero->visitedTown != town)
-	{
-		logAi->error("Hero %s expected visiting town %s", hero->getNameTranslated(), town->getNameTranslated());
-	}
-
 	ai->nullkiller->lockHero(hero, HeroLockedReason::DEFENCE);
 }
 

+ 1 - 1
AI/Nullkiller/Helpers/ExplorationHelper.cpp

@@ -175,7 +175,7 @@ void ExplorationHelper::scanTile(const int3 & tile)
 				continue;
 			}
 
-			if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger()))
+			if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger(), ai->settings->getSafeAttackRatio()))
 			{
 				bestGoal = goal;
 				bestValue = ourValue;

+ 33 - 13
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -39,17 +39,17 @@ const uint64_t CHAIN_MAX_DEPTH = 4;
 
 const bool DO_NOT_SAVE_TO_COMMITTED_TILES = false;
 
-AISharedStorage::AISharedStorage(int3 sizes)
+AISharedStorage::AISharedStorage(int3 sizes, int numChains)
 {
 	if(!shared){
 		shared.reset(new boost::multi_array<AIPathNode, 4>(
-			boost::extents[sizes.z][sizes.x][sizes.y][AIPathfinding::NUM_CHAINS]));
+			boost::extents[sizes.z][sizes.x][sizes.y][numChains]));
 
 		nodes = shared;
 
 		foreach_tile_pos([&](const int3 & pos)
 			{
-				for(auto i = 0; i < AIPathfinding::NUM_CHAINS; i++)
+				for(auto i = 0; i < numChains; i++)
 				{
 					auto & node = get(pos)[i];
 						
@@ -92,8 +92,18 @@ void AIPathNode::addSpecialAction(std::shared_ptr<const SpecialAction> action)
 	}
 }
 
+int AINodeStorage::getBucketCount() const
+{
+	return ai->settings->getPathfinderBucketsCount();
+}
+
+int AINodeStorage::getBucketSize() const
+{
+	return ai->settings->getPathfinderBucketSize();
+}
+
 AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & Sizes)
-	: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes)
+	: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes, ai->settings->getPathfinderBucketSize() * ai->settings->getPathfinderBucketsCount())
 {
 	accessibility = std::make_unique<boost::multi_array<EPathAccessibility, 4>>(
 		boost::extents[sizes.z][sizes.x][sizes.y][EPathfindingLayer::NUM_LAYERS]);
@@ -130,10 +140,10 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta
 				for(pos.y = 0; pos.y < sizes.y; ++pos.y)
 				{
 					const TerrainTile & tile = gs->map->getTile(pos);
-					if (!tile.terType->isPassable())
+					if (!tile.getTerrain()->isPassable())
 						continue;
 
-					if (tile.terType->isWater())
+					if (tile.isWater())
 					{
 						resetTile(pos, ELayer::SAIL, PathfinderUtil::evaluateAccessibility<ELayer::SAIL>(pos, tile, fow, player, gs));
 						if (useFlying)
@@ -169,8 +179,8 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
 	const EPathfindingLayer layer, 
 	const ChainActor * actor)
 {
-	int bucketIndex = ((uintptr_t)actor + static_cast<uint32_t>(layer)) % AIPathfinding::BUCKET_COUNT;
-	int bucketOffset = bucketIndex * AIPathfinding::BUCKET_SIZE;
+	int bucketIndex = ((uintptr_t)actor + static_cast<uint32_t>(layer)) % ai->settings->getPathfinderBucketsCount();
+	int bucketOffset = bucketIndex * ai->settings->getPathfinderBucketSize();
 	auto chains = nodes.get(pos);
 
 	if(blocked(pos, layer))
@@ -178,7 +188,7 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
 		return std::nullopt;
 	}
 
-	for(auto i = AIPathfinding::BUCKET_SIZE - 1; i >= 0; i--)
+	for(auto i = ai->settings->getPathfinderBucketSize() - 1; i >= 0; i--)
 	{
 		AIPathNode & node = chains[i + bucketOffset];
 
@@ -486,8 +496,8 @@ public:
 		AINodeStorage & storage, const std::vector<int3> & tiles, uint64_t chainMask, int heroChainTurn)
 		:existingChains(), newChains(), delayedWork(), storage(storage), chainMask(chainMask), heroChainTurn(heroChainTurn), heroChain(), tiles(tiles)
 	{
-		existingChains.reserve(AIPathfinding::NUM_CHAINS);
-		newChains.reserve(AIPathfinding::NUM_CHAINS);
+		existingChains.reserve(storage.getBucketCount() * storage.getBucketSize());
+		newChains.reserve(storage.getBucketCount() * storage.getBucketSize());
 	}
 
 	void execute(const tbb::blocked_range<size_t>& r)
@@ -719,6 +729,7 @@ void HeroChainCalculationTask::calculateHeroChain(
 		if(node->action == EPathNodeAction::BATTLE
 			|| node->action == EPathNodeAction::TELEPORT_BATTLE
 			|| node->action == EPathNodeAction::TELEPORT_NORMAL
+			|| node->action == EPathNodeAction::DISEMBARK
 			|| node->action == EPathNodeAction::TELEPORT_BLOCKING_VISIT)
 		{
 			continue;
@@ -961,7 +972,7 @@ void AINodeStorage::setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes)
 		// do not allow our own heroes in garrison to act on map
 		if(hero.first->getOwner() == ai->playerID
 			&& hero.first->inTownGarrison
-			&& (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached()))
+			&& (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached(false)))
 		{
 			continue;
 		}
@@ -1196,6 +1207,11 @@ void AINodeStorage::calculateTownPortal(
 					continue;
 			}
 
+			if (targetTown->visitingHero
+				&& (targetTown->visitingHero.get()->getFactionID() != actor->hero->getFactionID()
+					|| targetTown->getUpperArmy()->stacksCount()))
+				continue;
+
 			auto nodeOptional = townPortalFinder.createTownPortalNode(targetTown);
 
 			if(nodeOptional)
@@ -1418,6 +1434,10 @@ void AINodeStorage::calculateChainInfo(std::vector<AIPath> & paths, const int3 &
 		path.heroArmy = node.actor->creatureSet;
 		path.armyLoss = node.armyLoss;
 		path.targetObjectDanger = ai->dangerEvaluator->evaluateDanger(pos, path.targetHero, !node.actor->allowBattle);
+		for (auto pathNode : path.nodes)
+		{
+			path.targetObjectDanger = std::max(ai->dangerEvaluator->evaluateDanger(pathNode.coord, path.targetHero, !node.actor->allowBattle), path.targetObjectDanger);
+		}
 
 		if(path.targetObjectDanger > 0)
 		{
@@ -1564,7 +1584,7 @@ uint8_t AIPath::turn() const
 
 uint64_t AIPath::getHeroStrength() const
 {
-	return targetHero->getFightingStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy);
+	return targetHero->getHeroStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy);
 }
 
 uint64_t AIPath::getTotalDanger() const

+ 5 - 5
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -29,9 +29,6 @@ namespace NKAI
 {
 namespace AIPathfinding
 {
-	const int BUCKET_COUNT = 3;
-	const int BUCKET_SIZE = 7;
-	const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE;
 	const int CHAIN_MAX_DEPTH = 4;
 }
 
@@ -157,7 +154,7 @@ public:
 	static boost::mutex locker;
 	static uint32_t version;
 
-	AISharedStorage(int3 mapSize);
+	AISharedStorage(int3 sizes, int numChains);
 	~AISharedStorage();
 
 	STRONG_INLINE
@@ -197,6 +194,9 @@ public:
 	bool selectFirstActor();
 	bool selectNextActor();
 
+	int getBucketCount() const;
+	int getBucketSize() const;
+
 	std::vector<CGPathNode *> getInitialNodes() override;
 
 	virtual void calculateNeighbours(
@@ -298,7 +298,7 @@ public:
 
 	inline int getBucket(const ChainActor * actor) const
 	{
-		return ((uintptr_t)actor * 395) % AIPathfinding::BUCKET_COUNT;
+		return ((uintptr_t)actor * 395) % getBucketCount();
 	}
 
 	void calculateTownPortalTeleportations(std::vector<CGPathNode *> & neighbours);

+ 1 - 1
AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp

@@ -13,7 +13,7 @@
 #include "Rules/AIMovementAfterDestinationRule.h"
 #include "Rules/AIMovementToDestinationRule.h"
 #include "Rules/AIPreviousNodeRule.h"
-#include "../Engine//Nullkiller.h"
+#include "../Engine/Nullkiller.h"
 
 #include "../../../lib/pathfinder/CPathfinder.h"
 

+ 3 - 3
AI/Nullkiller/Pathfinding/Actors.cpp

@@ -46,7 +46,7 @@ ChainActor::ChainActor(const CGHeroInstance * hero, HeroRole heroRole, uint64_t
 	initialMovement = hero->movementPointsRemaining();
 	initialTurn = 0;
 	armyValue = getHeroArmyStrengthWithCommander(hero, hero);
-	heroFightingStrength = hero->getFightingStrength();
+	heroFightingStrength = hero->getHeroStrength();
 	tiCache.reset(new TurnInfo(hero));
 }
 
@@ -182,7 +182,7 @@ ExchangeResult HeroActor::tryExchangeNoLock(const ChainActor * specialActor, con
 		return &actor == specialActor;
 	});
 
-	result.actor = &(dynamic_cast<HeroActor *>(result.actor)->specialActors[index]);
+	result.actor = &(dynamic_cast<HeroActor *>(result.actor)->specialActors.at(index));
 
 	return result;
 }
@@ -440,7 +440,7 @@ int DwellingActor::getInitialTurn(bool waitForGrowth, int dayOfWeek)
 
 std::string DwellingActor::toString() const
 {
-	return dwelling->typeName + dwelling->visitablePos().toString();
+	return dwelling->getTypeName() + dwelling->visitablePos().toString();
 }
 
 CCreatureSet * DwellingActor::getDwellingCreatures(const CGDwelling * dwelling, bool waitForGrowth)

+ 1 - 1
AI/Nullkiller/Pathfinding/Actors.h

@@ -113,7 +113,7 @@ public:
 	static const int SPECIAL_ACTORS_COUNT = 7;
 
 private:
-	ChainActor specialActors[SPECIAL_ACTORS_COUNT];
+	std::array<ChainActor, SPECIAL_ACTORS_COUNT> specialActors;
 	std::unique_ptr<HeroExchangeMap> exchangeMap;
 
 	void setupSpecialActors();

+ 3 - 4
AI/VCAI/AIUtility.cpp

@@ -15,7 +15,6 @@
 
 #include "../../lib/UnlockGuard.h"
 #include "../../lib/CConfigHandler.h"
-#include "../../lib/CHeroHandler.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/mapObjects/CQuest.h"
 #include "../../lib/mapping/CMapDefines.h"
@@ -187,7 +186,7 @@ bool canBeEmbarkmentPoint(const TerrainTile * t, bool fromWater)
 {
 	// TODO: Such information should be provided by pathfinder
 	// Tile must be free or with unoccupied boat
-	if(!t->blocked)
+	if(!t->blocked())
 	{
 		return true;
 	}
@@ -248,8 +247,8 @@ bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2)
 
 bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2)
 {
-	auto art1 = a1->artType;
-	auto art2 = a2->artType;
+	auto art1 = a1->getType();
+	auto art2 = a2->getType();
 
 	if(art1->getPrice() == art2->getPrice())
 		return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL);

+ 0 - 2
AI/VCAI/AIUtility.h

@@ -25,11 +25,9 @@ using crstring = const std::string &;
 using dwellingContent = std::pair<ui32, std::vector<CreatureID>>;
 
 const int ACTUAL_RESOURCE_COUNT = 7;
-const int ALLOWED_ROAMING_HEROES = 8;
 
 //implementation-dependent
 extern const double SAFE_ATTACK_CONSTANT;
-extern const int GOLD_RESERVE;
 
 extern thread_local CCallback * cb;
 extern thread_local VCAI * ai;

+ 1 - 1
AI/VCAI/ArmyManager.cpp

@@ -36,7 +36,7 @@ std::vector<SlotInfo> ArmyManager::getSortedSlots(const CCreatureSet * target, c
 	{
 		for(auto & i : armyPtr->Slots())
 		{
-			auto cre = dynamic_cast<const CCreature*>(i.second->type);
+			auto cre = dynamic_cast<const CCreature*>(i.second->getType());
 			auto & slotInfp = creToPower[cre];
 
 			slotInfp.creature = cre;

+ 4 - 4
AI/VCAI/BuildingManager.cpp

@@ -23,13 +23,13 @@ bool BuildingManager::tryBuildThisStructure(const CGTownInstance * t, BuildingID
 		return false;
 	}
 
-	if (!vstd::contains(t->town->buildings, building))
+	if (!vstd::contains(t->getTown()->buildings, building))
 		return false; // no such building in town
 
 	if (t->hasBuilt(building)) //Already built? Shouldn't happen in general
 		return true;
 
-	const CBuilding * buildPtr = t->town->buildings.at(building);
+	const CBuilding * buildPtr = t->getTown()->buildings.at(building);
 
 	auto toBuild = buildPtr->requirements.getFulfillmentCandidates([&](const BuildingID & buildID)
 	{
@@ -51,7 +51,7 @@ bool BuildingManager::tryBuildThisStructure(const CGTownInstance * t, BuildingID
 
 	for (const auto & buildID : toBuild)
 	{
-		const CBuilding * b = t->town->buildings.at(buildID);
+		const CBuilding * b = t->getTown()->buildings.at(buildID);
 
 		EBuildingState canBuild = cb->canBuildStructure(t, buildID);
 		if (canBuild == EBuildingState::ALLOWED)
@@ -220,7 +220,7 @@ bool BuildingManager::getBuildingOptions(const CGTownInstance * t)
 
 	//at the end, try to get and build any extra buildings with nonstandard slots (for example HotA 3rd level dwelling)
 	std::vector<BuildingID> extraBuildings;
-	for (auto buildingInfo : t->town->buildings)
+	for (auto buildingInfo : t->getTown()->buildings)
 	{
 		if (buildingInfo.first > BuildingID::DWELL_UP2_FIRST)
 			extraBuildings.push_back(buildingInfo.first);

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

@@ -56,7 +56,7 @@ TSubgoal BuildThis::whatToDoToAchieve()
 		case EBuildingState::ALLOWED:
 		case EBuildingState::NO_RESOURCES:
 		{
-			auto res = town->town->buildings.at(BuildingID(bid))->resources;
+			auto res = town->getTown()->buildings.at(BuildingID(bid))->resources;
 			return ai->ah->whatToDo(res, iAmElementar()); //realize immediately or gather resources
 		}
 		break;

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

@@ -162,7 +162,7 @@ TGoalVec CompleteQuest::missionArmy() const
 
 	for(auto creature : q.quest->mission.creatures)
 	{
-		solutions.push_back(sptr(GatherTroops(creature.type->getId(), creature.count)));
+		solutions.push_back(sptr(GatherTroops(creature.getId(), creature.count)));
 	}
 
 	return solutions;

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

@@ -46,7 +46,7 @@ TSubgoal FindObj::whatToDoToAchieve()
 			}
 		}
 	}
-	if(o && ai->isAccessible(o->pos)) //we don't use isAccessibleForHero as we don't know which hero it is
+	if(o && ai->isAccessible(o->visitablePos())) //we don't use isAccessibleForHero as we don't know which hero it is
 		return sptr(VisitObj(o->id.getNum()));
 	else
 		return sptr(Explore());

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

@@ -88,13 +88,13 @@ TGoalVec GatherTroops::getAllPossibleSubgoals()
 		}
 
 		auto creature = VLC->creatures()->getByIndex(objid);
-		if(t->getFaction() == creature->getFaction()) //TODO: how to force AI to build unupgraded creatures? :O
+		if(t->getFactionID() == creature->getFactionID()) //TODO: how to force AI to build unupgraded creatures? :O
 		{
 			auto tryFindCreature = [&]() -> std::optional<std::vector<CreatureID>>
 			{
-				if(vstd::isValidIndex(t->town->creatures, creature->getLevel() - 1))
+				if(vstd::isValidIndex(t->getTown()->creatures, creature->getLevel() - 1))
 				{
-					auto itr = t->town->creatures.begin();
+					auto itr = t->getTown()->creatures.begin();
 					std::advance(itr, creature->getLevel() - 1);
 					return make_optional(*itr);
 				}
@@ -109,7 +109,7 @@ TGoalVec GatherTroops::getAllPossibleSubgoals()
 			if(upgradeNumber < 0)
 				continue;
 
-			BuildingID bid(BuildingID::DWELL_FIRST + creature->getLevel() - 1 + upgradeNumber * t->town->creatures.size());
+			BuildingID bid(BuildingID::DWELL_FIRST + creature->getLevel() - 1 + upgradeNumber * t->getTown()->creatures.size());
 			if(t->hasBuilt(bid) && ai->ah->freeResources().canAfford(creature->getFullRecruitCost())) //this assumes only creatures with dwellings are assigned to faction
 			{
 				solutions.push_back(sptr(BuyArmy(t, creature->getAIValue() * this->value).setobjid(objid)));

+ 3 - 3
AI/VCAI/MapObjectsEvaluator.cpp

@@ -12,7 +12,7 @@
 #include "../../lib/GameConstants.h"
 #include "../../lib/VCMI_Lib.h"
 #include "../../lib/CCreatureHandler.h"
-#include "../../lib/CHeroHandler.h"
+#include "../../lib/mapObjects/CompoundMapObjectID.h"
 #include "../../lib/mapObjectConstructors/AObjectTypeHandler.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
@@ -68,7 +68,7 @@ std::optional<int> MapObjectsEvaluator::getObjectValue(const CGObjectInstance *
 	{
 		//special case handling: in-game heroes have hero ID as object subID, but when reading configs available hero object subID's are hero classes
 		auto hero = dynamic_cast<const CGHeroInstance*>(obj);
-		return getObjectValue(obj->ID, hero->type->heroClass->getIndex());
+		return getObjectValue(obj->ID, hero->getHeroClassID());
 	}
 	else if(obj->ID == Obj::PRISON)
 	{
@@ -92,7 +92,7 @@ std::optional<int> MapObjectsEvaluator::getObjectValue(const CGObjectInstance *
 	else if(obj->ID == Obj::ARTIFACT)
 	{
 		auto artifactObject = dynamic_cast<const CGArtifact *>(obj);
-		switch(artifactObject->storedArtifact->artType->aClass)
+		switch(artifactObject->storedArtifact->getType()->aClass)
 		{
 		case CArtifact::EartClass::ART_TREASURE:
 			return 2000;

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

@@ -46,10 +46,10 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta
 			for(pos.y=0; pos.y < sizes.y; ++pos.y)
 			{
 				const TerrainTile & tile = gs->map->getTile(pos);
-				if(!tile.terType->isPassable())
+				if(!tile.getTerrain()->isPassable())
 					continue;
 				
-				if(tile.terType->isWater())
+				if(tile.getTerrain()->isWater())
 				{
 					resetTile(pos, ELayer::SAIL, PathfinderUtil::evaluateAccessibility<ELayer::SAIL>(pos, tile, fow, player, gs));
 					if(useFlying)

+ 0 - 2
AI/VCAI/ResourceManager.cpp

@@ -14,8 +14,6 @@
 #include "../../CCallback.h"
 #include "../../lib/mapObjects/MapObjects.h"
 
-#define GOLD_RESERVE (10000); //at least we'll be able to reach capitol
-
 ResourceObjective::ResourceObjective(const TResources & Res, Goals::TSubgoal Goal)
 	: resources(Res), goal(Goal)
 {

+ 11 - 14
AI/VCAI/VCAI.cpp

@@ -20,7 +20,6 @@
 #include "../../lib/mapObjects/MapObjects.h"
 #include "../../lib/mapObjects/ObjectTemplate.h"
 #include "../../lib/CConfigHandler.h"
-#include "../../lib/CHeroHandler.h"
 #include "../../lib/IGameSettings.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/bonuses/Limiters.h"
@@ -732,7 +731,7 @@ void VCAI::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance *
 	//you can't request action from action-response thread
 	requestActionASAP([=]()
 	{
-		if(removableUnits && !cb->getStartInfo()->isSteadwickFallCampaignMission())
+		if(removableUnits && !cb->getStartInfo()->isRestorationOfErathiaCampaign())
 			pickBestCreatures(down, up);
 
 		answerQuery(queryID, 0);
@@ -1032,7 +1031,7 @@ void VCAI::mainLoop()
 
 void VCAI::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h)
 {
-	LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->pos.toString());
+	LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->anchorPos().toString());
 	switch(obj->ID)
 	{
 	case Obj::TOWN:
@@ -1181,7 +1180,7 @@ void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * ot
 				//FIXME: why are the above possible to be null?
 
 				bool emptySlotFound = false;
-				for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType()))
+				for(auto slot : artifact->getType()->getPossibleSlots().at(target->bearerType()))
 				{
 					if(target->isPositionFree(slot) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move
 					{
@@ -1194,7 +1193,7 @@ void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * ot
 				}
 				if(!emptySlotFound) //try to put that atifact in already occupied slot
 				{
-					for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType()))
+					for(auto slot : artifact->getType()->getPossibleSlots().at(target->bearerType()))
 					{
 						auto otherSlot = target->getSlot(slot);
 						if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one
@@ -1315,8 +1314,6 @@ bool VCAI::canRecruitAnyHero(const CGTownInstance * t) const
 		return false;
 	if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //TODO: use ResourceManager
 		return false;
-	if(cb->getHeroesInfo().size() >= ALLOWED_ROAMING_HEROES)
-		return false;
 	if(cb->getHeroesInfo().size() >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
 		return false;
 	if(!cb->getAvailableHeroes(t).size())
@@ -1417,11 +1414,11 @@ 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->getNameTranslated(), t->getNameTranslated(), t->visitablePos().toString());
-				int3 pos1 = h->pos;
+				int3 posBefore = h->visitablePos();
 				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
 
-				if(pos1 == h->pos && h == primaryHero()) //hero can't move
+				if(posBefore == h->visitablePos() && h == primaryHero()) //hero can't move
 				{
 					if(canRecruitAnyHero(t))
 						recruitHero(t);
@@ -1471,7 +1468,7 @@ void VCAI::wander(HeroPtr h)
 				{
 					auto chosenObject = cb->getObjInstance(ObjectInstanceID(bestObjectGoal->objid));
 					if(chosenObject != nullptr)
-						logAi->debug("Of all %d destinations, object %s at pos=%s seems nice", dests.size(), chosenObject->getObjectName(), chosenObject->pos.toString());
+						logAi->debug("Of all %d destinations, object %s at pos=%s seems nice", dests.size(), chosenObject->getObjectName(), chosenObject->anchorPos().toString());
 				}
 				else
 					logAi->debug("Trying to realize goal of type %s as part of wandering.", bestObjectGoal->name());
@@ -1994,8 +1991,8 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
 
 void VCAI::buildStructure(const CGTownInstance * t, BuildingID building)
 {
-	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());
+	auto name = t->getTown()->buildings.at(building)->getNameTranslated();
+	logAi->debug("Player %d will build %s in town of %s at %s", ai->playerID, name, t->getNameTranslated(), t->anchorPos().toString());
 	cb->buildBuilding(t, building); //just do this;
 }
 
@@ -2081,7 +2078,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)->getNameTranslated(), t->getNameTranslated(), t->pos.toString());
+				playerID, t->getTown()->buildings.at(b)->getNameTranslated(), t->getNameTranslated(), t->anchorPos().toString());
 			cb->buildBuilding(t, b);
 			throw goalFulfilledException(sptr(g));
 		}
@@ -2819,7 +2816,7 @@ bool shouldVisit(HeroPtr h, const CGObjectInstance * obj)
 	{
 		for(auto slot : h->Slots())
 		{
-			if(slot.second->type->hasUpgrades())
+			if(slot.second->getType()->hasUpgrades())
 				return true; //TODO: check price?
 		}
 		return false;

+ 1 - 0
AUTHORS.h

@@ -46,6 +46,7 @@ const std::vector<std::vector<std::string>> contributors = {
    { "Developing", "",                     "vmarkovtsev",           ""                             },
    { "Developing", "Tom Zielinski",        "Warmonger",             "[email protected]"              },
    { "Developing", "Xiaomin Ding",         "",                      "[email protected]"        },
+   { "Developing", "Fenghuang Rumeng",     "kdmcser",               "[email protected]"             },
 
    { "Testing",    "Ben Yan",              "by003",                 "[email protected],"        },
    { "Testing",    "",                     "Misiokles",             ""                             },

+ 73 - 40
CCallback.cpp

@@ -18,31 +18,31 @@
 #include "lib/mapObjects/CGHeroInstance.h"
 #include "lib/mapObjects/CGTownInstance.h"
 #include "lib/texts/CGeneralTextHandler.h"
-#include "lib/CHeroHandler.h"
 #include "lib/CArtHandler.h"
 #include "lib/GameConstants.h"
 #include "lib/CPlayerState.h"
 #include "lib/UnlockGuard.h"
 #include "lib/battle/BattleInfo.h"
 #include "lib/networkPacks/PacksForServer.h"
+#include "lib/networkPacks/SaveLocalState.h"
 
 bool CCallback::teleportHero(const CGHeroInstance *who, const CGTownInstance *where)
 {
 	CastleTeleportHero pack(who->id, where->id, 1);
-	sendRequest(&pack);
+	sendRequest(pack);
 	return true;
 }
 
 void CCallback::moveHero(const CGHeroInstance *h, const int3 & destination, bool transit)
 {
 	MoveHero pack({destination}, h->id, transit);
-	sendRequest(&pack);
+	sendRequest(pack);
 }
 
 void CCallback::moveHero(const CGHeroInstance *h, const std::vector<int3> & path, bool transit)
 {
 	MoveHero pack(path, h->id, transit);
-	sendRequest(&pack);
+	sendRequest(pack);
 }
 
 int CCallback::selectionMade(int selection, QueryID queryID)
@@ -61,7 +61,7 @@ int CCallback::sendQueryReply(std::optional<int32_t> reply, QueryID queryID)
 
 	QueryReply pack(queryID, reply);
 	pack.player = *player;
-	return sendRequest(&pack);
+	return sendRequest(pack);
 }
 
 void CCallback::recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level)
@@ -71,7 +71,7 @@ void CCallback::recruitCreatures(const CGDwelling * obj, const CArmedInstance *
 		return;
 
 	RecruitCreatures pack(obj->id, dst->id, ID, amount, level);
-	sendRequest(&pack);
+	sendRequest(pack);
 }
 
 bool CCallback::dismissCreature(const CArmedInstance *obj, SlotID stackPos)
@@ -80,14 +80,14 @@ bool CCallback::dismissCreature(const CArmedInstance *obj, SlotID stackPos)
 		return false;
 
 	DisbandCreature pack(stackPos,obj->id);
-	sendRequest(&pack);
+	sendRequest(pack);
 	return true;
 }
 
 bool CCallback::upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID)
 {
 	UpgradeCreature pack(stackPos,obj->id,newID);
-	sendRequest(&pack);
+	sendRequest(pack);
 	return false;
 }
 
@@ -95,54 +95,54 @@ void CCallback::endTurn()
 {
 	logGlobal->trace("Player %d ended his turn.", player->getNum());
 	EndTurn pack;
-	sendRequest(&pack);
+	sendRequest(pack);
 }
 int CCallback::swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)
 {
 	ArrangeStacks pack(1,p1,p2,s1->id,s2->id,0);
-	sendRequest(&pack);
+	sendRequest(pack);
 	return 0;
 }
 
 int CCallback::mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)
 {
 	ArrangeStacks pack(2,p1,p2,s1->id,s2->id,0);
-	sendRequest(&pack);
+	sendRequest(pack);
 	return 0;
 }
 
 int CCallback::splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val)
 {
 	ArrangeStacks pack(3,p1,p2,s1->id,s2->id,val);
-	sendRequest(&pack);
+	sendRequest(pack);
 	return 0;
 }
 
 int CCallback::bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot)
 {
 	BulkMoveArmy pack(srcArmy, destArmy, srcSlot);
-	sendRequest(&pack);
+	sendRequest(pack);
 	return 0;
 }
 
 int CCallback::bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany)
 {
 	BulkSplitStack pack(armyId, srcSlot, howMany);
-	sendRequest(&pack);
+	sendRequest(pack);
 	return 0;
 }
 
 int CCallback::bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot)
 {
 	BulkSmartSplitStack pack(armyId, srcSlot);
-	sendRequest(&pack);
+	sendRequest(pack);
 	return 0;
 }
 
 int CCallback::bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot)
 {
 	BulkMergeStacks pack(armyId, srcSlot);
-	sendRequest(&pack);
+	sendRequest(pack);
 	return 0;
 }
 
@@ -151,7 +151,7 @@ bool CCallback::dismissHero(const CGHeroInstance *hero)
 	if(player!=hero->tempOwner) return false;
 
 	DismissHero pack(hero->id);
-	sendRequest(&pack);
+	sendRequest(pack);
 	return true;
 }
 
@@ -160,7 +160,7 @@ bool CCallback::swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation
 	ExchangeArtifacts ea;
 	ea.src = l1;
 	ea.dst = l2;
-	sendRequest(&ea);
+	sendRequest(ea);
 	return true;
 }
 
@@ -175,13 +175,13 @@ bool CCallback::swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation
 void CCallback::assembleArtifacts(const ObjectInstanceID & heroID, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo)
 {
 	AssembleArtifacts aa(heroID, artifactSlot, assemble, assembleTo);
-	sendRequest(&aa);
+	sendRequest(aa);
 }
 
 void CCallback::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack)
 {
 	BulkExchangeArtifacts bma(srcHero, dstHero, swap, equipped, backpack);
-	sendRequest(&bma);
+	sendRequest(bma);
 }
 
 void CCallback::scrollBackpackArtifacts(ObjectInstanceID hero, bool left)
@@ -189,19 +189,37 @@ void CCallback::scrollBackpackArtifacts(ObjectInstanceID hero, bool left)
 	ManageBackpackArtifacts mba(hero, ManageBackpackArtifacts::ManageCmd::SCROLL_RIGHT);
 	if(left)
 		mba.cmd = ManageBackpackArtifacts::ManageCmd::SCROLL_LEFT;
-	sendRequest(&mba);
+	sendRequest(mba);
+}
+
+void CCallback::sortBackpackArtifactsBySlot(const ObjectInstanceID hero)
+{
+	ManageBackpackArtifacts mba(hero, ManageBackpackArtifacts::ManageCmd::SORT_BY_SLOT);
+	sendRequest(mba);
+}
+
+void CCallback::sortBackpackArtifactsByCost(const ObjectInstanceID hero)
+{
+	ManageBackpackArtifacts mba(hero, ManageBackpackArtifacts::ManageCmd::SORT_BY_COST);
+	sendRequest(mba);
+}
+
+void CCallback::sortBackpackArtifactsByClass(const ObjectInstanceID hero)
+{
+	ManageBackpackArtifacts mba(hero, ManageBackpackArtifacts::ManageCmd::SORT_BY_CLASS);
+	sendRequest(mba);
 }
 
 void CCallback::manageHeroCostume(ObjectInstanceID hero, size_t costumeIndex, bool saveCostume)
 {
 	ManageEquippedArtifacts mea(hero, costumeIndex, saveCostume);
-	sendRequest(&mea);
+	sendRequest(mea);
 }
 
 void CCallback::eraseArtifactByClient(const ArtifactLocation & al)
 {
 	EraseArtifactByClient ea(al);
-	sendRequest(&ea);
+	sendRequest(ea);
 }
 
 bool CCallback::buildBuilding(const CGTownInstance *town, BuildingID buildingID)
@@ -213,7 +231,7 @@ bool CCallback::buildBuilding(const CGTownInstance *town, BuildingID buildingID)
 		return false;
 
 	BuildStructure pack(town->id,buildingID);
-	sendRequest(&pack);
+	sendRequest(pack);
 	return true;
 }
 
@@ -223,7 +241,7 @@ bool CCallback::visitTownBuilding(const CGTownInstance *town, BuildingID buildin
 		return false;
 
 	VisitTownBuilding pack(town->id, buildingID);
-	sendRequest(&pack);
+	sendRequest(pack);
 	return true;
 }
 
@@ -232,10 +250,10 @@ void CBattleCallback::battleMakeSpellAction(const BattleID & battleID, const Bat
 	assert(action.actionType == EActionType::HERO_SPELL);
 	MakeAction mca(action);
 	mca.battleID = battleID;
-	sendRequest(&mca);
+	sendRequest(mca);
 }
 
-int CBattleCallback::sendRequest(const CPackForServer * request)
+int CBattleCallback::sendRequest(const CPackForServer & request)
 {
 	int requestID = cl->sendRequest(request, *getPlayerID());
 	if(waitTillRealize)
@@ -249,12 +267,18 @@ int CBattleCallback::sendRequest(const CPackForServer * request)
 	return requestID;
 }
 
+void CCallback::spellResearch( const CGTownInstance *town, SpellID spellAtSlot, bool accepted )
+{
+	SpellResearch pack(town->id, spellAtSlot, accepted);
+	sendRequest(pack);
+}
+
 void CCallback::swapGarrisonHero( const CGTownInstance *town )
 {
 	if(town->tempOwner == *player || (town->garrisonHero && town->garrisonHero->tempOwner == *player ))
 	{
 		GarrisonHeroSwap pack(town->id);
-		sendRequest(&pack);
+		sendRequest(pack);
 	}
 }
 
@@ -263,7 +287,7 @@ void CCallback::buyArtifact(const CGHeroInstance *hero, ArtifactID aid)
 	if(hero->tempOwner != *player) return;
 
 	BuyArtifact pack(hero->id,aid);
-	sendRequest(&pack);
+	sendRequest(pack);
 }
 
 void CCallback::trade(const ObjectInstanceID marketId, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero)
@@ -280,13 +304,13 @@ void CCallback::trade(const ObjectInstanceID marketId, EMarketMode mode, const s
 	pack.r1 = id1;
 	pack.r2 = id2;
 	pack.val = val1;
-	sendRequest(&pack);
+	sendRequest(pack);
 }
 
 void CCallback::setFormation(const CGHeroInstance * hero, EArmyFormation mode)
 {
 	SetFormation pack(hero->id, mode);
-	sendRequest(&pack);
+	sendRequest(pack);
 }
 
 void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero, const HeroTypeID & nextHero)
@@ -294,9 +318,18 @@ void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroIn
 	assert(townOrTavern);
 	assert(hero);
 
-	HireHero pack(hero->getHeroType(), townOrTavern->id, nextHero);
+	HireHero pack(hero->getHeroTypeID(), townOrTavern->id, nextHero);
 	pack.player = *player;
-	sendRequest(&pack);
+	sendRequest(pack);
+}
+
+void CCallback::saveLocalState(const JsonNode & data)
+{
+	SaveLocalState state;
+	state.data = data;
+	state.player = *player;
+
+	sendRequest(state);
 }
 
 void CCallback::save( const std::string &fname )
@@ -310,7 +343,7 @@ void CCallback::gamePause(bool pause)
 	{
 		GamePause pack;
 		pack.player = *player;
-		sendRequest(&pack);
+		sendRequest(pack);
 	}
 	else
 	{
@@ -324,14 +357,14 @@ void CCallback::sendMessage(const std::string &mess, const CGObjectInstance * cu
 	PlayerMessage pm(mess, currentObject? currentObject->id : ObjectInstanceID(-1));
 	if(player)
 		pm.player = *player;
-	sendRequest(&pm);
+	sendRequest(pm);
 }
 
 void CCallback::buildBoat( const IShipyard *obj )
 {
 	BuildBoat bb;
 	bb.objid = dynamic_cast<const CGObjectInstance*>(obj)->id;
-	sendRequest(&bb);
+	sendRequest(bb);
 }
 
 CCallback::CCallback(CGameState * GS, std::optional<PlayerColor> Player, CClient * C)
@@ -373,7 +406,7 @@ void CCallback::dig( const CGObjectInstance *hero )
 {
 	DigWithHero dwh;
 	dwh.id = hero->id;
-	sendRequest(&dwh);
+	sendRequest(dwh);
 }
 
 void CCallback::castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos)
@@ -382,7 +415,7 @@ void CCallback::castSpell(const CGHeroInstance *hero, SpellID spellID, const int
 	cas.hid = hero->id;
 	cas.sid = spellID;
 	cas.pos = pos;
-	sendRequest(&cas);
+	sendRequest(cas);
 }
 
 int CCallback::mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)
@@ -415,7 +448,7 @@ void CBattleCallback::battleMakeUnitAction(const BattleID & battleID, const Batt
 	MakeAction ma;
 	ma.ba = action;
 	ma.battleID = battleID;
-	sendRequest(&ma);
+	sendRequest(ma);
 }
 
 void CBattleCallback::battleMakeTacticAction(const BattleID & battleID, const BattleAction & action )
@@ -424,7 +457,7 @@ void CBattleCallback::battleMakeTacticAction(const BattleID & battleID, const Ba
 	MakeAction ma;
 	ma.ba = action;
 	ma.battleID = battleID;
-	sendRequest(&ma);
+	sendRequest(ma);
 }
 
 std::optional<BattleAction> CBattleCallback::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState)

+ 11 - 1
CCallback.h

@@ -78,6 +78,7 @@ public:
 	virtual bool visitTownBuilding(const CGTownInstance *town, BuildingID buildingID)=0;
 	virtual void recruitCreatures(const CGDwelling *obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1)=0;
 	virtual bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE)=0; //if newID==-1 then best possible upgrade will be made
+	virtual void spellResearch(const CGTownInstance *town, SpellID spellAtSlot, bool accepted)=0;
 	virtual void swapGarrisonHero(const CGTownInstance *town)=0;
 
 	virtual void trade(const ObjectInstanceID marketId, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero)=0; //mode==0: sell val1 units of id1 resource for id2 resiurce
@@ -92,10 +93,14 @@ public:
 	//virtual bool swapArtifacts(const CGHeroInstance * hero1, ui16 pos1, const CGHeroInstance * hero2, ui16 pos2)=0; //swaps artifacts between two given heroes
 	virtual bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2)=0;
 	virtual void scrollBackpackArtifacts(ObjectInstanceID hero, bool left) = 0;
+	virtual void sortBackpackArtifactsBySlot(const ObjectInstanceID hero) = 0;
+	virtual void sortBackpackArtifactsByCost(const ObjectInstanceID hero) = 0;
+	virtual void sortBackpackArtifactsByClass(const ObjectInstanceID hero) = 0;
 	virtual void manageHeroCostume(ObjectInstanceID hero, size_t costumeIndex, bool saveCostume) = 0;
 	virtual void assembleArtifacts(const ObjectInstanceID & heroID, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo)=0;
 	virtual void eraseArtifactByClient(const ArtifactLocation & al)=0;
 	virtual bool dismissCreature(const CArmedInstance *obj, SlotID stackPos)=0;
+	virtual void saveLocalState(const JsonNode & data)=0;
 	virtual void endTurn()=0;
 	virtual void buyArtifact(const CGHeroInstance *hero, ArtifactID aid)=0; //used to buy artifacts in towns (including spell book in the guild and war machines in blacksmith)
 	virtual void setFormation(const CGHeroInstance * hero, EArmyFormation mode)=0;
@@ -123,7 +128,7 @@ class CBattleCallback : public IBattleCallback
 	std::optional<PlayerColor> player;
 
 protected:
-	int sendRequest(const CPackForServer * request); //returns requestID (that'll be matched to requestID in PackageApplied)
+	int sendRequest(const CPackForServer & request); //returns requestID (that'll be matched to requestID in PackageApplied)
 	CClient *cl;
 
 public:
@@ -179,6 +184,9 @@ public:
 	void assembleArtifacts(const ObjectInstanceID & heroID, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) override;
 	void bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped = true, bool backpack = true) override;
 	void scrollBackpackArtifacts(ObjectInstanceID hero, bool left) override;
+	void sortBackpackArtifactsBySlot(const ObjectInstanceID hero) override;
+	void sortBackpackArtifactsByCost(const ObjectInstanceID hero) override;
+	void sortBackpackArtifactsByClass(const ObjectInstanceID hero) override;
 	void manageHeroCostume(ObjectInstanceID hero, size_t costumeIdx, bool saveCostume) override;
 	void eraseArtifactByClient(const ArtifactLocation & al) override;
 	bool buildBuilding(const CGTownInstance *town, BuildingID buildingID) override;
@@ -186,7 +194,9 @@ public:
 	void recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1) override;
 	bool dismissCreature(const CArmedInstance *obj, SlotID stackPos) override;
 	bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE) override;
+	void saveLocalState(const JsonNode & data) override;
 	void endTurn() override;
+	void spellResearch(const CGTownInstance *town, SpellID spellAtSlot, bool accepted) override;
 	void swapGarrisonHero(const CGTownInstance *town) override;
 	void buyArtifact(const CGHeroInstance *hero, ArtifactID aid) override;
 	void trade(const ObjectInstanceID marketId, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero = nullptr) override;

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

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

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

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

+ 0 - 7
CI/android/before_install.sh

@@ -1,7 +0,0 @@
-#!/usr/bin/env bash
-
-echo "ANDROID_NDK_ROOT=$ANDROID_HOME/ndk/25.2.9519653" >> $GITHUB_ENV
-
-brew install ninja
-
-. CI/install_conan_dependencies.sh "$DEPS_FILENAME"

+ 4 - 0
CI/before_install/android.sh

@@ -0,0 +1,4 @@
+#!/bin/sh
+
+sudo apt-get update
+sudo apt-get install ninja-build

+ 2 - 3
CI/linux/before_install.sh → CI/before_install/linux_qt5.sh

@@ -1,6 +1,5 @@
 #!/bin/sh
 
-sudo apt remove needrestart
 sudo apt-get update
 
 # Dependencies
@@ -9,6 +8,6 @@ sudo apt-get update
 # - debian build settings at debian/control
 sudo apt-get install libboost-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-locale-dev libboost-iostreams-dev \
 libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \
-qtbase5-dev \
+qtbase5-dev qttools5-dev \
 ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \
-libminizip-dev libfuzzylite-dev qttools5-dev libsqlite3-dev # Optional dependencies
+libminizip-dev libfuzzylite-dev libsqlite3-dev # Optional dependencies

+ 3 - 1
CI/linux-qt6/before_install.sh → CI/before_install/linux_qt6.sh

@@ -1,9 +1,11 @@
 #!/bin/sh
 
-sudo apt remove needrestart
 sudo apt-get update
 
 # Dependencies
+# In case of change in dependencies list please also update:
+# - developer docs at docs/developer/Building_Linux.md
+# - debian build settings at debian/control
 sudo apt-get install libboost-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-locale-dev libboost-iostreams-dev \
 libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \
 qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools \

+ 0 - 2
CI/mac/before_install.sh → CI/before_install/macos.sh

@@ -3,5 +3,3 @@
 echo DEVELOPER_DIR=/Applications/Xcode_14.2.app >> $GITHUB_ENV
 
 brew install ninja
-
-. CI/install_conan_dependencies.sh "$DEPS_FILENAME"

+ 7 - 0
CI/before_install/mingw.sh

@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+sudo apt-get update
+sudo apt-get install ninja-build mingw-w64 nsis
+
+sudo update-alternatives --set i686-w64-mingw32-g++ /usr/bin/i686-w64-mingw32-g++-posix
+sudo update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix

+ 17 - 0
CI/before_install/msvc.sh

@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+
+MSVC_INSTALL_PATH=$(vswhere -latest -property installationPath)
+echo "MSVC_INSTALL_PATH = $MSVC_INSTALL_PATH"
+echo "Installed toolset versions:"
+ls -vr "$MSVC_INSTALL_PATH/VC/Tools/MSVC"
+
+TOOLS_DIR=$(ls -vr "$MSVC_INSTALL_PATH/VC/Tools/MSVC/" | head -1)
+DUMPBIN_PATH="$MSVC_INSTALL_PATH/VC/Tools/MSVC/$TOOLS_DIR/bin/Hostx64/x64/dumpbin.exe"
+
+# This command should work as well, but for some reason it is *extremely* slow on the Github CI (~7 minutes)
+#DUMPBIN_PATH=$(vswhere -latest -find **/dumpbin.exe | head -n 1)
+
+echo "TOOLS_DIR = $TOOLS_DIR"
+echo "DUMPBIN_PATH = $DUMPBIN_PATH"
+
+dirname "$DUMPBIN_PATH" > "$GITHUB_PATH"

+ 1 - 1
CI/conan/base/cross-macro.j2

@@ -10,7 +10,7 @@ STRIP={{ target_host }}-strip
 {%- endmacro -%}
 
 {% macro generate_env_win32(target_host) -%}
-CONAN_SYSTEM_LIBRARY_LOCATION=/usr/lib/gcc/{{ target_host }}/10-posix/
+CONAN_SYSTEM_LIBRARY_LOCATION=/usr/lib/gcc/{{ target_host }}/13-posix/
 RC={{ target_host }}-windres
 {%- endmacro -%}
 

+ 280 - 0
CI/example.markdownlint-cli2.jsonc

@@ -0,0 +1,280 @@
+{
+	"config" : {
+		"default" : true,
+
+		// MD001/heading-increment : Heading levels should only increment by one level at a time : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md001.md
+		"MD001": false,
+
+		// MD003/heading-style : Heading style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md003.md
+		"MD003": {
+			"style": "atx"
+		},
+
+		// MD004/ul-style : Unordered list style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md004.md
+		"MD004": false,
+		// FIXME: enable and consider fixing
+		//{
+		//	"style": "consistent"
+		//},
+
+		// MD005/list-indent : Inconsistent indentation for list items at the same level : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md005.md
+		"MD005": true,
+
+		// MD007/ul-indent : Unordered list indentation : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md007.md
+		"MD007": {
+			// Spaces for indent
+			"indent": 2,
+			// Whether to indent the first level of the list
+			"start_indented": false,
+			// Spaces for first level indent (when start_indented is set)
+			"start_indent": 0
+		},
+
+		// MD009/no-trailing-spaces : Trailing spaces : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md009.md
+		"MD009": {
+			// Spaces for line break
+			"br_spaces": 2,
+			// Allow spaces for empty lines in list items
+			"list_item_empty_lines": false,
+			// Include unnecessary breaks
+			"strict": false
+		},
+
+		// MD010/no-hard-tabs : Hard tabs : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md010.md
+		"MD010": {
+			// Include code blocks
+			"code_blocks": false,
+			// Fenced code languages to ignore
+			"ignore_code_languages": [],
+			// Number of spaces for each hard tab
+			"spaces_per_tab": 4
+		},
+		
+		// MD011/no-reversed-links : Reversed link syntax : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md011.md
+		"MD011": true,
+		
+		// MD012/no-multiple-blanks : Multiple consecutive blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md012.md
+		"MD012": {
+			// Consecutive blank lines
+			"maximum": 1
+		},
+
+		// MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md013.md
+		"MD013": false,
+		
+		// MD014/commands-show-output : Dollar signs used before commands without showing output : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md014.md
+		"MD014": true,
+
+		// MD018/no-missing-space-atx : No space after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md018.md
+		"MD018": true,
+
+		// MD019/no-multiple-space-atx : Multiple spaces after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md019.md
+		"MD019": true,
+
+		// MD020/no-missing-space-closed-atx : No space inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md020.md
+		"MD020": true,
+
+		// MD021/no-multiple-space-closed-atx : Multiple spaces inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md021.md
+		"MD021": true,
+
+		// MD022/blanks-around-headings : Headings should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md022.md
+		"MD022": {
+			// Blank lines above heading
+			"lines_above": 1,
+			// Blank lines below heading
+			"lines_below": 1
+		},
+
+		// MD023/heading-start-left : Headings must start at the beginning of the line : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md023.md
+		"MD023": true,
+
+		// MD024/no-duplicate-heading : Multiple headings with the same content : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md024.md
+		"MD024": false,
+		// FIXME: false positives?
+		//{
+		//	// Only check sibling headings
+		//	"allow_different_nesting": true,
+		//	// Only check sibling headings
+		//	"siblings_only": true
+		//},
+
+		// MD025/single-title/single-h1 : Multiple top-level headings in the same document : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md025.md
+		"MD025": {
+			// Heading level
+			"level": 1,
+			// RegExp for matching title in front matter
+			"front_matter_title": "^\\s*title\\s*[:=]"
+		},
+
+		// MD026/no-trailing-punctuation : Trailing punctuation in heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md026.md
+		"MD026": {
+			// Punctuation characters
+			"punctuation": ".,;:!。,;:!"
+		},
+
+		// MD027/no-multiple-space-blockquote : Multiple spaces after blockquote symbol : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md027.md
+		"MD027": true,
+
+		// MD028/no-blanks-blockquote : Blank line inside blockquote : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md028.md
+		"MD028": true,
+
+		// MD029/ol-prefix : Ordered list item prefix : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md029.md
+		"MD029": false,
+		// FIXME: false positives or broken formatting
+		//{
+		//	// List style
+		//	"style": "ordered"
+		//},
+
+		// MD030/list-marker-space : Spaces after list markers : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md030.md
+		"MD030": {
+			// Spaces for single-line unordered list items
+			"ul_single": 1,
+			// Spaces for single-line ordered list items
+			"ol_single": 1,
+			// Spaces for multi-line unordered list items
+			"ul_multi": 1,
+			// Spaces for multi-line ordered list items
+			"ol_multi": 1
+		},
+
+		// MD031/blanks-around-fences : Fenced code blocks should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md031.md
+		"MD031": {
+			// Include list items
+			"list_items": false
+		},
+
+		// MD032/blanks-around-lists : Lists should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md032.md
+		"MD032": true,
+
+		// MD033/no-inline-html : Inline HTML : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md033.md
+		"MD033": false,
+		// FIXME: enable and consider fixing
+		//{
+		//	// Allowed elements
+		//	"allowed_elements": []
+		//},
+
+		// MD034/no-bare-urls : Bare URL used : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md034.md
+		"MD034": true,
+
+		// MD035/hr-style : Horizontal rule style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md035.md
+		"MD035": {
+			// Horizontal rule style
+			"style": "consistent"
+		},
+
+		// MD036/no-emphasis-as-heading : Emphasis used instead of a heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md036.md
+		"MD036": false,
+		// FIXME: enable and consider fixing
+		// {
+		// 	// Punctuation characters
+		// 	"punctuation": ".,;:!?。,;:!?"
+		// },
+
+		// MD037/no-space-in-emphasis : Spaces inside emphasis markers : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md037.md
+		"MD037": true,
+
+		// MD038/no-space-in-code : Spaces inside code span elements : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md038.md
+		"MD038": true,
+
+		// MD039/no-space-in-links : Spaces inside link text : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md039.md
+		"MD039": true,
+
+		// MD040/fenced-code-language : Fenced code blocks should have a language specified : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md040.md
+		"MD040": false,
+		// FIXME: enable and consider fixing
+		//{
+		//// List of languages
+		//	"allowed_languages": [ "cpp", "json5", "sh" ],
+		//// Require language only
+		//	"language_only": true
+		//},
+
+		// MD041/first-line-heading/first-line-h1 : First line in a file should be a top-level heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md041.md
+		"MD041": {
+			// Heading level
+			"level": 1,
+			// RegExp for matching title in front matter
+			"front_matter_title": "^\\s*title\\s*[:=]"
+		},
+
+		// MD042/no-empty-links : No empty links : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md042.md
+		"MD042": true,
+
+		// MD043/required-headings : Required heading structure : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md043.md
+		"MD043": false,
+
+		// MD044/proper-names : Proper names should have the correct capitalization : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md044.md
+		"MD044": false,
+
+		// MD045/no-alt-text : Images should have alternate text (alt text) : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md045.md
+		"MD045": false,
+
+		// MD046/code-block-style : Code block style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md046.md
+		"MD046": {
+			// Block style
+			"style": "fenced"
+		},
+
+		// MD047/single-trailing-newline : Files should end with a single newline character : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md047.md
+		"MD047": true,
+		
+		// MD048/code-fence-style : Code fence style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md048.md
+		"MD048": {
+			// Code fence style
+			"style": "backtick"
+		},
+
+		// MD049/emphasis-style : Emphasis style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md049.md
+		"MD049": {
+			// Emphasis style
+			"style": "asterisk"
+		},
+
+		// MD050/strong-style : Strong style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md050.md
+		"MD050": {
+			// Strong style
+			"style": "asterisk"
+		},
+		
+
+
+		// MD051/link-fragments : Link fragments should be valid : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md051.md
+		"MD051": true,
+
+		// MD052/reference-links-images : Reference links and images should use a label that is defined : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md052.md
+		"MD052": {
+			// Include shortcut syntax
+			"shortcut_syntax": false
+		},
+
+		// MD053/link-image-reference-definitions : Link and image reference definitions should be needed : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md053.md
+		"MD053": {
+			// Ignored definitions
+			"ignored_definitions": [
+			  "//"
+			]
+		},
+
+		// MD054/link-image-style : Link and image style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md054.md
+		"MD054": {
+			// Allow autolinks
+			"autolink": true,
+			// Allow inline links and images
+			"inline": true,
+			// Allow full reference links and images
+			"full": true,
+			// Allow collapsed reference links and images
+			"collapsed": true,
+			// Allow shortcut reference links and images
+			"shortcut": true,
+			// Allow URLs as inline links
+			"url_inline": true
+		},
+		
+		// MD058 - Tables should be surrounded by blank lines
+		"MD058" : true
+
+	}
+}

+ 1 - 1
CI/install_conan_dependencies.sh

@@ -1,6 +1,6 @@
 #!/usr/bin/env bash
 
-RELEASE_TAG="1.2"
+RELEASE_TAG="1.3"
 FILENAME="$1"
 DOWNLOAD_URL="https://github.com/vcmi/vcmi-dependencies/releases/download/$RELEASE_TAG/$FILENAME.txz"
 

+ 7 - 0
CI/install_vcpkg_dependencies.sh

@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+RELEASE_TAG="v1.8"
+FILENAME="dependencies-$1"
+DOWNLOAD_URL="https://github.com/vcmi/vcmi-deps-windows/releases/download/$RELEASE_TAG/$FILENAME.txz"
+
+curl -L "$DOWNLOAD_URL" | tar -xf - --xz

+ 0 - 5
CI/ios/before_install.sh

@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-echo DEVELOPER_DIR=/Applications/Xcode_14.2.app >> $GITHUB_ENV
-
-. CI/install_conan_dependencies.sh "dependencies-ios"

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

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

+ 0 - 1
CI/linux/upload_package.sh

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

+ 0 - 4
CI/mac-arm/before_install.sh

@@ -1,4 +0,0 @@
-#!/usr/bin/env bash
-
-DEPS_FILENAME=dependencies-mac-arm
-. CI/mac/before_install.sh

+ 0 - 4
CI/mac-intel/before_install.sh

@@ -1,4 +0,0 @@
-#!/usr/bin/env bash
-
-DEPS_FILENAME=dependencies-mac-intel
-. CI/mac/before_install.sh

+ 0 - 14
CI/mingw-32/before_install.sh

@@ -1,14 +0,0 @@
-#!/usr/bin/env bash
-
-sudo apt-get update
-sudo apt-get install ninja-build mingw-w64 nsis
-sudo update-alternatives --set i686-w64-mingw32-g++ /usr/bin/i686-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-3_all.deb \
-  && sudo dpkg -i mingw-w64-common_10.0.0-3_all.deb;
-curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-i686-dev_10.0.0-3_all.deb \
-  && sudo dpkg -i mingw-w64-i686-dev_10.0.0-3_all.deb;
-
-. CI/install_conan_dependencies.sh "dependencies-mingw-32"

+ 0 - 14
CI/mingw/before_install.sh

@@ -1,14 +0,0 @@
-#!/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-3_all.deb \
-  && sudo dpkg -i mingw-w64-common_10.0.0-3_all.deb;
-curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-x86-64-dev_10.0.0-3_all.deb \
-  && sudo dpkg -i mingw-w64-x86-64-dev_10.0.0-3_all.deb;
-
-. CI/install_conan_dependencies.sh "dependencies-mingw"

+ 0 - 10
CI/msvc/before_install.sh

@@ -1,10 +0,0 @@
-curl -LfsS -o "vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z" \
-	"https://github.com/vcmi/vcmi-deps-windows/releases/download/v1.7/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
-
-DUMPBIN_DIR=$(vswhere -latest -find **/dumpbin.exe | head -n 1)
-dirname "$DUMPBIN_DIR" > $GITHUB_PATH

+ 0 - 6
CI/msvc/build_script.bat

@@ -1,6 +0,0 @@
-cd %APPVEYOR_BUILD_FOLDER%
-cd build_%VCMI_BUILD_PLATFORM%
-
-cmake --build . --config %VCMI_BUILD_CONFIGURATION% -- /maxcpucount:2
-
-cpack

+ 0 - 5
CI/msvc/coverity_build_script.bat

@@ -1,5 +0,0 @@
-cd %APPVEYOR_BUILD_FOLDER%
-cd build_%VCMI_BUILD_PLATFORM%
-
-echo Building with coverity...
-cov-build.exe --dir cov-int cmake --build . --config %VCMI_BUILD_CONFIGURATION% -- /maxcpucount:2

+ 0 - 17
CI/msvc/coverity_upload_script.ps

@@ -1,17 +0,0 @@
-7z a "$Env:APPVEYOR_BUILD_FOLDER\$Env:APPVEYOR_PROJECT_NAME.zip" "$Env:APPVEYOR_BUILD_FOLDER\build_$Env:VCMI_BUILD_PLATFORM\cov-int\"
-
-# cf. http://stackoverflow.com/a/25045154/335418
-Remove-item alias:curl
-
-Write-Host "Uploading Coverity analysis result..." -ForegroundColor "Green"
-
-curl --silent --show-error `
-     --output curl-out.txt `
-     --form token="$Env:coverity_token" `
-     --form email="$Env:coverity_email" `
-     --form "file=@$Env:APPVEYOR_BUILD_FOLDER\$Env:APPVEYOR_PROJECT_NAME.zip" `
-     --form version="$Env:APPVEYOR_REPO_COMMIT" `
-     --form description="CI server scheduled build." `
-     https://scan.coverity.com/builds?project=vcmi%2Fvcmi
-
-cat .\curl-out.txt

+ 0 - 0
CI/linux-qt6/validate_json.py → CI/validate_json.py


+ 18 - 20
CMakeLists.txt

@@ -180,11 +180,6 @@ else()
 	add_definitions(-DVCMI_NO_EXTRA_VERSION)
 endif(ENABLE_GITVERSION)
 
-# Precompiled header configuration
-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>>)
@@ -328,7 +323,6 @@ if(MINGW OR MSVC)
 		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
@@ -361,13 +355,6 @@ if(MINGW OR MSVC)
 		if(ICONV_FOUND)
 			set(SYSTEM_LIBS ${SYSTEM_LIBS} iconv)
 		endif()
-
-		# Prevent compiler issues when building Debug
-		# Assembler might fail with "too many sections"
-		# With big-obj or 64-bit build will take hours
-		if(CMAKE_BUILD_TYPE STREQUAL "Debug")
-			set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Og")
-		endif()
 	endif(MINGW)
 endif(MINGW OR MSVC)
 
@@ -486,25 +473,30 @@ if(NOT FORCE_BUNDLED_MINIZIP)
 endif()
 
 if (ENABLE_CLIENT)
-	set(FFMPEG_COMPONENTS avutil swscale avformat avcodec)
-	if(APPLE_IOS AND NOT USING_CONAN)
-		list(APPEND FFMPEG_COMPONENTS swresample)
-	endif()
-	find_package(ffmpeg COMPONENTS ${FFMPEG_COMPONENTS})
+	find_package(ffmpeg COMPONENTS avutil swscale avformat avcodec swresample)
 
 	find_package(SDL2 REQUIRED)
 	find_package(SDL2_image REQUIRED)
 	if(TARGET SDL2_image::SDL2_image)
 		add_library(SDL2::Image ALIAS SDL2_image::SDL2_image)
 	endif()
+	if(TARGET SDL2_image::SDL2_image-static)
+		add_library(SDL2::Image ALIAS SDL2_image::SDL2_image-static)
+	endif()
 	find_package(SDL2_mixer REQUIRED)
 	if(TARGET SDL2_mixer::SDL2_mixer)
 		add_library(SDL2::Mixer ALIAS SDL2_mixer::SDL2_mixer)
 	endif()
+	if(TARGET SDL2_mixer::SDL2_mixer-static)
+		add_library(SDL2::Mixer ALIAS SDL2_mixer::SDL2_mixer-static)
+	endif()
 	find_package(SDL2_ttf REQUIRED)
 	if(TARGET SDL2_ttf::SDL2_ttf)
 		add_library(SDL2::TTF ALIAS SDL2_ttf::SDL2_ttf)
 	endif()
+	if(TARGET SDL2_ttf::SDL2_ttf-static)
+		add_library(SDL2::TTF ALIAS SDL2_ttf::SDL2_ttf-static)
+	endif()
 endif()
 
 if(ENABLE_LOBBY)
@@ -666,6 +658,10 @@ if(NOT TARGET minizip::minizip)
 	add_library(minizip::minizip ALIAS minizip)
 endif()
 
+if(ENABLE_LAUNCHER OR ENABLE_EDITOR)
+	add_subdirectory(vcmiqt)
+endif()
+
 if(ENABLE_LAUNCHER)
 	add_subdirectory(launcher)
 endif()
@@ -727,7 +723,7 @@ endif()
 
 if(WIN32)
 	if(TBB_FOUND AND MSVC)
-		   install_vcpkg_imported_tgt(TBB::tbb)
+		install_vcpkg_imported_tgt(TBB::tbb)
 	endif()
 
 	if(USING_CONAN)
@@ -737,7 +733,9 @@ if(WIN32)
 				${dep_files}
 				"${CMAKE_SYSROOT}/bin/*.dll" 
 				"${CMAKE_SYSROOT}/lib/*.dll" 
-				"${CONAN_SYSTEM_LIBRARY_LOCATION}/*.dll")
+				"${CONAN_SYSTEM_LIBRARY_LOCATION}/libgcc_s_dw2-1.dll" # for 32-bit only?
+				"${CONAN_SYSTEM_LIBRARY_LOCATION}/libgcc_s_seh-1.dll" # for 64-bit only?
+				"${CONAN_SYSTEM_LIBRARY_LOCATION}/libstdc++-6.dll")
 	else()
 		file(GLOB dep_files
 				${dep_files}

+ 21 - 1
CMakePresets.json

@@ -134,7 +134,9 @@
             "description": "VCMI Windows Ninja using MinGW",
             "inherits": "default-release",
             "cacheVariables": {
-                "CMAKE_BUILD_TYPE": "Release"
+                "CMAKE_BUILD_TYPE": "Release",
+                "CMAKE_C_COMPILER": "gcc",
+                "CMAKE_CXX_COMPILER": "g++"
             }
         },
         {
@@ -154,6 +156,19 @@
 
             }
         },
+        {
+            "name": "windows-msvc-release-x86",
+            "displayName": "Windows x86 RelWithDebInfo",
+            "description": "VCMI RelWithDebInfo build",
+            "inherits": "default-release",
+            "generator": "Visual Studio 17 2022",
+            "cacheVariables": {
+                "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake",
+                "CMAKE_POLICY_DEFAULT_CMP0091": "NEW",
+                "FORCE_BUNDLED_MINIZIP": "ON",
+                "CMAKE_GENERATOR_PLATFORM": "WIN32"
+            }
+        },
         {
             "name": "windows-msvc-release-ccache",
             "displayName": "Windows x64 RelWithDebInfo with ccache",
@@ -382,6 +397,11 @@
             "configurePreset": "windows-msvc-release",
             "inherits": "default-release"
         },
+        {
+            "name": "windows-msvc-release-x86",
+            "configurePreset": "windows-msvc-release-x86",
+            "inherits": "default-release"
+        },
         {
             "name": "windows-msvc-release-ccache",
             "configurePreset": "windows-msvc-release-ccache",

文件差異過大導致無法顯示
+ 235 - 67
ChangeLog.md


+ 4 - 1
Global.h

@@ -154,7 +154,10 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 #endif
 #define BOOST_THREAD_DONT_PROVIDE_THREAD_DESTRUCTOR_CALLS_TERMINATE_IF_JOINABLE 1
 //need to link boost thread dynamically to avoid https://stackoverflow.com/questions/35978572/boost-thread-interupt-does-not-work-when-crossing-a-dll-boundary
-#define BOOST_THREAD_USE_DLL //for example VCAI::finish() may freeze on thread join after interrupt when linking this statically
+//for example VCAI::finish() may freeze on thread join after interrupt when linking this statically
+#ifndef BOOST_THREAD_USE_DLL
+#  define BOOST_THREAD_USE_DLL
+#endif
 #define BOOST_BIND_NO_PLACEHOLDERS
 
 #if BOOST_VERSION >= 106600

+ 0 - 0
Mods/vcmi/Data/NotoSans-Medium.ttf → Mods/vcmi/Content/Data/NotoSans-Medium.ttf


+ 0 - 0
Mods/vcmi/Data/NotoSerif-Black.ttf → Mods/vcmi/Content/Data/NotoSerif-Black.ttf


+ 0 - 0
Mods/vcmi/Data/NotoSerif-Bold.ttf → Mods/vcmi/Content/Data/NotoSerif-Bold.ttf


+ 0 - 0
Mods/vcmi/Data/NotoSerif-Medium.ttf → Mods/vcmi/Content/Data/NotoSerif-Medium.ttf


+ 0 - 0
Mods/vcmi/Data/s/std.verm → Mods/vcmi/Content/Data/s/std.verm


+ 0 - 0
Mods/vcmi/Data/s/testy.erm → Mods/vcmi/Content/Data/s/testy.erm


+ 0 - 0
Mods/vcmi/Sounds/we5.wav → Mods/vcmi/Content/Sounds/we5.wav


+ 0 - 0
Mods/vcmi/Sprites/PortraitsLarge.json → Mods/vcmi/Content/Sprites/PortraitsLarge.json


部分文件因文件數量過多而無法顯示