Browse Source

Merge remote-tracking branch 'origin/develop' into reward_select_all

# Conflicts:
#	lib/mapObjects/CRewardableObject.cpp
Tomasz Zieliński 1 year ago
parent
commit
fe8bcc5758
100 changed files with 728 additions and 639 deletions
  1. 11 0
      .github/dependabot.yml
  2. 71 99
      .github/workflows/github.yml
  3. 8 0
      .gitignore
  4. 9 10
      AI/BattleAI/AttackPossibility.cpp
  5. 0 1
      AI/BattleAI/AttackPossibility.h
  6. 3 4
      AI/BattleAI/BattleAI.cpp
  7. 34 27
      AI/BattleAI/BattleEvaluator.cpp
  8. 12 82
      AI/BattleAI/BattleExchangeVariant.cpp
  9. 0 5
      AI/BattleAI/BattleExchangeVariant.h
  10. 3 5
      AI/BattleAI/CMakeLists.txt
  11. 3 3
      AI/BattleAI/StackWithBonuses.cpp
  12. 1 1
      AI/BattleAI/StackWithBonuses.h
  13. 0 26
      AI/BattleAI/common.h
  14. 1 1
      AI/BattleAI/main.cpp
  15. 2 2
      AI/EmptyAI/CEmptyAI.cpp
  16. 2 2
      AI/EmptyAI/CEmptyAI.h
  17. 3 3
      AI/EmptyAI/CMakeLists.txt
  18. 0 1
      AI/EmptyAI/main.cpp
  19. 51 46
      AI/Nullkiller/AIGateway.cpp
  20. 4 4
      AI/Nullkiller/AIGateway.h
  21. 2 4
      AI/Nullkiller/AIUtility.cpp
  22. 5 4
      AI/Nullkiller/AIUtility.h
  23. 10 10
      AI/Nullkiller/Analyzers/ArmyManager.cpp
  24. 6 1
      AI/Nullkiller/Analyzers/BuildAnalyzer.cpp
  25. 1 0
      AI/Nullkiller/Analyzers/BuildAnalyzer.h
  26. 7 9
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp
  27. 1 1
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h
  28. 6 4
      AI/Nullkiller/Analyzers/HeroManager.cpp
  29. 3 3
      AI/Nullkiller/Analyzers/HeroManager.h
  30. 2 2
      AI/Nullkiller/Behaviors/BuildingBehavior.cpp
  31. 3 3
      AI/Nullkiller/Behaviors/BuildingBehavior.h
  32. 1 2
      AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp
  33. 3 3
      AI/Nullkiller/Behaviors/BuyArmyBehavior.h
  34. 3 3
      AI/Nullkiller/Behaviors/CaptureObjectsBehavior.h
  35. 3 3
      AI/Nullkiller/Behaviors/ClusterBehavior.h
  36. 3 3
      AI/Nullkiller/Behaviors/DefenceBehavior.h
  37. 3 3
      AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp
  38. 3 3
      AI/Nullkiller/Behaviors/GatherArmyBehavior.h
  39. 1 2
      AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp
  40. 3 3
      AI/Nullkiller/Behaviors/RecruitHeroBehavior.h
  41. 1 1
      AI/Nullkiller/Behaviors/StartupBehavior.cpp
  42. 3 3
      AI/Nullkiller/Behaviors/StartupBehavior.h
  43. 3 3
      AI/Nullkiller/Behaviors/StayAtTownBehavior.h
  44. 5 3
      AI/Nullkiller/CMakeLists.txt
  45. 3 1
      AI/Nullkiller/Engine/FuzzyEngines.cpp
  46. 8 2
      AI/Nullkiller/Engine/FuzzyEngines.h
  47. 3 6
      AI/Nullkiller/Engine/FuzzyHelper.cpp
  48. 13 13
      AI/Nullkiller/Engine/Nullkiller.cpp
  49. 3 1
      AI/Nullkiller/Engine/Nullkiller.h
  50. 52 24
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  51. 78 0
      AI/Nullkiller/Engine/Settings.cpp
  52. 42 0
      AI/Nullkiller/Engine/Settings.h
  53. 2 10
      AI/Nullkiller/Goals/AbstractGoal.h
  54. 1 1
      AI/Nullkiller/Goals/AdventureSpellCast.h
  55. 1 1
      AI/Nullkiller/Goals/Build.h
  56. 1 1
      AI/Nullkiller/Goals/BuildBoat.h
  57. 2 2
      AI/Nullkiller/Goals/BuildThis.h
  58. 2 2
      AI/Nullkiller/Goals/BuyArmy.cpp
  59. 3 3
      AI/Nullkiller/Goals/BuyArmy.h
  60. 6 6
      AI/Nullkiller/Goals/CGoal.h
  61. 5 5
      AI/Nullkiller/Goals/CaptureObject.h
  62. 2 2
      AI/Nullkiller/Goals/CompleteQuest.cpp
  63. 5 5
      AI/Nullkiller/Goals/CompleteQuest.h
  64. 5 5
      AI/Nullkiller/Goals/Composition.h
  65. 1 1
      AI/Nullkiller/Goals/DigAtTile.h
  66. 1 1
      AI/Nullkiller/Goals/DismissHero.h
  67. 1 1
      AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h
  68. 2 2
      AI/Nullkiller/Goals/ExecuteHeroChain.h
  69. 1 1
      AI/Nullkiller/Goals/GatherArmy.h
  70. 3 3
      AI/Nullkiller/Goals/Invalid.h
  71. 2 2
      AI/Nullkiller/Goals/RecruitHero.h
  72. 1 1
      AI/Nullkiller/Goals/SaveResources.h
  73. 2 2
      AI/Nullkiller/Goals/StayAtTown.h
  74. 1 1
      AI/Nullkiller/Goals/Trade.h
  75. 2 2
      AI/Nullkiller/Markers/ArmyUpgrade.h
  76. 2 2
      AI/Nullkiller/Markers/DefendTown.h
  77. 2 2
      AI/Nullkiller/Markers/HeroExchange.h
  78. 2 2
      AI/Nullkiller/Markers/UnlockCluster.h
  79. 26 8
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  80. 13 7
      AI/Nullkiller/Pathfinding/AINodeStorage.h
  81. 1 1
      AI/Nullkiller/Pathfinding/AIPathfinderConfig.h
  82. 8 7
      AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp
  83. 5 4
      AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h
  84. 2 2
      AI/Nullkiller/Pathfinding/Actions/BattleAction.h
  85. 10 10
      AI/Nullkiller/Pathfinding/Actions/BoatActions.h
  86. 4 4
      AI/Nullkiller/Pathfinding/Actions/QuestAction.h
  87. 2 2
      AI/Nullkiller/Pathfinding/Actions/TownPortalAction.h
  88. 7 7
      AI/Nullkiller/Pathfinding/Actors.cpp
  89. 19 19
      AI/Nullkiller/Pathfinding/Actors.h
  90. 15 8
      AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp
  91. 1 1
      AI/Nullkiller/main.cpp
  92. 3 3
      AI/StupidAI/CMakeLists.txt
  93. 15 13
      AI/StupidAI/StupidAI.cpp
  94. 1 1
      AI/StupidAI/main.cpp
  95. 2 3
      AI/VCAI/AIUtility.h
  96. 1 1
      AI/VCAI/ArmyManager.cpp
  97. 19 20
      AI/VCAI/BuildingManager.cpp
  98. 3 3
      AI/VCAI/CMakeLists.txt
  99. 4 2
      AI/VCAI/FuzzyEngines.cpp
  100. 8 2
      AI/VCAI/FuzzyEngines.h

+ 11 - 0
.github/dependabot.yml

@@ -0,0 +1,11 @@
+# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot#example-dependabotyml-file-for-github-actions
+# Set update schedule for GitHub Actions
+
+version: 2
+updates:
+
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      # Check for updates to GitHub Actions every day
+      interval: "daily"

+ 71 - 99
.github/workflows/github.yml

@@ -6,9 +6,8 @@ on:
       - features/*
       - beta
       - master
+      - develop
   pull_request:
-  schedule:
-    - cron: '0 2 * * *'
   workflow_dispatch:
 
 env:
@@ -16,55 +15,7 @@ env:
   BUILD_TYPE: Release
 
 jobs:
-  check_last_build:
-    if: github.event.schedule != ''
-    runs-on: ubuntu-latest
-    outputs:
-      skip_build: ${{ steps.check_if_built.outputs.skip_build }}
-    defaults:
-      run:
-        shell: bash
-    steps:
-      - name: Get repo name
-        id: get_repo_name
-        run: echo "::set-output name=value::${GITHUB_REPOSITORY#*/}"
-      - name: Get last successful build for ${{ github.sha }}
-        uses: octokit/[email protected]
-        id: get_last_scheduled_run
-        with:
-          route: GET /repos/{owner}/{repo}/actions/runs
-          owner: ${{ github.repository_owner }}
-          repo: ${{ steps.get_repo_name.outputs.value }}
-          status: success
-          per_page: 1
-          head_sha: ${{ github.sha }}
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-      - name: Check if successful build of the current commit exists
-        id: check_if_built
-        run: |
-          if [ ${{ fromJson(steps.get_last_scheduled_run.outputs.data).total_count }} -gt 0 ]; then
-            echo '::set-output name=skip_build::1'
-          else
-            echo '::set-output name=skip_build::0'
-          fi
-      - name: Cancel current run
-        if: steps.check_if_built.outputs.skip_build == 1
-        uses: octokit/[email protected]
-        with:
-          route: POST /repos/{owner}/{repo}/actions/runs/{run_id}/cancel
-          owner: ${{ github.repository_owner }}
-          repo: ${{ steps.get_repo_name.outputs.value }}
-          run_id: ${{ github.run_id }}
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-      - name: Wait for the run to be cancelled
-        if: steps.check_if_built.outputs.skip_build == 1
-        run: sleep 60
-
   build:
-    needs: check_last_build
-    if: always() && needs.check_last_build.skip_build != 1
     strategy:
       matrix:
         include:
@@ -73,8 +24,8 @@ jobs:
             test: 0
             preset: linux-clang-test
           - platform: linux
-            os: ubuntu-20.04
-            test: 0
+            os: ubuntu-22.04
+            test: 1
             preset: linux-gcc-test
           - platform: linux
             os: ubuntu-20.04
@@ -84,6 +35,7 @@ jobs:
             os: macos-12
             test: 0
             pack: 1
+            pack_type: Release
             extension: dmg
             preset: macos-conan-ninja-release
             conan_profile: macos-intel
@@ -93,6 +45,7 @@ jobs:
             os: macos-12
             test: 0
             pack: 1
+            pack_type: Release
             extension: dmg
             preset: macos-arm-conan-ninja-release
             conan_profile: macos-arm
@@ -102,6 +55,7 @@ jobs:
             os: macos-12
             test: 0
             pack: 1
+            pack_type: Release
             extension: ipa
             preset: ios-release-conan-ccache
             conan_profile: ios-arm64
@@ -110,17 +64,29 @@ jobs:
             os: windows-latest
             test: 0
             pack: 1
+            pack_type: RelWithDebInfo
             extension: exe
             preset: windows-msvc-release-ccache
-          - platform: mingw-ubuntu
+          - platform: mingw
             os: ubuntu-22.04
             test: 0
             pack: 1
+            pack_type: Release
             extension: exe
             cpack_args: -D CPACK_NSIS_EXECUTABLE=`which makensis`
             cmake_args: -G Ninja
             preset: windows-mingw-conan-linux
             conan_profile: mingw64-linux.jinja
+          - platform: mingw-32
+            os: ubuntu-22.04
+            test: 0
+            pack: 1
+            pack_type: Release
+            extension: exe
+            cpack_args: -D CPACK_NSIS_EXECUTABLE=`which makensis`
+            cmake_args: -G Ninja
+            preset: windows-mingw-conan-linux
+            conan_profile: mingw32-linux.jinja
           - platform: android-32
             os: ubuntu-22.04
             extension: apk
@@ -141,7 +107,7 @@ jobs:
         shell: bash
 
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: recursive
 
@@ -150,7 +116,7 @@ jobs:
       run: |
         find . -path ./.git -prune -o -path ./AI/FuzzyLite -prune -o -path ./test/googletest \
         -o -path ./osx  -prune -o -type f \
-        -not -name '*.png' -and -not -name '*.vcxproj*' -and -not -name '*.props' -and -not -name '*.wav' -and -not -name '*.ico' -and -not -name '*.bat' -print0 | \
+        -not -name '*.png' -and -not -name '*.vcxproj*' -and -not -name '*.props' -and -not -name '*.wav' -and -not -name '*.webm' -and -not -name '*.ico' -and -not -name '*.bat' -print0 | \
         { ! xargs -0 grep -l -z -P '\r\n'; }
 
     - name: Validate JSON
@@ -158,7 +124,7 @@ jobs:
       # also, running it on multiple presets is redundant and slightly increases already long CI built times
       if: ${{ startsWith(matrix.preset, 'linux-clang-test') }}
       run: |
-        pip3 install json5 jstyleson
+        pip3 install jstyleson
         python3 CI/linux-qt6/validate_json.py
 
     - name: Dependencies
@@ -168,7 +134,7 @@ jobs:
 
     # 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
+    - name: ccache for PRs
       uses: hendrikmuhs/[email protected]
       if: ${{ github.event.number != '' }}
       with:
@@ -180,9 +146,9 @@ jobs:
         max-size: "5G"
         verbose: 2
 
-    - name: Ccache for everything but PRs
+    - name: ccache for everything but PRs
       uses: hendrikmuhs/[email protected]
-      if: ${{ github.event.number == '' }}
+      if: ${{ (github.repository == 'vcmi/vcmi' && github.event.number == '' && github.ref == 'refs/heads/develop') ||  github.repository != 'vcmi/vcmi' }}
       with:
         key: ${{ matrix.preset }}-no-PR
         restore-keys: |
@@ -191,7 +157,17 @@ jobs:
         max-size: "5G"
         verbose: 2
 
-    - uses: actions/setup-python@v4
+    - name: Prepare Heroes 3 data
+      env:
+        HEROES_3_DATA_PASSWORD: ${{ secrets.HEROES_3_DATA_PASSWORD }}
+      if: ${{ env.HEROES_3_DATA_PASSWORD != '' && matrix.test == 1 }}
+      run: |
+        wget --progress=dot:giga https://github.com/vcmi-mods/vcmi-test-data/releases/download/v1.0/h3_assets.zip
+        7za x h3_assets.zip -p$HEROES_3_DATA_PASSWORD
+        mkdir -p ~/.local/share/vcmi/
+        mv h3_assets/* ~/.local/share/vcmi/
+
+    - uses: actions/setup-python@v5
       if: "${{ matrix.conan_profile != '' }}"
       with:
         python-version: '3.10'
@@ -211,10 +187,6 @@ jobs:
       env:
         GENERATE_ONLY_BUILT_CONFIG: 1
 
-    - name: Git branch name
-      id: git-branch-name
-      uses: EthanSK/git-branch-name-action@v1
-
     - name: Build Number
       run: |
         source '${{github.workspace}}/CI/get_package_name.sh'
@@ -227,31 +199,42 @@ jobs:
       env:
         PULL_REQUEST: ${{ github.event.pull_request.number }}
 
-    - name: CMake Preset with ccache
+    - name: Configure
       run: |
-        cmake -DCMAKE_CXX_COMPILER_LAUNCHER=ccache --preset ${{ matrix.preset }}
+        if [[ ${{matrix.preset}} == linux-gcc-test ]]; then GCC13=1; fi
+        cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }} ${GCC13:+-DCMAKE_C_COMPILER=gcc-13 -DCMAKE_CXX_COMPILER=g++-13}
 
-    - name: Build Preset
+    - name: Build
       run: |
         cmake --build --preset ${{matrix.preset}}
 
     - name: Test
-      if: ${{ matrix.test == 1 }}
+      env:
+        HEROES_3_DATA_PASSWORD: ${{ secrets.HEROES_3_DATA_PASSWORD }}
+      if: ${{ env.HEROES_3_DATA_PASSWORD != '' && matrix.test == 1 }}
       run: |
         ctest --preset ${{matrix.preset}}
 
+    - name: Kill XProtect to work around CPack issue on macOS 
+      if: ${{ startsWith(matrix.platform, 'mac') }}
+      run: |
+        # Cf. https://github.com/actions/runner-images/issues/7522#issuecomment-1556766641
+        echo Killing...; sudo pkill -9 XProtect >/dev/null || true;
+        echo "Waiting..."; counter=0; while pgrep XProtect && ((counter < 20)); do sleep 3; ((counter++)); done
+        pgrep XProtect || true
+
     - name: Pack
       id: cpack
       if: ${{ matrix.pack == 1 }}
       run: |
         cd '${{github.workspace}}/out/build/${{matrix.preset}}'
         CPACK_PATH=`which -a cpack | grep -m1 -v -i chocolatey`
-        "$CPACK_PATH" -C ${{env.BUILD_TYPE}} ${{ matrix.cpack_args }}
+        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 }}'.*)"
         rm -rf _CPack_Packages
 
-    - name: Create android package
+    - name: Create Android package
       if: ${{ startsWith(matrix.platform, 'android') }}
       run: |
         cd android
@@ -266,7 +249,7 @@ jobs:
 
     - name: Artifacts
       if: ${{ matrix.pack == 1 }}
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
         path: |
@@ -274,22 +257,30 @@ jobs:
           
     - name: Android artifacts
       if: ${{ startsWith(matrix.platform, 'android') }}
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
         path: |
           ${{ env.ANDROID_APK_PATH }}
+          
+    - name: Symbols
+      if: ${{ matrix.platform == 'msvc' }}
+      uses: actions/upload-artifact@v4
+      with:
+        name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - symbols
+        path: |
+            ${{github.workspace}}/**/*.pdb
 
     - name: Android JNI ${{matrix.platform}}
       if: ${{ startsWith(matrix.platform, 'android') && github.ref == 'refs/heads/master' }}
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
         name: Android JNI ${{matrix.platform}}
         path: |
           ${{ github.workspace }}/android/vcmi-app/src/main/jniLibs
 
     - 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' }}
+      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' }}
       continue-on-error: true
       run: |
         if cd '${{github.workspace}}/android/vcmi-app/build/outputs/apk/daily' ; then
@@ -301,14 +292,6 @@ jobs:
       env:
         DEPLOY_RSA: ${{ secrets.DEPLOY_RSA }}
         PACKAGE_EXTENSION: ${{ matrix.extension }}
-
-    - uses: act10ns/slack@v1
-      with:
-        status: ${{ job.status }}
-        channel: '#notifications'
-      env:
-        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
-      if: always()
   
   # copy-pasted mostly
   bundle_release:
