瀏覽代碼

Merge branch 'develop' into timed_events_objects_removal

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

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

@@ -15,6 +15,7 @@ Please attach game logs: `VCMI_client.txt`, `VCMI_server.txt` etc.
 
 
 **To Reproduce**
 **To Reproduce**
 Steps to reproduce the behavior:
 Steps to reproduce the behavior:
+
 1. Go to '...'
 1. Go to '...'
 2. Click on '....'
 2. Click on '....'
 3. Scroll down to '....'
 3. Scroll down to '....'
@@ -24,7 +25,7 @@ Steps to reproduce the behavior:
 A clear and concise description of what you expected to happen.
 A clear and concise description of what you expected to happen.
 
 
 **Actual behavior**
 **Actual behavior**
-A clear description what is currently happening 
+A clear description what is currently happening
 
 
 **Did it work earlier?**
 **Did it work earlier?**
 If this something which worked well some time ago, please let us know about version where it works or at date when it worked.
 If this something which worked well some time ago, please let us know about version where it works or at date when it worked.
@@ -33,8 +34,9 @@ If this something which worked well some time ago, please let us know about vers
 If applicable, add screenshots to help explain your problem.
 If applicable, add screenshots to help explain your problem.
 
 
 **Version**
 **Version**
- - OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS]
- - Version: [VCMI version]
+
+- OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS]
+- Version: [VCMI version]
 
 
 **Additional context**
 **Additional context**
 Add any other context about the problem here.
 Add any other context about the problem here.

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

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

+ 1 - 1
AI/BattleAI/AttackPossibility.cpp

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

+ 27 - 8
AI/BattleAI/BattleEvaluator.cpp

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

+ 1 - 1
AI/BattleAI/BattleExchangeVariant.cpp

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

+ 15 - 15
AI/BattleAI/StackWithBonuses.cpp

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

+ 9 - 9
AI/BattleAI/StackWithBonuses.h

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

+ 14 - 20
AI/Nullkiller/AIGateway.cpp

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

+ 10 - 11
AI/Nullkiller/AIUtility.cpp

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