@@ -331,7 +314,7 @@ jobs:
         shell: bash
 
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: recursive
 
@@ -340,10 +323,11 @@ jobs:
       env:
         VCMI_BUILD_PLATFORM: x64
 
-    - uses: actions/setup-python@v4
+    - uses: actions/setup-python@v5
       if: "${{ matrix.conan_profile != '' }}"
       with:
         python-version: '3.10'
+
     - name: Conan setup
       if: "${{ matrix.conan_profile != '' }}"
       run: |
@@ -359,10 +343,6 @@ jobs:
       env:
         GENERATE_ONLY_BUILT_CONFIG: 1
 
-    - name: Git branch name
-      id: git-branch-name
-      uses: EthanSK/git-branch-name-action@v1
-
     - name: Build Number
       run: |
         source '${{github.workspace}}/CI/get_package_name.sh'
@@ -384,12 +364,12 @@ jobs:
         cmake --build --preset ${{matrix.preset}}
 
     - name: Download libs x64
-      uses: actions/download-artifact@v3
+      uses: actions/download-artifact@v4
       with:
         name: Android JNI android-64
         path: ${{ github.workspace }}/android/vcmi-app/src/main/jniLibs/
  
-    - name: Create android package
+    - name: Create Android package
       run: |
         cd android
         ./gradlew bundleRelease --info
@@ -399,16 +379,8 @@ jobs:
         ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
 
     - name: Android artifacts
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }}
         path: |
           ${{ env.ANDROID_APK_PATH }}
-
-    - uses: act10ns/slack@v1
-      with:
-        status: ${{ job.status }}
-        channel: '#notifications'
-      env:
-        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
-      if: always()

+ 8 - 0
.gitignore

@@ -1,12 +1,15 @@
 /client/vcmiclient
 /server/vcmiserver
+/launcher/.lupdate
 /launcher/vcmilauncher
+/mapeditor/.lupdate
 /launcher/vcmilauncher_automoc.cpp
 /conan-*
 
 build/
 .cache/*
 out/
+/.qt
 *.dll
 *.exe
 *.depend
@@ -41,6 +44,8 @@ VCMI_VS11.sdf
 VCMI_VS11.opensdf
 .DS_Store
 CMakeUserPresets.json
+compile_commands.json
+fuzzylite.pc
 
 # Visual Studio
 *.suo
@@ -61,5 +66,8 @@ CMakeUserPresets.json
 /deps
 .vs/
 
+# Visual Studio Code
+/.vscode/
+
 # CLion
 .idea/

+ 9 - 10
AI/BattleAI/AttackPossibility.cpp

@@ -33,7 +33,8 @@ void DamageCache::buildDamageCache(std::shared_ptr<HypotheticBattle> hb, int sid
 			return u->isValidTarget();
 		});
 
-	std::vector<const battle::Unit *> ourUnits, enemyUnits;
+	std::vector<const battle::Unit *> ourUnits;
+	std::vector<const battle::Unit *> enemyUnits;
 
 	for(auto stack : stacks)
 	{
@@ -61,16 +62,12 @@ void DamageCache::buildDamageCache(std::shared_ptr<HypotheticBattle> hb, int sid
 
 int64_t DamageCache::getDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb)
 {
-	auto damage = damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount();
+	bool wasComputedBefore = damageCache[attacker->unitId()].count(defender->unitId());
 
-	if(damage == 0)
-	{
+	if (!wasComputedBefore)
 		cacheDamage(attacker, defender, hb);
 
-		damage = damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount();
-	}
-
-	return static_cast<int64_t>(damage);
+	return damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount();
 }
 
 int64_t DamageCache::getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb)
@@ -295,8 +292,10 @@ AttackPossibility AttackPossibility::evaluate(
 
 			for(int i = 0; i < totalAttacks; i++)
 			{
-				int64_t damageDealt, damageReceived;
-				float defenderDamageReduce, attackerDamageReduce;
+				int64_t damageDealt;
+				int64_t damageReceived;
+				float defenderDamageReduce;
+				float attackerDamageReduce;
 
 				DamageEstimation retaliation;
 				auto attackDmg = state->battleEstimateDamage(ap.attack, &retaliation);

+ 0 - 1
AI/BattleAI/AttackPossibility.h

@@ -10,7 +10,6 @@
 #pragma once
 #include "../../lib/battle/CUnitState.h"
 #include "../../CCallback.h"
-#include "common.h"
 #include "StackWithBonuses.h"
 
 #define BATTLE_TRACE_LEVEL 0

+ 3 - 4
AI/BattleAI/BattleAI.cpp

@@ -49,7 +49,6 @@ CBattleAI::~CBattleAI()
 
 void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
 {
-	setCbc(CB);
 	env = ENV;
 	cb = CB;
 	playerID = *CB->getPlayerID();
@@ -90,7 +89,8 @@ void CBattleAI::yourTacticPhase(const BattleID & battleID, int distance)
 static float getStrengthRatio(std::shared_ptr<CBattleInfoCallback> cb, int side)
 {
 	auto stacks = cb->battleGetAllStacks();
-	auto our = 0, enemy = 0;
+	auto our = 0;
+	auto enemy = 0;
 
 	for(auto stack : stacks)
 	{
@@ -120,7 +120,6 @@ void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
 	};
 
 	BattleAction result = BattleAction::makeDefend(stack);
-	setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too)
 
 	auto start = std::chrono::high_resolution_clock::now();
 
@@ -145,7 +144,7 @@ void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
 
 		result = evaluator.selectStackAction(stack);
 
-		if(!skipCastUntilNextBattle && evaluator.canCastSpell())
+		if(autobattlePreferences.enableSpellsUsage && !skipCastUntilNextBattle && evaluator.canCastSpell())
 		{
 			auto spelCasted = evaluator.attemptCastingSpell(stack);
 

+ 34 - 27
AI/BattleAI/BattleEvaluator.cpp

@@ -22,6 +22,8 @@
 #include "../../lib/battle/BattleStateInfoForRetreat.h"
 #include "../../lib/battle/CObstacleInstance.h"
 #include "../../lib/battle/BattleAction.h"
+#include "../../lib/CRandomGenerator.h"
+
 
 // TODO: remove
 // Eventually only IBattleInfoCallback and battle::Unit should be used,
@@ -70,8 +72,9 @@ std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
 std::optional<PossibleSpellcast> BattleEvaluator::findBestCreatureSpell(const CStack *stack)
 {
 	//TODO: faerie dragon type spell should be selected by server
-	SpellID creatureSpellToCast = cb->getBattle(battleID)->battleGetRandomStackSpell(CRandomGenerator::getDefault(), stack, CBattleInfoCallback::RANDOM_AIMED);
-	if(stack->hasBonusOfType(BonusType::SPELLCASTER) && stack->canCast() && creatureSpellToCast != SpellID::NONE)
+	SpellID creatureSpellToCast = cb->getBattle(battleID)->getRandomCastedSpell(CRandomGenerator::getDefault(), stack);
+
+	if(stack->canCast() && creatureSpellToCast != SpellID::NONE)
 	{
 		const CSpell * spell = creatureSpellToCast.toSpell();
 
@@ -146,7 +149,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 				(int)bestAttack.from,
 				(int)bestAttack.attack.attacker->getPosition().hex,
 				bestAttack.attack.chargeDistance,
-				bestAttack.attack.attacker->speed(0, true),
+				bestAttack.attack.attacker->getMovementRange(0),
 				bestAttack.defenderDamageReduce,
 				bestAttack.attackerDamageReduce,
 				score
@@ -224,7 +227,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 		}
 	}
 
-	return BattleAction::makeDefend(stack);
+	return stack->waited() ?  BattleAction::makeDefend(stack) : BattleAction::makeWait(stack);
 }
 
 uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock> start)
@@ -349,10 +352,11 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 	LOGL("Casting spells sounds like fun. Let's see...");
 	//Get all spells we can cast
 	std::vector<const CSpell*> possibleSpells;
-	vstd::copy_if(VLC->spellh->objects, std::back_inserter(possibleSpells), [hero, this](const CSpell *s) -> bool
-	{
-		return s->canBeCast(cb->getBattle(battleID).get(), spells::Mode::HERO, hero);
-	});
+
+	for (auto const & s : VLC->spellh->objects)
+		if (s->canBeCast(cb->getBattle(battleID).get(), spells::Mode::HERO, hero))
+			possibleSpells.push_back(s.get());
+
 	LOGFL("I can cast %d spells.", possibleSpells.size());
 
 	vstd::erase_if(possibleSpells, [](const CSpell *s)
@@ -427,33 +431,36 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 				state->nextTurn(unit->unitId());
 
-				PotentialTargets pt(unit, damageCache, state);
+				PotentialTargets potentialTargets(unit, damageCache, state);
 
-				if(!pt.possibleAttacks.empty())
+				if(!potentialTargets.possibleAttacks.empty())
 				{
-					AttackPossibility ap = pt.bestAction();
+					AttackPossibility attackPossibility = potentialTargets.bestAction();
 
-					auto swb = state->getForUpdate(unit->unitId());
-					*swb = *ap.attackerState;
+					auto stackWithBonuses = state->getForUpdate(unit->unitId());
+					*stackWithBonuses = *attackPossibility.attackerState;
 
-					if(ap.defenderDamageReduce > 0)
-						swb->removeUnitBonus(Bonus::UntilAttack);
-					if(ap.attackerDamageReduce > 0)
-						swb->removeUnitBonus(Bonus::UntilBeingAttacked);
+					if(attackPossibility.defenderDamageReduce > 0)
+					{
+						stackWithBonuses->removeUnitBonus(Bonus::UntilAttack);
+						stackWithBonuses->removeUnitBonus(Bonus::UntilOwnAttack);
+					}
+					if(attackPossibility.attackerDamageReduce > 0)
+						stackWithBonuses->removeUnitBonus(Bonus::UntilBeingAttacked);
 
-					for(auto affected : ap.affectedUnits)
+					for(auto affected : attackPossibility.affectedUnits)
 					{
-						swb = state->getForUpdate(affected->unitId());
-						*swb = *affected;
+						stackWithBonuses = state->getForUpdate(affected->unitId());
+						*stackWithBonuses = *affected;
 
-						if(ap.defenderDamageReduce > 0)
-							swb->removeUnitBonus(Bonus::UntilBeingAttacked);
-						if(ap.attackerDamageReduce > 0 && ap.attack.defender->unitId() == affected->unitId())
-							swb->removeUnitBonus(Bonus::UntilAttack);
+						if(attackPossibility.defenderDamageReduce > 0)
+							stackWithBonuses->removeUnitBonus(Bonus::UntilBeingAttacked);
+						if(attackPossibility.attackerDamageReduce > 0 && attackPossibility.attack.defender->unitId() == affected->unitId())
+							stackWithBonuses->removeUnitBonus(Bonus::UntilAttack);
 					}
 				}
 
-				auto bav = pt.bestActionValue();
+				auto bav = potentialTargets.bestActionValue();
 
 				//best action is from effective owner`s point if view, we need to convert to our point if view
 				if(state->battleGetOwner(unit) != playerID)
@@ -549,7 +556,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 				auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool
 					{
 						auto original = cb->getBattle(battleID)->battleGetUnitByID(u->unitId());
-						return  !original || u->speed() != original->speed();
+						return  !original || u->getMovementRange() != original->getMovementRange();
 					});
 
 				DamageCache safeCopy = damageCache;
@@ -605,7 +612,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 						if(ourUnit * goodEffect == 1)
 						{
-							if(ourUnit && goodEffect && (unit->isClone() || unit->isGhost() || !unit->unitSlot().validSlot()))
+							if(ourUnit && goodEffect && (unit->isClone() || unit->isGhost()))
 								continue;
 
 							ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier();

+ 12 - 82
AI/BattleAI/BattleExchangeVariant.cpp

@@ -258,7 +258,9 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
 
 	updateReachabilityMap(hb);
 
-	if(result.bestAttack.attack.shooting && hb->battleHasShootingPenalty(activeStack, result.bestAttack.dest))
+	if(result.bestAttack.attack.shooting
+		&& !activeStack->waited()
+		&& hb->battleHasShootingPenalty(activeStack, result.bestAttack.dest))
 	{
 		if(!canBeHitThisTurn(result.bestAttack))
 			return result; // lets wait
@@ -268,7 +270,7 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
 	{
 		float score = evaluateExchange(ap, 0, targets, damageCache, hb);
 
-		if(score > result.score || (score == result.score && result.wait))
+		if(score > result.score || (vstd::isAlmostEqual(score, result.score) && result.wait))
 		{
 			result.score = score;
 			result.bestAttack = ap;
@@ -295,7 +297,7 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
 	if(targets.unreachableEnemies.empty())
 		return result;
 
-	auto speed = activeStack->speed();
+	auto speed = activeStack->getMovementRange();
 
 	if(speed == 0)
 		return result;
@@ -322,7 +324,7 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
 
 		auto turnsToRich = (distance - 1) / speed + 1;
 		auto hexes = closestStack->getSurroundingHexes();
-		auto enemySpeed = closestStack->speed();
+		auto enemySpeed = closestStack->getMovementRange();
 		auto speedRatio = speed / static_cast<float>(enemySpeed);
 		auto multiplier = speedRatio > 1 ? 1 : speedRatio;
 
@@ -481,11 +483,6 @@ float BattleExchangeEvaluator::evaluateExchange(
 	DamageCache & damageCache,
 	std::shared_ptr<HypotheticBattle> hb)
 {
-	if(ap.from.hex == 127)
-	{
-		logAi->trace("x");
-	}
-
 	BattleScore score = calculateExchange(ap, turn, targets, damageCache, hb);
 
 #if BATTLE_TRACE_LEVEL >= 1
@@ -687,11 +684,6 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
 	for(auto hex : hexes)
 		reachabilityMap[hex] = getOneTurnReachableUnits(turn, hex);
 
-	if(!ap.attack.shooting)
-	{
-		v.adjustPositions(melleeAttackers, ap, reachabilityMap);
-	}
-
 #if BATTLE_TRACE_LEVEL>=1
 	logAi->trace("Exchange score: enemy: %2f, our -%2f", v.getScore().enemyDamageReduce, v.getScore().ourDamageReduce);
 #endif
@@ -699,69 +691,6 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
 	return v.getScore();
 }
 
-void BattleExchangeVariant::adjustPositions(
-	std::vector<const battle::Unit*> attackers,
-	const AttackPossibility & ap,
-	std::map<BattleHex, battle::Units> & reachabilityMap)
-{
-	auto hexes = ap.attack.defender->getSurroundingHexes();
-
-	boost::sort(attackers, [&](const battle::Unit * u1, const battle::Unit * u2) -> bool
-		{
-			if(attackerValue[u1->unitId()].isRetalitated && !attackerValue[u2->unitId()].isRetalitated)
-				return true;
-
-			if(attackerValue[u2->unitId()].isRetalitated && !attackerValue[u1->unitId()].isRetalitated)
-				return false;
-
-			return attackerValue[u1->unitId()].value > attackerValue[u2->unitId()].value;
-		});
-
-	vstd::erase_if_present(hexes, ap.from);
-	vstd::erase_if_present(hexes, ap.attack.attacker->occupiedHex(ap.attack.attackerPos));
-
-	float notRealizedDamage = 0;
-
-	for(auto unit : attackers)
-	{
-		if(unit->unitId() == ap.attack.attacker->unitId())
-			continue;
-
-		if(!vstd::contains_if(hexes, [&](BattleHex h) -> bool
-			{
-				return vstd::contains(reachabilityMap[h], unit);
-			}))
-		{
-			notRealizedDamage += attackerValue[unit->unitId()].value;
-			continue;
-		}
-
-		auto desiredPosition = vstd::minElementByFun(hexes, [&](BattleHex h) -> float
-			{
-				auto score = vstd::contains(reachabilityMap[h], unit)
-					? reachabilityMap[h].size()
-					: 0;
-
-				if(unit->doubleWide())
-				{
-					auto backHex = unit->occupiedHex(h);
-
-					if(vstd::contains(hexes, backHex))
-						score += reachabilityMap[backHex].size();
-				}
-
-				return score;
-			});
-
-		hexes.erase(desiredPosition);
-	}
-
-	if(notRealizedDamage > ap.attackValue() && notRealizedDamage > attackerValue[ap.attack.attacker->unitId()].value)
-	{
-		dpsScore = BattleScore(EvaluationResult::INEFFECTIVE_SCORE, 0);
-	}
-}
-
 bool BattleExchangeEvaluator::canBeHitThisTurn(const AttackPossibility & ap)
 {
 	for(auto pos : ap.attack.attacker->getSurroundingHexes())
@@ -824,7 +753,7 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUn
 				continue;
 			}
 
-			auto unitSpeed = unit->speed(turn);
+			auto unitSpeed = unit->getMovementRange(turn);
 			auto radius = unitSpeed * (turn + 1);
 
 			ReachabilityInfo unitReachability = vstd::getOrCompute(
@@ -887,14 +816,15 @@ bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb
 				continue;
 
 			auto blockedUnitDamage = unit->getMinDamage(hb.battleCanShoot(unit)) * unit->getCount();
-			auto ratio = blockedUnitDamage / (blockedUnitDamage + activeUnitDamage);
+			float ratio = blockedUnitDamage / (float)(blockedUnitDamage + activeUnitDamage + 0.01);
 
 			auto unitReachability = turnBattle.getReachability(unit);
+			auto unitSpeed = unit->getMovementRange(turn); // Cached value, to avoid performance hit
 
 			for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1)
 			{
 				bool enemyUnit = false;
-				bool reachable = unitReachability.distances[hex] <= unit->speed(turn);
+				bool reachable = unitReachability.distances[hex] <= unitSpeed;
 
 				if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK)
 				{
@@ -906,14 +836,14 @@ bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb
 
 						for(BattleHex neighbor : hex.neighbouringTiles())
 						{
-							reachable = unitReachability.distances[neighbor] <= unit->speed(turn);
+							reachable = unitReachability.distances[neighbor] <= unitSpeed;
 
 							if(reachable) break;
 						}
 					}
 				}
 
-				if(!reachable && vstd::contains(reachabilityMap[hex], unit))
+				if(!reachable && std::count(reachabilityMap[hex].begin(), reachabilityMap[hex].end(), unit) > 1)
 				{
 					blockingScore += ratio * (enemyUnit ? BLOCKING_OWN_ATTACK_PENALTY : BLOCKING_OWN_MOVE_PENALTY);
 				}

+ 0 - 5
AI/BattleAI/BattleExchangeVariant.h

@@ -106,11 +106,6 @@ public:
 
 	const BattleScore & getScore() const { return dpsScore; }
 
-	void adjustPositions(
-		std::vector<const battle::Unit *> attackers,
-		const AttackPossibility & ap,
-		std::map<BattleHex, battle::Units> & reachabilityMap);
-
 private:
 	BattleScore dpsScore;
 	std::map<uint32_t, AttackerValue> attackerValue;

+ 3 - 5
AI/BattleAI/CMakeLists.txt

@@ -2,7 +2,6 @@ set(battleAI_SRCS
 		AttackPossibility.cpp
 		BattleAI.cpp
 		BattleEvaluator.cpp
-		common.cpp
 		EnemyInfo.cpp
 		PossibleSpellcast.cpp
 		PotentialTargets.cpp
@@ -17,7 +16,6 @@ set(battleAI_HEADERS
 		AttackPossibility.h
 		BattleAI.h
 		BattleEvaluator.h
-		common.h
 		EnemyInfo.h
 		PotentialTargets.h
 		PossibleSpellcast.h
@@ -26,12 +24,12 @@ set(battleAI_HEADERS
 		BattleExchangeVariant.h
 )
 
-if(NOT ENABLE_STATIC_AI_LIBS)
+if(NOT ENABLE_STATIC_LIBS)
 	list(APPEND battleAI_SRCS main.cpp StdInc.cpp)
 endif()
 assign_source_group(${battleAI_SRCS} ${battleAI_HEADERS})
 
-if(ENABLE_STATIC_AI_LIBS)
+if(ENABLE_STATIC_LIBS)
 	add_library(BattleAI STATIC ${battleAI_SRCS} ${battleAI_HEADERS})
 else()
 	add_library(BattleAI SHARED ${battleAI_SRCS} ${battleAI_HEADERS})
@@ -39,7 +37,7 @@ else()
 endif()
 
 target_include_directories(BattleAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
-target_link_libraries(BattleAI PRIVATE ${VCMI_LIB_TARGET} TBB::tbb)
+target_link_libraries(BattleAI PRIVATE vcmi TBB::tbb)
 
 vcmi_set_output_dir(BattleAI "AI")
 enable_pch(BattleAI)

+ 3 - 3
AI/BattleAI/StackWithBonuses.cpp

@@ -134,7 +134,7 @@ SlotID StackWithBonuses::unitSlot() const
 TConstBonusListPtr StackWithBonuses::getAllBonuses(const CSelector & selector, const CSelector & limit,
 	const CBonusSystemNode * root, const std::string & cachingStr) const
 {
-	TBonusListPtr ret = std::make_shared<BonusList>();
+	auto ret = std::make_shared<BonusList>();
 	TConstBonusListPtr originalList = origBearer->getAllBonuses(selector, limit, root, cachingStr);
 
 	vstd::copy_if(*originalList, std::back_inserter(*ret), [this](const std::shared_ptr<Bonus> & b)
@@ -298,7 +298,7 @@ std::shared_ptr<StackWithBonuses> HypotheticBattle::getForUpdate(uint32_t id)
 	}
 }
 
-battle::Units HypotheticBattle::getUnitsIf(battle::UnitFilter predicate) const
+battle::Units HypotheticBattle::getUnitsIf(const battle::UnitFilter & predicate) const
 {
 	battle::Units proxyed = BattleProxy::getUnitsIf(predicate);
 
@@ -356,7 +356,7 @@ void HypotheticBattle::addUnit(uint32_t id, const JsonNode & data)
 {
 	battle::UnitInfo info;
 	info.load(id, data);
-	std::shared_ptr<StackWithBonuses> newUnit = std::make_shared<StackWithBonuses>(this, info);
+	auto newUnit = std::make_shared<StackWithBonuses>(this, info);
 	stackStates[newUnit->unitId()] = newUnit;
 }
 

+ 1 - 1
AI/BattleAI/StackWithBonuses.h

@@ -114,7 +114,7 @@ public:
 
 	int32_t getActiveStackID() const override;
 
-	battle::Units getUnitsIf(battle::UnitFilter predicate) const override;
+	battle::Units getUnitsIf(const battle::UnitFilter & predicate) const override;
 
 	void nextRound() override;
 	void nextTurn(uint32_t unitId) override;

+ 0 - 26
AI/BattleAI/common.h

@@ -1,26 +0,0 @@
-/*
- * common.h, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-#pragma once
-
-class CBattleCallback;
-
-template<typename Key, typename Val, typename Val2>
-const Val getValOr(const std::map<Key, Val> &Map, const Key &key, const Val2 defaultValue)
-{
-	//returning references here won't work: defaultValue must be converted into Val, creating temporary
-	auto i = Map.find(key);
-	if(i != Map.end())
-		return i->second;
-	else
-		return defaultValue;
-}
-
-void setCbc(std::shared_ptr<CBattleCallback> cb);
-std::shared_ptr<CBattleCallback> getCbc();

+ 1 - 1
AI/BattleAI/main.cpp

@@ -15,7 +15,7 @@
 #define strcpy_s(a, b, c) strncpy(a, c, b)
 #endif
 
-static const char *g_cszAiName = "Battle AI";
+static const char * const g_cszAiName = "Battle AI";
 
 extern "C" DLL_EXPORT int GetGlobalAiVersion()
 {

+ 2 - 2
AI/EmptyAI/CEmptyAI.cpp

@@ -14,11 +14,11 @@
 #include "../../lib/CStack.h"
 #include "../../lib/battle/BattleAction.h"
 
-void CEmptyAI::saveGame(BinarySerializer & h, const int version)
+void CEmptyAI::saveGame(BinarySerializer & h)
 {
 }
 
-void CEmptyAI::loadGame(BinaryDeserializer & h, const int version)
+void CEmptyAI::loadGame(BinaryDeserializer & h)
 {
 }
 

+ 2 - 2
AI/EmptyAI/CEmptyAI.h

@@ -19,8 +19,8 @@ class CEmptyAI : public CGlobalAI
 	std::shared_ptr<CCallback> cb;
 
 public:
-	virtual void saveGame(BinarySerializer & h, const int version) override;
-	virtual void loadGame(BinaryDeserializer & h, const int version) override;
+	void saveGame(BinarySerializer & h) override;
+	void loadGame(BinaryDeserializer & h) override;
 
 	void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
 	void yourTurn(QueryID queryID) override;

+ 3 - 3
AI/EmptyAI/CMakeLists.txt

@@ -8,12 +8,12 @@ set(emptyAI_HEADERS
 		CEmptyAI.h
 )
 
-if(NOT ENABLE_STATIC_AI_LIBS)
+if(NOT ENABLE_STATIC_LIBS)
 	list(APPEND emptyAI_SRCS main.cpp StdInc.cpp)
 endif()
 assign_source_group(${emptyAI_SRCS} ${emptyAI_HEADERS})
 
-if(ENABLE_STATIC_AI_LIBS)
+if(ENABLE_STATIC_LIBS)
 	add_library(EmptyAI STATIC ${emptyAI_SRCS} ${emptyAI_HEADERS})
 else()
 	add_library(EmptyAI SHARED ${emptyAI_SRCS} ${emptyAI_HEADERS})
@@ -21,7 +21,7 @@ else()
 endif()
 
 target_include_directories(EmptyAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
-target_link_libraries(EmptyAI PRIVATE ${VCMI_LIB_TARGET})
+target_link_libraries(EmptyAI PRIVATE vcmi)
 
 vcmi_set_output_dir(EmptyAI "AI")
 enable_pch(EmptyAI)

+ 0 - 1
AI/EmptyAI/main.cpp

@@ -11,7 +11,6 @@
 
 #include "CEmptyAI.h"
 
-std::set<CGlobalAI*> ais;
 extern "C" DLL_EXPORT int GetGlobalAiVersion()
 {
 	return AI_INTERFACE_VER;

+ 51 - 46
AI/Nullkiller/AIGateway.cpp

@@ -9,6 +9,7 @@
  */
 #include "StdInc.h"
 
+#include "../../lib/ArtifactUtils.h"
 #include "../../lib/UnlockGuard.h"
 #include "../../lib/mapObjects/MapObjects.h"
 #include "../../lib/mapObjects/ObjectTemplate.h"
@@ -63,7 +64,7 @@ struct SetGlobalState
 };
 
 
-#define SET_GLOBAL_STATE(ai) SetGlobalState _hlpSetState(ai);
+#define SET_GLOBAL_STATE(ai) SetGlobalState _hlpSetState(ai)
 
 #define NET_EVENT_HANDLER SET_GLOBAL_STATE(this)
 #define MAKING_TURN SET_GLOBAL_STATE(this)
@@ -100,7 +101,7 @@ void AIGateway::heroMoved(const TryMoveHero & details, bool verbose)
 	if(!hero)
 		validateObject(details.id); //enemy hero may have left visible area
 
-	const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));;
+	const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));
 	const int3 to   = hero ? hero->convertToVisitablePos(details.end)   : (details.end   - int3(0,1,0));
 
 	const CGObjectInstance * o1 = vstd::frontOrNull(cb->getVisitableObjs(from, verbose));
@@ -420,14 +421,14 @@ void AIGateway::requestRealized(PackageApplied * pa)
 	NET_EVENT_HANDLER;
 	if(status.haveTurn())
 	{
-		if(pa->packType == typeList.getTypeID<EndTurn>())
+		if(pa->packType == CTypeList::getInstance().getTypeID<EndTurn>(nullptr))
 		{
 			if(pa->result)
 				status.madeTurn();
 		}
 	}
 
-	if(pa->packType == typeList.getTypeID<QueryReply>())
+	if(pa->packType == CTypeList::getInstance().getTypeID<QueryReply>(nullptr))
 	{
 		status.receivedAnswerConfirmation(pa->requestID, pa->result);
 	}
@@ -480,7 +481,7 @@ void AIGateway::objectPropertyChanged(const SetObjectProperty * sop)
 	NET_EVENT_HANDLER;
 	if(sop->what == ObjProperty::OWNER)
 	{
-		auto relations = myCb->getPlayerRelations(playerID, (PlayerColor)sop->val);
+		auto relations = myCb->getPlayerRelations(playerID, sop->identifier.as<PlayerColor>());
 		auto obj = myCb->getObj(sop->id, false);
 
 		if(!nullkiller) // crash protection
@@ -585,11 +586,18 @@ void AIGateway::heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, s
 
 	requestActionASAP([=]()
 	{ 
+		int sel = 0;
+
 		if(hPtr.validAndSet())
 		{
+			std::unique_lock<std::mutex> lockGuard(nullkiller->aiStateMutex);
+
 			nullkiller->heroManager->update();
-			answerQuery(queryID, nullkiller->heroManager->selectBestSkill(hPtr, skills));
+
+			sel = nullkiller->heroManager->selectBestSkill(hPtr, skills);
 		}
+
+		answerQuery(queryID, sel);
 	});
 }
 
@@ -624,7 +632,8 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
 				auto topObj = objects.front()->id == hero->id ? objects.back() : objects.front();
 				auto objType = topObj->ID; // top object should be our hero
 				auto goalObjectID = nullkiller->getTargetObject();
-				auto ratio = (float)nullkiller->dangerEvaluator->evaluateDanger(target, hero.get()) / (float)hero->getTotalStrength();
+				auto danger = nullkiller->dangerEvaluator->evaluateDanger(target, hero.get());
+				auto ratio = static_cast<float>(danger) / hero->getTotalStrength();
 
 				answer = topObj->id == goalObjectID; // no if we do not aim to visit this object
 				logAi->trace("Query hook: %s(%s) by %s danger ratio %f", target.toString(), topObj->getObjectName(), hero.name, ratio);
@@ -640,7 +649,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
 				}
 				else if(objType == Obj::ARTIFACT || objType == Obj::RESOURCE)
 				{
-					bool dangerUnknown = ratio == 0;
+					bool dangerUnknown = danger == 0;
 					bool dangerTooHigh = ratio > (1 / SAFE_ATTACK_CONSTANT);
 
 					answer = !dangerUnknown && !dangerTooHigh;
@@ -660,14 +669,18 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
 		if(selection) //select from multiple components -> take the last one (they're indexed [1-size])
 			sel = components.size();
 
-		// TODO: Find better way to understand it is Chest of Treasures
-		if(hero.validAndSet()
-			&& components.size() == 2
-			&& components.front().id == Component::EComponentType::RESOURCE
-			&& (nullkiller->heroManager->getHeroRole(hero) != HeroRole::MAIN
-			|| nullkiller->buildAnalyzer->getGoldPreasure() > MAX_GOLD_PEASURE))
 		{
-			sel = 1; // for now lets pick gold from a chest.
+				std::unique_lock<std::mutex> mxLock(nullkiller->aiStateMutex);
+
+				// TODO: Find better way to understand it is Chest of Treasures
+				if(hero.validAndSet()
+					&& components.size() == 2
+					&& components.front().type == ComponentType::RESOURCE
+					&& (nullkiller->heroManager->getHeroRole(hero) != HeroRole::MAIN
+						|| nullkiller->buildAnalyzer->isGoldPreasureHigh()))
+				{
+					sel = 1;
+				}
 		}
 
 		answerQuery(askID, sel);
@@ -746,27 +759,25 @@ void AIGateway::showMapObjectSelectDialog(QueryID askID, const Component & icon,
 	requestActionASAP([=](){ answerQuery(askID, selectedObject.getNum()); });
 }
 
-void AIGateway::saveGame(BinarySerializer & h, const int version)
+void AIGateway::saveGame(BinarySerializer & h)
 {
-	LOG_TRACE_PARAMS(logAi, "version '%i'", version);
 	NET_EVENT_HANDLER;
 	nullkiller->memory->removeInvisibleObjects(myCb.get());
 
-	CAdventureAI::saveGame(h, version);
-	serializeInternal(h, version);
+	CAdventureAI::saveGame(h);
+	serializeInternal(h);
 }
 
-void AIGateway::loadGame(BinaryDeserializer & h, const int version)
+void AIGateway::loadGame(BinaryDeserializer & h)
 {
-	LOG_TRACE_PARAMS(logAi, "version '%i'", version);
 	//NET_EVENT_HANDLER;
 
 	#if 0
 	//disabled due to issue 2890
 	registerGoals(h);
 	#endif // 0
-	CAdventureAI::loadGame(h, version);
-	serializeInternal(h, version);
+	CAdventureAI::loadGame(h);
+	serializeInternal(h);
 }
 
 bool AIGateway::makePossibleUpgrades(const CArmedInstance * obj)
@@ -858,6 +869,8 @@ void AIGateway::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h
 		{
 			makePossibleUpgrades(h.get());
 
+			std::unique_lock<std::mutex>  lockGuard(nullkiller->aiStateMutex);
+
 			if(!h->visitedTown->garrisonHero || !nullkiller->isHeroLocked(h->visitedTown->garrisonHero))
 				moveCreaturesToHero(h->visitedTown);
 
@@ -995,21 +1008,21 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance
 				for(auto p : h->artifactsWorn)
 				{
 					if(p.second.artifact)
-						allArtifacts.push_back(ArtifactLocation(h, p.first));
+						allArtifacts.push_back(ArtifactLocation(h->id, p.first));
 				}
 			}
 			for(auto slot : h->artifactsInBackpack)
-				allArtifacts.push_back(ArtifactLocation(h, h->getArtPos(slot.artifact)));
+				allArtifacts.push_back(ArtifactLocation(h->id, h->getArtPos(slot.artifact)));
 
 			if(otherh)
 			{
 				for(auto p : otherh->artifactsWorn)
 				{
 					if(p.second.artifact)
-						allArtifacts.push_back(ArtifactLocation(otherh, p.first));
+						allArtifacts.push_back(ArtifactLocation(otherh->id, p.first));
 				}
 				for(auto slot : otherh->artifactsInBackpack)
-					allArtifacts.push_back(ArtifactLocation(otherh, otherh->getArtPos(slot.artifact)));
+					allArtifacts.push_back(ArtifactLocation(otherh->id, otherh->getArtPos(slot.artifact)));
 			}
 			//we give stuff to one hero or another, depending on giveStuffToFirstHero
 
@@ -1021,13 +1034,13 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance
 
 			for(auto location : allArtifacts)
 			{
-				if(location.relatedObj() == target && location.slot < ArtifactPosition::AFTER_LAST)
+				if(location.artHolder == target->id && ArtifactUtils::isSlotEquipment(location.slot))
 					continue; //don't reequip artifact we already wear
 
 				if(location.slot == ArtifactPosition::MACH4) // don't attempt to move catapult
 					continue;
 
-				auto s = location.getSlot();
+				auto s = cb->getHero(location.artHolder)->getSlot(location.slot);
 				if(!s || s->locked) //we can't move locks
 					continue;
 				auto artifact = s->artifact;
@@ -1038,9 +1051,9 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance
 				bool emptySlotFound = false;
 				for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType()))
 				{
-					ArtifactLocation destLocation(target, slot);
-					if(target->isPositionFree(slot) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move
+					if(target->isPositionFree(slot) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move
 					{
+						ArtifactLocation destLocation(target->id, slot);
 						cb->swapArtifacts(location, destLocation); //just put into empty slot
 						emptySlotFound = true;
 						changeMade = true;
@@ -1054,11 +1067,11 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance
 						auto otherSlot = target->getSlot(slot);
 						if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one
 						{
-							ArtifactLocation destLocation(target, slot);
 							//if that artifact is better than what we have, pick it
-							if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move
+							if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move
 							{
-								cb->swapArtifacts(location, ArtifactLocation(target, target->getArtPos(otherSlot->artifact)));
+								ArtifactLocation destLocation(target->id, slot);
+								cb->swapArtifacts(location, ArtifactLocation(target->id, target->getArtPos(otherSlot->artifact)));
 								changeMade = true;
 								break;
 							}
@@ -1119,15 +1132,6 @@ void AIGateway::battleEnd(const BattleID & battleID, const BattleResult * br, Qu
 	logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.toString(), (won ? "won" : "lost"), battlename);
 	battlename.clear();
 
-	if (queryID != QueryID::NONE)
-	{
-		status.addQuery(queryID, "Combat result dialog");
-		const int confirmAction = 0;
-		requestActionASAP([=]()
-		{
-			answerQuery(queryID, confirmAction);
-		});
-	}
 	CAdventureAI::battleEnd(battleID, br, queryID);
 }
 
@@ -1403,7 +1407,7 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade
 	int accquiredResources = 0;
 	if(const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid), false))
 	{
-		if(const IMarket * m = IMarket::castFrom(obj, false))
+		if(const auto * m = dynamic_cast<const IMarket*>(obj))
 		{
 			auto freeRes = cb->getResourceAmount(); //trade only resources which are not reserved
 			for(auto it = ResourceSet::nziterator(freeRes); it.valid(); it++)
@@ -1412,13 +1416,14 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade
 				if(res.getNum() == g.resID) //sell any other resource
 					continue;
 
-				int toGive, toGet;
+				int toGive;
+				int toGet;
 				m->getOffer(res, g.resID, toGive, toGet, EMarketMode::RESOURCE_RESOURCE);
 				toGive = static_cast<int>(toGive * (it->resVal / toGive)); //round down
 				//TODO trade only as much as needed
 				if (toGive) //don't try to sell 0 resources
 				{
-					cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, g.resID, toGive);
+					cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, GameResID(g.resID), toGive);
 					accquiredResources = static_cast<int>(toGet * (it->resVal / toGive));
 					logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, accquiredResources, g.resID, obj->getObjectName());
 				}

+ 4 - 4
AI/Nullkiller/AIGateway.h

@@ -62,7 +62,7 @@ public:
 	void heroVisit(const CGObjectInstance * obj, bool started);
 
 
-	template<typename Handler> void serialize(Handler & h, const int version)
+	template<typename Handler> void serialize(Handler & h)
 	{
 		h & battle;
 		h & remainingQueries;
@@ -119,8 +119,8 @@ public:
 	void showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) override; //all stacks operations between these objects become allowed, interface has to call onEnd when done
 	void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override;
 	void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects) override;