+ 2 - 7
AI/Nullkiller/AIUtility.h

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -23,7 +23,7 @@ BuildThis::BuildThis(BuildingID Bid, const CGTownInstance * tid)
 	: ElementarGoal(Goals::BUILD_STRUCTURE)
 	: ElementarGoal(Goals::BUILD_STRUCTURE)
 {
 {
 	buildingInfo = BuildingInfo(
 	buildingInfo = BuildingInfo(
-		tid->town->buildings.at(Bid),
+		tid->getTown()->buildings.at(Bid),
 		nullptr,
 		nullptr,
 		CreatureID::NONE,
 		CreatureID::NONE,
 		tid,
 		tid,
@@ -52,7 +52,7 @@ void BuildThis::accept(AIGateway * ai)
 		if(cb->canBuildStructure(town, b) == EBuildingState::ALLOWED)
 		if(cb->canBuildStructure(town, b) == EBuildingState::ALLOWED)
 		{
 		{
 			logAi->debug("Player %d will build %s in town of %s at %s",
 			logAi->debug("Player %d will build %s in town of %s at %s",
-				ai->playerID, town->town->buildings.at(b)->getNameTranslated(), town->getNameTranslated(), town->pos.toString());
+				ai->playerID, town->getTown()->buildings.at(b)->getNameTranslated(), town->getNameTranslated(), town->anchorPos().toString());
 			cb->buildBuilding(town, b);
 			cb->buildBuilding(town, b);
 
 
 			return;
 			return;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 3 - 4
AI/VCAI/AIUtility.cpp

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

+ 0 - 2
AI/VCAI/AIUtility.h

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

+ 1 - 1
AI/VCAI/ArmyManager.cpp

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

+ 4 - 4
AI/VCAI/BuildingManager.cpp

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

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

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

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

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

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

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

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

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

+ 3 - 3
AI/VCAI/MapObjectsEvaluator.cpp

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

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

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

+ 0 - 2
AI/VCAI/ResourceManager.cpp

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

+ 11 - 14
AI/VCAI/VCAI.cpp

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

+ 1 - 0
AUTHORS.h

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

+ 73 - 40
CCallback.cpp

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

+ 11 - 1
CCallback.h

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

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

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

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

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

+ 0 - 7
CI/android/before_install.sh

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

+ 4 - 0
CI/before_install/android.sh

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

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

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

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

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

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

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

+ 7 - 0
CI/before_install/mingw.sh

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

+ 17 - 0
CI/before_install/msvc.sh

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

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

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

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

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

+ 1 - 1
CI/install_conan_dependencies.sh

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

+ 7 - 0
CI/install_vcpkg_dependencies.sh

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

+ 0 - 5
CI/ios/before_install.sh

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

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

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

+ 0 - 1
CI/linux/upload_package.sh

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

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

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

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

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

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

@@ -1,14 +0,0 @@
-#!/usr/bin/env bash
-
-sudo apt-get update
-sudo apt-get install ninja-build mingw-w64 nsis
-sudo update-alternatives --set i686-w64-mingw32-g++ /usr/bin/i686-w64-mingw32-g++-posix
-
-# Workaround for getting new MinGW headers on Ubuntu 22.04.
-# Remove it once MinGW headers version in repository will be 10.0 at least
-curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-common_10.0.0-3_all.deb \
-  && sudo dpkg -i mingw-w64-common_10.0.0-3_all.deb;
-curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-i686-dev_10.0.0-3_all.deb \
-  && sudo dpkg -i mingw-w64-i686-dev_10.0.0-3_all.deb;
-
-. CI/install_conan_dependencies.sh "dependencies-mingw-32"

+ 0 - 14
CI/mingw/before_install.sh

@@ -1,14 +0,0 @@
-#!/usr/bin/env bash
-
-sudo apt-get update
-sudo apt-get install ninja-build mingw-w64 nsis
-sudo update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix
-
-# Workaround for getting new MinGW headers on Ubuntu 22.04.
-# Remove it once MinGW headers version in repository will be 10.0 at least
-curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-common_10.0.0-3_all.deb \
-  && sudo dpkg -i mingw-w64-common_10.0.0-3_all.deb;
-curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-x86-64-dev_10.0.0-3_all.deb \
-  && sudo dpkg -i mingw-w64-x86-64-dev_10.0.0-3_all.deb;
-
-. CI/install_conan_dependencies.sh "dependencies-mingw"

+ 0 - 10
CI/msvc/before_install.sh

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

+ 0 - 6
CI/msvc/build_script.bat

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

+ 0 - 5
CI/msvc/coverity_build_script.bat

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

+ 0 - 17
CI/msvc/coverity_upload_script.ps

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

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


+ 18 - 20
CMakeLists.txt

@@ -180,11 +180,6 @@ else()
 	add_definitions(-DVCMI_NO_EXTRA_VERSION)
 	add_definitions(-DVCMI_NO_EXTRA_VERSION)
 endif(ENABLE_GITVERSION)
 endif(ENABLE_GITVERSION)
 
 
-# Precompiled header configuration
-if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 6.0 )
-	set(ENABLE_PCH OFF) # broken
-endif()
-
 if(ENABLE_PCH)
 if(ENABLE_PCH)
 	macro(enable_pch name)
 	macro(enable_pch name)
 		target_precompile_headers(${name} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:<StdInc.h$<ANGLE-R>>)
 		target_precompile_headers(${name} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:<StdInc.h$<ANGLE-R>>)
@@ -328,7 +323,6 @@ if(MINGW OR MSVC)
 		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4244") # 4244: conversion from 'xxx' to 'yyy', possible loss of data
 		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4244") # 4244: conversion from 'xxx' to 'yyy', possible loss of data
 		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4267") # 4267: conversion from 'xxx' to 'yyy', possible loss of data
 		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4267") # 4267: conversion from 'xxx' to 'yyy', possible loss of data
 		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4275") # 4275: non dll-interface class 'xxx' used as base for dll-interface class
 		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4275") # 4275: non dll-interface class 'xxx' used as base for dll-interface class
-		#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4800") # 4800: implicit conversion from 'xxx' to bool. Possible information loss
 
 
 		if(ENABLE_STRICT_COMPILATION)
 		if(ENABLE_STRICT_COMPILATION)
 			set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /WX") # Treats all compiler warnings as errors
 			set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /WX") # Treats all compiler warnings as errors
@@ -361,13 +355,6 @@ if(MINGW OR MSVC)
 		if(ICONV_FOUND)
 		if(ICONV_FOUND)
 			set(SYSTEM_LIBS ${SYSTEM_LIBS} iconv)
 			set(SYSTEM_LIBS ${SYSTEM_LIBS} iconv)
 		endif()
 		endif()
-
-		# Prevent compiler issues when building Debug
-		# Assembler might fail with "too many sections"
-		# With big-obj or 64-bit build will take hours
-		if(CMAKE_BUILD_TYPE STREQUAL "Debug")
-			set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Og")
-		endif()
 	endif(MINGW)
 	endif(MINGW)
 endif(MINGW OR MSVC)
 endif(MINGW OR MSVC)
 
 
@@ -486,25 +473,30 @@ if(NOT FORCE_BUNDLED_MINIZIP)
 endif()
 endif()
 
 
 if (ENABLE_CLIENT)
 if (ENABLE_CLIENT)
-	set(FFMPEG_COMPONENTS avutil swscale avformat avcodec)
-	if(APPLE_IOS AND NOT USING_CONAN)
-		list(APPEND FFMPEG_COMPONENTS swresample)
-	endif()
-	find_package(ffmpeg COMPONENTS ${FFMPEG_COMPONENTS})
+	find_package(ffmpeg COMPONENTS avutil swscale avformat avcodec swresample)
 
 
 	find_package(SDL2 REQUIRED)
 	find_package(SDL2 REQUIRED)
 	find_package(SDL2_image REQUIRED)
 	find_package(SDL2_image REQUIRED)
 	if(TARGET SDL2_image::SDL2_image)
 	if(TARGET SDL2_image::SDL2_image)
 		add_library(SDL2::Image ALIAS SDL2_image::SDL2_image)
 		add_library(SDL2::Image ALIAS SDL2_image::SDL2_image)
 	endif()
 	endif()
+	if(TARGET SDL2_image::SDL2_image-static)
+		add_library(SDL2::Image ALIAS SDL2_image::SDL2_image-static)
+	endif()
 	find_package(SDL2_mixer REQUIRED)
 	find_package(SDL2_mixer REQUIRED)
 	if(TARGET SDL2_mixer::SDL2_mixer)
 	if(TARGET SDL2_mixer::SDL2_mixer)
 		add_library(SDL2::Mixer ALIAS SDL2_mixer::SDL2_mixer)
 		add_library(SDL2::Mixer ALIAS SDL2_mixer::SDL2_mixer)
 	endif()
 	endif()
+	if(TARGET SDL2_mixer::SDL2_mixer-static)
+		add_library(SDL2::Mixer ALIAS SDL2_mixer::SDL2_mixer-static)
+	endif()
 	find_package(SDL2_ttf REQUIRED)
 	find_package(SDL2_ttf REQUIRED)
 	if(TARGET SDL2_ttf::SDL2_ttf)
 	if(TARGET SDL2_ttf::SDL2_ttf)
 		add_library(SDL2::TTF ALIAS SDL2_ttf::SDL2_ttf)
 		add_library(SDL2::TTF ALIAS SDL2_ttf::SDL2_ttf)
 	endif()
 	endif()
+	if(TARGET SDL2_ttf::SDL2_ttf-static)
+		add_library(SDL2::TTF ALIAS SDL2_ttf::SDL2_ttf-static)
+	endif()
 endif()
 endif()
 
 
 if(ENABLE_LOBBY)
 if(ENABLE_LOBBY)
@@ -666,6 +658,10 @@ if(NOT TARGET minizip::minizip)
 	add_library(minizip::minizip ALIAS minizip)
 	add_library(minizip::minizip ALIAS minizip)
 endif()
 endif()
 
 
+if(ENABLE_LAUNCHER OR ENABLE_EDITOR)
+	add_subdirectory(vcmiqt)
+endif()
+
 if(ENABLE_LAUNCHER)
 if(ENABLE_LAUNCHER)
 	add_subdirectory(launcher)
 	add_subdirectory(launcher)
 endif()
 endif()
@@ -727,7 +723,7 @@ endif()
 
 
 if(WIN32)
 if(WIN32)
 	if(TBB_FOUND AND MSVC)
 	if(TBB_FOUND AND MSVC)
-		   install_vcpkg_imported_tgt(TBB::tbb)
+		install_vcpkg_imported_tgt(TBB::tbb)
 	endif()
 	endif()
 
 
 	if(USING_CONAN)
 	if(USING_CONAN)
@@ -737,7 +733,9 @@ if(WIN32)
 				${dep_files}
 				${dep_files}
 				"${CMAKE_SYSROOT}/bin/*.dll" 
 				"${CMAKE_SYSROOT}/bin/*.dll" 
 				"${CMAKE_SYSROOT}/lib/*.dll" 
 				"${CMAKE_SYSROOT}/lib/*.dll" 
-				"${CONAN_SYSTEM_LIBRARY_LOCATION}/*.dll")
+				"${CONAN_SYSTEM_LIBRARY_LOCATION}/libgcc_s_dw2-1.dll" # for 32-bit only?
+				"${CONAN_SYSTEM_LIBRARY_LOCATION}/libgcc_s_seh-1.dll" # for 64-bit only?
+				"${CONAN_SYSTEM_LIBRARY_LOCATION}/libstdc++-6.dll")
 	else()
 	else()
 		file(GLOB dep_files
 		file(GLOB dep_files
 				${dep_files}
 				${dep_files}

+ 21 - 1
CMakePresets.json

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

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


+ 4 - 1
Global.h

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

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


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


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


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


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


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


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


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


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