-	void saveGame(BinarySerializer & h, const int version) override; //saving
-	void loadGame(BinaryDeserializer & h, const int version) override; //loading
+	void saveGame(BinarySerializer & h) override; //saving
+	void loadGame(BinaryDeserializer & h) override; //loading
 	void finish() override;
 
 	void availableCreaturesChanged(const CGDwelling * town) override;
@@ -203,7 +203,7 @@ public:
 	//special function that can be called ONLY from game events handling thread and will send request ASAP
 	void requestActionASAP(std::function<void()> whatToDo);
 
-	template<typename Handler> void serializeInternal(Handler & h, const int version)
+	template<typename Handler> void serializeInternal(Handler & h)
 	{
 		h & nullkiller->memory->knownTeleportChannels;
 		h & nullkiller->memory->knownSubterraneanGates;

+ 2 - 4
AI/Nullkiller/AIUtility.cpp

@@ -276,12 +276,10 @@ creInfo infoFromDC(const dwellingContent & dc)
 	ci.creID = dc.second.size() ? dc.second.back() : CreatureID(-1); //should never be accessed
 	if (ci.creID != CreatureID::NONE)
 	{
-		ci.cre = VLC->creatures()->getById(ci.creID);
-		ci.level = ci.cre->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore.
+		ci.level = ci.creID.toCreature()->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore.
 	}
 	else
 	{
-		ci.cre = nullptr;
 		ci.level = 0;
 	}
 	return ci;
@@ -439,7 +437,7 @@ bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObject
 	case Obj::MAGIC_WELL:
 		return h->mana < h->manaLimit();
 	case Obj::PRISON:
-		return ai->cb->getHeroesInfo().size() < VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP);
+		return !ai->heroManager->heroCapReached();
 	case Obj::TAVERN:
 	case Obj::EYE_OF_MAGI:
 	case Obj::BOAT:

+ 5 - 4
AI/Nullkiller/AIUtility.h

@@ -60,7 +60,9 @@ struct creInfo;
 class AIGateway;
 class Nullkiller;
 
-const int GOLD_MINE_PRODUCTION = 1000, WOOD_ORE_MINE_PRODUCTION = 2, RESOURCE_MINE_PRODUCTION = 1;
+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;
 
@@ -113,7 +115,7 @@ public:
 	bool validAndSet() const;
 
 
-	template<typename Handler> void serialize(Handler & h, const int version)
+	template<typename Handler> void serialize(Handler & h)
 	{
 		h & this->h;
 		h & hid;
@@ -145,7 +147,7 @@ struct ObjectIdRef
 	bool operator<(const ObjectIdRef & rhs) const;
 
 
-	template<typename Handler> void serialize(Handler & h, const int version)
+	template<typename Handler> void serialize(Handler & h)
 	{
 		h & id;
 	}
@@ -161,7 +163,6 @@ struct creInfo
 {
 	int count;
 	CreatureID creID;
-	const Creature * cre;
 	int level;
 };
 creInfo infoFromDC(const dwellingContent & dc);

+ 10 - 10
AI/Nullkiller/Analyzers/ArmyManager.cpp

@@ -63,9 +63,9 @@ std::vector<SlotInfo> ArmyManager::toSlotInfo(std::vector<creInfo> army) const
 	{
 		SlotInfo slot;
 
-		slot.creature = VLC->creh->objects[i.cre->getId()];
+		slot.creature = i.creID.toCreature();
 		slot.count = i.count;
-		slot.power = evaluateStackPower(i.cre, i.count);
+		slot.power = evaluateStackPower(i.creID.toCreature(), i.count);
 
 		result.push_back(slot);
 	}
@@ -117,7 +117,7 @@ std::vector<SlotInfo>::iterator ArmyManager::getWeakestCreature(std::vector<Slot
 		if(left.creature->getLevel() != right.creature->getLevel())
 			return left.creature->getLevel() < right.creature->getLevel();
 		
-		return left.creature->speed() > right.creature->speed();
+		return left.creature->getMovementRange() > right.creature->getMovementRange();
 	});
 
 	return weakest;
@@ -128,7 +128,7 @@ class TemporaryArmy : public CArmedInstance
 public:
 	void armyChanged() override {}
 	TemporaryArmy()
-		:CArmedInstance(true)
+		:CArmedInstance(nullptr, true)
 	{
 	}
 };
@@ -259,7 +259,7 @@ std::shared_ptr<CCreatureSet> ArmyManager::getArmyAvailableToBuyAsCCreatureSet(
 		if(!ci.count || ci.creID == CreatureID::NONE)
 			continue;
 
-		vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford
+		vstd::amin(ci.count, availableRes / ci.creID.toCreature()->getFullRecruitCost()); //max count we can afford
 
 		if(!ci.count)
 			continue;
@@ -270,7 +270,7 @@ std::shared_ptr<CCreatureSet> ArmyManager::getArmyAvailableToBuyAsCCreatureSet(
 			break;
 
 		army->setCreature(dst, ci.creID, ci.count);
-		availableRes -= ci.cre->getFullRecruitCost() * ci.count;
+		availableRes -= ci.creID.toCreature()->getFullRecruitCost() * ci.count;
 	}
 
 	return army;
@@ -287,7 +287,7 @@ ui64 ArmyManager::howManyReinforcementsCanBuy(
 
 	for(const creInfo & ci : army)
 	{
-		aivalue += ci.count * ci.cre->getAIValue();
+		aivalue += ci.count * ci.creID.toCreature()->getAIValue();
 	}
 
 	return aivalue;
@@ -320,7 +320,7 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
 
 		if(i < GameConstants::CREATURES_PER_TOWN && countGrowth)
 		{
-			ci.count += town ? town->creatureGrowth(i) : ci.cre->getGrowth();
+			ci.count += town ? town->creatureGrowth(i) : ci.creID.toCreature()->getGrowth();
 		}
 
 		if(!ci.count) continue;
@@ -334,13 +334,13 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
 				freeHeroSlots--; //new slot will be occupied
 		}
 
-		vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford
+		vstd::amin(ci.count, availableRes / ci.creID.toCreature()->getFullRecruitCost()); //max count we can afford
 
 		if(!ci.count) continue;
 
 		ci.level = i; //this is important for Dungeon Summoning Portal
 		creaturesInDwellings.push_back(ci);
-		availableRes -= ci.cre->getFullRecruitCost() * ci.count;
+		availableRes -= ci.creID.toCreature()->getFullRecruitCost() * ci.count;
 	}
 
 	return creaturesInDwellings;

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

@@ -120,6 +120,11 @@ TResources BuildAnalyzer::getTotalResourcesRequired() const
 	return result;
 }
 
+bool BuildAnalyzer::isGoldPreasureHigh() const
+{
+	return goldPreasure > ai->settings->getMaxGoldPreasure();
+}
+
 void BuildAnalyzer::update()
 {
 	logAi->trace("Start analysing build");
@@ -318,7 +323,7 @@ bool BuildAnalyzer::hasAnyBuilding(int32_t alignment, BuildingID bid) const
 {
 	for(auto tdi : developmentInfos)
 	{
-		if(tdi.town->subID == alignment && tdi.town->hasBuilt(bid))
+		if(tdi.town->getFaction() == alignment && tdi.town->hasBuilt(bid))
 			return true;
 	}
 

+ 1 - 0
AI/Nullkiller/Analyzers/BuildAnalyzer.h

@@ -96,6 +96,7 @@ public:
 	const std::vector<TownDevelopmentInfo> & getDevelopmentInfo() const { return developmentInfos; }
 	TResources getDailyIncome() const { return dailyIncome; }
 	float getGoldPreasure() const { return goldPreasure; }
+	bool isGoldPreasureHigh() const;
 	bool hasAnyBuilding(int32_t alignment, BuildingID bid) const;
 
 private:

+ 7 - 9
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp

@@ -11,11 +11,12 @@
 #include "DangerHitMapAnalyzer.h"
 
 #include "../Engine/Nullkiller.h"
+#include "../../../lib/CRandomGenerator.h"
 
 namespace NKAI
 {
 
-HitMapInfo HitMapInfo::NoThreat;
+const HitMapInfo HitMapInfo::NoThreat;
 
 double HitMapInfo::value() const
 {
@@ -165,7 +166,7 @@ void DangerHitMapAnalyzer::calculateTileOwners()
 
 	auto addTownHero = [&](const CGTownInstance * town)
 	{
-			auto townHero = new CGHeroInstance();
+			auto townHero = new CGHeroInstance(town->cb);
 			CRandomGenerator rng;
 			auto visitablePos = town->visitablePos();
 			
@@ -225,7 +226,7 @@ void DangerHitMapAnalyzer::calculateTileOwners()
 				}
 			}
 
-			if(ourDistance == enemyDistance)
+			if(vstd::isAlmostEqual(ourDistance, enemyDistance))
 			{
 				hitMap[pos.x][pos.y][pos.z].closestTown = nullptr;
 			}
@@ -265,8 +266,9 @@ uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath &
 {
 	int3 tile = path.targetTile();
 	int turn = path.turn();
-	const HitMapNode & info = hitMap[tile.x][tile.y][tile.z];
 
+	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));
 }
@@ -280,13 +282,9 @@ const HitMapNode & DangerHitMapAnalyzer::getObjectThreat(const CGObjectInstance
 
 const HitMapNode & DangerHitMapAnalyzer::getTileThreat(const int3 & tile) const
 {
-	const HitMapNode & info = hitMap[tile.x][tile.y][tile.z];
-
-	return info;
+	return hitMap[tile.x][tile.y][tile.z];
 }
 
-const std::set<const CGObjectInstance *> empty = {};
-
 std::set<const CGObjectInstance *> DangerHitMapAnalyzer::getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const
 {
 	std::set<const CGObjectInstance *> result;

+ 1 - 1
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h

@@ -18,7 +18,7 @@ struct AIPath;
 
 struct HitMapInfo
 {
-	static HitMapInfo NoThreat;
+	static const HitMapInfo NoThreat;
 
 	uint64_t danger;
 	uint8_t turn;

+ 6 - 4
AI/Nullkiller/Analyzers/HeroManager.cpp

@@ -17,7 +17,7 @@
 namespace NKAI
 {
 
-SecondarySkillEvaluator HeroManager::wariorSkillsScores = SecondarySkillEvaluator(
+const SecondarySkillEvaluator HeroManager::wariorSkillsScores = SecondarySkillEvaluator(
 	{
 		std::make_shared<SecondarySkillScoreMap>(
 			std::map<SecondarySkill, float>
@@ -46,7 +46,7 @@ SecondarySkillEvaluator HeroManager::wariorSkillsScores = SecondarySkillEvaluato
 		std::make_shared<AtLeastOneMagicRule>()
 	});
 
-SecondarySkillEvaluator HeroManager::scountSkillsScores = SecondarySkillEvaluator(
+const SecondarySkillEvaluator HeroManager::scountSkillsScores = SecondarySkillEvaluator(
 	{
 		std::make_shared<SecondarySkillScoreMap>(
 			std::map<SecondarySkill, float>
@@ -187,7 +187,9 @@ bool HeroManager::heroCapReached() const
 	int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned);
 
 	return heroCount >= ALLOWED_ROAMING_HEROES
-		|| heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP);
+		|| heroCount >= ai->settings->getMaxRoamingHeroes()
+		|| heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)
+		|| heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP);
 }
 
 float HeroManager::getMagicStrength(const CGHeroInstance * hero) const
@@ -331,7 +333,7 @@ void WisdomRule::evaluateScore(const CGHeroInstance * hero, SecondarySkill skill
 		score += 1.5;
 }
 
-std::vector<SecondarySkill> AtLeastOneMagicRule::magicSchools = {
+const std::vector<SecondarySkill> AtLeastOneMagicRule::magicSchools = {
 	SecondarySkill::AIR_MAGIC,
 	SecondarySkill::EARTH_MAGIC,
 	SecondarySkill::FIRE_MAGIC,

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

@@ -58,8 +58,8 @@ public:
 class DLL_EXPORT HeroManager : public IHeroManager
 {
 private:
-	static SecondarySkillEvaluator wariorSkillsScores;
-	static SecondarySkillEvaluator scountSkillsScores;
+	static const SecondarySkillEvaluator wariorSkillsScores;
+	static const SecondarySkillEvaluator scountSkillsScores;
 
 	CCallback * cb; //this is enough, but we downcast from CCallback
 	const Nullkiller * ai;
@@ -114,7 +114,7 @@ public:
 class AtLeastOneMagicRule : public ISecondarySkillRule
 {
 private:
-	static std::vector<SecondarySkill> magicSchools;
+	static const std::vector<SecondarySkill> magicSchools;
 
 public:
 	void evaluateScore(const CGHeroInstance * hero, SecondarySkill skill, float & score) const override;

+ 2 - 2
AI/Nullkiller/Behaviors/BuildingBehavior.cpp

@@ -47,13 +47,13 @@ Goals::TGoalVec BuildingBehavior::decompose() const
 		totalDevelopmentCost.toString());
 
 	auto & developmentInfos = ai->nullkiller->buildAnalyzer->getDevelopmentInfo();
-	auto goldPreasure = ai->nullkiller->buildAnalyzer->getGoldPreasure();
+	auto isGoldPreasureLow = !ai->nullkiller->buildAnalyzer->isGoldPreasureHigh();
 
 	for(auto & developmentInfo : developmentInfos)
 	{
 		for(auto & buildingInfo : developmentInfo.toBuild)
 		{
-			if(goldPreasure < MAX_GOLD_PEASURE || buildingInfo.dailyIncome[EGameResID::GOLD] > 0)
+			if(isGoldPreasureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0)
 			{
 				if(buildingInfo.notEnoughRes)
 				{

+ 3 - 3
AI/Nullkiller/Behaviors/BuildingBehavior.h

@@ -25,9 +25,9 @@ namespace Goals
 		{
 		}
 
-		virtual Goals::TGoalVec decompose() const override;
-		virtual std::string toString() const override;
-		virtual bool operator==(const BuildingBehavior & other) const override
+		Goals::TGoalVec decompose() const override;
+		std::string toString() const override;
+		bool operator==(const BuildingBehavior & other) const override
 		{
 			return true;
 		}

+ 1 - 2
AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp

@@ -46,8 +46,7 @@ Goals::TGoalVec BuyArmyBehavior::decompose() const
 
 		for(const CGHeroInstance * targetHero : heroes)
 		{
-			if(ai->nullkiller->buildAnalyzer->getGoldPreasure() > MAX_GOLD_PEASURE
-				&& !town->hasBuilt(BuildingID::CITY_HALL))
+			if(ai->nullkiller->buildAnalyzer->isGoldPreasureHigh()	&& !town->hasBuilt(BuildingID::CITY_HALL))
 			{
 				continue;
 			}

+ 3 - 3
AI/Nullkiller/Behaviors/BuyArmyBehavior.h

@@ -24,9 +24,9 @@ namespace Goals
 		{
 		}
 
-		virtual Goals::TGoalVec decompose() const override;
-		virtual std::string toString() const override;
-		virtual bool operator==(const BuyArmyBehavior & other) const override
+		Goals::TGoalVec decompose() const override;
+		std::string toString() const override;
+		bool operator==(const BuyArmyBehavior & other) const override
 		{
 			return true;
 		}

+ 3 - 3
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.h

@@ -48,8 +48,8 @@ namespace Goals
 			specificObjects = true;
 		}
 
-		virtual Goals::TGoalVec decompose() const override;
-		virtual std::string toString() const override;
+		Goals::TGoalVec decompose() const override;
+		std::string toString() const override;
 
 		CaptureObjectsBehavior & ofType(int type)
 		{
@@ -65,7 +65,7 @@ namespace Goals
 			return *this;
 		}
 
-		virtual bool operator==(const CaptureObjectsBehavior & other) const override;
+		bool operator==(const CaptureObjectsBehavior & other) const override;
 
 		static Goals::TGoalVec getVisitGoals(const std::vector<AIPath> & paths, const CGObjectInstance * objToVisit = nullptr);
 

+ 3 - 3
AI/Nullkiller/Behaviors/ClusterBehavior.h

@@ -28,10 +28,10 @@ namespace Goals
 		{
 		}
 
-		virtual TGoalVec decompose() const override;
-		virtual std::string toString() const override;
+		TGoalVec decompose() const override;
+		std::string toString() const override;
 
-		virtual bool operator==(const ClusterBehavior & other) const override
+		bool operator==(const ClusterBehavior & other) const override
 		{
 			return true;
 		}

+ 3 - 3
AI/Nullkiller/Behaviors/DefenceBehavior.h

@@ -29,10 +29,10 @@ namespace Goals
 		{
 		}
 
-		virtual Goals::TGoalVec decompose() const override;
-		virtual std::string toString() const override;
+		Goals::TGoalVec decompose() const override;
+		std::string toString() const override;
 
-		virtual bool operator==(const DefenceBehavior & other) const override
+		bool operator==(const DefenceBehavior & other) const override
 		{
 			return true;
 		}

+ 3 - 3
AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp

@@ -246,7 +246,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 	{
 		auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero);
 
-		if(heroRole == HeroRole::MAIN && path.turn() < SCOUT_TURN_DISTANCE_LIMIT)
+		if(heroRole == HeroRole::MAIN && path.turn() < ai->nullkiller->settings->getScoutHeroTurnDistanceLimit())
 			hasMainAround = true;
 	}
 
@@ -335,7 +335,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 			if(!upgrade.upgradeValue
 				&& armyToGetOrBuy.upgradeValue > 20000
 				&& ai->nullkiller->heroManager->canRecruitHero(town)
-				&& path.turn() < SCOUT_TURN_DISTANCE_LIMIT)
+				&& path.turn() < ai->nullkiller->settings->getScoutHeroTurnDistanceLimit())
 			{
 				for(auto hero : cb->getAvailableHeroes(town))
 				{
@@ -344,7 +344,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 
 					if(scoutReinforcement >= armyToGetOrBuy.upgradeValue
 						&& ai->nullkiller->getFreeGold() >20000
-						&& ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE)
+						&& !ai->nullkiller->buildAnalyzer->isGoldPreasureHigh())
 					{
 						Composition recruitHero;
 

+ 3 - 3
AI/Nullkiller/Behaviors/GatherArmyBehavior.h

@@ -25,10 +25,10 @@ namespace Goals
 		{
 		}
 
-		virtual TGoalVec decompose() const override;
-		virtual std::string toString() const override;
+		TGoalVec decompose() const override;
+		std::string toString() const override;
 
-		virtual bool operator==(const GatherArmyBehavior & other) const override
+		bool operator==(const GatherArmyBehavior & other) const override
 		{
 			return true;
 		}

+ 1 - 2
AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp

@@ -85,8 +85,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const
 				continue;
 
 			if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1
-				|| (ai->nullkiller->getFreeResources()[EGameResID::GOLD] > 10000
-					&& ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE))
+				|| (ai->nullkiller->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->nullkiller->buildAnalyzer->isGoldPreasureHigh()))
 			{
 				tasks.push_back(Goals::sptr(Goals::RecruitHero(town).setpriority(3)));
 			}

+ 3 - 3
AI/Nullkiller/Behaviors/RecruitHeroBehavior.h

@@ -25,10 +25,10 @@ namespace Goals
 		{
 		}
 
-		virtual TGoalVec decompose() const override;
-		virtual std::string toString() const override;
+		TGoalVec decompose() const override;
+		std::string toString() const override;
 
-		virtual bool operator==(const RecruitHeroBehavior & other) const override
+		bool operator==(const RecruitHeroBehavior & other) const override
 		{
 			return true;
 		}

+ 1 - 1
AI/Nullkiller/Behaviors/StartupBehavior.cpp

@@ -71,7 +71,7 @@ bool needToRecruitHero(const CGTownInstance * startupTown)
 
 	for(auto obj : ai->nullkiller->objectClusterizer->getNearbyObjects())
 	{
-		if((obj->ID == Obj::RESOURCE && obj->subID == GameResID(EGameResID::GOLD))
+		if((obj->ID == Obj::RESOURCE && dynamic_cast<const CGResource *>(obj)->resourceID() == EGameResID::GOLD)
 			|| obj->ID == Obj::TREASURE_CHEST
 			|| obj->ID == Obj::CAMPFIRE
 			|| obj->ID == Obj::WATER_WHEEL)

+ 3 - 3
AI/Nullkiller/Behaviors/StartupBehavior.h

@@ -25,10 +25,10 @@ namespace Goals
 		{
 		}
 
-		virtual TGoalVec decompose() const override;
-		virtual std::string toString() const override;
+		TGoalVec decompose() const override;
+		std::string toString() const override;
 
-		virtual bool operator==(const StartupBehavior & other) const override
+		bool operator==(const StartupBehavior & other) const override
 		{
 			return true;
 		}

+ 3 - 3
AI/Nullkiller/Behaviors/StayAtTownBehavior.h

@@ -25,10 +25,10 @@ namespace Goals
 		{
 		}
 
-		virtual TGoalVec decompose() const override;
-		virtual std::string toString() const override;
+		TGoalVec decompose() const override;
+		std::string toString() const override;
 
-		virtual bool operator==(const StayAtTownBehavior & other) const override
+		bool operator==(const StayAtTownBehavior & other) const override
 		{
 			return true;
 		}

+ 5 - 3
AI/Nullkiller/CMakeLists.txt

@@ -17,6 +17,7 @@ set(Nullkiller_SRCS
 		AIUtility.cpp
 		Analyzers/ArmyManager.cpp
 		Analyzers/HeroManager.cpp
+		Engine/Settings.cpp
 		Engine/FuzzyEngines.cpp
 		Engine/FuzzyHelper.cpp
 		Engine/AIMemory.cpp
@@ -80,6 +81,7 @@ set(Nullkiller_HEADERS
 		AIUtility.h
 		Analyzers/ArmyManager.h
 		Analyzers/HeroManager.h
+		Engine/Settings.h
 		Engine/FuzzyEngines.h
 		Engine/FuzzyHelper.h
 		Engine/AIMemory.h
@@ -125,12 +127,12 @@ set(Nullkiller_HEADERS
 		AIGateway.h
 )
 
-if(NOT ENABLE_STATIC_AI_LIBS)
+if(NOT ENABLE_STATIC_LIBS)
 	list(APPEND Nullkiller_SRCS main.cpp StdInc.cpp)
 endif()
 assign_source_group(${Nullkiller_SRCS} ${Nullkiller_HEADERS})
 
-if(ENABLE_STATIC_AI_LIBS)
+if(ENABLE_STATIC_LIBS)
 	add_library(Nullkiller STATIC ${Nullkiller_SRCS} ${Nullkiller_HEADERS})
 else()
 	add_library(Nullkiller SHARED ${Nullkiller_SRCS} ${Nullkiller_HEADERS})
@@ -138,7 +140,7 @@ else()
 endif()
 
 target_include_directories(Nullkiller PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
-target_link_libraries(Nullkiller PUBLIC ${VCMI_LIB_TARGET} fuzzylite::fuzzylite TBB::tbb)
+target_link_libraries(Nullkiller PUBLIC vcmi fuzzylite::fuzzylite TBB::tbb)
 
 vcmi_set_output_dir(Nullkiller "AI")
 enable_pch(Nullkiller)

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

@@ -39,7 +39,9 @@ void engineBase::addRule(const std::string & txt)
 
 struct armyStructure
 {
-	float walkers, shooters, flyers;
+	float walkers;
+	float shooters;
+	float flyers;
 	ui32 maxSpeed;
 };
 

+ 8 - 2
AI/Nullkiller/Engine/FuzzyEngines.h

@@ -41,8 +41,14 @@ public:
 	TacticalAdvantageEngine();
 	float getTacticalAdvantage(const CArmedInstance * we, const CArmedInstance * enemy); //returns factor how many times enemy is stronger than us
 private:
-	fl::InputVariable * ourWalkers, *ourShooters, *ourFlyers, *enemyWalkers, *enemyShooters, *enemyFlyers;
-	fl::InputVariable * ourSpeed, *enemySpeed;
+	fl::InputVariable * ourWalkers;
+	fl::InputVariable * ourShooters;
+	fl::InputVariable * ourFlyers;
+	fl::InputVariable * enemyWalkers;
+	fl::InputVariable * enemyShooters;
+	fl::InputVariable * enemyFlyers;
+	fl::InputVariable * ourSpeed;
+	fl::InputVariable * enemySpeed;
 	fl::InputVariable * bankPresent;
 	fl::InputVariable * castleWalls;
 	fl::OutputVariable * threat;

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

@@ -24,13 +24,13 @@ ui64 FuzzyHelper::estimateBankDanger(const CBank * bank)
 {
 	//this one is not fuzzy anymore, just calculate weighted average
 
-	auto objectInfo = VLC->objtypeh->getHandlerFor(bank->ID, bank->subID)->getObjectInfo(bank->appearance);
+	auto objectInfo = bank->getObjectHandler()->getObjectInfo(bank->appearance);
 
 	CBankInfo * bankInfo = dynamic_cast<CBankInfo *>(objectInfo.get());
 
 	ui64 totalStrength = 0;
 	ui8 totalChance = 0;
-	for(auto config : bankInfo->getPossibleGuards())
+	for(auto config : bankInfo->getPossibleGuards(bank->cb))
 	{
 		totalStrength += config.second.totalStrength * config.first;
 		totalChance += config.first;
@@ -161,10 +161,7 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
 	}
 	case Obj::PYRAMID:
 	{
-		if(obj->subID == 0)
-			return estimateBankDanger(dynamic_cast<const CBank *>(obj));
-		else
-			return 0;
+		return estimateBankDanger(dynamic_cast<const CBank *>(obj));
 	}
 	default:
 		return 0;

+ 13 - 13
AI/Nullkiller/Engine/Nullkiller.cpp

@@ -27,15 +27,11 @@ namespace NKAI
 
 using namespace Goals;
 
-#if NKAI_TRACE_LEVEL >= 1
-#define MAXPASS 1000000
-#else
-#define MAXPASS 30
-#endif
-
 Nullkiller::Nullkiller()
+	:activeHero(nullptr), scanDepth(ScanDepth::MAIN_FULL), useHeroChain(true)
 {
-	memory.reset(new AIMemory());
+	memory = std::make_unique<AIMemory>();
+	settings = std::make_unique<Settings>();
 }
 
 void Nullkiller::init(std::shared_ptr<CCallback> cb, PlayerColor playerID)
@@ -115,6 +111,8 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositi
 
 void Nullkiller::resetAiState()
 {
+	std::unique_lock<std::mutex> lockGuard(aiStateMutex);
+
 	lockedResources = TResources();
 	scanDepth = ScanDepth::MAIN_FULL;
 	playerID = ai->playerID;
@@ -127,6 +125,8 @@ void Nullkiller::updateAiState(int pass, bool fast)
 {
 	boost::this_thread::interruption_point();
 
+	std::unique_lock<std::mutex> lockGuard(aiStateMutex);
+
 	auto start = std::chrono::high_resolution_clock::now();
 
 	activeHero = nullptr;
@@ -162,12 +162,12 @@ void Nullkiller::updateAiState(int pass, bool fast)
 
 		if(scanDepth == ScanDepth::SMALL)
 		{
-			cfg.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT;
+			cfg.mainTurnDistanceLimit = ai->nullkiller->settings->getMainHeroTurnDistanceLimit();
 		}
 
 		if(scanDepth != ScanDepth::ALL_FULL)
 		{
-			cfg.scoutTurnDistanceLimit = SCOUT_TURN_DISTANCE_LIMIT;
+			cfg.scoutTurnDistanceLimit = ai->nullkiller->settings->getScoutHeroTurnDistanceLimit();
 		}
 
 		boost::this_thread::interruption_point();
@@ -231,13 +231,13 @@ void Nullkiller::makeTurn()
 
 	resetAiState();
 
-	for(int i = 1; i <= MAXPASS; i++)
+	for(int i = 1; i <= settings->getMaxPass(); i++)
 	{
 		updateAiState(i);
 
 		Goals::TTask bestTask = taskptr(Goals::Invalid());
 
-		for(;i <= MAXPASS; i++)
+		for(;i <= settings->getMaxPass(); i++)
 		{
 			Goals::TTaskVec fastTasks = {
 				choseBestTask(sptr(BuyArmyBehavior()), 1),
@@ -324,9 +324,9 @@ void Nullkiller::makeTurn()
 
 		executeTask(bestTask);
 
-		if(i == MAXPASS)
+		if(i == settings->getMaxPass())
 		{
-			logAi->error("Goal %s exceeded maxpass. Terminating AI turn.", taskDescription);
+			logAi->warn("Goal %s exceeded maxpass. Terminating AI turn.", taskDescription);
 		}
 	}
 }

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

@@ -11,6 +11,7 @@
 
 #include "PriorityEvaluator.h"
 #include "FuzzyHelper.h"
+#include "Settings.h"
 #include "AIMemory.h"
 #include "DeepDecomposer.h"
 #include "../Analyzers/DangerHitMapAnalyzer.h"
@@ -23,7 +24,6 @@
 namespace NKAI
 {
 
-const float MAX_GOLD_PEASURE = 0.3f;
 const float MIN_PRIORITY = 0.01f;
 const float SMALL_SCAN_MIN_PRIORITY = 0.4f;
 
@@ -71,8 +71,10 @@ public:
 	std::unique_ptr<FuzzyHelper> dangerEvaluator;
 	std::unique_ptr<DeepDecomposer> decomposer;
 	std::unique_ptr<ArmyFormation> armyFormation;
+	std::unique_ptr<Settings> settings;
 	PlayerColor playerID;
 	std::shared_ptr<CCallback> cb;
+	std::mutex aiStateMutex;
 
 	Nullkiller();
 	void init(std::shared_ptr<CCallback> cb, PlayerColor playerID);

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

@@ -69,7 +69,7 @@ PriorityEvaluator::~PriorityEvaluator()
 
 void PriorityEvaluator::initVisitTile()
 {
-	auto file = CResourceHandler::get()->load(ResourcePath("config/ai/object-priorities.txt"))->readAll();
+	auto file = CResourceHandler::get()->load(ResourcePath("config/ai/nkai/object-priorities.txt"))->readAll();
 	std::string str = std::string((char *)file.first.get(), file.second);
 	engine = fl::FllImporter().fromString(str);
 	armyLossPersentageVariable = engine->getInputVariable("armyLoss");
@@ -122,7 +122,7 @@ TResources getCreatureBankResources(const CGObjectInstance * target, const CGHer
 {
 	//Fixme: unused variable hero
 
-	auto objectInfo = VLC->objtypeh->getHandlerFor(target->ID, target->subID)->getObjectInfo(target->appearance);
+	auto objectInfo = target->getObjectHandler()->getObjectInfo(target->appearance);
 	CBankInfo * bankInfo = dynamic_cast<CBankInfo *>(objectInfo.get());
 	auto resources = bankInfo->getPossibleResourcesReward();
 	TResources result = TResources();
@@ -137,11 +137,23 @@ TResources getCreatureBankResources(const CGObjectInstance * target, const CGHer
 	return sum > 1 ? result / sum : result;
 }
 
+uint64_t getResourcesGoldReward(const TResources & res)
+{
+	int nonGoldResources = res[EGameResID::GEMS]
+		+ res[EGameResID::SULFUR]
+		+ res[EGameResID::WOOD]
+		+ res[EGameResID::ORE]
+		+ res[EGameResID::CRYSTAL]
+		+ res[EGameResID::MERCURY];
+
+	return res[EGameResID::GOLD] + 100 * nonGoldResources;
+}
+
 uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHeroInstance * hero)
 {
-	auto objectInfo = VLC->objtypeh->getHandlerFor(target->ID, target->subID)->getObjectInfo(target->appearance);
+	auto objectInfo = target->getObjectHandler()->getObjectInfo(target->appearance);
 	CBankInfo * bankInfo = dynamic_cast<CBankInfo *>(objectInfo.get());
-	auto creatures = bankInfo->getPossibleCreaturesReward();
+	auto creatures = bankInfo->getPossibleCreaturesReward(target->cb);
 	uint64_t result = 0;
 
 	const auto& slots = hero->Slots();
@@ -236,7 +248,7 @@ int getDwellingArmyCost(const CGObjectInstance * target)
 	return cost;
 }
 
-uint64_t evaluateArtifactArmyValue(CArtifactInstance * art)
+static uint64_t evaluateArtifactArmyValue(const CArtifactInstance * art)
 {
 	if(art->artType->getId() == ArtifactID::SPELL_SCROLL)
 		return 1500;
@@ -467,14 +479,20 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons
 	switch(target->ID)
 	{
 	case Obj::MINE:
-		return target->subID == GameResID(EGameResID::GOLD)
+	{
+		auto mine = dynamic_cast<const CGMine *>(target);
+		return mine->producedResource == EGameResID::GOLD
 			? 0.5f 
-			: 0.4f * getTotalResourceRequirementStrength(target->subID) + 0.1f * getResourceRequirementStrength(target->subID);
+			: 0.4f * getTotalResourceRequirementStrength(mine->producedResource) + 0.1f * getResourceRequirementStrength(mine->producedResource);
+	}
 
 	case Obj::RESOURCE:
-		return target->subID == GameResID(EGameResID::GOLD)
+	{
+		auto resource = dynamic_cast<const CGResource *>(target);
+		return resource->resourceID() == EGameResID::GOLD
 			? 0
-			: 0.2f * getTotalResourceRequirementStrength(target->subID) + 0.4f * getResourceRequirementStrength(target->subID);
+			: 0.2f * getTotalResourceRequirementStrength(resource->resourceID()) + 0.4f * getResourceRequirementStrength(resource->resourceID());
+	}
 
 	case Obj::CREATURE_BANK:
 	{
@@ -485,7 +503,7 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons
 			//Evaluate resources used for construction. Gold is evaluated separately.
 			if (it->resType != EGameResID::GOLD)
 			{
-				sum += 0.1f * getResourceRequirementStrength(it->resType);
+				sum += 0.1f * it->resVal * getResourceRequirementStrength(it->resType);
 			}
 		}
 		return sum;
@@ -523,6 +541,9 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons
 			? getEnemyHeroStrategicalValue(dynamic_cast<const CGHeroInstance *>(target))
 			: 0;
 
+	case Obj::KEYMASTER:
+		return 0.6f;
+
 	default:
 		return 0;
 	}
@@ -582,6 +603,8 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH
 	case Obj::PANDORAS_BOX:
 		//Can contains experience, spells, or skills (only on custom maps)
 		return 2.5f;
+	case Obj::PYRAMID:
+		return 3.0f;
 	case Obj::HERO:
 		return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
 			? enemyHeroEliminationSkillRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->level
@@ -626,12 +649,14 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
 	const int dailyIncomeMultiplier = 5;
 	const float enemyArmyEliminationGoldRewardRatio = 0.2f;
 	const int32_t heroEliminationBonus = GameConstants::HERO_GOLD_COST / 2;
-	auto isGold = target->subID == GameResID(EGameResID::GOLD); // TODO: other resorces could be sold but need to evaluate market power
 
 	switch(target->ID)
 	{
 	case Obj::RESOURCE:
-		return isGold ? 600 : 100;
+	{
+		auto * res = dynamic_cast<const CGResource*>(target);
+		return res->resourceID() == GameResID::GOLD ? 600 : 100;
+	}
 	case Obj::TREASURE_CHEST:
 		return 1500;
 	case Obj::WATER_WHEEL:
@@ -640,7 +665,10 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
 		return dailyIncomeMultiplier * estimateTownIncome(ai->cb.get(), target, hero);
 	case Obj::MINE:
 	case Obj::ABANDONED_MINE:
-		return dailyIncomeMultiplier * (isGold ? 1000 : 75);
+	{
+		auto * mine = dynamic_cast<const CGMine*>(target);
+		return dailyIncomeMultiplier * (mine->producedResource == GameResID::GOLD ? 1000 : 75);
+	}
 	case Obj::MYSTICAL_GARDEN:
 	case Obj::WINDMILL:
 		return 100;
@@ -649,7 +677,7 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
 	case Obj::WAGON:
 		return 100;
 	case Obj::CREATURE_BANK:
-		return getCreatureBankResources(target, hero)[EGameResID::GOLD];
+		return getResourcesGoldReward(getCreatureBankResources(target, hero));
 	case Obj::CRYPT:
 	case Obj::DERELICT_SHIP:
 		return 3000;
@@ -674,7 +702,7 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
 class HeroExchangeEvaluator : public IEvaluationContextBuilder
 {
 public:
-	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	{
 		if(task->goalType != Goals::HERO_EXCHANGE)
 			return;
@@ -691,7 +719,7 @@ public:
 class ArmyUpgradeEvaluator : public IEvaluationContextBuilder
 {
 public:
-	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	{
 		if(task->goalType != Goals::ARMY_UPGRADE)
 			return;
@@ -708,7 +736,7 @@ public:
 class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder
 {
 public:
-	virtual 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)
 			return;
@@ -743,7 +771,7 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin
 class DefendTownEvaluator : public IEvaluationContextBuilder
 {
 public:
-	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	{
 		if(task->goalType != Goals::DEFEND_TOWN)
 			return;
@@ -793,7 +821,7 @@ private:
 public:
 	ExecuteHeroChainEvaluationContextBuilder(const Nullkiller * ai) : ai(ai) {}
 
-	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	{
 		if(task->goalType != Goals::EXECUTE_HERO_CHAIN)
 			return;
@@ -851,7 +879,7 @@ class ClusterEvaluationContextBuilder : public IEvaluationContextBuilder
 public:
 	ClusterEvaluationContextBuilder(const Nullkiller * ai) {}
 
-	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	{
 		if(task->goalType != Goals::UNLOCK_CLUSTER)
 			return;
@@ -898,7 +926,7 @@ public:
 class ExchangeSwapTownHeroesContextBuilder : public IEvaluationContextBuilder
 {
 public:
-	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	{
 		if(task->goalType != Goals::EXCHANGE_SWAP_TOWN_HEROES)
 			return;
@@ -926,7 +954,7 @@ private:
 public:
 	DismissHeroContextBuilder(const Nullkiller * ai) : ai(ai) {}
 
-	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	{
 		if(task->goalType != Goals::DISMISS_HERO)
 			return;
@@ -946,7 +974,7 @@ public:
 class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder
 {
 public:
-	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	{
 		if(task->goalType != Goals::BUILD_STRUCTURE)
 			return;
@@ -1005,7 +1033,7 @@ public:
 
 uint64_t RewardEvaluator::getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const
 {
-	if(ai->buildAnalyzer->hasAnyBuilding(town->subID, bi.id))
+	if(ai->buildAnalyzer->hasAnyBuilding(town->getFaction(), bi.id))
 		return 0;
 
 	auto creaturesToUpgrade = ai->armyManager->getTotalCreaturesAvailable(bi.baseCreatureID);

+ 78 - 0
AI/Nullkiller/Engine/Settings.cpp

@@ -0,0 +1,78 @@
+/*
+* Settings.cpp, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+#include "StdInc.h"
+#include <limits>
+
+#include "Settings.h"
+#include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h"
+#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
+#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
+#include "../../../lib/mapObjects/MapObjects.h"
+#include "../../../lib/modding/CModHandler.h"
+#include "../../../lib/VCMI_Lib.h"
+#include "../../../lib/filesystem/Filesystem.h"
+#include "../../../lib/json/JsonNode.h"
+
+namespace NKAI
+{
+	Settings::Settings()
+		: maxRoamingHeroes(8),
+		mainHeroTurnDistanceLimit(10),
+		scoutHeroTurnDistanceLimit(5),
+		maxGoldPreasure(0.3f), 
+		maxpass(30)
+	{
+		ResourcePath resource("config/ai/nkai/nkai-settings", EResType::JSON);
+
+		loadFromMod("core", resource);
+
+		for(const auto & modName : VLC->modh->getActiveMods())
+		{
+			if(CResourceHandler::get(modName)->existsResource(resource))
+				loadFromMod(modName, resource);
+		}
+	}
+
+	void Settings::loadFromMod(const std::string & modName, const ResourcePath & resource)
+	{
+		if(!CResourceHandler::get(modName)->existsResource(resource))
+		{
+			logGlobal->error("Failed to load font %s from mod %s", resource.getName(), modName);
+			return;
+		}
+
+	    JsonNode node(JsonPath::fromResource(resource), modName);
+		
+		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()["maxGoldPreasure"].isNumber())
+		{
+			maxGoldPreasure = node.Struct()["maxGoldPreasure"].Float();
+		}
+	}
+}

+ 42 - 0
AI/Nullkiller/Engine/Settings.h

@@ -0,0 +1,42 @@
+/*
+* Settings.h, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+#pragma once
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class JsonNode;
+class ResourcePath;
+
+VCMI_LIB_NAMESPACE_END
+
+namespace NKAI
+{
+	class Settings
+	{
+	private:
+		int maxRoamingHeroes;
+		int mainHeroTurnDistanceLimit;
+		int scoutHeroTurnDistanceLimit;
+		int maxpass;
+		float maxGoldPreasure;
+
+	public:
+		Settings();
+
+		int getMaxPass() const { return maxpass; }
+		float getMaxGoldPreasure() const { return maxGoldPreasure; }
+		int getMaxRoamingHeroes() const { return maxRoamingHeroes; }
+		int getMainHeroTurnDistanceLimit() const { return mainHeroTurnDistanceLimit; }
+		int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; }
+
+	private:
+		void loadFromMod(const std::string & modName, const ResourcePath & resource);
+	};
+}

+ 2 - 10
AI/Nullkiller/Goals/AbstractGoal.h

@@ -180,11 +180,7 @@ public:
 	{
 	}
 
-	virtual ~cannotFulfillGoalException() throw ()
-	{
-	};
-
-	const char * what() const throw () override
+	const char * what() const noexcept override
 	{
 		return msg.c_str();
 	}
@@ -203,11 +199,7 @@ public:
 		msg = goal->toString();
 	}
 
-	virtual ~goalFulfilledException() throw ()
-	{
-	};
-
-	const char * what() const throw () override
+	const char * what() const noexcept override
 	{
 		return msg.c_str();
 	}

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

@@ -35,7 +35,7 @@ namespace Goals
 
 		void accept(AIGateway * ai) override;
 		std::string toString() const override;
-		virtual bool operator==(const AdventureSpellCast & other) const override;
+		bool operator==(const AdventureSpellCast & other) const override;
 	};
 }
 

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

@@ -32,7 +32,7 @@ namespace Goals
 		TSubgoal whatToDoToAchieve() override;
 		bool fulfillsMe(TSubgoal goal) override;
 
-		virtual bool operator==(const Build & other) const override
+		bool operator==(const Build & other) const override
 		{
 			return true;
 		}

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

@@ -29,7 +29,7 @@ namespace Goals
 
 		void accept(AIGateway * ai) override;
 		std::string toString() const override;
-		virtual bool operator==(const BuildBoat & other) const override;
+		bool operator==(const BuildBoat & other) const override;
 	};
 }
 

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

@@ -39,8 +39,8 @@ namespace Goals
 		}
 		BuildThis(BuildingID Bid, const CGTownInstance * tid);
 
-		virtual bool operator==(const BuildThis & other) const override;
-		virtual std::string toString() const override;
+		bool operator==(const BuildThis & other) const override;
+		std::string toString() const override;
 		void accept(AIGateway * ai) override;
 	};
 }

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

@@ -54,12 +54,12 @@ void BuyArmy::accept(AIGateway * ai)
 		if(objid != CreatureID::NONE && ci.creID.getNum() != objid)
 			continue;
 
-		vstd::amin(ci.count, res / ci.cre->getFullRecruitCost());
+		vstd::amin(ci.count, res / ci.creID.toCreature()->getFullRecruitCost());
 
 		if(ci.count)
 		{
 			cb->recruitCreatures(town, town->getUpperArmy(), ci.creID, ci.count, ci.level);
-			valueBought += ci.count * ci.cre->getAIValue();
+			valueBought += ci.count * ci.creID.toCreature()->getAIValue();
 		}
 	}
 

+ 3 - 3
AI/Nullkiller/Goals/BuyArmy.h

@@ -36,11 +36,11 @@ namespace Goals
 			priority = 3;//TODO: evaluate?
 		}
 
-		virtual bool operator==(const BuyArmy & other) const override;
+		bool operator==(const BuyArmy & other) const override;
 
-		virtual std::string toString() const override;
+		std::string toString() const override;
 
-		virtual void accept(AIGateway * ai) override;
+		void accept(AIGateway * ai) override;
 	};
 }
 

+ 6 - 6
AI/Nullkiller/Goals/CGoal.h

@@ -37,14 +37,14 @@ namespace Goals
 		{
 			return new T(static_cast<T const &>(*this)); //casting enforces template instantiation
 		}
-		template<typename Handler> void serialize(Handler & h, const int version)
+		template<typename Handler> void serialize(Handler & h)
 		{
 			h & static_cast<AbstractGoal &>(*this);
 			//h & goalType & isElementar & isAbstract & priority;
 			//h & value & resID & objid & aid & tile & hero & town & bid;
 		}
 
-		virtual bool operator==(const AbstractGoal & g) const override
+		bool operator==(const AbstractGoal & g) const override
 		{
 			if(goalType != g.goalType)
 				return false;
@@ -54,7 +54,7 @@ namespace Goals
 
 		virtual bool operator==(const T & other) const = 0;
 
-		virtual TGoalVec decompose() const override
+		TGoalVec decompose() const override
 		{
 			TSubgoal single = decomposeSingle();
 
@@ -90,11 +90,11 @@ namespace Goals
 			return *((T *)this);
 		}
 
-		virtual bool isElementar() const override { return true; }
+		bool isElementar() const override { return true; }
 
-		virtual HeroPtr getHero() const override { return AbstractGoal::hero; }
+		HeroPtr getHero() const override { return AbstractGoal::hero; }
 
-		virtual int getHeroExchangeCount() const override { return 0; }
+		int getHeroExchangeCount() const override { return 0; }
 	};
 }
 

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

@@ -34,11 +34,11 @@ namespace Goals
 			name = obj->getObjectName();
 		}
 
-		virtual bool operator==(const CaptureObject & other) const override;
-		virtual Goals::TGoalVec decompose() const override;
-		virtual std::string toString() const override;
-		virtual bool hasHash() const override { return true; }
-		virtual uint64_t getHash() const override;
+		bool operator==(const CaptureObject & other) const override;
+		Goals::TGoalVec decompose() const override;
+		std::string toString() const override;
+		bool hasHash() const override { return true; }
+		uint64_t getHash() const override;
 	};
 }
 

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

@@ -98,7 +98,7 @@ std::string CompleteQuest::questToString() const
 		return "inactive quest";
 
 	MetaString ms;
-	q.quest->getRolloverText(ms, false);
+	q.quest->getRolloverText(q.obj->cb, ms, false);
 
 	return ms.toString();
 }
@@ -210,7 +210,7 @@ TGoalVec CompleteQuest::missionResources() const
 
 TGoalVec CompleteQuest::missionDestroyObj() const
 {
-	auto obj = cb->getObjByQuestIdentifier(q.quest->killTarget);
+	auto obj = cb->getObj(q.quest->killTarget);
 
 	if(!obj)
 		return CaptureObjectsBehavior(q.obj).decompose();

+ 5 - 5
AI/Nullkiller/Goals/CompleteQuest.h

@@ -29,12 +29,12 @@ namespace Goals
 		{
 		}
 
-		virtual Goals::TGoalVec decompose() const override;
-		virtual std::string toString() const override;
-		virtual bool hasHash() const override { return true; }
-		virtual uint64_t getHash() const override;
+		Goals::TGoalVec decompose() const override;
+		std::string toString() const override;
+		bool hasHash() const override { return true; }
+		uint64_t getHash() const override;
 
-		virtual bool operator==(const CompleteQuest & other) const override;
+		bool operator==(const CompleteQuest & other) const override;
 
 	private:
 		TGoalVec tryCompleteQuest() const;

+ 5 - 5
AI/Nullkiller/Goals/Composition.h

@@ -26,15 +26,15 @@ namespace Goals
 		{
 		}
 
-		virtual bool operator==(const Composition & other) const override;
-		virtual std::string toString() const override;
+		bool operator==(const Composition & other) const override;
+		std::string toString() const override;
 		void accept(AIGateway * ai) override;
 		Composition & addNext(const AbstractGoal & goal);
 		Composition & addNext(TSubgoal goal);
 		Composition & addNextSequence(const TGoalVec & taskSequence);
-		virtual TGoalVec decompose() const override;
-		virtual bool isElementar() const override;
-		virtual int getHeroExchangeCount() const override;
+		TGoalVec decompose() const override;
+		bool isElementar() const override;
+		int getHeroExchangeCount() const override;
 	};
 }
 

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

@@ -33,7 +33,7 @@ namespace Goals
 		{
 			tile = Tile;
 		}
-		virtual bool operator==(const DigAtTile & other) const override;
+		bool operator==(const DigAtTile & other) const override;
 
 	private:
 		//TSubgoal decomposeSingle() const override;

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

@@ -26,7 +26,7 @@ namespace Goals
 
 		void accept(AIGateway * ai) override;
 		std::string toString() const override;
-		virtual bool operator==(const DismissHero & other) const override;
+		bool operator==(const DismissHero & other) const override;
 	};
 }
 

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

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

+ 2 - 2
AI/Nullkiller/Goals/ExecuteHeroChain.h

@@ -30,10 +30,10 @@ namespace Goals
 		
 		void accept(AIGateway * ai) override;
 		std::string toString() const override;
-		virtual bool operator==(const ExecuteHeroChain & other) const override;
+		bool operator==(const ExecuteHeroChain & other) const override;
 		const AIPath & getPath() const { return chainPath; }
 
-		virtual int getHeroExchangeCount() const override { return chainPath.exchangeCount; }
+		int getHeroExchangeCount() const override { return chainPath.exchangeCount; }
 
 	private:
 		bool moveHeroToTile(const CGHeroInstance * hero, const int3 & tile);

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

@@ -36,7 +36,7 @@ namespace Goals
 		TGoalVec getAllPossibleSubgoals() override;
 		TSubgoal whatToDoToAchieve() override;
 		std::string completeMessage() const override;
-		virtual bool operator==(const GatherArmy & other) const override;
+		bool operator==(const GatherArmy & other) const override;
 	};
 }
 

+ 3 - 3
AI/Nullkiller/Goals/Invalid.h

@@ -32,17 +32,17 @@ namespace Goals
 			return TGoalVec();
 		}
 
-		virtual bool operator==(const Invalid & other) const override
+		bool operator==(const Invalid & other) const override
 		{
 			return true;
 		}
 
-		virtual std::string toString() const override
+		std::string toString() const override
 		{
 			return "Invalid";
 		}
 
-		virtual void accept(AIGateway * ai) override
+		void accept(AIGateway * ai) override
 		{
 			throw cannotFulfillGoalException("Can not fulfill Invalid goal!");
 		}

+ 2 - 2
AI/Nullkiller/Goals/RecruitHero.h

@@ -38,12 +38,12 @@ namespace Goals
 		{
 		}
 
-		virtual bool operator==(const RecruitHero & other) const override
+		bool operator==(const RecruitHero & other) const override
 		{
 			return true;
 		}
 
-		virtual std::string toString() const override;
+		std::string toString() const override;
 		void accept(AIGateway * ai) override;
 	};
 }

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

@@ -28,7 +28,7 @@ namespace Goals
 
 		void accept(AIGateway * ai) override;
 		std::string toString() const override;
-		virtual bool operator==(const SaveResources & other) const override;
+		bool operator==(const SaveResources & other) const override;
 	};
 }
 

+ 2 - 2
AI/Nullkiller/Goals/StayAtTown.h

@@ -26,8 +26,8 @@ namespace Goals
 	public:
 		StayAtTown(const CGTownInstance * town, AIPath & path);
 
-		virtual bool operator==(const StayAtTown & other) const override;
-		virtual std::string toString() const override;
+		bool operator==(const StayAtTown & other) const override;
+		std::string toString() const override;
 		void accept(AIGateway * ai) override;
 		float getMovementWasted() const { return movementWasted; }
 	};

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

@@ -34,7 +34,7 @@ namespace Goals
 			value = val;
 			objid = Objid;
 		}
-		virtual bool operator==(const Trade & other) const override;
+		bool operator==(const Trade & other) const override;
 	};
 }
 

+ 2 - 2
AI/Nullkiller/Markers/ArmyUpgrade.h

@@ -29,8 +29,8 @@ namespace Goals
 		ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade);
 		ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade);
 
-		virtual bool operator==(const ArmyUpgrade & other) const override;
-		virtual std::string toString() const override;
+		bool operator==(const ArmyUpgrade & other) const override;
+		std::string toString() const override;
 
 		uint64_t getUpgradeValue() const { return upgradeValue; }
 		uint64_t getInitialArmyValue() const { return initialValue; }

+ 2 - 2
AI/Nullkiller/Markers/DefendTown.h

@@ -30,8 +30,8 @@ namespace Goals
 		DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isCounterAttack = false);
 		DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const CGHeroInstance * defender);
 
-		virtual bool operator==(const DefendTown & other) const override;
-		virtual std::string toString() const override;
+		bool operator==(const DefendTown & other) const override;
+		std::string toString() const override;
 
 		const HitMapInfo & getTreat() const { return treat; }
 

+ 2 - 2
AI/Nullkiller/Markers/HeroExchange.h

@@ -28,8 +28,8 @@ namespace Goals
 			sethero(targetHero);
 		}
 
-		virtual bool operator==(const HeroExchange & other) const override;
-		virtual std::string toString() const override;
+		bool operator==(const HeroExchange & other) const override;
+		std::string toString() const override;
 
 		uint64_t getReinforcementArmyStrength() const;
 	};

+ 2 - 2
AI/Nullkiller/Markers/UnlockCluster.h

@@ -36,8 +36,8 @@ namespace Goals
 			sethero(pathToCenter.targetHero);
 		}
 
-		virtual bool operator==(const UnlockCluster & other) const override;
-		virtual std::string toString() const override;
+		bool operator==(const UnlockCluster & other) const override;
+		std::string toString() const override;
 		std::shared_ptr<ObjectCluster> getCluster() const { return cluster; }
 		const AIPath & getPathToCenter() { return pathToCenter; }
 	};

+ 26 - 8
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -35,6 +35,8 @@ const uint64_t MIN_ARMY_STRENGTH_FOR_CHAIN = 5000;
 const uint64_t MIN_ARMY_STRENGTH_FOR_NEXT_ACTOR = 1000;
 const uint64_t CHAIN_MAX_DEPTH = 4;
 
+const bool DO_NOT_SAVE_TO_COMMITED_TILES = false;
+
 AISharedStorage::AISharedStorage(int3 sizes)
 {
 	if(!shared){
@@ -90,7 +92,7 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta
 
 	//TODO: fix this code duplication with NodeStorage::initialize, problem is to keep `resetTile` inline
 	const PlayerColor fowPlayer = ai->playerID;
-	const auto fow = static_cast<const CGameInfoCallback *>(gs)->getPlayerTeam(fowPlayer)->fogOfWarMap;
+	const auto & fow = static_cast<const CGameInfoCallback *>(gs)->getPlayerTeam(fowPlayer)->fogOfWarMap;
 	const int3 sizes = gs->getMapSize();
 
 	//Each thread gets different x, but an array of y located next to each other in memory
@@ -234,6 +236,7 @@ void AINodeStorage::resetTile(const int3 & coord, EPathfindingLayer layer, EPath
 		heroNode.specialAction.reset();
 		heroNode.armyLoss = 0;
 		heroNode.chainOther = nullptr;
+		heroNode.dayFlags = DayFlags::NONE;
 		heroNode.update(coord, layer, accessibility);
 	}
 }
@@ -265,7 +268,8 @@ void AINodeStorage::commit(
 	EPathNodeAction action, 
 	int turn, 
 	int movementLeft, 
-	float cost) const
+	float cost,
+	bool saveToCommited) const
 {
 	destination->action = action;
 	destination->setCost(cost);
@@ -291,10 +295,15 @@ void AINodeStorage::commit(
 		destination->actor->armyValue);
 #endif
 
-	if(destination->turns <= heroChainTurn)
+	if(saveToCommited && destination->turns <= heroChainTurn)
 	{
 		commitedTiles.insert(destination->coord);
 	}
+
+	if(destination->turns == source->turns)
+	{
+		destination->dayFlags = source->dayFlags;
+	}
 }
 
 std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
@@ -323,7 +332,7 @@ std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
 	return neighbours;
 }
 
-EPathfindingLayer phisycalLayers[2] = {EPathfindingLayer::LAND, EPathfindingLayer::SAIL};
+constexpr std::array phisycalLayers = {EPathfindingLayer::LAND, EPathfindingLayer::SAIL};
 
 bool AINodeStorage::increaseHeroChainTurnLimit()
 {
@@ -778,7 +787,14 @@ void HeroChainCalculationTask::addHeroChain(const std::vector<ExchangeCandidate>
 			continue;
 		}
 
-		storage.commit(exchangeNode, carrier, carrier->action, chainInfo.turns, chainInfo.moveRemains, chainInfo.getCost());
+		storage.commit(
+			exchangeNode,
+			carrier,
+			carrier->action,
+			chainInfo.turns,
+			chainInfo.moveRemains, 
+			chainInfo.getCost(),
+			DO_NOT_SAVE_TO_COMMITED_TILES);
 
 		if(carrier->specialAction || carrier->chainOther)
 		{
@@ -827,6 +843,7 @@ ExchangeCandidate HeroChainCalculationTask::calculateExchange(
 	candidate.turns = carrierParentNode->turns;
 	candidate.setCost(carrierParentNode->getCost() + otherParentNode->getCost() / 1000.0);
 	candidate.moveRemains = carrierParentNode->moveRemains;
+	candidate.danger = carrierParentNode->danger;
 
 	if(carrierParentNode->turns < otherParentNode->turns)
 	{
@@ -1070,7 +1087,8 @@ struct TowmPortalFinder
 				EPathNodeAction::TELEPORT_NORMAL,
 				bestNode->turns,
 				bestNode->moveRemains - movementNeeded,
-				movementCost);
+				movementCost,
+				DO_NOT_SAVE_TO_COMMITED_TILES);
 
 			node->theNodeBefore = bestNode;
 			node->addSpecialAction(std::make_shared<AIPathfinding::TownPortalAction>(targetTown));
@@ -1247,8 +1265,8 @@ bool AINodeStorage::hasBetterChain(
 				&& nodeActor->heroFightingStrength >= candidateActor->heroFightingStrength
 				&& node.getCost() <= candidateNode->getCost())
 			{
-				if(nodeActor->heroFightingStrength == candidateActor->heroFightingStrength
-					&& node.getCost() == candidateNode->getCost()
+				if(vstd::isAlmostEqual(nodeActor->heroFightingStrength, candidateActor->heroFightingStrength)
+					&& vstd::isAlmostEqual(node.getCost(), candidateNode->getCost())
 					&& &node < candidateNode)
 				{
 					continue;

+ 13 - 7
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -24,9 +24,6 @@
 
 namespace NKAI
 {
-	const int SCOUT_TURN_DISTANCE_LIMIT = 5;
-	const int MAIN_TURN_DISTANCE_LIMIT = 10;
-
 namespace AIPathfinding
 {
 #ifdef ENVIRONMENT64
@@ -41,11 +38,19 @@ namespace AIPathfinding
 	const int CHAIN_MAX_DEPTH = 4;
 }
 
+enum DayFlags : ui8
+{
+	NONE = 0,
+	FLY_CAST = 1,
+	WATER_WALK_CAST = 2
+};
+
 struct AIPathNode : public CGPathNode
 {
 	uint64_t danger;
 	uint64_t armyLoss;
-	int32_t manaCost;
+	int16_t manaCost;
+	DayFlags dayFlags;
 	const AIPathNode * chainOther;
 	std::shared_ptr<const SpecialAction> specialAction;
 	const ChainActor * actor;
@@ -180,7 +185,7 @@ public:
 	bool selectFirstActor();
 	bool selectNextActor();
 
-	virtual std::vector<CGPathNode *> getInitialNodes() override;
+	std::vector<CGPathNode *> getInitialNodes() override;
 
 	virtual std::vector<CGPathNode *> calculateNeighbours(
 		const PathNodeInfo & source,
@@ -192,7 +197,7 @@ public:
 		const PathfinderConfig * pathfinderConfig,
 		const CPathfinderHelper * pathfinderHelper) override;
 
-	virtual void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override;
+	void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override;
 
 	void commit(
 		AIPathNode * destination,
@@ -200,7 +205,8 @@ public:
 		EPathNodeAction action,
 		int turn,
 		int movementLeft,
-		float cost) const;
+		float cost,
+		bool saveToCommited = true) const;
 
 	inline const AIPathNode * getAINode(const CGPathNode * node) const
 	{

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

@@ -34,7 +34,7 @@ namespace AIPathfinding
 
 		~AIPathfinderConfig();
 
-		virtual CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override;
+		CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override;
 	};
 }
 

+ 8 - 7
AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp

@@ -22,18 +22,18 @@ namespace NKAI
 
 namespace AIPathfinding
 {
-	AdventureCastAction::AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero)
-		:spellToCast(spellToCast), hero(hero)
+	AdventureCastAction::AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero, DayFlags flagsToAdd)
+		:spellToCast(spellToCast), hero(hero), flagsToAdd(flagsToAdd)
 	{
 		manaCost = hero->getSpellCost(spellToCast.toSpell());
 	}
 
 	WaterWalkingAction::WaterWalkingAction(const CGHeroInstance * hero)
-		:AdventureCastAction(SpellID::WATER_WALK, hero)
+		:AdventureCastAction(SpellID::WATER_WALK, hero, DayFlags::WATER_WALK_CAST)
 	{ }
 
 	AirWalkingAction::AirWalkingAction(const CGHeroInstance * hero)
-		: AdventureCastAction(SpellID::FLY, hero)
+		: AdventureCastAction(SpellID::FLY, hero, DayFlags::FLY_CAST)
 	{
 	}
 
@@ -41,11 +41,12 @@ namespace AIPathfinding
 		const CGHeroInstance * hero,
 		CDestinationNodeInfo & destination,
 		const PathNodeInfo & source,
-		AIPathNode * dstMode,
+		AIPathNode * dstNode,
 		const AIPathNode * srcNode) const
 	{
-		dstMode->manaCost = srcNode->manaCost + manaCost;
-		dstMode->theNodeBefore = source.node;
+		dstNode->manaCost = srcNode->manaCost + manaCost;
+		dstNode->theNodeBefore = source.node;
+		dstNode->dayFlags = static_cast<DayFlags>(dstNode->dayFlags | flagsToAdd);
 	}
 
 	void AdventureCastAction::execute(const CGHeroInstance * hero) const

+ 5 - 4
AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h

@@ -24,11 +24,12 @@ namespace AIPathfinding
 		SpellID spellToCast;
 		const CGHeroInstance * hero;
 		int manaCost;
+		DayFlags flagsToAdd;
 
 	public:
-		AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero);
+		AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero, DayFlags flagsToAdd = DayFlags::NONE);
 
-		virtual void execute(const CGHeroInstance * hero) const override;
+		void execute(const CGHeroInstance * hero) const override;
 
 		virtual void applyOnDestination(
 			const CGHeroInstance * hero,
@@ -37,9 +38,9 @@ namespace AIPathfinding
 			AIPathNode * dstMode,
 			const AIPathNode * srcNode) const override;
 
-		virtual bool canAct(const AIPathNode * source) const override;
+		bool canAct(const AIPathNode * source) const override;
 
-		virtual std::string toString() const override;
+		std::string toString() const override;
 	};
 
 	class WaterWalkingAction : public AdventureCastAction

+ 2 - 2
AI/Nullkiller/Pathfinding/Actions/BattleAction.h

@@ -28,9 +28,9 @@ namespace AIPathfinding
 		{
 		}
 
-		virtual void execute(const CGHeroInstance * hero) const override;
+		void execute(const CGHeroInstance * hero) const override;
 
-		virtual std::string toString() const override;
+		std::string toString() const override;
 	};
 }
 

+ 10 - 10
AI/Nullkiller/Pathfinding/Actions/BoatActions.h

@@ -25,7 +25,7 @@ namespace AIPathfinding
 	class SummonBoatAction : public VirtualBoatAction
 	{
 	public:
-		virtual void execute(const CGHeroInstance * hero) const override;
+		void execute(const CGHeroInstance * hero) const override;
 
 		virtual void applyOnDestination(
 			const CGHeroInstance * hero,
@@ -34,11 +34,11 @@ namespace AIPathfinding
 			AIPathNode * dstMode,
 			const AIPathNode * srcNode) const override;
 
-		virtual bool canAct(const AIPathNode * source) const override;
+		bool canAct(const AIPathNode * source) const override;
 
-		virtual const ChainActor * getActor(const ChainActor * sourceActor) const override;
+		const ChainActor * getActor(const ChainActor * sourceActor) const override;
 
-		virtual std::string toString() const override;
+		std::string toString() const override;
 
 	private:
 		int32_t getManaCost(const CGHeroInstance * hero) const;
@@ -56,17 +56,17 @@ namespace AIPathfinding
 		{
 		}
 
-		virtual bool canAct(const AIPathNode * source) const override;
+		bool canAct(const AIPathNode * source) const override;
 
-		virtual void execute(const CGHeroInstance * hero) const override;
+		void execute(const CGHeroInstance * hero) const override;
 
-		virtual Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
+		Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
 
-		virtual const ChainActor * getActor(const ChainActor * sourceActor) const override;
+		const ChainActor * getActor(const ChainActor * sourceActor) const override;
 
-		virtual std::string toString() const override;
+		std::string toString() const override;
 
-		virtual const CGObjectInstance * targetObject() const override;
+		const CGObjectInstance * targetObject() const override;
 	};
 }
 

+ 4 - 4
AI/Nullkiller/Pathfinding/Actions/QuestAction.h

@@ -28,13 +28,13 @@ namespace AIPathfinding
 		{
 		}
 
-		virtual bool canAct(const AIPathNode * node) const override;
+		bool canAct(const AIPathNode * node) const override;
 
-		virtual Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
+		Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
 
-		virtual void execute(const CGHeroInstance * hero) const override;
+		void execute(const CGHeroInstance * hero) const override;
 
-		virtual std::string toString() const override;
+		std::string toString() const override;
 	};
 }
 

+ 2 - 2
AI/Nullkiller/Pathfinding/Actions/TownPortalAction.h

@@ -29,9 +29,9 @@ namespace AIPathfinding
 		{
 		}
 
-		virtual void execute(const CGHeroInstance * hero) const override;
+		void execute(const CGHeroInstance * hero) const override;
 
-		virtual std::string toString() const override;
+		std::string toString() const override;
 	};
 }
 

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

@@ -18,7 +18,7 @@
 
 using namespace NKAI;
 
-CCreatureSet emptyArmy;
+const CCreatureSet emptyArmy;
 
 bool HeroExchangeArmy::needsLastStack() const
 {
@@ -327,7 +327,7 @@ ExchangeResult HeroExchangeMap::tryExchangeNoLock(const ChainActor * other)
 			return result;
 		}
 
-		HeroActor * exchanged = new HeroActor(actor, other, newArmy, ai);
+		auto * exchanged = new HeroActor(actor, other, newArmy, ai);
 
 		exchanged->armyCost += newArmy->armyCost;
 		result.actor = exchanged;
@@ -342,7 +342,7 @@ HeroExchangeArmy * HeroExchangeMap::tryUpgrade(
 	const CGObjectInstance * upgrader,
 	TResources resources) const
 {
-	HeroExchangeArmy * target = new HeroExchangeArmy();
+	auto * target = new HeroExchangeArmy();
 	auto upgradeInfo = ai->armyManager->calculateCreaturesUpgrade(army, upgrader, resources);
 
 	if(upgradeInfo.upgradeValue)
@@ -373,10 +373,10 @@ HeroExchangeArmy * HeroExchangeMap::tryUpgrade(
 
 		for(auto & creatureToBuy : buyArmy)
 		{
-			auto targetSlot = target->getSlotFor(dynamic_cast<const CCreature*>(creatureToBuy.cre));
+			auto targetSlot = target->getSlotFor(creatureToBuy.creID.toCreature());
 
 			target->addToSlot(targetSlot, creatureToBuy.creID, creatureToBuy.count);
-			target->armyCost += creatureToBuy.cre->getFullRecruitCost() * creatureToBuy.count;
+			target->armyCost += creatureToBuy.creID.toCreature()->getFullRecruitCost() * creatureToBuy.count;
 			target->requireBuyArmy = true;
 		}
 	}
@@ -393,7 +393,7 @@ HeroExchangeArmy * HeroExchangeMap::tryUpgrade(
 
 HeroExchangeArmy * HeroExchangeMap::pickBestCreatures(const CCreatureSet * army1, const CCreatureSet * army2) const
 {
-	HeroExchangeArmy * target = new HeroExchangeArmy();
+	auto * target = new HeroExchangeArmy();
 	auto bestArmy = ai->armyManager->getBestArmy(actor->hero, army1, army2);
 
 	for(auto & slotInfo : bestArmy)
@@ -445,7 +445,7 @@ std::string DwellingActor::toString() const
 
 CCreatureSet * DwellingActor::getDwellingCreatures(const CGDwelling * dwelling, bool waitForGrowth)
 {
-	CCreatureSet * dwellingCreatures = new CCreatureSet();
+	auto * dwellingCreatures = new CCreatureSet();
 
 	for(auto & creatureInfo : dwelling->creatures)
 	{

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

@@ -28,10 +28,10 @@ class HeroExchangeArmy : public CArmedInstance
 public:
 	TResources armyCost;
 	bool requireBuyArmy;
-	virtual bool needsLastStack() const override;
+	bool needsLastStack() const override;
 	std::shared_ptr<SpecialAction> getActorAction() const;
 
-	HeroExchangeArmy(): CArmedInstance(true), requireBuyArmy(false) {}
+	HeroExchangeArmy(): CArmedInstance(nullptr, true), requireBuyArmy(false) {}
 };
 
 struct ExchangeResult
@@ -51,24 +51,24 @@ protected:
 
 public:
 	uint64_t chainMask;
-	bool isMovable;
-	bool allowUseResources;
-	bool allowBattle;
-	bool allowSpellCast;
+	bool isMovable = false;
+	bool allowUseResources = false;
+	bool allowBattle = false;
+	bool allowSpellCast = false;
 	std::shared_ptr<SpecialAction> actorAction;
 	const CGHeroInstance * hero;
 	HeroRole heroRole;
-	const CCreatureSet * creatureSet;
-	const ChainActor * battleActor;
-	const ChainActor * castActor;
-	const ChainActor * resourceActor;
-	const ChainActor * carrierParent;
-	const ChainActor * otherParent;
-	const ChainActor * baseActor;
+	const CCreatureSet * creatureSet = nullptr;
+	const ChainActor * battleActor = nullptr;
+	const ChainActor * castActor = nullptr;
+	const ChainActor * resourceActor = nullptr;
+	const ChainActor * carrierParent = nullptr;
+	const ChainActor * otherParent = nullptr;
+	const ChainActor * baseActor = nullptr;
 	int3 initialPosition;
 	EPathfindingLayer layer;
-	uint32_t initialMovement;
-	uint32_t initialTurn;
+	uint32_t initialMovement = 0;
+	uint32_t initialTurn = 0;
 	uint64_t armyValue;
 	float heroFightingStrength;
 	uint8_t actorExchangeCount;
@@ -126,7 +126,7 @@ public:
 	HeroActor(const ChainActor * carrier, const ChainActor * other, const HeroExchangeArmy * army, const Nullkiller * ai);
 
 protected:
-	virtual ExchangeResult tryExchangeNoLock(const ChainActor * specialActor, const ChainActor * other) const override;
+	ExchangeResult tryExchangeNoLock(const ChainActor * specialActor, const ChainActor * other) const override;
 };
 
 class ObjectActor : public ChainActor
@@ -136,7 +136,7 @@ private:
 
 public:
 	ObjectActor(const CGObjectInstance * obj, const CCreatureSet * army, uint64_t chainMask, int initialTurn);
-	virtual std::string toString() const override;
+	std::string toString() const override;
 	const CGObjectInstance * getActorObject() const override;
 };
 
@@ -154,7 +154,7 @@ private:
 public:
 	DwellingActor(const CGDwelling * dwelling, uint64_t chainMask, bool waitForGrowth, int dayOfWeek);
 	~DwellingActor();
-	virtual std::string toString() const override;
+	std::string toString() const override;
 
 protected:
 	int getInitialTurn(bool waitForGrowth, int dayOfWeek);
@@ -168,7 +168,7 @@ private:
 
 public:
 	TownGarrisonActor(const CGTownInstance * town, uint64_t chainMask);
-	virtual std::string toString() const override;
+	std::string toString() const override;
 };
 
 }

+ 15 - 8
AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp

@@ -61,6 +61,12 @@ namespace AIPathfinding
 
 		if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::WATER)
 		{
+			if(nodeStorage->getAINode(source.node)->dayFlags & DayFlags::WATER_WALK_CAST)
+			{
+				destination.blocked = false;
+				return;
+			}
+
 			auto action = waterWalkingActions.find(nodeStorage->getHero(source.node));
 
 			if(action != waterWalkingActions.end() && tryUseSpecialAction(destination, source, action->second, EPathNodeAction::NORMAL))
@@ -73,6 +79,12 @@ namespace AIPathfinding
 
 		if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::AIR)
 		{
+			if(nodeStorage->getAINode(source.node)->dayFlags & DayFlags::FLY_CAST)
+			{
+				destination.blocked = false;
+				return;
+			}
+
 			auto action = airWalkingActions.find(nodeStorage->getHero(source.node));
 
 			if(action != airWalkingActions.end() && tryUseSpecialAction(destination, source, action->second, EPathNodeAction::NORMAL))
@@ -91,12 +103,12 @@ namespace AIPathfinding
 
 		for(const CGHeroInstance * hero : nodeStorage->getAllHeroes())
 		{
-			if(hero->canCastThisSpell(waterWalk.toSpell()))
+			if(hero->canCastThisSpell(waterWalk.toSpell()) && hero->mana >= hero->getSpellCost(waterWalk.toSpell()))
 			{
 				waterWalkingActions[hero] = std::make_shared<WaterWalkingAction>(hero);
 			}
 
-			if(hero->canCastThisSpell(airWalk.toSpell()))
+			if(hero->canCastThisSpell(airWalk.toSpell()) && hero->mana >= hero->getSpellCost(airWalk.toSpell()))
 			{
 				airWalkingActions[hero] = std::make_shared<AirWalkingAction>(hero);
 			}
@@ -119,7 +131,7 @@ namespace AIPathfinding
 		{
 			if(obj->ID != Obj::TOWN) //towns were handled in the previous loop
 			{
-				if(const IShipyard * shipyard = IShipyard::castFrom(obj))
+				if(const auto * shipyard = dynamic_cast<const IShipyard *>(obj))
 					shipyards.push_back(shipyard);
 			}
 		}
@@ -179,11 +191,6 @@ namespace AIPathfinding
 	{
 		bool result = false;
 
-		if(!specialAction->canAct(nodeStorage->getAINode(source.node)))
-		{
-			return false;
-		}
-
 		nodeStorage->updateAINode(destination.node, [&](AIPathNode * node)
 			{
 				auto castNodeOptional = nodeStorage->getOrCreateNode(

+ 1 - 1
AI/Nullkiller/main.cpp

@@ -14,7 +14,7 @@
 #define strcpy_s(a, b, c) strncpy(a, c, b)
 #endif
 
-static const char * g_cszAiName = "Nullkiller";
+static const char * const g_cszAiName = "Nullkiller";
 
 extern "C" DLL_EXPORT int GetGlobalAiVersion()
 {

+ 3 - 3
AI/StupidAI/CMakeLists.txt

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

+ 15 - 13
AI/StupidAI/StupidAI.cpp

@@ -15,8 +15,7 @@
 #include "../../lib/CCreatureHandler.h"
 #include "../../lib/battle/BattleAction.h"
 #include "../../lib/battle/BattleInfo.h"
-
-static std::shared_ptr<CBattleCallback> cbc;
+#include "../../lib/CRandomGenerator.h"
 
 CStupidAI::CStupidAI()
 	: side(-1)
@@ -41,7 +40,7 @@ void CStupidAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::share
 {
 	print("init called, saving ptr to IBattleCallback");
 	env = ENV;
-	cbc = cb = CB;
+	cb = CB;
 
 	wasWaitingForRealize = CB->waitTillRealize;
 	wasUnlockingGs = CB->unlockGsWhenWaiting;
@@ -68,15 +67,16 @@ class EnemyInfo
 {
 public:
 	const CStack * s;
-	int adi, adr;
+	int adi;
+	int adr;
 	std::vector<BattleHex> attackFrom; //for melee fight
 	EnemyInfo(const CStack * _s) : s(_s), adi(0), adr(0)
 	{}
-	void calcDmg(const BattleID & battleID, const CStack * ourStack)
+	void calcDmg(std::shared_ptr<CBattleCallback> cb, const BattleID & battleID, const CStack * ourStack)
 	{
 		// FIXME: provide distance info for Jousting bonus
 		DamageEstimation retal;
-		DamageEstimation dmg = cbc->getBattle(battleID)->battleEstimateDamage(ourStack, s, 0, &retal);
+		DamageEstimation dmg = cb->getBattle(battleID)->battleEstimateDamage(ourStack, s, 0, &retal);
 		adi = static_cast<int>((dmg.damage.min + dmg.damage.max) / 2);
 		adr = static_cast<int>((retal.damage.min + retal.damage.max) / 2);
 	}
@@ -92,14 +92,14 @@ bool isMoreProfitable(const EnemyInfo &ei1, const EnemyInfo& ei2)
 	return (ei1.adi-ei1.adr) < (ei2.adi - ei2.adr);
 }
 
-static bool willSecondHexBlockMoreEnemyShooters(const BattleID & battleID, const BattleHex &h1, const BattleHex &h2)
+static bool willSecondHexBlockMoreEnemyShooters(std::shared_ptr<CBattleCallback> cb, const BattleID & battleID, const BattleHex &h1, const BattleHex &h2)
 {
 	int shooters[2] = {0}; //count of shooters on hexes
 
 	for(int i = 0; i < 2; i++)
 	{
 		for (auto & neighbour : (i ? h2 : h1).neighbouringTiles())
-			if(const auto * s = cbc->getBattle(battleID)->battleGetUnitByPos(neighbour))
+			if(const auto * s = cb->getBattle(battleID)->battleGetUnitByPos(neighbour))
 				if(s->isShooter())
 					shooters[i]++;
 	}
@@ -117,7 +117,9 @@ void CStupidAI::activeStack(const BattleID & battleID, const CStack * stack)
 	//boost::this_thread::sleep_for(boost::chrono::seconds(2));
 	print("activeStack called for " + stack->nodeName());
 	ReachabilityInfo dists = cb->getBattle(battleID)->getReachability(stack);
-	std::vector<EnemyInfo> enemiesShootable, enemiesReachable, enemiesUnreachable;
+	std::vector<EnemyInfo> enemiesShootable;
+	std::vector<EnemyInfo> enemiesReachable;
+	std::vector<EnemyInfo> enemiesUnreachable;
 
 	if(stack->creatureId() == CreatureID::CATAPULT)
 	{
@@ -152,7 +154,7 @@ void CStupidAI::activeStack(const BattleID & battleID, const CStack * stack)
 			{
 				if(CStack::isMeleeAttackPossible(stack, s, hex))
 				{
-					std::vector<EnemyInfo>::iterator i = std::find(enemiesReachable.begin(), enemiesReachable.end(), s);
+					auto i = std::find(enemiesReachable.begin(), enemiesReachable.end(), s);
 					if(i == enemiesReachable.end())
 					{
 						enemiesReachable.push_back(s);
@@ -169,10 +171,10 @@ void CStupidAI::activeStack(const BattleID & battleID, const CStack * stack)
 	}
 
 	for ( auto & enemy : enemiesReachable )
-		enemy.calcDmg(battleID, stack);
+		enemy.calcDmg(cb, battleID, stack);
 
 	for ( auto & enemy : enemiesShootable )
-		enemy.calcDmg(battleID, stack);
+		enemy.calcDmg(cb, battleID, stack);
 
 	if(enemiesShootable.size())
 	{
@@ -183,7 +185,7 @@ void CStupidAI::activeStack(const BattleID & battleID, const CStack * stack)
 	else if(enemiesReachable.size())
 	{
 		const EnemyInfo &ei= *std::max_element(enemiesReachable.begin(), enemiesReachable.end(), &isMoreProfitable);
-		BattleHex targetHex = *std::max_element(ei.attackFrom.begin(), ei.attackFrom.end(), [&](auto a, auto b) { return willSecondHexBlockMoreEnemyShooters(battleID, a, b);});
+		BattleHex targetHex = *std::max_element(ei.attackFrom.begin(), ei.attackFrom.end(), [&](auto a, auto b) { return willSecondHexBlockMoreEnemyShooters(cb, battleID, a, b);});
 
 		cb->battleMakeUnitAction(battleID, BattleAction::makeMeleeAttack(stack, ei.s->getPosition(), targetHex));
 		return;

+ 1 - 1
AI/StupidAI/main.cpp

@@ -16,7 +16,7 @@
 #define strcpy_s(a, b, c) strncpy(a, c, b)
 #endif
 
-static const char *g_cszAiName = "Stupid AI 0.1";
+static const char * const g_cszAiName = "Stupid AI 0.1";
 
 extern "C" DLL_EXPORT int GetGlobalAiVersion()
 {

+ 2 - 3
AI/VCAI/AIUtility.h

@@ -26,7 +26,6 @@ using crint3 = const int3 &;
 using crstring = const std::string &;
 using dwellingContent = std::pair<ui32, std::vector<CreatureID>>;
 
-const int GOLD_MINE_PRODUCTION = 1000, WOOD_ORE_MINE_PRODUCTION = 2, RESOURCE_MINE_PRODUCTION = 1;
 const int ACTUAL_RESOURCE_COUNT = 7;
 const int ALLOWED_ROAMING_HEROES = 8;
 
@@ -71,7 +70,7 @@ public:
 	bool validAndSet() const;
 
 
-	template<typename Handler> void serialize(Handler & h, const int version)
+	template<typename Handler> void serialize(Handler & h)
 	{
 		h & this->h;
 		h & hid;
@@ -103,7 +102,7 @@ struct ObjectIdRef
 	bool operator<(const ObjectIdRef & rhs) const;
 
 
-	template<typename Handler> void serialize(Handler & h, const int version)
+	template<typename Handler> void serialize(Handler & h)
 	{
 		h & id;
 	}

+ 1 - 1
AI/VCAI/ArmyManager.cpp

@@ -63,7 +63,7 @@ std::vector<SlotInfo>::iterator ArmyManager::getWeakestCreature(std::vector<Slot
 		if(left.creature->getLevel() != right.creature->getLevel())
 			return left.creature->getLevel() < right.creature->getLevel();
 		
-		return left.creature->speed() > right.creature->speed();
+		return left.creature->getMovementRange() > right.creature->getMovementRange();
 	});
 
 	return weakest;

+ 19 - 20
AI/VCAI/BuildingManager.cpp

@@ -139,17 +139,17 @@ void BuildingManager::setAI(VCAI * AI)
 //Set of buildings for different goals. Does not include any prerequisites.
 static const std::vector<BuildingID> essential = { BuildingID::TAVERN, BuildingID::TOWN_HALL };
 static const std::vector<BuildingID> basicGoldSource = { BuildingID::TOWN_HALL, BuildingID::CITY_HALL };
+static const std::vector<BuildingID> defence = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE };
 static const std::vector<BuildingID> capitolAndRequirements = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE, BuildingID::CAPITOL };
 static const std::vector<BuildingID> unitsSource = { BuildingID::DWELL_LVL_1, BuildingID::DWELL_LVL_2, BuildingID::DWELL_LVL_3,
 BuildingID::DWELL_LVL_4, BuildingID::DWELL_LVL_5, BuildingID::DWELL_LVL_6, BuildingID::DWELL_LVL_7 };
 static const std::vector<BuildingID> unitsUpgrade = { BuildingID::DWELL_LVL_1_UP, BuildingID::DWELL_LVL_2_UP, BuildingID::DWELL_LVL_3_UP,
 BuildingID::DWELL_LVL_4_UP, BuildingID::DWELL_LVL_5_UP, BuildingID::DWELL_LVL_6_UP, BuildingID::DWELL_LVL_7_UP };
-static const std::vector<BuildingID> unitGrowth = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE, BuildingID::HORDE_1,
-BuildingID::HORDE_1_UPGR, BuildingID::HORDE_2, BuildingID::HORDE_2_UPGR };
+static const std::vector<BuildingID> unitGrowth = { BuildingID::HORDE_1, BuildingID::HORDE_1_UPGR, BuildingID::HORDE_2, BuildingID::HORDE_2_UPGR };
 static const std::vector<BuildingID> _spells = { BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3,
 BuildingID::MAGES_GUILD_4, BuildingID::MAGES_GUILD_5 };
-static const std::vector<BuildingID> extra = { BuildingID::RESOURCE_SILO, BuildingID::SPECIAL_1, BuildingID::SPECIAL_2, BuildingID::SPECIAL_3,
-BuildingID::SPECIAL_4, BuildingID::SHIPYARD }; // all remaining buildings
+static const std::vector<BuildingID> extra = { BuildingID::MARKETPLACE, BuildingID::BLACKSMITH, BuildingID::RESOURCE_SILO, BuildingID::SPECIAL_1, BuildingID::SPECIAL_2,
+BuildingID::SPECIAL_3, BuildingID::SPECIAL_4, BuildingID::SHIPYARD }; // all remaining buildings
 
 bool BuildingManager::getBuildingOptions(const CGTownInstance * t)
 {
@@ -172,33 +172,32 @@ bool BuildingManager::getBuildingOptions(const CGTownInstance * t)
 	if(tryBuildAnyStructure(t, essential))
 		return true;
 
-	//the more gold the better and less problems later //TODO: what about building mage guild / marketplace etc. with city hall disabled in editor?
-	if(tryBuildNextStructure(t, basicGoldSource))
-		return true;
-
-	//workaround for mantis #2696 - build capitol with separate algorithm if it is available
-	if(vstd::contains(t->builtBuildings, BuildingID::CITY_HALL) && getMaxPossibleGoldBuilding(t) == BuildingID::CAPITOL)
+	if (cb->getDate(Date::DAY_OF_WEEK) < 5) // first 4 days of week - try to focus on dwellings
 	{
-		if(tryBuildNextStructure(t, capitolAndRequirements))
+		if (tryBuildNextStructure(t, unitsSource, 4))
 			return true;
 	}
 
-	if(!t->hasBuilt(BuildingID::FORT)) //in vast majority of situations fort is top priority building if we already have city hall, TODO: unite with unitGrowth building chain
-		if(tryBuildThisStructure(t, BuildingID::FORT))
+	if (cb->getDate(Date::DAY_OF_WEEK) > 4) // last 3 days of week - try to focus on growth by building Fort/Citadel/Castle
+	{
+		if (tryBuildNextStructure(t, defence, 3))
 			return true;
+	}
 
-
-
-	if (cb->getDate(Date::DAY_OF_WEEK) > 6) // last 2 days of week - try to focus on growth
+	if (t->hasBuilt(BuildingID::CASTLE))
 	{
-		if (tryBuildNextStructure(t, unitGrowth, 2))
+		if (tryBuildAnyStructure(t, unitGrowth))
 			return true;
 	}
 
-	//try building dwellings
-	if (t->hasBuilt(BuildingID::FORT))
+	//try to make City Hall
+	if (tryBuildNextStructure(t, basicGoldSource))
+		return true;
+
+	//workaround for mantis #2696 - build capitol with separate algorithm if it is available
+	if(vstd::contains(t->builtBuildings, BuildingID::CITY_HALL) && getMaxPossibleGoldBuilding(t) == BuildingID::CAPITOL)
 	{
-		if (tryBuildAnyStructure(t, unitsSource, 8 - cb->getDate(Date::DAY_OF_WEEK)))
+		if(tryBuildNextStructure(t, capitolAndRequirements))
 			return true;
 	}
 

+ 3 - 3
AI/VCAI/CMakeLists.txt

@@ -94,12 +94,12 @@ set(VCAI_HEADERS
 		VCAI.h
 )
 
-if(NOT ENABLE_STATIC_AI_LIBS)
+if(NOT ENABLE_STATIC_LIBS)
 	list(APPEND VCAI_SRCS main.cpp StdInc.cpp)
 endif()
 assign_source_group(${VCAI_SRCS} ${VCAI_HEADERS})
 
-if(ENABLE_STATIC_AI_LIBS)
+if(ENABLE_STATIC_LIBS)
 	add_library(VCAI STATIC ${VCAI_SRCS} ${VCAI_HEADERS})
 else()
 	add_library(VCAI SHARED ${VCAI_SRCS} ${VCAI_HEADERS})
@@ -107,7 +107,7 @@ else()
 endif()
 
 target_include_directories(VCAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
-target_link_libraries(VCAI PUBLIC ${VCMI_LIB_TARGET} fuzzylite::fuzzylite)
+target_link_libraries(VCAI PUBLIC vcmi fuzzylite::fuzzylite)
 
 vcmi_set_output_dir(VCAI "AI")
 enable_pch(VCAI)

+ 4 - 2
AI/VCAI/FuzzyEngines.cpp

@@ -37,7 +37,9 @@ void engineBase::addRule(const std::string & txt)
 
 struct armyStructure
 {
-	float walkers, shooters, flyers;
+	float walkers;
+	float shooters;
+	float flyers;
 	ui32 maxSpeed;
 };
 
@@ -406,7 +408,7 @@ float VisitObjEngine::evaluate(Goals::VisitObj & goal)
 	else
 	{
 		MapObjectsEvaluator::getInstance().addObjectData(obj->ID, obj->subID, 0);
-		logGlobal->error("AI met object type it doesn't know - ID: " + std::to_string(obj->ID) + ", subID: " + std::to_string(obj->subID) + " - adding to database with value " + std::to_string(objValue));
+		logGlobal->error("AI met object type it doesn't know - ID: %d, subID: %d - adding to database with value %d ", obj->ID, obj->subID, objValue);
 	}
 
 	setSharedFuzzyVariables(goal);

+ 8 - 2
AI/VCAI/FuzzyEngines.h

@@ -38,8 +38,14 @@ public:
 	TacticalAdvantageEngine();
 	float getTacticalAdvantage(const CArmedInstance * we, const CArmedInstance * enemy); //returns factor how many times enemy is stronger than us
 private:
-	fl::InputVariable * ourWalkers, *ourShooters, *ourFlyers, *enemyWalkers, *enemyShooters, *enemyFlyers;
-	fl::InputVariable * ourSpeed, *enemySpeed;
+	fl::InputVariable * ourWalkers;
+	fl::InputVariable * ourShooters;
+	fl::InputVariable * ourFlyers;
+	fl::InputVariable * enemyWalkers;
+	fl::InputVariable * enemyShooters;
+	fl::InputVariable * enemyFlyers;
+	fl::InputVariable * ourSpeed;
+	fl::InputVariable * enemySpeed;
 	fl::InputVariable * bankPresent;
 	fl::InputVariable * castleWalls;
 	fl::OutputVariable * threat;

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