Browse Source

Merge branch 'develop' into sod-fly

Dydzio 1 year ago
parent
commit
365fedc7e8
100 changed files with 1451 additions and 660 deletions
  1. 11 0
      .github/dependabot.yml
  2. 44 95
      .github/workflows/github.yml
  3. 1 0
      .gitignore
  4. 6 3
      AI/BattleAI/AttackPossibility.cpp
  5. 2 1
      AI/BattleAI/BattleAI.cpp
  6. 7 4
      AI/BattleAI/BattleEvaluator.cpp
  7. 2 2
      AI/BattleAI/StackWithBonuses.cpp
  8. 2 2
      AI/EmptyAI/CEmptyAI.cpp
  9. 2 2
      AI/EmptyAI/CEmptyAI.h
  10. 31 28
      AI/Nullkiller/AIGateway.cpp
  11. 4 4
      AI/Nullkiller/AIGateway.h
  12. 1 3
      AI/Nullkiller/AIUtility.cpp
  13. 5 4
      AI/Nullkiller/AIUtility.h
  14. 9 9
      AI/Nullkiller/Analyzers/ArmyManager.cpp
  15. 5 5
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp
  16. 3 1
      AI/Nullkiller/Engine/FuzzyEngines.cpp
  17. 8 2
      AI/Nullkiller/Engine/FuzzyEngines.h
  18. 1 1
      AI/Nullkiller/Engine/FuzzyHelper.cpp
  19. 4 0
      AI/Nullkiller/Engine/Nullkiller.cpp
  20. 1 0
      AI/Nullkiller/Engine/Nullkiller.h
  21. 21 4
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  22. 2 2
      AI/Nullkiller/Goals/BuyArmy.cpp
  23. 1 1
      AI/Nullkiller/Goals/CGoal.h
  24. 2 2
      AI/Nullkiller/Goals/CompleteQuest.cpp
  25. 21 4
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  26. 11 2
      AI/Nullkiller/Pathfinding/AINodeStorage.h
  27. 8 7
      AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp
  28. 2 1
      AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h
  29. 6 6
      AI/Nullkiller/Pathfinding/Actors.cpp
  30. 1 1
      AI/Nullkiller/Pathfinding/Actors.h
  31. 12 0
      AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp
  32. 15 13
      AI/StupidAI/StupidAI.cpp
  33. 2 2
      AI/VCAI/AIUtility.h
  34. 19 20
      AI/VCAI/BuildingManager.cpp
  35. 3 1
      AI/VCAI/FuzzyEngines.cpp
  36. 8 2
      AI/VCAI/FuzzyEngines.h
  37. 1 1
      AI/VCAI/FuzzyHelper.cpp
  38. 1 1
      AI/VCAI/Goals/AbstractGoal.h
  39. 1 1
      AI/VCAI/Goals/CGoal.h
  40. 2 1
      AI/VCAI/Goals/CollectRes.cpp
  41. 2 2
      AI/VCAI/Goals/CompleteQuest.cpp
  42. 1 3
      AI/VCAI/Pathfinding/AINodeStorage.cpp
  43. 2 2
      AI/VCAI/ResourceManager.h
  44. 16 26
      AI/VCAI/VCAI.cpp
  45. 5 5
      AI/VCAI/VCAI.h
  46. 3 2
      CCallback.cpp
  47. 2 2
      CCallback.h
  48. 45 32
      CMakeLists.txt
  49. 47 0
      CMakePresets.json
  50. 84 1
      ChangeLog.md
  51. 4 0
      Global.h
  52. 17 7
      Mods/vcmi/config/vcmi/chinese.json
  53. 25 9
      Mods/vcmi/config/vcmi/english.json
  54. 21 7
      Mods/vcmi/config/vcmi/german.json
  55. 86 70
      Mods/vcmi/config/vcmi/polish.json
  56. 223 48
      Mods/vcmi/config/vcmi/spanish.json
  57. 7 0
      Mods/vcmi/config/vcmi/ukrainian.json
  58. 2 2
      android/vcmi-app/build.gradle
  59. 71 0
      android/vcmi-app/src/main/res/values-es/strings.xml
  60. 1 4
      client/CGameInfo.cpp
  61. 16 15
      client/CGameInfo.h
  62. 18 18
      client/CMT.cpp
  63. 10 4
      client/CMakeLists.txt
  64. 29 0
      client/CMusicHandler.cpp
  65. 1 0
      client/CMusicHandler.h
  66. 46 16
      client/CPlayerInterface.cpp
  67. 3 2
      client/CPlayerInterface.h
  68. 22 4
      client/CServerHandler.cpp
  69. 8 1
      client/CServerHandler.h
  70. 31 7
      client/CVideoHandler.cpp
  71. 9 3
      client/CVideoHandler.h
  72. 38 31
      client/Client.cpp
  73. 2 2
      client/Client.h
  74. 7 5
      client/ClientCommandManager.cpp
  75. 21 9
      client/NetPacksClient.cpp
  76. 4 0
      client/NetPacksLobbyClient.cpp
  77. 2 2
      client/PlayerLocalState.h
  78. 1 1
      client/adventureMap/AdventureMapInterface.cpp
  79. 10 6
      client/adventureMap/AdventureOptions.cpp
  80. 1 1
      client/adventureMap/AdventureOptions.h
  81. 2 1
      client/adventureMap/CInfoBar.cpp
  82. 1 1
      client/adventureMap/CList.cpp
  83. 6 5
      client/battle/BattleActionsController.cpp
  84. 4 4
      client/battle/BattleInterface.cpp
  85. 3 5
      client/battle/BattleInterfaceClasses.cpp
  86. 1 1
      client/battle/BattleInterfaceClasses.h
  87. 5 2
      client/battle/BattleStacksController.cpp
  88. 94 19
      client/battle/BattleWindow.cpp
  89. 8 0
      client/battle/BattleWindow.h
  90. 14 29
      client/eventsSDL/InputSourceText.cpp
  91. 1 1
      client/gui/InterfaceObjectConfigurable.cpp
  92. 1 0
      client/gui/Shortcut.h
  93. 2 0
      client/gui/ShortcutHandler.cpp
  94. 4 0
      client/lobby/CBonusSelection.cpp
  95. 1 0
      client/lobby/CBonusSelection.h
  96. 33 4
      client/lobby/CLobbyScreen.cpp
  97. 1 1
      client/lobby/CSelectionBase.cpp
  98. 3 0
      client/lobby/CSelectionBase.h
  99. 18 0
      client/lobby/ExtraOptionsTab.cpp
  100. 18 0
      client/lobby/ExtraOptionsTab.h

+ 11 - 0
.github/dependabot.yml

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

+ 44 - 95
.github/workflows/github.yml

@@ -6,9 +6,8 @@ on:
       - features/*
       - beta
       - master
+      - develop
   pull_request:
-  schedule:
-    - cron: '0 2 * * *'
   workflow_dispatch:
 
 env:
@@ -16,55 +15,7 @@ env:
   BUILD_TYPE: Release
 
 jobs:
-  check_last_build:
-    if: github.event.schedule != ''
-    runs-on: ubuntu-latest
-    outputs:
-      skip_build: ${{ steps.check_if_built.outputs.skip_build }}
-    defaults:
-      run:
-        shell: bash
-    steps:
-      - name: Get repo name
-        id: get_repo_name
-        run: echo "::set-output name=value::${GITHUB_REPOSITORY#*/}"
-      - name: Get last successful build for ${{ github.sha }}
-        uses: octokit/[email protected]
-        id: get_last_scheduled_run
-        with:
-          route: GET /repos/{owner}/{repo}/actions/runs
-          owner: ${{ github.repository_owner }}
-          repo: ${{ steps.get_repo_name.outputs.value }}
-          status: success
-          per_page: 1
-          head_sha: ${{ github.sha }}
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-      - name: Check if successful build of the current commit exists
-        id: check_if_built
-        run: |
-          if [ ${{ fromJson(steps.get_last_scheduled_run.outputs.data).total_count }} -gt 0 ]; then
-            echo '::set-output name=skip_build::1'
-          else
-            echo '::set-output name=skip_build::0'
-          fi
-      - name: Cancel current run
-        if: steps.check_if_built.outputs.skip_build == 1
-        uses: octokit/[email protected]
-        with:
-          route: POST /repos/{owner}/{repo}/actions/runs/{run_id}/cancel
-          owner: ${{ github.repository_owner }}
-          repo: ${{ steps.get_repo_name.outputs.value }}
-          run_id: ${{ github.run_id }}
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-      - name: Wait for the run to be cancelled
-        if: steps.check_if_built.outputs.skip_build == 1
-        run: sleep 60
-
   build:
-    needs: check_last_build
-    if: always() && needs.check_last_build.skip_build != 1
     strategy:
       matrix:
         include:
@@ -73,8 +24,8 @@ jobs:
             test: 0
             preset: linux-clang-test
           - platform: linux
-            os: ubuntu-20.04
-            test: 0
+            os: ubuntu-22.04
+            test: 1
             preset: linux-gcc-test
           - platform: linux
             os: ubuntu-20.04
@@ -156,7 +107,7 @@ jobs:
         shell: bash
 
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: recursive
 
@@ -195,9 +146,9 @@ jobs:
         max-size: "5G"
         verbose: 2
 
-    - name: Ccache for everything but PRs
+    - name: Ccache for vcmi/vcmi's develop branch
       uses: hendrikmuhs/[email protected]
-      if: ${{ github.event.number == '' }}
+      if: ${{ github.event.number == '' && github.ref == 'refs/heads/develop' }}
       with:
         key: ${{ matrix.preset }}-no-PR
         restore-keys: |
@@ -206,7 +157,17 @@ jobs:
         max-size: "5G"
         verbose: 2
 
-    - uses: actions/setup-python@v4
+    - name: Prepare Heroes 3 data
+      env:
+        HEROES_3_DATA_PASSWORD: ${{ secrets.HEROES_3_DATA_PASSWORD }}
+      if: ${{ env.HEROES_3_DATA_PASSWORD != '' && matrix.test == 1 }}
+      run: |
+        wget --progress=dot:giga https://github.com/vcmi-mods/vcmi-test-data/releases/download/v1.0/h3_assets.zip
+        7za x h3_assets.zip -p$HEROES_3_DATA_PASSWORD
+        mkdir -p ~/.local/share/vcmi/
+        mv h3_assets/* ~/.local/share/vcmi/
+
+    - uses: actions/setup-python@v5
       if: "${{ matrix.conan_profile != '' }}"
       with:
         python-version: '3.10'
@@ -226,10 +187,6 @@ jobs:
       env:
         GENERATE_ONLY_BUILT_CONFIG: 1
 
-    - name: Git branch name
-      id: git-branch-name
-      uses: EthanSK/git-branch-name-action@v1
-
     - name: Build Number
       run: |
         source '${{github.workspace}}/CI/get_package_name.sh'
@@ -242,31 +199,42 @@ jobs:
       env:
         PULL_REQUEST: ${{ github.event.pull_request.number }}
 
-    - name: CMake Preset with ccache
+    - name: Configure
       run: |
-        cmake -DCMAKE_CXX_COMPILER_LAUNCHER=ccache --preset ${{ matrix.preset }}
+        if [[ ${{matrix.preset}} == linux-gcc-test ]]; then GCC13=1; fi
+        cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }} ${GCC13:+-DCMAKE_C_COMPILER=gcc-13 -DCMAKE_CXX_COMPILER=g++-13}
 
-    - name: Build Preset
+    - name: Build
       run: |
         cmake --build --preset ${{matrix.preset}}
 
     - name: Test
-      if: ${{ matrix.test == 1 }}
+      env:
+        HEROES_3_DATA_PASSWORD: ${{ secrets.HEROES_3_DATA_PASSWORD }}
+      if: ${{ env.HEROES_3_DATA_PASSWORD != '' && matrix.test == 1 }}
       run: |
         ctest --preset ${{matrix.preset}}
 
+    - name: Kill XProtect to work around CPack issue on macOS 
+      if: ${{ startsWith(matrix.platform, 'mac') }}
+      run: |
+        # Cf. https://github.com/actions/runner-images/issues/7522#issuecomment-1556766641
+        echo Killing...; sudo pkill -9 XProtect >/dev/null || true;
+        echo "Waiting..."; counter=0; while pgrep XProtect && ((counter < 20)); do sleep 3; ((counter++)); done
+        pgrep XProtect || true
+
     - name: Pack
       id: cpack
       if: ${{ matrix.pack == 1 }}
       run: |
         cd '${{github.workspace}}/out/build/${{matrix.preset}}'
         CPACK_PATH=`which -a cpack | grep -m1 -v -i chocolatey`
-        "$CPACK_PATH" -C ${{matrix.pack_type}} ${{ matrix.cpack_args }}
+        counter=0; until "$CPACK_PATH" -C ${{matrix.pack_type}} ${{ matrix.cpack_args }} || ((counter > 20)); do sleep 3; ((counter++)); done
         test -f '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' \
           && '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' '${{github.workspace}}' "$(ls '${{ env.VCMI_PACKAGE_FILE_NAME }}'.*)"
         rm -rf _CPack_Packages
 
-    - name: Create android package
+    - name: Create Android package
       if: ${{ startsWith(matrix.platform, 'android') }}
       run: |
         cd android
@@ -281,7 +249,7 @@ jobs:
 
     - name: Artifacts
       if: ${{ matrix.pack == 1 }}
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
         path: |
@@ -289,7 +257,7 @@ jobs:
           
     - name: Android artifacts
       if: ${{ startsWith(matrix.platform, 'android') }}
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
         path: |
@@ -297,7 +265,7 @@ jobs:
           
     - name: Symbols
       if: ${{ matrix.platform == 'msvc' }}
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - symbols
         path: |
@@ -305,7 +273,7 @@ jobs:
 
     - name: Android JNI ${{matrix.platform}}
       if: ${{ startsWith(matrix.platform, 'android') && github.ref == 'refs/heads/master' }}
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
         name: Android JNI ${{matrix.platform}}
         path: |
@@ -324,14 +292,6 @@ jobs:
       env:
         DEPLOY_RSA: ${{ secrets.DEPLOY_RSA }}
         PACKAGE_EXTENSION: ${{ matrix.extension }}
-
-    - uses: act10ns/slack@v1
-      with:
-        status: ${{ job.status }}
-        channel: '#notifications'
-      env:
-        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
-      if: always()
   
   # copy-pasted mostly
   bundle_release:
@@ -354,7 +314,7 @@ jobs:
         shell: bash
 
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: recursive
 
@@ -363,10 +323,11 @@ jobs:
       env:
         VCMI_BUILD_PLATFORM: x64
 
-    - uses: actions/setup-python@v4
+    - uses: actions/setup-python@v5
       if: "${{ matrix.conan_profile != '' }}"
       with:
         python-version: '3.10'
+
     - name: Conan setup
       if: "${{ matrix.conan_profile != '' }}"
       run: |
@@ -382,10 +343,6 @@ jobs:
       env:
         GENERATE_ONLY_BUILT_CONFIG: 1
 
-    - name: Git branch name
-      id: git-branch-name
-      uses: EthanSK/git-branch-name-action@v1
-
     - name: Build Number
       run: |
         source '${{github.workspace}}/CI/get_package_name.sh'
@@ -407,12 +364,12 @@ jobs:
         cmake --build --preset ${{matrix.preset}}
 
     - name: Download libs x64
-      uses: actions/download-artifact@v3
+      uses: actions/download-artifact@v4
       with:
         name: Android JNI android-64
         path: ${{ github.workspace }}/android/vcmi-app/src/main/jniLibs/
  
-    - name: Create android package
+    - name: Create Android package
       run: |
         cd android
         ./gradlew bundleRelease --info
@@ -422,16 +379,8 @@ jobs:
         ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
 
     - name: Android artifacts
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }}
         path: |
           ${{ env.ANDROID_APK_PATH }}
-
-    - uses: act10ns/slack@v1
-      with:
-        status: ${{ job.status }}
-        channel: '#notifications'
-      env:
-        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
-      if: always()

+ 1 - 0
.gitignore

@@ -41,6 +41,7 @@ VCMI_VS11.sdf
 VCMI_VS11.opensdf
 .DS_Store
 CMakeUserPresets.json
+compile_commands.json
 
 # Visual Studio
 *.suo

+ 6 - 3
AI/BattleAI/AttackPossibility.cpp

@@ -33,7 +33,8 @@ void DamageCache::buildDamageCache(std::shared_ptr<HypotheticBattle> hb, int sid
 			return u->isValidTarget();
 		});
 
-	std::vector<const battle::Unit *> ourUnits, enemyUnits;
+	std::vector<const battle::Unit *> ourUnits;
+	std::vector<const battle::Unit *> enemyUnits;
 
 	for(auto stack : stacks)
 	{
@@ -295,8 +296,10 @@ AttackPossibility AttackPossibility::evaluate(
 
 			for(int i = 0; i < totalAttacks; i++)
 			{
-				int64_t damageDealt, damageReceived;
-				float defenderDamageReduce, attackerDamageReduce;
+				int64_t damageDealt;
+				int64_t damageReceived;
+				float defenderDamageReduce;
+				float attackerDamageReduce;
 
 				DamageEstimation retaliation;
 				auto attackDmg = state->battleEstimateDamage(ap.attack, &retaliation);

+ 2 - 1
AI/BattleAI/BattleAI.cpp

@@ -90,7 +90,8 @@ void CBattleAI::yourTacticPhase(const BattleID & battleID, int distance)
 static float getStrengthRatio(std::shared_ptr<CBattleInfoCallback> cb, int side)
 {
 	auto stacks = cb->battleGetAllStacks();
-	auto our = 0, enemy = 0;
+	auto our = 0;
+	auto enemy = 0;
 
 	for(auto stack : stacks)
 	{

+ 7 - 4
AI/BattleAI/BattleEvaluator.cpp

@@ -22,6 +22,8 @@
 #include "../../lib/battle/BattleStateInfoForRetreat.h"
 #include "../../lib/battle/CObstacleInstance.h"
 #include "../../lib/battle/BattleAction.h"
+#include "../../lib/CRandomGenerator.h"
+
 
 // TODO: remove
 // Eventually only IBattleInfoCallback and battle::Unit should be used,
@@ -350,10 +352,11 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 	LOGL("Casting spells sounds like fun. Let's see...");
 	//Get all spells we can cast
 	std::vector<const CSpell*> possibleSpells;
-	vstd::copy_if(VLC->spellh->objects, std::back_inserter(possibleSpells), [hero, this](const CSpell *s) -> bool
-	{
-		return s->canBeCast(cb->getBattle(battleID).get(), spells::Mode::HERO, hero);
-	});
+
+	for (auto const & s : VLC->spellh->objects)
+		if (s->canBeCast(cb->getBattle(battleID).get(), spells::Mode::HERO, hero))
+			possibleSpells.push_back(s.get());
+
 	LOGFL("I can cast %d spells.", possibleSpells.size());
 
 	vstd::erase_if(possibleSpells, [](const CSpell *s)

+ 2 - 2
AI/BattleAI/StackWithBonuses.cpp

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

+ 2 - 2
AI/EmptyAI/CEmptyAI.cpp

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

+ 2 - 2
AI/EmptyAI/CEmptyAI.h

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

+ 31 - 28
AI/Nullkiller/AIGateway.cpp

@@ -64,7 +64,7 @@ struct SetGlobalState
 };
 
 
-#define SET_GLOBAL_STATE(ai) SetGlobalState _hlpSetState(ai);
+#define SET_GLOBAL_STATE(ai) SetGlobalState _hlpSetState(ai)
 
 #define NET_EVENT_HANDLER SET_GLOBAL_STATE(this)
 #define MAKING_TURN SET_GLOBAL_STATE(this)
@@ -101,7 +101,7 @@ void AIGateway::heroMoved(const TryMoveHero & details, bool verbose)
 	if(!hero)
 		validateObject(details.id); //enemy hero may have left visible area
 
-	const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));;
+	const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));
 	const int3 to   = hero ? hero->convertToVisitablePos(details.end)   : (details.end   - int3(0,1,0));
 
 	const CGObjectInstance * o1 = vstd::frontOrNull(cb->getVisitableObjs(from, verbose));
@@ -586,11 +586,18 @@ void AIGateway::heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, s
 
 	requestActionASAP([=]()
 	{ 
+		int sel = 0;
+
 		if(hPtr.validAndSet())
 		{
+			std::unique_lock<std::mutex> lockGuard(nullkiller->aiStateMutex);
+
 			nullkiller->heroManager->update();
-			answerQuery(queryID, nullkiller->heroManager->selectBestSkill(hPtr, skills));
+
+			sel = nullkiller->heroManager->selectBestSkill(hPtr, skills);
 		}
+
+		answerQuery(queryID, sel);
 	});
 }
 
@@ -661,14 +668,18 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
 		if(selection) //select from multiple components -> take the last one (they're indexed [1-size])
 			sel = components.size();
 
-		// TODO: Find better way to understand it is Chest of Treasures
-		if(hero.validAndSet()
-			&& components.size() == 2
-			&& components.front().type == ComponentType::RESOURCE
-			&& (nullkiller->heroManager->getHeroRole(hero) != HeroRole::MAIN
-			|| nullkiller->buildAnalyzer->getGoldPreasure() > MAX_GOLD_PEASURE))
 		{
-			sel = 1; // for now lets pick gold from a chest.
+				std::unique_lock<std::mutex> mxLock(nullkiller->aiStateMutex);
+
+				// TODO: Find better way to understand it is Chest of Treasures
+				if(hero.validAndSet()
+					&& components.size() == 2
+					&& components.front().type == ComponentType::RESOURCE
+					&& (nullkiller->heroManager->getHeroRole(hero) != HeroRole::MAIN
+						|| nullkiller->buildAnalyzer->getGoldPreasure() > MAX_GOLD_PEASURE))
+				{
+					sel = 1; // for now lets pick gold from a chest.
+				}
 		}
 
 		answerQuery(askID, sel);
@@ -747,27 +758,25 @@ void AIGateway::showMapObjectSelectDialog(QueryID askID, const Component & icon,
 	requestActionASAP([=](){ answerQuery(askID, selectedObject.getNum()); });
 }
 
-void AIGateway::saveGame(BinarySerializer & h, const int version)
+void AIGateway::saveGame(BinarySerializer & h)
 {
-	LOG_TRACE_PARAMS(logAi, "version '%i'", version);
 	NET_EVENT_HANDLER;
 	nullkiller->memory->removeInvisibleObjects(myCb.get());
 
-	CAdventureAI::saveGame(h, version);
-	serializeInternal(h, version);
+	CAdventureAI::saveGame(h);
+	serializeInternal(h);
 }
 
-void AIGateway::loadGame(BinaryDeserializer & h, const int version)
+void AIGateway::loadGame(BinaryDeserializer & h)
 {
-	LOG_TRACE_PARAMS(logAi, "version '%i'", version);
 	//NET_EVENT_HANDLER;
 
 	#if 0
 	//disabled due to issue 2890
 	registerGoals(h);
 	#endif // 0
-	CAdventureAI::loadGame(h, version);
-	serializeInternal(h, version);
+	CAdventureAI::loadGame(h);
+	serializeInternal(h);
 }
 
 bool AIGateway::makePossibleUpgrades(const CArmedInstance * obj)
@@ -859,6 +868,8 @@ void AIGateway::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h
 		{
 			makePossibleUpgrades(h.get());
 
+			std::unique_lock<std::mutex>  lockGuard(nullkiller->aiStateMutex);
+
 			if(!h->visitedTown->garrisonHero || !nullkiller->isHeroLocked(h->visitedTown->garrisonHero))
 				moveCreaturesToHero(h->visitedTown);
 
@@ -1120,15 +1131,6 @@ void AIGateway::battleEnd(const BattleID & battleID, const BattleResult * br, Qu
 	logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.toString(), (won ? "won" : "lost"), battlename);
 	battlename.clear();
 
-	if (queryID != QueryID::NONE)
-	{
-		status.addQuery(queryID, "Combat result dialog");
-		const int confirmAction = 0;
-		requestActionASAP([=]()
-		{
-			answerQuery(queryID, confirmAction);
-		});
-	}
 	CAdventureAI::battleEnd(battleID, br, queryID);
 }
 
@@ -1413,7 +1415,8 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade
 				if(res.getNum() == g.resID) //sell any other resource
 					continue;
 
-				int toGive, toGet;
+				int toGive;
+				int toGet;
 				m->getOffer(res, g.resID, toGive, toGet, EMarketMode::RESOURCE_RESOURCE);
 				toGive = static_cast<int>(toGive * (it->resVal / toGive)); //round down
 				//TODO trade only as much as needed

+ 4 - 4
AI/Nullkiller/AIGateway.h

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

+ 1 - 3
AI/Nullkiller/AIUtility.cpp

@@ -276,12 +276,10 @@ creInfo infoFromDC(const dwellingContent & dc)
 	ci.creID = dc.second.size() ? dc.second.back() : CreatureID(-1); //should never be accessed
 	if (ci.creID != CreatureID::NONE)
 	{
-		ci.cre = VLC->creatures()->getById(ci.creID);
-		ci.level = ci.cre->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore.
+		ci.level = ci.creID.toCreature()->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore.
 	}
 	else
 	{
-		ci.cre = nullptr;
 		ci.level = 0;
 	}
 	return ci;

+ 5 - 4
AI/Nullkiller/AIUtility.h

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

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

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

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

@@ -11,6 +11,7 @@
 #include "DangerHitMapAnalyzer.h"
 
 #include "../Engine/Nullkiller.h"
+#include "../../../lib/CRandomGenerator.h"
 
 namespace NKAI
 {
@@ -165,7 +166,7 @@ void DangerHitMapAnalyzer::calculateTileOwners()
 
 	auto addTownHero = [&](const CGTownInstance * town)
 	{
-			auto townHero = new CGHeroInstance();
+			auto townHero = new CGHeroInstance(town->cb);
 			CRandomGenerator rng;
 			auto visitablePos = town->visitablePos();
 			
@@ -265,8 +266,9 @@ uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath &
 {
 	int3 tile = path.targetTile();
 	int turn = path.turn();
-	const HitMapNode & info = hitMap[tile.x][tile.y][tile.z];
 
+	const auto& info = getTileThreat(tile);
+	
 	return (info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger))
 		|| (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger));
 }
@@ -280,9 +282,7 @@ const HitMapNode & DangerHitMapAnalyzer::getObjectThreat(const CGObjectInstance
 
 const HitMapNode & DangerHitMapAnalyzer::getTileThreat(const int3 & tile) const
 {
-	const HitMapNode & info = hitMap[tile.x][tile.y][tile.z];
-
-	return info;
+	return hitMap[tile.x][tile.y][tile.z];
 }
 
 const std::set<const CGObjectInstance *> empty = {};

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

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

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

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

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

@@ -30,7 +30,7 @@ ui64 FuzzyHelper::estimateBankDanger(const CBank * bank)
 
 	ui64 totalStrength = 0;
 	ui8 totalChance = 0;
-	for(auto config : bankInfo->getPossibleGuards())
+	for(auto config : bankInfo->getPossibleGuards(bank->cb))
 	{
 		totalStrength += config.second.totalStrength * config.first;
 		totalChance += config.first;

+ 4 - 0
AI/Nullkiller/Engine/Nullkiller.cpp

@@ -115,6 +115,8 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositi
 
 void Nullkiller::resetAiState()
 {
+	std::unique_lock<std::mutex> lockGuard(aiStateMutex);
+
 	lockedResources = TResources();
 	scanDepth = ScanDepth::MAIN_FULL;
 	playerID = ai->playerID;
@@ -127,6 +129,8 @@ void Nullkiller::updateAiState(int pass, bool fast)
 {
 	boost::this_thread::interruption_point();
 
+	std::unique_lock<std::mutex> lockGuard(aiStateMutex);
+
 	auto start = std::chrono::high_resolution_clock::now();
 
 	activeHero = nullptr;

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

@@ -73,6 +73,7 @@ public:
 	std::unique_ptr<ArmyFormation> armyFormation;
 	PlayerColor playerID;
 	std::shared_ptr<CCallback> cb;
+	std::mutex aiStateMutex;
 
 	Nullkiller();
 	void init(std::shared_ptr<CCallback> cb, PlayerColor playerID);

+ 21 - 4
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -137,11 +137,23 @@ TResources getCreatureBankResources(const CGObjectInstance * target, const CGHer
 	return sum > 1 ? result / sum : result;
 }
 
+uint64_t getResourcesGoldReward(const TResources & res)
+{
+	int nonGoldResources = res[EGameResID::GEMS]
+		+ res[EGameResID::SULFUR]
+		+ res[EGameResID::WOOD]
+		+ res[EGameResID::ORE]
+		+ res[EGameResID::CRYSTAL]
+		+ res[EGameResID::MERCURY];
+
+	return res[EGameResID::GOLD] + 100 * nonGoldResources;
+}
+
 uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHeroInstance * hero)
 {
 	auto objectInfo = target->getObjectHandler()->getObjectInfo(target->appearance);
 	CBankInfo * bankInfo = dynamic_cast<CBankInfo *>(objectInfo.get());
-	auto creatures = bankInfo->getPossibleCreaturesReward();
+	auto creatures = bankInfo->getPossibleCreaturesReward(target->cb);
 	uint64_t result = 0;
 
 	const auto& slots = hero->Slots();
@@ -236,7 +248,7 @@ int getDwellingArmyCost(const CGObjectInstance * target)
 	return cost;
 }
 
-uint64_t evaluateArtifactArmyValue(CArtifactInstance * art)
+static uint64_t evaluateArtifactArmyValue(const CArtifactInstance * art)
 {
 	if(art->artType->getId() == ArtifactID::SPELL_SCROLL)
 		return 1500;
@@ -491,7 +503,7 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons
 			//Evaluate resources used for construction. Gold is evaluated separately.
 			if (it->resType != EGameResID::GOLD)
 			{
-				sum += 0.1f * getResourceRequirementStrength(it->resType);
+				sum += 0.1f * it->resVal * getResourceRequirementStrength(it->resType);
 			}
 		}
 		return sum;
@@ -529,6 +541,9 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons
 			? getEnemyHeroStrategicalValue(dynamic_cast<const CGHeroInstance *>(target))
 			: 0;
 
+	case Obj::KEYMASTER:
+		return 0.6f;
+
 	default:
 		return 0;
 	}
@@ -588,6 +603,8 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH
 	case Obj::PANDORAS_BOX:
 		//Can contains experience, spells, or skills (only on custom maps)
 		return 2.5f;
+	case Obj::PYRAMID:
+		return 3.0f;
 	case Obj::HERO:
 		return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
 			? enemyHeroEliminationSkillRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->level
@@ -660,7 +677,7 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
 	case Obj::WAGON:
 		return 100;
 	case Obj::CREATURE_BANK:
-		return getCreatureBankResources(target, hero)[EGameResID::GOLD];
+		return getResourcesGoldReward(getCreatureBankResources(target, hero));
 	case Obj::CRYPT:
 	case Obj::DERELICT_SHIP:
 		return 3000;

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

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

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

@@ -37,7 +37,7 @@ namespace Goals
 		{
 			return new T(static_cast<T const &>(*this)); //casting enforces template instantiation
 		}
-		template<typename Handler> void serialize(Handler & h, const int version)
+		template<typename Handler> void serialize(Handler & h)
 		{
 			h & static_cast<AbstractGoal &>(*this);
 			//h & goalType & isElementar & isAbstract & priority;

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

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

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

@@ -35,6 +35,8 @@ const uint64_t MIN_ARMY_STRENGTH_FOR_CHAIN = 5000;
 const uint64_t MIN_ARMY_STRENGTH_FOR_NEXT_ACTOR = 1000;
 const uint64_t CHAIN_MAX_DEPTH = 4;
 
+const bool DO_NOT_SAVE_TO_COMMITED_TILES = false;
+
 AISharedStorage::AISharedStorage(int3 sizes)
 {
 	if(!shared){
@@ -234,6 +236,7 @@ void AINodeStorage::resetTile(const int3 & coord, EPathfindingLayer layer, EPath
 		heroNode.specialAction.reset();
 		heroNode.armyLoss = 0;
 		heroNode.chainOther = nullptr;
+		heroNode.dayFlags = DayFlags::NONE;
 		heroNode.update(coord, layer, accessibility);
 	}
 }
@@ -265,7 +268,8 @@ void AINodeStorage::commit(
 	EPathNodeAction action, 
 	int turn, 
 	int movementLeft, 
-	float cost) const
+	float cost,
+	bool saveToCommited) const
 {
 	destination->action = action;
 	destination->setCost(cost);
@@ -291,10 +295,15 @@ void AINodeStorage::commit(
 		destination->actor->armyValue);
 #endif
 
-	if(destination->turns <= heroChainTurn)
+	if(saveToCommited && destination->turns <= heroChainTurn)
 	{
 		commitedTiles.insert(destination->coord);
 	}
+
+	if(destination->turns == source->turns)
+	{
+		destination->dayFlags = source->dayFlags;
+	}
 }
 
 std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
@@ -778,7 +787,14 @@ void HeroChainCalculationTask::addHeroChain(const std::vector<ExchangeCandidate>
 			continue;
 		}
 
-		storage.commit(exchangeNode, carrier, carrier->action, chainInfo.turns, chainInfo.moveRemains, chainInfo.getCost());
+		storage.commit(
+			exchangeNode,
+			carrier,
+			carrier->action,
+			chainInfo.turns,
+			chainInfo.moveRemains, 
+			chainInfo.getCost(),
+			DO_NOT_SAVE_TO_COMMITED_TILES);
 
 		if(carrier->specialAction || carrier->chainOther)
 		{
@@ -1070,7 +1086,8 @@ struct TowmPortalFinder
 				EPathNodeAction::TELEPORT_NORMAL,
 				bestNode->turns,
 				bestNode->moveRemains - movementNeeded,
-				movementCost);
+				movementCost,
+				DO_NOT_SAVE_TO_COMMITED_TILES);
 
 			node->theNodeBefore = bestNode;
 			node->addSpecialAction(std::make_shared<AIPathfinding::TownPortalAction>(targetTown));

+ 11 - 2
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -41,11 +41,19 @@ namespace AIPathfinding
 	const int CHAIN_MAX_DEPTH = 4;
 }
 
+enum DayFlags : ui8
+{
+	NONE = 0,
+	FLY_CAST = 1,
+	WATER_WALK_CAST = 2
+};
+
 struct AIPathNode : public CGPathNode
 {
 	uint64_t danger;
 	uint64_t armyLoss;
-	int32_t manaCost;
+	int16_t manaCost;
+	DayFlags dayFlags;
 	const AIPathNode * chainOther;
 	std::shared_ptr<const SpecialAction> specialAction;
 	const ChainActor * actor;
@@ -200,7 +208,8 @@ public:
 		EPathNodeAction action,
 		int turn,
 		int movementLeft,
-		float cost) const;
+		float cost,
+		bool saveToCommited = true) const;
 
 	inline const AIPathNode * getAINode(const CGPathNode * node) const
 	{

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

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

+ 2 - 1
AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h

@@ -24,9 +24,10 @@ namespace AIPathfinding
 		SpellID spellToCast;
 		const CGHeroInstance * hero;
 		int manaCost;
+		DayFlags flagsToAdd;
 
 	public:
-		AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero);
+		AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero, DayFlags flagsToAdd = DayFlags::NONE);
 
 		virtual void execute(const CGHeroInstance * hero) const override;
 

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

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

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

@@ -31,7 +31,7 @@ public:
 	virtual bool needsLastStack() const override;
 	std::shared_ptr<SpecialAction> getActorAction() const;
 
-	HeroExchangeArmy(): CArmedInstance(true), requireBuyArmy(false) {}
+	HeroExchangeArmy(): CArmedInstance(nullptr, true), requireBuyArmy(false) {}
 };
 
 struct ExchangeResult

+ 12 - 0
AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp

@@ -61,6 +61,12 @@ namespace AIPathfinding
 
 		if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::WATER)
 		{
+			if(nodeStorage->getAINode(source.node)->dayFlags & DayFlags::WATER_WALK_CAST)
+			{
+				destination.blocked = false;
+				return;
+			}
+
 			auto action = waterWalkingActions.find(nodeStorage->getHero(source.node));
 
 			if(action != waterWalkingActions.end() && tryUseSpecialAction(destination, source, action->second, EPathNodeAction::NORMAL))
@@ -73,6 +79,12 @@ namespace AIPathfinding
 
 		if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::AIR)
 		{
+			if(nodeStorage->getAINode(source.node)->dayFlags & DayFlags::FLY_CAST)
+			{
+				destination.blocked = false;
+				return;
+			}
+
 			auto action = airWalkingActions.find(nodeStorage->getHero(source.node));
 
 			if(action != airWalkingActions.end() && tryUseSpecialAction(destination, source, action->second, EPathNodeAction::NORMAL))

+ 15 - 13
AI/StupidAI/StupidAI.cpp

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

+ 2 - 2
AI/VCAI/AIUtility.h

@@ -70,7 +70,7 @@ public:
 	bool validAndSet() const;
 
 
-	template<typename Handler> void serialize(Handler & h, const int version)
+	template<typename Handler> void serialize(Handler & h)
 	{
 		h & this->h;
 		h & hid;
@@ -102,7 +102,7 @@ struct ObjectIdRef
 	bool operator<(const ObjectIdRef & rhs) const;
 
 
-	template<typename Handler> void serialize(Handler & h, const int version)
+	template<typename Handler> void serialize(Handler & h)
 	{
 		h & id;
 	}

+ 19 - 20
AI/VCAI/BuildingManager.cpp

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

+ 3 - 1
AI/VCAI/FuzzyEngines.cpp

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

+ 8 - 2
AI/VCAI/FuzzyEngines.h

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

+ 1 - 1
AI/VCAI/FuzzyHelper.cpp

@@ -72,7 +72,7 @@ ui64 FuzzyHelper::estimateBankDanger(const CBank * bank)
 
 	ui64 totalStrength = 0;
 	ui8 totalChance = 0;
-	for(auto config : bankInfo->getPossibleGuards())
+	for(auto config : bankInfo->getPossibleGuards(bank->cb))
 	{
 		totalStrength += config.second.totalStrength * config.first;
 		totalChance += config.first;

+ 1 - 1
AI/VCAI/Goals/AbstractGoal.h

@@ -176,7 +176,7 @@ namespace Goals
 			return !(*this == g);
 		}
 
-		template<typename Handler> void serialize(Handler & h, const int version)
+		template<typename Handler> void serialize(Handler & h)
 		{
 			h & goalType;
 			h & isElementar;

+ 1 - 1
AI/VCAI/Goals/CGoal.h

@@ -69,7 +69,7 @@ namespace Goals
 
 			return ptr;
 		}
-		template<typename Handler> void serialize(Handler & h, const int version)
+		template<typename Handler> void serialize(Handler & h)
 		{
 			h & static_cast<AbstractGoal &>(*this);
 			//h & goalType & isElementar & isAbstract & priority;

+ 2 - 1
AI/VCAI/Goals/CollectRes.cpp

@@ -169,7 +169,8 @@ TSubgoal CollectRes::whatToDoToTrade()
 		{
 			if (i.getNum() == resID)
 				continue;
-			int toGive = -1, toReceive = -1;
+			int toGive = -1;
+			int toReceive = -1;
 			m->getOffer(i, resID, toGive, toReceive, EMarketMode::RESOURCE_RESOURCE);
 			assert(toGive > 0 && toReceive > 0);
 			howManyCanWeBuy += toReceive * (ai->ah->freeResources()[i] / toGive);

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

@@ -103,7 +103,7 @@ std::string CompleteQuest::questToString() const
 		return "inactive quest";
 
 	MetaString ms;
-	q.quest->getRolloverText(ms, false);
+	q.quest->getRolloverText(q.obj->cb, ms, false);
 
 	return ms.toString();
 }
@@ -241,7 +241,7 @@ TGoalVec CompleteQuest::missionDestroyObj() const
 {
 	TGoalVec solutions;
 
-	auto obj = cb->getObjByQuestIdentifier(q.quest->killTarget);
+	auto obj = cb->getObj(q.quest->killTarget);
 
 	if(!obj)
 		return ai->ah->howToVisitObj(q.obj);

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

@@ -321,9 +321,7 @@ bool AINodeStorage::hasBetterChain(const PathNodeInfo & source, CDestinationNode
 
 bool AINodeStorage::isTileAccessible(const int3 & pos, const EPathfindingLayer layer) const
 {
-	const AIPathNode & node = nodes[layer][pos.z][pos.x][pos.y][0];
-
-	return node.action != EPathNodeAction::UNKNOWN;
+	return nodes[layer][pos.z][pos.x][pos.y][0].action != EPathNodeAction::UNKNOWN;
 }
 
 std::vector<AIPath> AINodeStorage::getChainInfo(const int3 & pos, bool isOnLand) const

+ 2 - 2
AI/VCAI/ResourceManager.h

@@ -28,7 +28,7 @@ struct DLL_EXPORT ResourceObjective
 	Goals::TSubgoal goal; //what for (build, gather army etc...)
 
 	 //TODO: register?
-	template<typename Handler> void serializeInternal(Handler & h, const int version)
+	template<typename Handler> void serializeInternal(Handler & h)
 	{
 		h & resources;
 		//h & goal; //FIXME: goal serialization is broken
@@ -105,7 +105,7 @@ private:
 	void dumpToLog() const;
 
 	//TODO: register?
-	template<typename Handler> void serializeInternal(Handler & h, const int version)
+	template<typename Handler> void serializeInternal(Handler & h)
 	{
 		h & saving;
 		h & queue;

+ 16 - 26
AI/VCAI/VCAI.cpp

@@ -22,7 +22,6 @@
 #include "../../lib/CHeroHandler.h"
 #include "../../lib/GameSettings.h"
 #include "../../lib/gameState/CGameState.h"
-#include "../../lib/bonuses/CBonusSystemNode.h"
 #include "../../lib/bonuses/Limiters.h"
 #include "../../lib/bonuses/Updaters.h"
 #include "../../lib/bonuses/Propagators.h"
@@ -66,7 +65,7 @@ struct SetGlobalState
 };
 
 
-#define SET_GLOBAL_STATE(ai) SetGlobalState _hlpSetState(ai);
+#define SET_GLOBAL_STATE(ai) SetGlobalState _hlpSetState(ai)
 
 #define NET_EVENT_HANDLER SET_GLOBAL_STATE(this)
 #define MAKING_TURN SET_GLOBAL_STATE(this)
@@ -104,7 +103,7 @@ void VCAI::heroMoved(const TryMoveHero & details, bool verbose)
 	validateObject(details.id);
 	auto hero = cb->getHero(details.id);
 
-	const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));;
+	const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));
 	const int3 to   = hero ? hero->convertToVisitablePos(details.end)   : (details.end   - int3(0,1,0));
 
 	const CGObjectInstance * o1 = vstd::frontOrNull(cb->getVisitableObjs(from, verbose));
@@ -312,7 +311,8 @@ void VCAI::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, Q
 
 	requestActionASAP([=]()
 	{
-		float goalpriority1 = 0, goalpriority2 = 0;
+		float goalpriority1 = 0;
+		float goalpriority2 = 0;
 
 		auto firstGoal = getGoal(firstHero);
 		if(firstGoal->goalType == Goals::GATHER_ARMY)
@@ -746,9 +746,8 @@ void VCAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, cons
 	requestActionASAP([=](){ answerQuery(askID, selectedObject.getNum()); });
 }
 
-void VCAI::saveGame(BinarySerializer & h, const int version)
+void VCAI::saveGame(BinarySerializer & h)
 {
-	LOG_TRACE_PARAMS(logAi, "version '%i'", version);
 	NET_EVENT_HANDLER;
 	validateVisitableObjs();
 
@@ -756,21 +755,20 @@ void VCAI::saveGame(BinarySerializer & h, const int version)
 	//disabled due to issue 2890
 	registerGoals(h);
 	#endif // 0
-	CAdventureAI::saveGame(h, version);
-	serializeInternal(h, version);
+	CAdventureAI::saveGame(h);
+	serializeInternal(h);
 }
 
-void VCAI::loadGame(BinaryDeserializer & h, const int version)
+void VCAI::loadGame(BinaryDeserializer & h)
 {
-	LOG_TRACE_PARAMS(logAi, "version '%i'", version);
 	//NET_EVENT_HANDLER;
 
 	#if 0
 	//disabled due to issue 2890
 	registerGoals(h);
 	#endif // 0
-	CAdventureAI::loadGame(h, version);
-	serializeInternal(h, version);
+	CAdventureAI::loadGame(h);
+	serializeInternal(h);
 }
 
 void makePossibleUpgrades(const CArmedInstance * obj)
@@ -1415,8 +1413,8 @@ void VCAI::wander(HeroPtr h)
 			auto compareReinforcements = [&](const CGTownInstance * lhs, const CGTownInstance * rhs) -> bool
 			{
 				const CGHeroInstance * hptr = h.get();
-				auto r1 = ah->howManyReinforcementsCanGet(hptr, lhs),
-					r2 = ah->howManyReinforcementsCanGet(hptr, rhs);
+				auto r1 = ah->howManyReinforcementsCanGet(hptr, lhs);
+				auto r2 = ah->howManyReinforcementsCanGet(hptr, rhs);
 				if (r1 != r2)
 					return r1 < r2;
 				else
@@ -1610,15 +1608,6 @@ void VCAI::battleEnd(const BattleID & battleID, const BattleResult * br, QueryID
 	logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.toString(), (won ? "won" : "lost"), battlename);
 	battlename.clear();
 
-	if (queryID != QueryID::NONE)
-	{
-		status.addQuery(queryID, "Combat result dialog");
-		const int confirmAction = 0;
-		requestActionASAP([=]()
-		{
-			answerQuery(queryID, confirmAction);
-		});
-	}
 	CAdventureAI::battleEnd(battleID, br, queryID);
 }
 
@@ -2154,7 +2143,8 @@ void VCAI::tryRealize(Goals::Trade & g) //trade
 				if(res.getNum() == g.resID) //sell any other resource
 					continue;
 
-				int toGive, toGet;
+				int toGive;
+				int toGet;
 				m->getOffer(res, g.resID, toGive, toGet, EMarketMode::RESOURCE_RESOURCE);
 				toGive = static_cast<int>(toGive * (it->resVal / toGive)); //round down
 				//TODO trade only as much as needed
@@ -2221,8 +2211,8 @@ void VCAI::tryRealize(Goals::BuyArmy & g)
 			*boost::max_element(creaturesInDwellings, [](const creInfo & lhs, const creInfo & rhs)
 		{
 			//max value of creatures we can buy with our res
-			int value1 = lhs.cre->getAIValue() * lhs.count,
-				value2 = rhs.cre->getAIValue() * rhs.count;
+			int value1 = lhs.cre->getAIValue() * lhs.count;
+			int value2 = rhs.cre->getAIValue() * rhs.count;
 
 			return value1 < value2;
 		});

+ 5 - 5
AI/VCAI/VCAI.h

@@ -68,7 +68,7 @@ public:
 	void heroVisit(const CGObjectInstance * obj, bool started);
 
 
-	template<typename Handler> void serialize(Handler & h, const int version)
+	template<typename Handler> void serialize(Handler & h)
 	{
 		h & battle;
 		h & remainingQueries;
@@ -152,8 +152,8 @@ public:
 	void showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) override; //all stacks operations between these objects become allowed, interface has to call onEnd when done
 	void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override;
 	void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects) override;
-	void saveGame(BinarySerializer & h, const int version) override; //saving
-	void loadGame(BinaryDeserializer & h, const int version) override; //loading
+	void saveGame(BinarySerializer & h) override; //saving
+	void loadGame(BinaryDeserializer & h) override; //loading
 	void finish() override;
 
 	void availableCreaturesChanged(const CGDwelling * town) override;
@@ -301,7 +301,7 @@ public:
 	}
 	#endif
 
-	template<typename Handler> void serializeInternal(Handler & h, const int version)
+	template<typename Handler> void serializeInternal(Handler & h)
 	{
 		h & knownTeleportChannels;
 		h & knownSubterraneanGates;
@@ -341,7 +341,7 @@ public:
 							//we have to explicitly ignore invalid goal class type id
 							h & typeId;
 							Goals::AbstractGoal ignored2;
-							ignored2.serialize(h, version);
+							ignored2.serialize(h);
 						}
 					}
 				}

+ 3 - 2
CCallback.cpp

@@ -16,6 +16,7 @@
 #include "client/Client.h"
 #include "lib/mapping/CMap.h"
 #include "lib/mapObjects/CGHeroInstance.h"
+#include "lib/mapObjects/CGTownInstance.h"
 #include "lib/CBuildingHandler.h"
 #include "lib/CGeneralTextHandler.h"
 #include "lib/CHeroHandler.h"
@@ -260,12 +261,12 @@ void CCallback::setFormation(const CGHeroInstance * hero, EArmyFormation mode)
 	sendRequest(&pack);
 }
 
-void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero)
+void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero, const HeroTypeID & nextHero)
 {
 	assert(townOrTavern);
 	assert(hero);
 
-	HireHero pack(hero->getHeroType(), townOrTavern->id);
+	HireHero pack(hero->getHeroType(), townOrTavern->id, nextHero);
 	pack.player = *player;
 	sendRequest(&pack);
 }

+ 2 - 2
CCallback.h

@@ -73,7 +73,7 @@ public:
 	virtual void castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos = int3(-1, -1, -1))=0; //cast adventure map spell
 
 	//town
-	virtual void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero)=0;
+	virtual void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero, const HeroTypeID & nextHero=HeroTypeID::NONE)=0;
 	virtual bool buildBuilding(const CGTownInstance *town, BuildingID buildingID)=0;
 	virtual void recruitCreatures(const CGDwelling *obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1)=0;
 	virtual bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE)=0; //if newID==-1 then best possible upgrade will be made
@@ -185,7 +185,7 @@ public:
 	void trade(const IMarket * market, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero = nullptr) override;
 	void trade(const IMarket * market, EMarketMode mode, const std::vector<TradeItemSell> & id1, const std::vector<TradeItemBuy> & id2, const std::vector<ui32> & val1, const CGHeroInstance * hero = nullptr) override;
 	void setFormation(const CGHeroInstance * hero, EArmyFormation mode) override;
-	void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero) override;
+	void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero, const HeroTypeID & nextHero=HeroTypeID::NONE) override;
 	void save(const std::string &fname) override;
 	void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) override;
 	void gamePause(bool pause) override;

+ 45 - 32
CMakeLists.txt

@@ -74,33 +74,45 @@ option(ENABLE_MULTI_PROCESS_BUILDS "Enable /MP flag for MSVS solution" ON)
 option(COPY_CONFIG_ON_BUILD "Copies config folder into output directory at building phase" ON)
 option(ENABLE_STATIC_AI_LIBS "Add AI code into VCMI lib directly" ${staticAI})
 
+option(ENABLE_COLORIZED_COMPILER_OUTPUT "Colorize compilation output (Clang/GNU)." ON)
+
+if(ENABLE_COLORIZED_COMPILER_OUTPUT)
+	if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
+		add_compile_options(-fcolor-diagnostics)
+	elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
+		add_compile_options(-fdiagnostics-color=always)
+	endif()
+endif()
+
 # Used for Snap packages and also useful for debugging
 if(NOT APPLE_IOS AND NOT ANDROID)
 	option(ENABLE_MONOLITHIC_INSTALL "Install everything in single directory on Linux and Mac" OFF)
 endif()
 
-# On Linux, use -DCMAKE_CXX_COMPILER_LAUNCHER=ccache instead.
-# The XCode and MSVC builds each require some more configuration, which is enabled by the following option:
-if(MSVC OR (CMAKE_GENERATOR STREQUAL "Xcode"))
-  option(ENABLE_CCACHE "Speed up recompilation by caching previous compilations" OFF)
+option(ENABLE_CCACHE "Speed up recompilation by caching previous compilations" OFF)
+if(ENABLE_CCACHE)
+	find_program(CCACHE ccache REQUIRED)
+endif()
+
+# The XCode and MSVC builds each require some more configuration further down.
+if(ENABLE_CCACHE)
+	set(CMAKE_C_COMPILER_LAUNCHER "ccache")
+	set(CMAKE_CXX_COMPILER_LAUNCHER "ccache")
 endif()
 
 if(ENABLE_CCACHE AND (CMAKE_GENERATOR STREQUAL "Xcode"))
-	find_program(CCACHE ccache REQUIRED)
-	if(CCACHE)
-		# https://stackoverflow.com/a/36515503/2278742
-		# Set up wrapper scripts
-		configure_file(xcode/launch-c.in   xcode/launch-c)
-		configure_file(xcode/launch-cxx.in xcode/launch-cxx)
-		execute_process(COMMAND chmod a+rx
-												"${CMAKE_BINARY_DIR}/xcode/launch-c"
-												"${CMAKE_BINARY_DIR}/xcode/launch-cxx")
-		# Set Xcode project attributes to route compilation through our scripts
-		set(CMAKE_XCODE_ATTRIBUTE_CC         	"${CMAKE_BINARY_DIR}/xcode/launch-c")
-		set(CMAKE_XCODE_ATTRIBUTE_CXX        	"${CMAKE_BINARY_DIR}/xcode/launch-cxx")
-		set(CMAKE_XCODE_ATTRIBUTE_LD         	"${CMAKE_BINARY_DIR}/xcode/launch-c")
-		set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS 	"${CMAKE_BINARY_DIR}/xcode/launch-cxx")
-	endif()
+	# https://stackoverflow.com/a/36515503/2278742
+	# Set up wrapper scripts
+	configure_file(xcode/launch-c.in   xcode/launch-c)
+	configure_file(xcode/launch-cxx.in xcode/launch-cxx)
+	execute_process(COMMAND chmod a+rx
+											"${CMAKE_BINARY_DIR}/xcode/launch-c"
+											"${CMAKE_BINARY_DIR}/xcode/launch-cxx")
+	# Set Xcode project attributes to route compilation through our scripts
+	set(CMAKE_XCODE_ATTRIBUTE_CC         	"${CMAKE_BINARY_DIR}/xcode/launch-c")
+	set(CMAKE_XCODE_ATTRIBUTE_CXX        	"${CMAKE_BINARY_DIR}/xcode/launch-cxx")
+	set(CMAKE_XCODE_ATTRIBUTE_LD         	"${CMAKE_BINARY_DIR}/xcode/launch-c")
+	set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS 	"${CMAKE_BINARY_DIR}/xcode/launch-cxx")
 endif()
 
 # Allow to pass package name from Travis CI
@@ -266,19 +278,16 @@ if(MINGW OR MSVC)
 	if(MSVC)
 		if(ENABLE_CCACHE)
 			# https://github.com/ccache/ccache/discussions/1154#discussioncomment-3611387
-			find_program(CCACHE ccache REQUIRED)
-			if (CCACHE)
-				file(COPY_FILE
-					${CCACHE} ${CMAKE_BINARY_DIR}/cl.exe
-					ONLY_IF_DIFFERENT)
-
-				set(CMAKE_VS_GLOBALS
-					"CLToolExe=cl.exe"
-					"CLToolPath=${CMAKE_BINARY_DIR}"
-					"TrackFileAccess=false"
-					"UseMultiToolTask=true"
-				)
-			endif()
+			file(COPY_FILE
+				${CCACHE} ${CMAKE_BINARY_DIR}/cl.exe
+				ONLY_IF_DIFFERENT)
+
+			set(CMAKE_VS_GLOBALS
+				"CLToolExe=cl.exe"
+				"CLToolPath=${CMAKE_BINARY_DIR}"
+				"TrackFileAccess=false"
+				"UseMultiToolTask=true"
+			)
 		endif()
 
 		add_definitions(-DBOOST_ALL_NO_LIB)
@@ -806,6 +815,10 @@ elseif(APPLE_MACOS AND NOT ENABLE_MONOLITHIC_INSTALL)
 	set(CPACK_MONOLITHIC_INSTALL 1)
 	set(CPACK_GENERATOR "DragNDrop")
 	set(CPACK_DMG_BACKGROUND_IMAGE "${CMAKE_SOURCE_DIR}/osx/dmg_background.png")
+	# Workaround for this issue:
+	# CPack Error: Error executing: /usr/bin/hdiutil detach "/Volumes/VCMI"
+	# https://github.com/actions/runner-images/issues/7522#issuecomment-1564467252
+	set(CPACK_COMMAND_HDIUTIL "/usr/bin/sudo /usr/bin/hdiutil")
 	# CMake code for CPACK_DMG_DS_STORE executed before DS_STORE_SETUP_SCRIPT
 	# So we can keep both enabled and this shouldn't hurt
 	# set(CPACK_DMG_DS_STORE "${CMAKE_SOURCE_DIR}/osx/dmg_DS_Store")

+ 47 - 0
CMakePresets.json

@@ -65,6 +65,15 @@
                 "CMAKE_CXX_COMPILER": "/usr/bin/clang++"
             }
         },
+        {
+            "name": "linux-clang-release-ccache",
+            "displayName": "Clang x86_64-pc-linux-gnu with ccache",
+            "description": "VCMI Linux Clang with ccache",
+            "inherits": "linux-release",
+            "cacheVariables": {
+                "ENABLE_CCACHE": "ON"
+            }
+        },
         {
             "name": "linux-gcc-release",
             "displayName": "GCC x86_64-pc-linux-gnu",
@@ -76,6 +85,15 @@
                 "CMAKE_CXX_COMPILER": "/usr/bin/g++"
             }
         },
+        {
+            "name": "linux-gcc-release-ccache",
+            "displayName": "GCC x86_64-pc-linux-gnu with ccache",
+            "description": "VCMI Linux GCC with ccache",
+            "inherits": "linux-release",
+            "cacheVariables": {
+                "ENABLE_CCACHE": "ON"
+            }
+        },
         {
             "name": "linux-gcc-debug",
             "displayName": "GCC x86_64-pc-linux-gnu (debug)",
@@ -109,6 +127,15 @@
                 "CMAKE_CXX_COMPILER": "/usr/bin/g++"
             }
         },
+        {
+            "name": "windows-mingw-release",
+            "displayName": "Windows x64 MinGW Release",
+            "description": "VCMI Windows Ninja using MinGW",
+            "inherits": "default-release",
+            "cacheVariables": {
+                "CMAKE_BUILD_TYPE": "Release"
+            }
+        },
         {
             "name": "windows-msvc-release",
             "displayName": "Windows x64 RelWithDebInfo",
@@ -284,6 +311,11 @@
             "configurePreset": "linux-clang-release",
             "inherits": "default-release"
         },
+        {
+            "name": "linux-clang-release-ccache",
+            "configurePreset": "linux-clang-release-ccache",
+            "inherits": "linux-clang-release"
+        },
         {
             "name": "linux-clang-test",
             "configurePreset": "linux-clang-test",
@@ -299,6 +331,11 @@
             "configurePreset": "linux-gcc-release",
             "inherits": "default-release"
         },
+        {
+            "name": "linux-gcc-release-ccache",
+            "configurePreset": "linux-gcc-release-ccache",
+            "inherits": "linux-gcc-release"
+        },
         {
             "name": "linux-gcc-debug",
             "configurePreset": "linux-gcc-debug",
@@ -324,6 +361,11 @@
             "configurePreset": "macos-arm-conan-ninja-release",
             "inherits": "default-release"
         },
+        {
+            "name": "windows-mingw-release",
+            "configurePreset": "windows-mingw-release",
+            "inherits": "default-release"
+        },
         {
             "name": "windows-msvc-release",
             "configurePreset": "windows-msvc-release",
@@ -410,6 +452,11 @@
             "configurePreset": "macos-ninja-release",
             "inherits": "default-release"
         },
+        {
+            "name": "windows-mingw-release",
+            "configurePreset": "windows-mingw-release",
+            "inherits": "default-release"
+        },
         {
             "name": "windows-msvc-release",
             "configurePreset": "windows-msvc-release",

+ 84 - 1
ChangeLog.md

@@ -1,3 +1,81 @@
+# 1.4.5 -> 1.5.0
+
+### General
+* Added option to disable cheats in game
+
+### Interface
+* Town Portal dialog will now show town icons
+* Town Portal dialog will now show town info on right click
+* Town Portal dialog will center on town on clicking it
+* Town Portal dialog now uses same town ordering as in adventure map interface
+* Heroes can now be recruited from the tavern by double-clicking on them
+* Added status bar to the backpack window
+* Quick backpack window is now only available when enabled Interface enhancements
+* Fixed assembly of artifacts in the backpack when backpack is full
+* Attempt to use enemy turn replay feature will now show "Not implemented" message
+
+### Campaigns
+* Game will now correctly track who defeated the hero or wandering monsters for related quests and victory conditions
+* Birth of a Barbarian: Yog will now start the third scenario with Angelic Alliance in his inventory
+* Birth of a Barbarian: Heroes with Angelic Alliance components are now considered to be mission-critical and can't be dismissed or lost in combat
+* Birth of a Barbarian: Yog can no longer purchase spellbook from the Mage Guild
+* Birth of a Barbarian: Yog will no longer gain Spellpower or Knowledge when leveling up
+* Birth of a Barbarian: Scenarios with mission to deliver an artifact will no longer end after just defeating enemies
+* Gem will now have her class set to "Sorceress" in campaigns
+* Fixed missing names for heroes who have their names customized in map after being transferred to the next scenario
+* Artifact transfer will now work correctly if the hero holding the transferable artifact is not also transferring
+* Fixed crash on opening of some campaigns in the French version from gog.com
+* It is now possible to replay the intro movie from the scenario information window
+* When playing the intro video, the subtitles are now correctly synchronized with the audio
+
+### Battles
+* Added option to enable unlimited combat replays during game setup
+* Added option to instantly end battle using quick combat (shotcut: 'e')
+* Added option to replace auto-combat button action with instant end using quick combat
+* Battles against AI players can now be done using quick combat
+* Disabling battle queue will now correctly reposition hero statistics preview popup
+* Fixed positioning of unit stack size label
+
+### Launcher
+* Added Spanish translation to launcher
+
+### Map Editor
+* Added Chinese translation to map editor
+
+### AI
+* Fixed possible crash on updating NKAI pathfinding data
+* Fixed counting mana usage cost of Fly spell
+* Added estimation of value of Pyramid and Cyclops Stockpile
+
+### Modding
+* Added new game setting that allows inviting heroes to taverns
+* Fixed reversed Overlord and Warlock classes mapping
+
+# 1.4.4 -> 1.4.5
+
+### Stability
+* Fixed crash on creature spellcasting
+* Fixed crash on unit entering magical obstacles such as quicksands
+* Fixed freeze on map loading on some systems
+* Fixed crash on attempt to start campaign with unsupported map
+* Fixed crash on opening creature information window with invalid SPELL_IMMUNITY bonus
+
+### Random Maps Generator
+* Fixed placement of guards sometimes resulting into open connection into third zone
+* Fixed rare crash on multithreaded access during placement of artifacts or wandering monsters
+
+### Map Editor
+* Fixed inspector using wrong editor for some values
+
+### AI
+* Fixed bug leading to AI not attacking wandering monsters in some cases
+* Fixed crash on using StupidAI for autocombat or for enemy players 
+
+# 1.4.3 -> 1.4.4
+
+### General
+* Fixed crash on generation of random maps
+
 # 1.4.2 -> 1.4.3
 
 ### General
@@ -23,6 +101,7 @@
 * Fixed positioning of prologue and epilogue text during campaign scenario intros
 
 ### Interface
+* Added an option to hide adventure map window when town or battle window are open
 * Fixed switching between pages on small version of spellbook
 * Saves with long filenames are now truncated in the UI to prevent overflow.
 * Added option to sort saved games by change date
@@ -32,7 +111,6 @@
 * Fixed incorrect cursor display when hovering over water objects accessible from shore
 
 ### Stability
-* Fixed possible creation of a duplicate hero in a random map when the player has chosen the starting hero.
 * Fixed a crash when using the 'vcmiobelisk' cheat more than once.
 * Fixed crash when reaching level 201. The maximum level is now limited to 197.
 * Fixed crash when accessing a spell with an invalid SPELLCASTER bonus
@@ -43,6 +121,11 @@
 * Fixed crash on invalid creature in hero army due to outdated or broken mods
 * Failure to initialise video subsystem now displays error message instead of silent crash
 
+### Random Maps Generator
+* Fixed possible creation of a duplicate hero in a random map when the player has chosen the starting hero.
+* Fixed banning of quest artifacts on random maps
+* Fixed banning of heroes in prison on random maps
+
 ### Battles
 * Battle turn queue now displays current turn
 * Added option to show unit statistics sidebar in battle

+ 4 - 0
Global.h

@@ -41,6 +41,10 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 #  define VCMI_UNIX
 #  define VCMI_XDG
 #  define VCMI_FREEBSD
+#elif defined(__OpenBSD__)
+#  define VCMI_UNIX
+#  define VCMI_XDG
+#  define VCMI_OPENBSD
 #elif defined(__HAIKU__)
 #  define VCMI_UNIX
 #  define VCMI_XDG

+ 17 - 7
Mods/vcmi/config/vcmi/chinese.json

@@ -54,6 +54,8 @@
 	"vcmi.radialWheel.moveDown" : "下移",
 	"vcmi.radialWheel.moveBottom" : "移到底端",
 	
+	"vcmi.spellBook.search" : "搜索中...",
+	
 	"vcmi.mainMenu.serverConnecting" : "连接中...",
 	"vcmi.mainMenu.serverAddressEnter" : "使用地址:",
 	"vcmi.mainMenu.serverConnectionFailed" : "连接失败",
@@ -62,13 +64,14 @@
 	"vcmi.mainMenu.joinTCP" : "加入TCP/IP游戏",
 	"vcmi.mainMenu.playerName" : "Player",
 
-	"vcmi.lobby.filename" : "文件名",
+	"vcmi.lobby.filepath" : "文件路径",
 	"vcmi.lobby.creationDate" : "创建时间",
 	"vcmi.lobby.scenarioName" : "场景名称",
 	"vcmi.lobby.mapPreview" : "地图预览",
 	"vcmi.lobby.noPreview" : "无地上部分",
 	"vcmi.lobby.noUnderground" : "无地下部分",
 
+	"vcmi.client.errors.missingCampaigns" : "{找不到数据文件}\n\n没有找到战役数据文件!你可能使用了不完整或损坏的英雄无敌3数据文件,请重新安装数据文件。",
 	"vcmi.server.errors.existingProcess"     : "一个VCMI进程已经在运行,启动新进程前请结束它。",
 	"vcmi.server.errors.modsToEnable"    : "{需要启用的mod列表}",
 	"vcmi.server.errors.modsToDisable"   : "{需要禁用的mod列表}",
@@ -90,9 +93,9 @@
 	"vcmi.systemOptions.townsGroup" : "城镇画面",
 
 	"vcmi.systemOptions.fullscreenBorderless.hover" : "全屏 (无边框)",
-	"vcmi.systemOptions.fullscreenBorderless.help"  : "{全屏}\n\n选中时,VCMI将以全屏运行,否则运行在窗口模式。",
-	"vcmi.systemOptions.fullscreenExclusive.hover"  : "全屏 (边框)",
-	"vcmi.systemOptions.fullscreenExclusive.help"   : "{全屏}\n\n选中时,VCMI将以全屏运行,否则运行在窗口模式。",
+	"vcmi.systemOptions.fullscreenBorderless.help"  : "{全屏}\n\n选中时,VCMI将以无边框全屏模式运行。在这种模式下,游戏会使用和桌面一致的分辨率而非设置的分辨率。 ",
+	"vcmi.systemOptions.fullscreenExclusive.hover"  : "全屏 (独占)",
+	"vcmi.systemOptions.fullscreenExclusive.help"   : "{全屏}\n\n选中时,VCMI将以独占全屏模式运行。在这种模式下,游戏会将显示器分辨率改变为设置值。",
 	"vcmi.systemOptions.resolutionButton.hover" : "分辨率",
 	"vcmi.systemOptions.resolutionButton.help"  : "{分辨率选择}\n\n改变游戏内的分辨率,更改后需要重启游戏使其生效。",
 	"vcmi.systemOptions.resolutionMenu.hover"   : "分辨率选择",
@@ -133,6 +136,8 @@
 	"vcmi.adventureOptions.leftButtonDrag.help" : "{左键拖动地图}\n\n启用后,按住左键移动鼠标将拖动冒险地图视图。",
 	"vcmi.adventureOptions.smoothDragging.hover" : "平滑地图拖动",
 	"vcmi.adventureOptions.smoothDragging.help" : "{平滑地图拖动}\n\n启用后,地图拖动会产生柔和的羽化效果。",
+	"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "关闭淡入淡出特效",
+	"vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{关闭淡入淡出特效}\n\n启用后,跳过物体淡出或类似特效(资源收集,登船等)。设置此项能在渲染开销重时能够加快UI的响应,尤其是在PvP对战中。当移动速度被设置为最大时忽略此项设置。",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
@@ -161,8 +166,6 @@
 	"vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{显示生物射程限制}\n\n当您将鼠标悬停在其上时,显示射手的射程限制。",
 	"vcmi.battleOptions.showStickyHeroInfoWindows.hover": "显示英雄统计数据窗口",
 	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{显示英雄统计数据窗口}\n\n永久切换并显示主要统计数据和法术点的英雄统计数据窗口。",
-	"vcmi.battleOptions.enableAutocombatSpells.hover": "魔法",
-	"vcmi.battleOptions.enableAutocombatSpells.help": "{魔法}\n\n快速战斗时不会使用魔法。",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "跳过战斗开始音乐",
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{跳过战斗开始音乐}\n\n战斗开始音乐播放期间,你也能够进行操作。",
 	
@@ -234,7 +237,7 @@
 	"vcmi.questLog.hideComplete.hover" : "隐藏完成任务",
 	"vcmi.questLog.hideComplete.help"  : "隐藏所有完成的任务",
 
-	"vcmi.randomMapTab.widgets.defaultTemplate"      : "默认",
+	"vcmi.randomMapTab.widgets.randomTemplate"      : "(随机)",
 	"vcmi.randomMapTab.widgets.templateLabel"        : "模板",
 	"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "设定...",
 	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "同盟关系",
@@ -302,6 +305,13 @@
 	"vcmi.optionsTab.simturns.months.1" : " %d 月",
 	"vcmi.optionsTab.simturns.months.2" : " %d 月",
 
+	"vcmi.optionsTab.extraOptions.hover" : "额外选项",
+	"vcmi.optionsTab.extraOptions.help" : "游戏的额外设置",
+
+	"vcmi.optionsTab.cheatAllowed.hover" : "允许作弊",
+	"vcmi.optionsTab.unlimitedReplay.hover" : "无限战斗回放",
+	"vcmi.optionsTab.cheatAllowed.help" : "{允许作弊}\n允许在游戏过程中输入作弊码。",
+	"vcmi.optionsTab.unlimitedReplay.help" : "{无限战斗回放}\n战斗回放没有任何限制。",
 
 	// Custom victory conditions for H3 campaigns and HotA maps
 	"vcmi.map.victoryCondition.daysPassed.toOthers" : "敌人依然存活至今,你失败了!",

+ 25 - 9
Mods/vcmi/config/vcmi/english.json

@@ -13,13 +13,14 @@
 	"vcmi.adventureMap.monsterThreat.levels.10" : "Deadly",
 	"vcmi.adventureMap.monsterThreat.levels.11" : "Impossible",
 
-	"vcmi.adventureMap.confirmRestartGame"     : "Are you sure you want to restart the game?",
-	"vcmi.adventureMap.noTownWithMarket"       : "There are no available marketplaces!",
-	"vcmi.adventureMap.noTownWithTavern"       : "There are no available towns with taverns!",
-	"vcmi.adventureMap.spellUnknownProblem"    : "There is an unknown problem with this spell! No more information is available.",
-	"vcmi.adventureMap.playerAttacked"         : "Player has been attacked: %s",
-	"vcmi.adventureMap.moveCostDetails"        : "Movement points - Cost: %TURNS turns + %POINTS points, Remaining points: %REMAINING",
-	"vcmi.adventureMap.moveCostDetailsNoTurns" : "Movement points - Cost: %POINTS points, Remaining points: %REMAINING",
+	"vcmi.adventureMap.confirmRestartGame"               : "Are you sure you want to restart the game?",
+	"vcmi.adventureMap.noTownWithMarket"                 : "There are no available marketplaces!",
+	"vcmi.adventureMap.noTownWithTavern"                 : "There are no available towns with taverns!",
+	"vcmi.adventureMap.spellUnknownProblem"              : "There is an unknown problem with this spell! No more information is available.",
+	"vcmi.adventureMap.playerAttacked"                   : "Player has been attacked: %s",
+	"vcmi.adventureMap.moveCostDetails"                  : "Movement points - Cost: %TURNS turns + %POINTS points, Remaining points: %REMAINING",
+	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Movement points - Cost: %POINTS points, Remaining points: %REMAINING",
+	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Sorry, replay opponent turn is not implemented yet!",
 
 	"vcmi.capitalColors.0" : "Red",
 	"vcmi.capitalColors.1" : "Blue",
@@ -72,6 +73,7 @@
 	"vcmi.lobby.noUnderground" : "no underground",
 	"vcmi.lobby.sortDate" : "Sorts maps by change date",
 
+	"vcmi.client.errors.invalidMap" : "{Invalid map or campaign}\n\nFailed to start game! Selected map or campaign might be invalid or corrupted. Reason:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{Missing data files}\n\nCampaigns data files were not found! You may be using incomplete or corrupted Heroes 3 data files. Please reinstall game data.",
 	"vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.",
 	"vcmi.server.errors.modsToEnable"    : "{Following mods are required}",
@@ -170,8 +172,10 @@
 	"vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Show heroes statistics windows",
 	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Show heroes statistics windows}\n\nPermanently toggle on heroes statistics windows that show primary stats and spell points.",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Skip Intro Music",
-	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Skip Intro Music}\n\nAllow actions during the intro music that plays at the beginning of each battle.",
-	
+	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Skip Intro Music}\n\nAllow actions during the intro music that plays at the beginning of each battle.",	
+	"vcmi.battleOptions.endWithAutocombat.hover": "Ends battle",
+	"vcmi.battleOptions.endWithAutocombat.help": "{Ends battle}\n\nAuto-Combat plays battle to end instant",
+
 	"vcmi.adventureMap.revisitObject.hover" : "Revisit Object",
 	"vcmi.adventureMap.revisitObject.help" : "{Revisit Object}\n\nIf a hero currently stands on a Map Object, he can revisit the location.",
 
@@ -190,6 +194,7 @@
 	"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s were killed by accurate shots!",
 	"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s was killed with an accurate shot!",
 	"vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s were killed by accurate shots!",
+	"vcmi.battleWindow.endWithAutocombat" : "Are you sure you wish to end the battle with auto combat?",
 
 	"vcmi.battleResultsWindow.applyResultsLabel" : "Apply battle result",
 
@@ -232,6 +237,8 @@
 	"vcmi.heroWindow.openBackpack.hover" : "Open artifact backpack window",
 	"vcmi.heroWindow.openBackpack.help"  : "Opens window that allows easier artifact backpack management.",
 
+	"vcmi.tavernWindow.inviteHero"  : "Invite hero",
+
 	"vcmi.commanderWindow.artifactMessage" : "Do you want to return this artifact to the hero?",
 
 	"vcmi.creatureWindow.showBonuses.hover"    : "Switch to bonuses view",
@@ -312,6 +319,14 @@
 	"vcmi.optionsTab.simturns.months.1" : " %d month",
 	"vcmi.optionsTab.simturns.months.2" : " %d months",
 
+	"vcmi.optionsTab.extraOptions.hover" : "Extra Options",
+	"vcmi.optionsTab.extraOptions.help" : "Additional settings for the game",
+
+	"vcmi.optionsTab.cheatAllowed.hover" : "Allow cheats",
+	"vcmi.optionsTab.unlimitedReplay.hover" : "Unlimited battle replay",
+	"vcmi.optionsTab.cheatAllowed.help" : "{Allow cheats}\nAllows the inputs of cheats during the game.",
+	"vcmi.optionsTab.unlimitedReplay.help" : "{Unlimited battle replay}\nNo limit of replaying battles.",
+
 	// Custom victory conditions for H3 campaigns and HotA maps
 	"vcmi.map.victoryCondition.daysPassed.toOthers" : "The enemy has managed to survive till this day. Victory is theirs!",
 	"vcmi.map.victoryCondition.daysPassed.toSelf" : "Congratulations! You have managed to survive. Victory is yours!",
@@ -320,6 +335,7 @@
 	"vcmi.map.victoryCondition.collectArtifacts.message" : "Acquire Three Artifacts",
 	"vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Congratulations! All your enemies have been defeated and you have Angelic Alliance! Victory is yours!",
 	"vcmi.map.victoryCondition.angelicAlliance.message" : "Defeat All Enemies and create Angelic Alliance",
+	"vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" : "Alas, you have lost part of the Angelic Alliance. All is lost.",
 
 	// few strings from WoG used by vcmi
 	"vcmi.stackExperience.description" : "» S t a c k   E x p e r i e n c e   D e t a i l s «\n\nCreature Type ................... : %s\nExperience Rank ................. : %s (%i)\nExperience Points ............... : %i\nExperience Points to Next Rank .. : %i\nMaximum Experience per Battle ... : %i%% (%i)\nNumber of Creatures in stack .... : %i\nMaximum New Recruits\n without losing current Rank .... : %i\nExperience Multiplier ........... : %.2f\nUpgrade Multiplier .............. : %.2f\nExperience after Rank 10 ........ : %i\nMaximum New Recruits to remain at\n Rank 10 if at Maximum Experience : %i",

+ 21 - 7
Mods/vcmi/config/vcmi/german.json

@@ -13,13 +13,14 @@
 	"vcmi.adventureMap.monsterThreat.levels.10" : "Tödlich",
 	"vcmi.adventureMap.monsterThreat.levels.11" : "Unmöglich",
 
-	"vcmi.adventureMap.confirmRestartGame"     : "Seid Ihr sicher, dass Ihr das Spiel neu starten wollt?",
-	"vcmi.adventureMap.noTownWithMarket"       : "Kein Marktplatz verfügbar!",
-	"vcmi.adventureMap.noTownWithTavern"       : "Keine Stadt mit Taverne verfügbar!",
-	"vcmi.adventureMap.spellUnknownProblem"    : "Unbekanntes Problem mit diesem Zauberspruch, keine weiteren Informationen verfügbar.",
-	"vcmi.adventureMap.playerAttacked"         : "Spieler wurde attackiert: %s",
-	"vcmi.adventureMap.moveCostDetails"        : "Bewegungspunkte - Kosten: %TURNS Runden + %POINTS Punkte, Verbleibende Punkte: %REMAINING",
-	"vcmi.adventureMap.moveCostDetailsNoTurns" : "Bewegungspunkte - Kosten: %POINTS Punkte, Verbleibende Punkte: %REMAINING",
+	"vcmi.adventureMap.confirmRestartGame"               : "Seid Ihr sicher, dass Ihr das Spiel neu starten wollt?",
+	"vcmi.adventureMap.noTownWithMarket"                 : "Kein Marktplatz verfügbar!",
+	"vcmi.adventureMap.noTownWithTavern"                 : "Keine Stadt mit Taverne verfügbar!",
+	"vcmi.adventureMap.spellUnknownProblem"              : "Unbekanntes Problem mit diesem Zauberspruch, keine weiteren Informationen verfügbar.",
+	"vcmi.adventureMap.playerAttacked"                   : "Spieler wurde attackiert: %s",
+	"vcmi.adventureMap.moveCostDetails"                  : "Bewegungspunkte - Kosten: %TURNS Runden + %POINTS Punkte, Verbleibende Punkte: %REMAINING",
+	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Bewegungspunkte - Kosten: %POINTS Punkte, Verbleibende Punkte: %REMAINING",
+	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Das Wiederholen des gegnerischen Zuges ist aktuell noch nicht implementiert!",
 
 	"vcmi.capitalColors.0" : "Rot",
 	"vcmi.capitalColors.1" : "Blau",
@@ -171,6 +172,8 @@
 	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Statistikfenster für Helden anzeigen}\n\nDauerhaftes Einschalten des Statistikfenster für Helden, das die primären Werte und Zauberpunkte anzeigt.",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Intro-Musik überspringen",
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Intro-Musik überspringen}\n\n Überspringe die kurze Musik, die zu Beginn eines jeden Kampfes gespielt wird, bevor die Action beginnt. Kann auch durch Drücken der ESC-Taste übersprungen werden.",
+	"vcmi.battleOptions.endWithAutocombat.hover": "Kampf beenden",
+	"vcmi.battleOptions.endWithAutocombat.help": "{Kampf beenden}\n\nAutokampf spielt den Kampf sofort zu Ende",
 	
 	"vcmi.adventureMap.revisitObject.hover" : "Objekt erneut besuchen",
 	"vcmi.adventureMap.revisitObject.help" : "{Objekt erneut besuchen}\n\nSteht ein Held gerade auf einem Kartenobjekt, kann er den Ort erneut aufsuchen.",
@@ -190,6 +193,7 @@
 	"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s wurden durch gezielte Schüsse getötet!",
 	"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s wurde mit einem gezielten Schuss getötet!",
 	"vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s wurden durch gezielte Schüsse getötet!",
+	"vcmi.battleWindow.endWithAutocombat" : "Seid Ihr sicher, dass Ihr den Kampf mit Auto-Kampf beenden wollt?",
 
 	"vcmi.battleResultsWindow.applyResultsLabel" : "Kampfergebnis übernehmen",
 
@@ -232,6 +236,8 @@
 	"vcmi.heroWindow.openBackpack.hover" : "Artefakt-Rucksack-Fenster öffnen",
 	"vcmi.heroWindow.openBackpack.help"  : "Öffnet ein Fenster, das die Verwaltung des Artefakt-Rucksacks erleichtert",
 
+	"vcmi.tavernWindow.inviteHero"  : "Helden einladen",
+
 	"vcmi.commanderWindow.artifactMessage" : "Möchtet Ihr diesen Artefakt dem Helden zurückgeben?",
 
 	"vcmi.creatureWindow.showBonuses.hover"    : "Wechsle zur Bonus-Ansicht",
@@ -312,6 +318,14 @@
 	"vcmi.optionsTab.simturns.months.1" : "%d Monat",
 	"vcmi.optionsTab.simturns.months.2" : "%d Monate",
 
+	"vcmi.optionsTab.extraOptions.hover" : "Extra Optionen",
+	"vcmi.optionsTab.extraOptions.help" : "Zusätzliche Einstellungen für das Spiel",
+	
+	"vcmi.optionsTab.cheatAllowed.hover" : "Cheats erlauben",
+	"vcmi.optionsTab.unlimitedReplay.hover" : "Unbegrenzte Kampfwiederholung",
+	"vcmi.optionsTab.cheatAllowed.help" : "{Cheats erlauben}\nErlaubt die Eingabe von Cheats während des Spiels.",
+	"vcmi.optionsTab.unlimitedReplay.help" : "{Unbegrenzte Kampfwiederholung}\nKämpfe lassen sich unbegrenzt wiederholen.",
+
 	// Custom victory conditions for H3 campaigns and HotA maps
 	"vcmi.map.victoryCondition.daysPassed.toOthers" : "Der Feind hat es geschafft, bis zum heutigen Tag zu überleben. Der Sieg gehört ihm!",
 	"vcmi.map.victoryCondition.daysPassed.toSelf" : "Herzlichen Glückwunsch! Ihr habt es geschafft, zu überleben. Der Sieg ist euer!",

+ 86 - 70
Mods/vcmi/config/vcmi/polish.json

@@ -54,6 +54,8 @@
 	"vcmi.radialWheel.moveDown" : "Przenieś w dół",
 	"vcmi.radialWheel.moveBottom" : "Przenieś na spód",
 
+	"vcmi.spellBook.search" : "szukaj...",
+
 	"vcmi.mainMenu.serverConnecting" : "Łączenie...",
 	"vcmi.mainMenu.serverAddressEnter" : "Wprowadź adres:",
 	"vcmi.mainMenu.serverConnectionFailed" : "Połączenie nie powiodło się",
@@ -68,7 +70,9 @@
 	"vcmi.lobby.mapPreview" : "Podgląd mapy",
 	"vcmi.lobby.noPreview" : "brak podglądu",
 	"vcmi.lobby.noUnderground" : "brak podziemi",
+	"vcmi.lobby.sortDate" : "Sortuj mapy według daty modyfikacji",
 
+	"vcmi.client.errors.missingCampaigns" : "{Brakujące pliki gry}\n\nPliki kampanii nie zostały znalezione! Możliwe że używasz niekompletnych lub uszkodzonych plików Heroes 3. Spróbuj ponownej instalacji plików gry.",
 	"vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej",
 	"vcmi.server.errors.modsToEnable"    : "{Następujące mody są wymagane do wczytania gry}",
 	"vcmi.server.errors.modsToDisable"   : "{Następujące mody muszą zostać wyłączone}",
@@ -89,7 +93,7 @@
 	"vcmi.systemOptions.otherGroup" : "Inne ustawienia", // unused right now
 	"vcmi.systemOptions.townsGroup" : "Ekran miasta",
 
-	"vcmi.systemOptions.fullscreenBorderless.hover" : "Pełny ekran (borderless/okno pełnoekranowe)",
+	"vcmi.systemOptions.fullscreenBorderless.hover" : "Pełny ekran (bez ramek)",
 	"vcmi.systemOptions.fullscreenBorderless.help"  : "{Pełny ekran w trybie okna}\n\nPo wybraniu VCMI będzie działać w trybie okna pełnoekranowego. W tym trybie gra będzie zawsze używać rozdzielczości pulpitu, ignorując wybraną rozdzielczość.",
 	"vcmi.systemOptions.fullscreenExclusive.hover"  : "Pełny ekran (tradycyjny)",
 	"vcmi.systemOptions.fullscreenExclusive.help"   : "{Pełny ekran}\n\nPo wybraniu VCMI będzie działać w trybie ekskluzywnego pełnego ekranu. W tym trybie gra zmieni rozdzielczość monitora do wybranej rozdzielczości.",
@@ -104,7 +108,7 @@
 	"vcmi.systemOptions.longTouchButton.hover"   : "Czas długiego dotyku: %d ms", // Translation note: "ms" = "milliseconds"
 	"vcmi.systemOptions.longTouchButton.help"    : "{Czas długiego dotyku}\n\nPodczas używania ekranu dotykowego, wyskakujące okienka pojawią się po dotknięciu ekranu przez określony czas, w milisekundach",
 	"vcmi.systemOptions.longTouchMenu.hover"     : "Wybierz czas długiego dotyku",
-	"vcmi.systemOptions.longTouchMenu.help"      : "Zmienia czas wymagany do aktywacji długiego dotyku.",
+	"vcmi.systemOptions.longTouchMenu.help"      : "Nowy czas aktywacji długiego dotyku.",
 	"vcmi.systemOptions.longTouchMenu.entry"     : "%d milisekund",
 	"vcmi.systemOptions.framerateButton.hover"  : "Pokaż FPS",
 	"vcmi.systemOptions.framerateButton.help"   : "{Pokaż FPS}\n\n Przełącza widoczność licznika klatek na sekundę (FPS) w rogu okna gry.",
@@ -114,8 +118,8 @@
 	"vcmi.systemOptions.enableUiEnhancementsButton.help"   : "{Ulepszenia interfejsu}\n\nWłącza różne ulepszenia interfejsu poprawiające wygodę rozgrywki. Takie jak przycisk sakwy bohatera itp. Wyłącz jeśli szukasz bardziej klasycznej wersji gry.",
 	"vcmi.systemOptions.enableLargeSpellbookButton.hover"  : "Duża księga zaklęć",
 	"vcmi.systemOptions.enableLargeSpellbookButton.help"   : "{Duża księga zaklęć}\n\nWłącza dużą księgę czarów, która mieści więcej zaklęć na stronę. Animacja zmiany strony nie działa gdy ta opcja jest włączona.",
-	"vcmi.systemOptions.audioMuteFocus.hover"  : "Wycisz przy zdezaktywowaniu",
-	"vcmi.systemOptions.audioMuteFocus.help"   : "{Wycisz przy zdezaktywowaniu}\n\nWycisz dźwięk gdy okno gry staje się nieaktywne. Wyjątkiem są dźwięki wiadomości i nowej tury.",
+	"vcmi.systemOptions.audioMuteFocus.hover"  : "Wycisz nieaktywną grę",
+	"vcmi.systemOptions.audioMuteFocus.help"   : "{Wycisz nieaktywną grę}\n\nWycisza dźwięk gdy okno gry staje się nieaktywne. Wyjątkiem są dźwięki wiadomości i nowej tury.",
 
 	"vcmi.adventureOptions.infoBarPick.hover" : "Pokaż komunikaty w panelu informacyjnym",
 	"vcmi.adventureOptions.infoBarPick.help" : "{Pokaż komunikaty w panelu informacyjnym}\n\nGdy to możliwe, wiadomości z odwiedzania obiektów będą pokazywane w panelu informacyjnym zamiast w osobnym okienku.",
@@ -139,6 +143,8 @@
 	"vcmi.adventureOptions.mapScrollSpeed1.help": "Ustaw szybkość przesuwania mapy na bardzo wolną.",
 	"vcmi.adventureOptions.mapScrollSpeed5.help": "Ustaw szybkość przesuwania mapy na bardzo szybką.",
 	"vcmi.adventureOptions.mapScrollSpeed6.help": "Ustaw szybkość przesuwania mapy na błyskawiczną.",
+	"vcmi.adventureOptions.hideBackground.hover" : "Ukryj tło",
+	"vcmi.adventureOptions.hideBackground.help" : "{Ukryj tło}\n\nUkryj mapę przygody w tle i pokaż zastępczo teksturę.",
 
 	"vcmi.battleOptions.queueSizeLabel.hover": "Pokaż kolejkę ruchu jednostek",
 	"vcmi.battleOptions.queueSizeNoneButton.hover": "BRAK",
@@ -178,6 +184,10 @@
 	"vcmi.battleWindow.damageEstimation.damage.1" : "obrażenia: %d",
 	"vcmi.battleWindow.damageEstimation.kills" : "zginie: %d",
 	"vcmi.battleWindow.damageEstimation.kills.1" : "zginie: %d",
+	"vcmi.battleWindow.killed" : "Zabici",
+	"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s zostało zabitych poprzez celne strzały!",
+	"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s został zabity poprzez celny strzał!",
+	"vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s zostali zabici poprzez celne strzały!",
 
 	"vcmi.battleResultsWindow.applyResultsLabel" : "Zatwierdź wynik bitwy",
 
@@ -191,9 +201,9 @@
 	"vcmi.tutorialWindow.decription.AbortSpell" : "Naciśnij i przytrzymaj by anulować rzucenie zaklęcia.",
 
 	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Pokaż dostępne stworzenia",
-	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Pokaż dostępne stworzenia}\n\n Pokazuje dostępne stworzenia zamiast tygodniowego przyrostu w podsumowaniu miasta (lewy dolny róg).",
+	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Pokaż dostępne stworzenia}\n\n Wyświetla dostępne stworzenia zamiast tygodniowego przyrostu w podsumowaniu miasta (lewy dolny róg).",
 	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Pokaż tygodniowy przyrost stworzeń",
-	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Pokaż tygodniowy przyrost stworzeń}\n\n Shows creatures' weekly growth instead of avaialable amount in town summary (lewy dolny r óg).",
+	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Pokaż tygodniowy przyrost stworzeń}\n\n Wyświetla tygodniowy przyrost jednostek zamiast dostępnej ilości do zakupu (lewy dolny róg).",
 	"vcmi.otherOptions.compactTownCreatureInfo.hover": "Kompaktowa informacja o stworzeniu",
 	"vcmi.otherOptions.compactTownCreatureInfo.help": "{Kompaktowa informacja o stworzeniu}\n\n Zmniejszona informacja o stworzeniu w podsumowaniu miasta.",
 
@@ -239,31 +249,31 @@
 	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Typy dróg",
 
 	"vcmi.optionsTab.turnOptions.hover" : "Ustawienia tur",
-	"vcmi.optionsTab.turnOptions.help" : "Ustaw limity czasu (timery) oraz tury równoczesne",
+	"vcmi.optionsTab.turnOptions.help" : "Ustaw limity czasu oraz tury symultaniczne",
 	"vcmi.optionsTab.selectPreset" : "Szablonowe ustawienie",
 
-	"vcmi.optionsTab.chessFieldBase.hover" : "Timer startowy",
-	"vcmi.optionsTab.chessFieldTurn.hover" : "Timer tury",
-	"vcmi.optionsTab.chessFieldBattle.hover" : "Timer bitwy",
-	"vcmi.optionsTab.chessFieldUnit.hover" : "Timer jednostki",
-	"vcmi.optionsTab.chessFieldBase.help" : "Używany gdy {Timer tury} osiągnie 0. Ustawiany raz przy starcie gry. Gdy osiągnie 0 tura się kończy, a trwająca bitwa zostanie przegrana.",
-	"vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Używany poza bitwą lub gdy {Timer bitwy} się wyczerpie. Odnawia się co turę. Nadwyżka czasu dodaje się do {Timera startowego} pod koniec tury.",
-	"vcmi.optionsTab.chessFieldTurnDiscard.help" : "Używany poza bitwą lub gdy {Timer bitwy} się wyczerpie. Odnawia się co turę. Niewykorzystany czas zostaje utracony.",
-	"vcmi.optionsTab.chessFieldBattle.help" : "Używany w bitwach z graczem AI lub gdy {Timer jednostki} się wyczerpie w bitwie pomiędzy graczami. Odnawia się przy starcie każdej bitwy.",
+	"vcmi.optionsTab.chessFieldBase.hover" : "Zegar startowy",
+	"vcmi.optionsTab.chessFieldTurn.hover" : "Zegar tury",
+	"vcmi.optionsTab.chessFieldBattle.hover" : "Zegar bitewny",
+	"vcmi.optionsTab.chessFieldUnit.hover" : "Zegar jednostki",
+	"vcmi.optionsTab.chessFieldBase.help" : "Odlicza czas gdy {zegar tury} osiągnie 0. Czas nie odnawia się. Koniec czasu oznacza zakończenie tury, a trwająca bitwa zostanie przegrana.",
+	"vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Używany poza bitwą lub gdy {Zegar bitewny} się wyczerpie. Odnawia się co turę. Nadwyżkę dodaje się do {Zegara startowego} pod koniec tury.",
+	"vcmi.optionsTab.chessFieldTurnDiscard.help" : "Używany poza bitwą lub gdy {Zegar bitewny} się wyczerpie. Odnawia się co turę. Niewykorzystany czas zostaje utracony.",
+	"vcmi.optionsTab.chessFieldBattle.help" : "Używany w bitwach z graczem AI lub gdy {Zegar jednostki} się wyczerpie w bitwie pomiędzy ludźmi. Odnawia się przy starcie każdej bitwy.",
 	"vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Używany podczas oczekiwania na podjęcie akcji jednostką w bitwie pomiędzy ludźmi. Nadwyżka czasu dodaje się do {Timera bitwy}",
 	"vcmi.optionsTab.chessFieldUnitDiscard.help" : "Używany podczas oczekiwania na podjęcie akcji jednostką w bitwie pomiędzy ludźmi. Resetuje się przy rozpoczęciu akcji jednostką.",
 
 	"vcmi.optionsTab.accumulate" : "Akumuluj",
 
-	"vcmi.optionsTab.simturnsTitle" : "Tury równoczesne / symultaniczne",
-	"vcmi.optionsTab.simturnsMin.hover" : "Co najmniej przez",
-	"vcmi.optionsTab.simturnsMax.hover" : "Maks. przez",
+	"vcmi.optionsTab.simturnsTitle" : "Tury symultaniczne",
+	"vcmi.optionsTab.simturnsMin.hover" : "Minimum",
+	"vcmi.optionsTab.simturnsMax.hover" : "Maksimum",
 	"vcmi.optionsTab.simturnsAI.hover" : "(Eksperymentalne) Równoczesne tury graczy AI",
 	"vcmi.optionsTab.simturnsMin.help" : "Graj równocześnie przez określoną liczbę dni. Kontakt pomiędzy graczami do tego czasu jest zablokowany.",
 	"vcmi.optionsTab.simturnsMax.help" : "Graj równocześnie przez określoną liczbę dni lub do momentu napotkania innego gracza.",
 	"vcmi.optionsTab.simturnsAI.help" : "{Równoczesne tury graczy AI}\nOpcja eksperymentalna. Pozwala graczom sterowanym przez komputer wykonywać akcje w tym samym czasie co gracz ludzki gdy jednoczesne tury są włączone.",
 
-	"vcmi.optionsTab.turnTime.select"     : "Predefiniowane schematy zegarów",
+	"vcmi.optionsTab.turnTime.select"     : "Gotowe schematy zegarów",
 	"vcmi.optionsTab.turnTime.unlimited"  : "Nieograniczony czas tury",
 	"vcmi.optionsTab.turnTime.classic.1"  : "Klasyczny: 1 minuta",
 	"vcmi.optionsTab.turnTime.classic.2"  : "Klasyczny: 2 minuty",
@@ -278,15 +288,15 @@
 	"vcmi.optionsTab.turnTime.chess.2"    : "Szach: 02:00 + 01:00 + 00:15 + 00:00",
 	"vcmi.optionsTab.turnTime.chess.1"    : "Szach: 01:00 + 01:00 + 00:00 + 00:00",
 
-	"vcmi.optionsTab.simturns.select"         : "Predefiniowane schematy tur sym.",
-	"vcmi.optionsTab.simturns.none"           : "Brak tur symultanicznych / równocz.",
+	"vcmi.optionsTab.simturns.select"         : "Gotowe schematy tur sym.",
+	"vcmi.optionsTab.simturns.none"           : "Brak tur symultanicznych",
 	"vcmi.optionsTab.simturns.tillContactMax" : "Tury sym.: Do kontaktu",
-	"vcmi.optionsTab.simturns.tillContact1"   : "Tury sym.: 1 tydz., przerw. przy kontakcie",
-	"vcmi.optionsTab.simturns.tillContact2"   : "Tury sym.: 2 tyg., przerw. przy kontakcie",
-	"vcmi.optionsTab.simturns.tillContact4"   : "Tury sym.: 1 mies., przerw. przy kontakcie",
-	"vcmi.optionsTab.simturns.blocked1"       : "Tury sym.: 1 tydz., kontakt zablokowany",
-	"vcmi.optionsTab.simturns.blocked2"       : "Tury sym.: 2 tyg., kontakt zablokowany",
-	"vcmi.optionsTab.simturns.blocked4"       : "Tury sym.: 1 mies., kontakt zablokowany",
+	"vcmi.optionsTab.simturns.tillContact1"   : "Tury sym.: 1 tydz., kontakt: wł.",
+	"vcmi.optionsTab.simturns.tillContact2"   : "Tury sym.: 2 tyg., kontakt: wł.",
+	"vcmi.optionsTab.simturns.tillContact4"   : "Tury sym.: 1 mies., kontakt: wł.",
+	"vcmi.optionsTab.simturns.blocked1"       : "Tury sym.: 1 tydz., kontakt: wył.",
+	"vcmi.optionsTab.simturns.blocked2"       : "Tury sym.: 2 tyg., kontakt: wył.",
+	"vcmi.optionsTab.simturns.blocked4"       : "Tury sym.: 1 mies., kontakt: wył.",
 
 	// Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language
 	// Using this information, VCMI will automatically select correct plural form for every possible amount
@@ -312,12 +322,12 @@
 	//WoG strings translations missing here - should be taken from polish WoG translation
 
 	"core.bonus.ADDITIONAL_ATTACK.name": "Podwójne Uderzenie",
-	"core.bonus.ADDITIONAL_ATTACK.description": "Atakuje podwójnie",
-	"core.bonus.ADDITIONAL_RETALIATION.name": "Dodatkowe kontrataki",
-	"core.bonus.ADDITIONAL_RETALIATION.description": "Może kontratakować ${val} dodatkowych razy",
-	"core.bonus.AIR_IMMUNITY.name": "Odporność na powietrze",
+	"core.bonus.ADDITIONAL_ATTACK.description": "Atakuje dwa razy",
+	"core.bonus.ADDITIONAL_RETALIATION.name": "Dodatkowy odwet",
+	"core.bonus.ADDITIONAL_RETALIATION.description": "${val} dodatkowy kontratak",
+	"core.bonus.AIR_IMMUNITY.name": "Odporność: Powietrze",
 	"core.bonus.AIR_IMMUNITY.description": "Odporny na wszystkie czary szkoły powietrza",
-	"core.bonus.ATTACKS_ALL_ADJACENT.name": "Atakuje wszystko dookoła",
+	"core.bonus.ATTACKS_ALL_ADJACENT.name": "Obrotowy atak",
 	"core.bonus.ATTACKS_ALL_ADJACENT.description": "Atakuje wszystkich sąsiadujących wrogów",
 	"core.bonus.BLOCKS_RETALIATION.name": "Bez kontrataku",
 	"core.bonus.BLOCKS_RETALIATION.description": "Wróg nie może kontratakować",
@@ -325,11 +335,11 @@
 	"core.bonus.BLOCKS_RANGED_RETALIATION.description": "Wróg nie może kontratakować poprzez strzelanie",
 	"core.bonus.CATAPULT.name": "Katapulta",
 	"core.bonus.CATAPULT.description": "Atakuje mury obronne",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Zmniejsz koszt czarów (${val})",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Zmniejsza koszt czaru bohatera",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Tłumienie magii (${val})",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Zwiększa koszt wrogich czarów",
-	"core.bonus.CHARGE_IMMUNITY.name": "Odporność na szarżę",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Profesja magii",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Zmniejsza o ${val} koszt czarów bohatera",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Tłumienie magii",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Zwiększa o ${val} koszt wrogich czarów",
+	"core.bonus.CHARGE_IMMUNITY.name": "Odporność: Szarża",
 	"core.bonus.CHARGE_IMMUNITY.description": "Odporny na szarżę czempionów",
 	"core.bonus.DARKNESS.name": "Całun ciemności",
 	"core.bonus.DARKNESS.description": "Generuje ${val} wartości promienia mgły wojny",
@@ -343,15 +353,17 @@
 	"core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}% szans na podwójne obrażenia",
 	"core.bonus.DRAGON_NATURE.name": "Smok",
 	"core.bonus.DRAGON_NATURE.description": "Stworzenie posiada smoczą naturę",
-	"core.bonus.EARTH_IMMUNITY.name": "Odporność na ziemię",
+	"core.bonus.EARTH_IMMUNITY.name": "Odporność: Ziemia",
 	"core.bonus.EARTH_IMMUNITY.description": "Odporny na wszystkie czary szkoły ziemi",
 	"core.bonus.ENCHANTER.name": "Czarodziej",
-	"core.bonus.ENCHANTER.description": "Może rzucać masowy czar ${subtype.spell} każdej tury",
+	"core.bonus.ENCHANTER.description": "Rzuca czar ${subtype.spell}",
 	"core.bonus.ENCHANTED.name": "Zaczarowany",
-	"core.bonus.ENCHANTED.description": "Pod wpływem trwałego ${subtype.spell}",
-	"core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignoruje Obronę (${val}%)",
-	"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Ignoruje część obrony podczas ataku",
-	"core.bonus.FIRE_IMMUNITY.name": "Odporność na ogień",
+	"core.bonus.ENCHANTED.description": "Pod trwałym wpływem czaru ${subtype.spell}",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.name": "Ignoruje Atak (${val}%)",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.description": "Przy zostaniu zaatakowanym ignoruje ${val}% ataku wroga",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Osłabienie Obrony (${val}%)",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Osłabia obronę wroga podczas ataku",
+	"core.bonus.FIRE_IMMUNITY.name": "Odporność: Ogień",
 	"core.bonus.FIRE_IMMUNITY.description": "Odporny na wszystkie czary szkoły ognia",
 	"core.bonus.FIRE_SHIELD.name": "Ognista tarcza (${val}%)",
 	"core.bonus.FIRE_SHIELD.description": "Odbija część obrażeń z walki wręcz",
@@ -361,26 +373,28 @@
 	"core.bonus.FEAR.description": "Wzbudza strach na wrogim stworzeniu",
 	"core.bonus.FEARLESS.name": "Nieustraszony",
 	"core.bonus.FEARLESS.description": "Odporny na strach",
+	"core.bonus.FEROCITY.name": "Dzikość",
+	"core.bonus.FEROCITY.description": "Dodatkowe ${val} ataków jeżeli zabito kogokolwiek",
 	"core.bonus.FLYING.name": "Lot",
 	"core.bonus.FLYING.description": "Może latać (ignoruje przeszkody)",
 	"core.bonus.FREE_SHOOTING.name": "Bliski Strzał",
 	"core.bonus.FREE_SHOOTING.description": "Może strzelać w zasięgu walki wręcz",
 	"core.bonus.GARGOYLE.name": "Gargulec",
-	"core.bonus.GARGOYLE.description": "Nie może być wskrzeszony lub uleczony",
-	"core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Zmniejsz obrażenia (${val}%)",
+	"core.bonus.GARGOYLE.description": "Nie może się wskrzesić i uleczyć",
+	"core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Redukcja obrażeń (${val}%)",
 	"core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Zmiejsza obrażenia fizyczne z dystansu lub walki wręcz",
-	"core.bonus.HATE.name": "Nienawidzi ${subtype.creature}",
-	"core.bonus.HATE.description": "Zadaje ${val}% więcej obrażeń",
+	"core.bonus.HATE.name": "${subtype.creature}",
+	"core.bonus.HATE.description": "+${val}% dodatkowych obrażeń",
 	"core.bonus.HEALER.name": "Uzdrowiciel",
 	"core.bonus.HEALER.description": "Leczy sprzymierzone jednostki",
 	"core.bonus.HP_REGENERATION.name": "Regeneracja",
-	"core.bonus.HP_REGENERATION.description": "Leczy ${val} punktów zdrowia każdej rundy",
+	"core.bonus.HP_REGENERATION.description": "Leczy ${val} pkt. zdrowia co rundę",
 	"core.bonus.JOUSTING.name": "Szarża Czempiona",
 	"core.bonus.JOUSTING.description": "+${val}% obrażeń na przebytego heksa",
-	"core.bonus.KING.name": "Król",
-	"core.bonus.KING.description": "Wrażliwy na czar POGROMCA stopnia zaawansowania ${val} lub wyższego",
-	"core.bonus.LEVEL_SPELL_IMMUNITY.name": "Odporność na czary 1-${val}",
-	"core.bonus.LEVEL_SPELL_IMMUNITY.description": "Odporny na czary 1-${val} poziomu",
+	"core.bonus.KING.name": "Król - wrażliwy na",
+	"core.bonus.KING.description": "czar POGROMCA stopnia ${val}+",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.name": "Odporność: Czary 1-${val}",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.description": "Odporny na czary 1-${val} poz.",
 	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Ograniczony zasięg strzelania",
 	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Nie może strzelać do celów będących dalej niż ${val} heksów",
 	"core.bonus.LIFE_DRAIN.name": "Wysysa życie (${val}%)",
@@ -391,17 +405,17 @@
 	"core.bonus.MANA_DRAIN.description": "Wysysa ${val} many każdej tury",
 	"core.bonus.MAGIC_MIRROR.name": "Magiczne Zwierciadło (${val}%)",
 	"core.bonus.MAGIC_MIRROR.description": "${val}% szans na odbicie ofensywnego czaru do wroga",
-	"core.bonus.MAGIC_RESISTANCE.name": "Odporność na Magię(${val}%)",
-	"core.bonus.MAGIC_RESISTANCE.description": "${val}% szans na przeciwstawienie się wrogiemu czarowi",
-	"core.bonus.MIND_IMMUNITY.name": "Odporność na czasy umysłu",
+	"core.bonus.MAGIC_RESISTANCE.name": "Odporność: Magia(${val}%)",
+	"core.bonus.MAGIC_RESISTANCE.description": "${val}% szans na unik wrogiego czaru",
+	"core.bonus.MIND_IMMUNITY.name": "Odporność: Czary umysłu",
 	"core.bonus.MIND_IMMUNITY.description": "Odporny na czary typu umysłu",
 	"core.bonus.NO_DISTANCE_PENALTY.name": "Brak ograniczeń za odległość",
 	"core.bonus.NO_DISTANCE_PENALTY.description": "Pełne obrażenia z każdego zasięgu",
-	"core.bonus.NO_MELEE_PENALTY.name": "Brak ograniczeń za walkę wręcz",
+	"core.bonus.NO_MELEE_PENALTY.name": "Bez ograniczeń",
 	"core.bonus.NO_MELEE_PENALTY.description": "Stworzenie nie ma kar w walce wręcz",
 	"core.bonus.NO_MORALE.name": "Neutralne Morale",
-	"core.bonus.NO_MORALE.description": "Stworzenie jest odporne na efekty morale",
-	"core.bonus.NO_WALL_PENALTY.name": "Brak kar za strzelanie przez przeszkody",
+	"core.bonus.NO_MORALE.description": "Odporność na efekty morale",
+	"core.bonus.NO_WALL_PENALTY.name": "Bez przeszkód",
 	"core.bonus.NO_WALL_PENALTY.description": "Pełne obrażenia podczas oblężenia",
 	"core.bonus.NON_LIVING.name": "Nie żyjący",
 	"core.bonus.NON_LIVING.description": "Niewrażliwość na wiele efektów",
@@ -415,6 +429,8 @@
 	"core.bonus.REBIRTH.description": "${val}% stworzeń powstanie po śmierci",
 	"core.bonus.RETURN_AFTER_STRIKE.name": "Atak i Powrót",
 	"core.bonus.RETURN_AFTER_STRIKE.description": "Wraca po ataku wręcz",
+	"core.bonus.REVENGE.name": "Odwet",
+	"core.bonus.REVENGE.description": "Zadaje dodatkowe obrażenia zależne od strat własnych oddziału",
 	"core.bonus.SHOOTER.name": "Dystansowy",
 	"core.bonus.SHOOTER.description": "Stworzenie może strzelać",
 	"core.bonus.SHOOTS_ALL_ADJACENT.name": "Ostrzeliwuje wszystko dookoła",
@@ -423,20 +439,20 @@
 	"core.bonus.SOUL_STEAL.description": "Zdobywa ${val} nowych stworzeń za każdego zabitego wroga",
 	"core.bonus.SPELLCASTER.name": "Czarodziej",
 	"core.bonus.SPELLCASTER.description": "Może rzucić ${subtype.spell}",
-	"core.bonus.SPELL_AFTER_ATTACK.name": "Rzuca czar po ataku",
-	"core.bonus.SPELL_AFTER_ATTACK.description": "${val}% szans aby rzucić ${subtype.spell} po ataku",
-	"core.bonus.SPELL_BEFORE_ATTACK.name": "Rzuca czar przed atakiem",
-	"core.bonus.SPELL_BEFORE_ATTACK.description": "${val}% szans aby rzucić ${subtype.spell} przed atakiem",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "Odporność na czary",
+	"core.bonus.SPELL_AFTER_ATTACK.name": "${val}% szans na czar",
+	"core.bonus.SPELL_AFTER_ATTACK.description": "${subtype.spell} po ataku",
+	"core.bonus.SPELL_BEFORE_ATTACK.name": "${val}% szans na czar",
+	"core.bonus.SPELL_BEFORE_ATTACK.description": "${subtype.spell} przed atakiem",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "Obrona przed magią",
 	"core.bonus.SPELL_DAMAGE_REDUCTION.description": "Obrażenia od czarów są zmniejszone o ${val}%.",
-	"core.bonus.SPELL_IMMUNITY.name": "Odporność na czar",
-	"core.bonus.SPELL_IMMUNITY.description": "Odporny na ${subtype.spell}",
+	"core.bonus.SPELL_IMMUNITY.name": "Odporność: Zaklęcie",
+	"core.bonus.SPELL_IMMUNITY.description": "${subtype.spell}",
 	"core.bonus.SPELL_LIKE_ATTACK.name": "Atak czaropodobny",
 	"core.bonus.SPELL_LIKE_ATTACK.description": "Atakuje z użyciem ${subtype.spell}",
-	"core.bonus.SPELL_RESISTANCE_AURA.name": "Aura Odporności",
-	"core.bonus.SPELL_RESISTANCE_AURA.description": "Pobliskie stworzenia otrzymują ${val}% magicznej odporności",
-	"core.bonus.SUMMON_GUARDIANS.name": "Wezwij strażników",
-	"core.bonus.SUMMON_GUARDIANS.description": "Na początku walki wzywa ${subtype.creature} (${val}%)",
+	"core.bonus.SPELL_RESISTANCE_AURA.name": "O ${val}% słabszy",
+	"core.bonus.SPELL_RESISTANCE_AURA.description": "efekt czarów dla pobl. stwor.",
+	"core.bonus.SUMMON_GUARDIANS.name": "Wzywa na początku walki",
+	"core.bonus.SUMMON_GUARDIANS.description": "${subtype.creature} (${val}%)",
 	"core.bonus.SYNERGY_TARGET.name": "Synergiczny",
 	"core.bonus.SYNERGY_TARGET.description": "To stworzenie jest podatne na efekt synergii",
 	"core.bonus.TWO_HEX_ATTACK_BREATH.name": "Zionięcie",
@@ -449,7 +465,7 @@
 	"core.bonus.UNDEAD.description": "Stworzenie jest nieumarłe",
 	"core.bonus.UNLIMITED_RETALIATIONS.name": "Nieskończone kontrataki",
 	"core.bonus.UNLIMITED_RETALIATIONS.description": "Kontratakuje nieskończoną ilość razy",
-	"core.bonus.WATER_IMMUNITY.name": "Odporność na wodę",
+	"core.bonus.WATER_IMMUNITY.name": "Odporność: Woda",
 	"core.bonus.WATER_IMMUNITY.description": "Odporny na wszystkie czary szkoły wody",
 	"core.bonus.WIDE_BREATH.name": "Szerokie zionięcie",
 	"core.bonus.WIDE_BREATH.description": "Szeroki atak zionięciem (wiele heksów)"

+ 223 - 48
Mods/vcmi/config/vcmi/spanish.json

@@ -30,9 +30,55 @@
 	"vcmi.capitalColors.6" : "Turquesa",
 	"vcmi.capitalColors.7" : "Rosa",
 
-	"vcmi.server.errors.existingProcess" : "Otro proceso de vcmiserver está en ejecución, por favor termínalo primero",
-	"vcmi.server.errors.modsToEnable"    : "{Mods necesarios para cargar el juego}",
-	"vcmi.server.confirmReconnect"       : "¿Conectar a la última sesión?",
+	"vcmi.heroOverview.startingArmy" : "Unidades iniciales",
+	"vcmi.heroOverview.warMachine" : "Máquinas de Guerra",
+	"vcmi.heroOverview.secondarySkills" : "Habilidades secundarias",
+	"vcmi.heroOverview.spells" : "Hechizos",
+
+	"vcmi.radialWheel.mergeSameUnit" : "Mezclar criaturas idénticas",
+	"vcmi.radialWheel.fillSingleUnit" : "Rellenar con criaturas individuales",
+	"vcmi.radialWheel.splitSingleUnit" : "Separar una sola criatura",
+	"vcmi.radialWheel.splitUnitEqually" : "Dividir criaturas por igual",
+	"vcmi.radialWheel.moveUnit" : "Trasladar criaturas a otro ejército",
+	"vcmi.radialWheel.splitUnit" : "Dividir criatura a otra ranura",
+
+	"vcmi.radialWheel.heroGetArmy" : "Obtener ejército de otro héroe",
+	"vcmi.radialWheel.heroSwapArmy" : "Intercambiar ejército con otro héroe",
+	"vcmi.radialWheel.heroExchange" : "intercambio entre héroes",
+	"vcmi.radialWheel.heroGetArtifacts" : "Obtener artefactos de otro héroe",
+	"vcmi.radialWheel.heroSwapArtifacts" : "Intercambiar artefactos con otro héroe",
+	"vcmi.radialWheel.heroDismiss" : "Despedir al héroe",
+
+	"vcmi.radialWheel.moveTop" : "Mover arriba",
+	"vcmi.radialWheel.moveUp" : "Mover hacia arriba",
+	"vcmi.radialWheel.moveDown" : "Mover hacia abajo",
+	"vcmi.radialWheel.moveBottom" : "Mover abajo",
+
+	"vcmi.spellBook.search" : "buscar...",
+
+	"vcmi.mainMenu.serverConnecting" : "Conectando...",
+	"vcmi.mainMenu.serverAddressEnter" : "Ingrese la dirección:",
+	"vcmi.mainMenu.serverConnectionFailed" : "Falló la conexión",
+	"vcmi.mainMenu.serverClosing" : "Cerrando...",
+	"vcmi.mainMenu.hostTCP" : "Crear juego TCP/IP",
+	"vcmi.mainMenu.joinTCP" : "Unirse a juego TCP/IP",
+	"vcmi.mainMenu.playerName" : "Jugador",
+
+	"vcmi.lobby.filepath" : "Ruta del archivo",
+	"vcmi.lobby.creationDate" : "Fecha de creación",
+	"vcmi.lobby.scenarioName" : "Nombre del escenario",
+	"vcmi.lobby.mapPreview" : "Vista previa del mapa",
+	"vcmi.lobby.noPreview" : "sin vista previa",
+	"vcmi.lobby.noUnderground" : "sin subterráneo",
+
+	"vcmi.client.errors.missingCampaigns" : "{Archivos de datos faltantes}\n\n¡No se encontraron los archivos de datos de las campañas! Quizás estés utilizando archivos de datos incompletos o dañados de Heroes 3. Por favor, reinstala los datos del juego.",
+	"vcmi.server.errors.existingProcess" : "Otro servidor VCMI está en ejecución. Por favor, termínalo antes de comenzar un nuevo juego.",
+	"vcmi.server.errors.modsToEnable"    : "{Se requieren los siguientes mods}",
+	"vcmi.server.errors.modsToDisable"   : "{Deben desactivarse los siguientes mods}",
+	"vcmi.server.confirmReconnect"       : "¿Quieres reconectar a la última sesión?",
+	"vcmi.server.errors.modNoDependency" : "Error al cargar el mod {'%s'}.\n Depende del mod {'%s'}, que no está activo.\n",
+	"vcmi.server.errors.modConflict" : "Error al cargar el mod {'%s'}.\n Conflicto con el mod activo {'%s'}.\n",
+	"vcmi.server.errors.unknownEntity" : "Error al cargar la partida guardada. ¡Se encontró una entidad desconocida '%s' en la partida guardada! Es posible que la partida no sea compatible con la versión actualmente instalada de los mods.",
 
 	"vcmi.settingsMainWindow.generalTab.hover" : "General",
 	"vcmi.settingsMainWindow.generalTab.help"     : "Cambiar a la pestaña de opciones generales, que contiene ajustes relacionados con el comportamiento general del juego",
@@ -46,12 +92,33 @@
 	"vcmi.systemOptions.otherGroup" : "Otras configuraciones", // actualmente no utilizada
 	"vcmi.systemOptions.townsGroup" : "Pantalla de la ciudad",
 
+	"vcmi.systemOptions.fullscreenBorderless.hover" : "Pantalla completa (sin bordes)",
+	"vcmi.systemOptions.fullscreenBorderless.help"  : "{Pantalla completa sin bordes}\n\nSi está seleccionado, VCMI se ejecutará en modo de pantalla completa sin bordes. En este modo, el juego siempre usará la misma resolución que el escritorio, ignorando la resolución seleccionada.",
+	"vcmi.systemOptions.fullscreenExclusive.hover"  : "Pantalla completa (exclusiva)",
+	"vcmi.systemOptions.fullscreenExclusive.help"   : "{Pantalla completa}\n\nSi está seleccionado, VCMI se ejecutará en modo de pantalla completa exclusiva. En este modo, el juego cambiará la resolución del monitor a la resolución seleccionada.",
 	"vcmi.systemOptions.resolutionButton.hover" : "Resolución: %wx%h",
-	"vcmi.systemOptions.resolutionButton.help"  : "{Seleccionar resolución}\n\n Cambia la resolución de la pantalla del juego. Se requiere reiniciar el juego para aplicar la nueva resolución.",
-	"vcmi.systemOptions.resolutionMenu.hover"   : "Seleccionar resolución",
-	"vcmi.systemOptions.resolutionMenu.help"    : "Cambia la resolución de la pantalla del juego.",
+	"vcmi.systemOptions.resolutionButton.help"  : "{Seleccionar Resolución}\n\nCambia la resolución de pantalla del juego.",
+	"vcmi.systemOptions.resolutionMenu.hover"   : "Seleccionar Resolución",
+	"vcmi.systemOptions.resolutionMenu.help"    : "Cambia la resolución de pantalla del juego.",
+	"vcmi.systemOptions.scalingButton.hover"   : "Escalado de interfaz: %p%",
+	"vcmi.systemOptions.scalingButton.help"    : "{Escalado de interfaz}\n\nCambia el escalado de la interfaz del juego.",
+	"vcmi.systemOptions.scalingMenu.hover"     : "Seleccionar Escalado de Interfaz",
+	"vcmi.systemOptions.scalingMenu.help"      : "Cambia el escalado de la interfaz del juego.",
+	"vcmi.systemOptions.longTouchButton.hover"   : "Intervalo de toque largo: %d ms", // Nota de traducción: "ms" = "milisegundos"
+	"vcmi.systemOptions.longTouchButton.help"    : "{Intervalo de toque largo}\n\nCuando se utiliza una pantalla táctil, las ventanas emergentes aparecerán después de tocar la pantalla durante la duración especificada, en milisegundos.",
+	"vcmi.systemOptions.longTouchMenu.hover"     : "Seleccionar Intervalo de Toque Largo",
+	"vcmi.systemOptions.longTouchMenu.help"      : "Cambia la duración del intervalo de toque largo.",
+	"vcmi.systemOptions.longTouchMenu.entry"     : "%d milisegundos",
 	"vcmi.systemOptions.framerateButton.hover"  : "Mostrar FPS",
-	"vcmi.systemOptions.framerateButton.help"   : "{Mostrar FPS}\n\n Muestra el contador de Frames Por Segundo en la esquina de la ventana del juego.",
+	"vcmi.systemOptions.framerateButton.help"   : "{Mostrar FPS}\n\nAlternar la visibilidad del contador de Frames Per Second en la esquina de la ventana del juego.",
+	"vcmi.systemOptions.hapticFeedbackButton.hover"  : "Retroalimentación háptica",
+	"vcmi.systemOptions.hapticFeedbackButton.help"   : "{Retroalimentación háptica}\n\nAlternar la retroalimentación háptica en las entradas táctiles.",
+	"vcmi.systemOptions.enableUiEnhancementsButton.hover"  : "Mejoras de la interfaz",
+	"vcmi.systemOptions.enableUiEnhancementsButton.help"   : "{Mejoras de la interfaz}\n\nAlternar diversas mejoras de interfaz de calidad de vida. Como un botón de mochila, etc. Desactiva para tener una experiencia más clásica.",
+	"vcmi.systemOptions.enableLargeSpellbookButton.hover"  : "Libro de hechizos grande",
+	"vcmi.systemOptions.enableLargeSpellbookButton.help"   : "{Libro de hechizos grande}\n\nPermite un libro de hechizos más grande que permite más hechizos por página. La animación de cambio de página del libro de hechizos no funciona con esta configuración habilitada.",
+	"vcmi.systemOptions.audioMuteFocus.hover"  : "Silenciar con inactividad",
+	"vcmi.systemOptions.audioMuteFocus.help"   : "{Silenciar con inactividad}\n\nSilenciar el audio cuando la ventana no está activa. Las excepciones son los mensajes en el juego y el sonido de turno nuevo.",
 
 	"vcmi.adventureOptions.infoBarPick.hover" : "Mostrar mensajes en el panel de información",
 	"vcmi.adventureOptions.infoBarPick.help" : "{Mostrar mensajes en el panel de información}\n\nSiempre que sea posible, los mensajes del juego sobre los objetos del mapa que se visiten se mostrarán en el panel de información, en lugar de aparecer en una ventana separada.",
@@ -61,6 +128,16 @@
 	"vcmi.adventureOptions.forceMovementInfo.help": "{Mostrar siempre el coste de movimiento}\n\n Reemplaza la información predeterminada de la barra de estado con datos de puntos de movimiento sin necesidad de mantener presionado el botón ALT.",
 	"vcmi.adventureOptions.showGrid.hover": "Mostrar cuadrícula",
 	"vcmi.adventureOptions.showGrid.help": "{Mostrar cuadrícula}\n\n Muestra una superposición de cuadrícula que muestra las fronteras entre las casillas del mapa de aventuras.",
+	"vcmi.adventureOptions.borderScroll.hover" : "Desplazamiento de borde",
+	"vcmi.adventureOptions.borderScroll.help" : "{Desplazamiento de borde}\n\nDesplaza el mapa de aventuras cuando el cursor está adyacente al borde de la ventana. Puede desactivarse manteniendo presionada la tecla CTRL.",
+	"vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Gestión de criaturas en el panel de información",
+	"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Gestión de criaturas en el panel de información}\n\nPermite reorganizar criaturas en el panel de información en lugar de cambiar entre los componentes predeterminados.",
+	"vcmi.adventureOptions.leftButtonDrag.hover" : "Arrastrar mapa con clic izquierdo",
+	"vcmi.adventureOptions.leftButtonDrag.help" : "{Arrastrar mapa con clic izquierdo}\n\nCuando está habilitado, mover el ratón con el botón izquierdo presionado arrastrará la vista del mapa de aventuras.",
+	"vcmi.adventureOptions.smoothDragging.hover" : "Arrastre suave del mapa",
+	"vcmi.adventureOptions.smoothDragging.help" : "{Arrastre suave del mapa}\n\nCuando está habilitado, el arrastre del mapa tiene un efecto de deslizamiento moderno.",
+	"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Omitir efectos de desvanecimiento",
+	"vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Omitir efectos de desvanecimiento}\n\nCuando está habilitado, omite el desvanecimiento de objetos y otros efectos similares (recolección de recursos, embarque en barco, etc.). Hace que la interfaz sea más reactiva en algunos casos a expensas de la estética. Especialmente útil en juegos JcJ. Para obtener la máxima velocidad de movimiento, la omisión está activa independientemente de esta configuración.",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
@@ -85,68 +162,166 @@
 	"vcmi.battleOptions.animationsSpeed6.help": "Establece la velocidad de animación como instantánea.",
 	"vcmi.battleOptions.movementHighlightOnHover.hover": "Resaltado de movimiento al pasar el ratón",
 	"vcmi.battleOptions.movementHighlightOnHover.help": "{Resaltado de movimiento al pasar el ratón}\n\nResalta el rango de movimiento de la unidad cuando el cursor esta sobre esta.",
+	"vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Mostrar límites de alcance para tiradores",
+	"vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Mostrar límites de alcance para tiradores al pasar el ratón}\n\nMuestra los límites de alcance de los tiradores al pasar el ratón sobre ellos.",
+	"vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Mostrar ventanas de estadísticas de héroes",
+	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Mostrar ventanas de estadísticas de héroes}\n\nAlternar permanentemente las ventanas de estadísticas de héroes que muestran estadísticas primarias y puntos de hechizo.",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Omitir música de introducción",
-	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Omitir música de introducción}\n\n Omitir la breve música que se reproduce al comienzo de cada batalla antes de que comience la acción. También se puede omitir presionando la tecla ESC.",
-	"vcmi.battleWindow.pressKeyToSkipIntro" : "Presiona una tecla para comenzar la batalla inmediatamente.",
+	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Omitir música de introducción}\n\nPermitir acciones durante la música de introducción que se reproduce al comienzo de cada batalla.",
+
+	"vcmi.adventureMap.revisitObject.hover" : "Revisitar objeto",
+	"vcmi.adventureMap.revisitObject.help" : "{Revisitar objeto}\n\nSi un héroe se encuentra actualmente en un objeto del mapa, puede volver a visitar la ubicación.",
 
-	"vcmi.battleWindow.damageEstimation.melee" : "Ataque %CREATURE (%DAMAGE).",
-	"vcmi.battleWindow.damageEstimation.meleeKills" : "Ataque %CREATURE (%DAMAGE, %KILLS).",
-	"vcmi.battleWindow.damageEstimation.ranged" : "Disparo %CREATURE (%SHOTS, %DAMAGE).",
-	"vcmi.battleWindow.damageEstimation.rangedKills" : "Disparo %CREATURE (%SHOTS, %DAMAGE, %KILLS).",
+	"vcmi.battleWindow.pressKeyToSkipIntro" : "Presiona cualquier tecla para empezar la batalla inmediatamente",
+	"vcmi.battleWindow.damageEstimation.melee" : "Atacar %CREATURE (%DAÑO).",
+	"vcmi.battleWindow.damageEstimation.meleeKills" : "Atacar %CREATURE (%DAÑO, %BAJAS).",
+	"vcmi.battleWindow.damageEstimation.ranged" : "Disparar a %CREATURE (%DISPAROS, %DAÑO).",
+	"vcmi.battleWindow.damageEstimation.rangedKills" : "Disparar a %CREATURE (%DISPAROS, %DAÑO, %BAJAS).",
 	"vcmi.battleWindow.damageEstimation.shots" : "%d disparos restantes",
-	"vcmi.battleWindow.damageEstimation.shots.1" : "%d disparos restantes",
+	"vcmi.battleWindow.damageEstimation.shots.1" : "%d disparo restante",
 	"vcmi.battleWindow.damageEstimation.damage" : "%d daño",
 	"vcmi.battleWindow.damageEstimation.damage.1" : "%d daño",
 	"vcmi.battleWindow.damageEstimation.kills" : "%d perecerán",
-	"vcmi.battleWindow.damageEstimation.kills.1" : "%d perecerán",
+	"vcmi.battleWindow.damageEstimation.kills.1" : "%d perecerá",
 
 	"vcmi.battleResultsWindow.applyResultsLabel" : "Aplicar resultado de la batalla",
 
-	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover": "Mostrar criaturas disponibles",
-	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help": "{Mostrar criaturas disponibles}\n\n Muestra las criaturas disponibles para comprar en lugar de su crecimiento en el resumen de la ciudad (esquina inferior izquierda).",
+	"vcmi.tutorialWindow.title" : "Introducción a la pantalla táctil",
+	"vcmi.tutorialWindow.decription.RightClick" : "Toca y mantén presionado el elemento en el que quieres hacer clic derecho. Toca el área libre para cerrar.",
+	"vcmi.tutorialWindow.decription.MapPanning" : "Toca y arrastra con un dedo para mover el mapa.",
+	"vcmi.tutorialWindow.decription.MapZooming" : "Hace zoom con dos dedos para cambiar el zoom del mapa.",
+	"vcmi.tutorialWindow.decription.RadialWheel" : "Deslizar abre la rueda radial para diversas acciones, como la gestión de criaturas/héroes y el pedido de ciudad.",
+	"vcmi.tutorialWindow.decription.BattleDirection" : "Para atacar desde una dirección específica, desliza en la dirección desde la cual se va a realizar el ataque.",
+	"vcmi.tutorialWindow.decription.BattleDirectionAbort" : "El gesto de dirección de ataque se puede cancelar si el dedo está lo suficientemente alejado.",
+	"vcmi.tutorialWindow.decription.AbortSpell" : "Toca y mantén presionado para cancelar un hechizo.",
+
+	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Mostrar criaturas disponibles",
+	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Mostrar criaturas disponibles}\n\nMuestra la cantidad de criaturas disponibles para comprar en lugar de su crecimiento en el resumen de la ciudad (esquina inferior izquierda de la pantalla de la ciudad).",
 	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover": "Mostrar crecimiento semanal de criaturas",
-	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.help": "{Mostrar crecimiento semanal de criaturas}\n\n Muestra el crecimiento semanal de las criaturas en lugar de la cantidad disponible en el resumen de la ciudad (esquina inferior izquierda).",
+	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.help": "{Mostrar crecimiento semanal de criaturas}\n\nMuestra el crecimiento semanal de las criaturas en lugar de la cantidad disponible en el resumen de la ciudad (esquina inferior izquierda de la pantalla de la ciudad).",
 	"vcmi.otherOptions.compactTownCreatureInfo.hover": "Información compacta de criaturas de la ciudad",
-	"vcmi.otherOptions.compactTownCreatureInfo.help": "{Información compacta de criaturas de la ciudad}\n\n Información más pequeña de las criaturas de la ciudad en el resumen de la ciudad.",
-
-	"vcmi.townHall.missingBase": "El edificio base %s debe ser construido primero",
-	"vcmi.townHall.noCreaturesToRecruit": "¡No hay criaturas para reclutar!",
-	"vcmi.townHall.greetingManaVortex": "A medida que te acercas a %s, tu cuerpo se llena de nueva energía. Has duplicado tus puntos de magia normales.",
-	"vcmi.townHall.greetingKnowledge": "Estudias los glifos en %s y obtienes una visión de cómo funcionan varias magias (+1 Conocimiento).",
-	"vcmi.townHall.greetingSpellPower": "%s te enseña nuevas formas de enfocar tus poderes mágicos (+1 Poder).",
-	"vcmi.townHall.greetingExperience": "Una visita a %s te enseña muchas habilidades nuevas (+1000 experiencia).",
-	"vcmi.townHall.greetingAttack": "El tiempo pasado en %s te permite aprender habilidades de combate más efectivas (+1 habilidad de Ataque).",
-	"vcmi.townHall.greetingDefence": "Pasando tiempo en %s, los guerreros experimentados allí te enseñan habilidades defensivas adicionales (+1 Defensa).",
-	"vcmi.townHall.hasNotProduced": "%s no ha producido nada todavía.",
-	"vcmi.townHall.hasProduced": "%s produjo %d %s esta semana.",
-	"vcmi.townHall.greetingCustomBonus": "%s te da +%d %s%s",
-	"vcmi.townHall.greetingCustomUntil": " hasta la próxima batalla.",
-	"vcmi.townHall.greetingInTownMagicWell": "%s ha restaurado tus puntos de magia al máximo.",
-
-	"vcmi.logicalExpressions.anyOf"  : "Cualquiera de los siguientes:",
-	"vcmi.logicalExpressions.allOf"  : "Todos los siguientes:",
-	"vcmi.logicalExpressions.noneOf" : "Ninguno de los siguientes:",
-
-	"vcmi.heroWindow.openCommander.hover" : "Abrir ventana de comandante",
-	"vcmi.heroWindow.openCommander.help"  : "Muestra información sobre el comandante de este héroe",
+	"vcmi.otherOptions.compactTownCreatureInfo.help": "{Información compacta de criaturas de la ciudad}\n\nMuestra información más pequeña para las criaturas de la ciudad en el resumen de la ciudad (esquina inferior izquierda de la pantalla de la ciudad).",
+
+	"vcmi.townHall.missingBase"             : "Primero se debe construir el edificio base %s",
+	"vcmi.townHall.noCreaturesToRecruit"    : "¡No hay criaturas para reclutar!",
+	"vcmi.townHall.greetingManaVortex"      : "Al acercarte a %s, tu cuerpo se llena de nueva energía. Has duplicado tus puntos de hechizo normales.",
+	"vcmi.townHall.greetingKnowledge"       : "Estudias los glifos en %s y obtienes una visión de los entresijos de varias magias (+1 conocimiento).",
+	"vcmi.townHall.greetingSpellPower"      : "El %s te enseña nuevas formas de enfocar tus poderes mágicos (+1 Poder).",
+	"vcmi.townHall.greetingExperience"      : "Una visita a %s te enseña muchas habilidades nuevas (+1000 Experiencia).",
+	"vcmi.townHall.greetingAttack"          : "El tiempo dedicado en %s te permite aprender habilidades de combate más efectivas (+1 habilidad de ataque).",
+	"vcmi.townHall.greetingDefence"         : "Pasando tiempo en %s, los guerreros experimentados allí te enseñan habilidades defensivas adicionales (+1 Defensa).",
+	"vcmi.townHall.hasNotProduced"          : "%s aún no ha producido nada.",
+	"vcmi.townHall.hasProduced"             : "%s ha producido %d %s esta semana.",
+	"vcmi.townHall.greetingCustomBonus"     : "%s te da +%d %s%s",
+	"vcmi.townHall.greetingCustomUntil"     : " hasta la próxima batalla.",
+	"vcmi.townHall.greetingInTownMagicWell" : "%s ha restaurado tus puntos de hechizo al máximo.",
+
+	"vcmi.logicalExpressions.anyOf"  : "Cualquiera de lo siguiente:",
+	"vcmi.logicalExpressions.allOf"  : "Todo lo siguiente:",
+	"vcmi.logicalExpressions.noneOf" : "Ninguno de lo siguiente:",
+
+	"vcmi.heroWindow.openCommander.hover" : "Abrir ventana de información del comandante",
+	"vcmi.heroWindow.openCommander.help"  : "Muestra detalles sobre el comandante de este héroe.",
+	"vcmi.heroWindow.openBackpack.hover" : "Abrir ventana de mochila de artefactos",
+	"vcmi.heroWindow.openBackpack.help"  : "Abre la ventana que facilita la gestión de la mochila de artefactos.",
 
 	"vcmi.commanderWindow.artifactMessage" : "¿Quieres devolver este artefacto al héroe?",
 
 	"vcmi.creatureWindow.showBonuses.hover"    : "Cambiar a vista de bonificaciones",
-	"vcmi.creatureWindow.showBonuses.help"     : "Muestra todas las bonificaciones activas del comandante",
+	"vcmi.creatureWindow.showBonuses.help"     : "Mostrar todas las bonificaciones activas del comandante.",
 	"vcmi.creatureWindow.showSkills.hover"     : "Cambiar a vista de habilidades",
-	"vcmi.creatureWindow.showSkills.help"      : "Muestra todas las habilidades aprendidas del comandante",
+	"vcmi.creatureWindow.showSkills.help"      : "Mostrar todas las habilidades aprendidas del comandante.",
 	"vcmi.creatureWindow.returnArtifact.hover" : "Devolver artefacto",
-	"vcmi.creatureWindow.returnArtifact.help"  : "Usa este botón para devolver un artefacto del almacen al inventario del héroe",
+	"vcmi.creatureWindow.returnArtifact.help"  : "Haz clic en este botón para devolver el artefacto a la mochila del héroe.",
 
-	"vcmi.questLog.hideComplete.hover" : "Ocultar misiones completas",
-	"vcmi.questLog.hideComplete.help" : "Ocultar todas las misiones que ya han sido completadas",
+	"vcmi.questLog.hideComplete.hover" : "Ocultar misiones completadas",
+	"vcmi.questLog.hideComplete.help"  : "Ocultar todas las misiones completadas.",
 
+	"vcmi.randomMapTab.widgets.randomTemplate"      : "(Aleatorio)",
 	"vcmi.randomMapTab.widgets.templateLabel"        : "Plantilla",
-	"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Configurar...",
+	"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Configuración...",
 	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Alineaciones de equipos",
 	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Tipos de caminos",
 
+	"vcmi.optionsTab.turnOptions.hover" : "Opciones de turno",
+	"vcmi.optionsTab.turnOptions.help" : "Seleccionar temporizador de turno y opciones de turnos simultáneos",
+	"vcmi.optionsTab.selectPreset" : "Preconfigurado",
+
+	"vcmi.optionsTab.chessFieldBase.hover" : "Cronómetro base",
+	"vcmi.optionsTab.chessFieldTurn.hover" : "Cronómetro de turno",
+	"vcmi.optionsTab.chessFieldBattle.hover" : "Cronómetro de batalla",
+	"vcmi.optionsTab.chessFieldUnit.hover" : "Cronómetro de unidad",
+	"vcmi.optionsTab.chessFieldBase.help" : "Se utiliza cuando el {Cronómetro de Turno} alcanza 0. Se establece una vez al inicio del juego. Al alcanzar cero, termina el turno actual. Cualquier combate en curso terminará con una derrota.",
+	"vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Se utiliza fuera de combate o cuando el {Cronómetro de Batalla} se agota. Se restablece cada turno. El tiempo restante se suma al {Temporizador Base} al final del turno.",
+	"vcmi.optionsTab.chessFieldTurnDiscard.help" : "Se utiliza fuera de combate o cuando el {Cronómetro de Batalla} se agota. Se restablece cada turno. Se pierde cualquier tiempo no utilizado.",
+	"vcmi.optionsTab.chessFieldBattle.help" : "Se utiliza en batallas con IA o en combates JcJ cuando el {Cronómetro de Unidad} se agota. Se restablece al inicio de cada combate.",
+	"vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Se utiliza al seleccionar la acción de una unidad en combate JcJ. El tiempo restante se suma al {Temporizador de Batalla} al final del turno de la unidad.",
+	"vcmi.optionsTab.chessFieldUnitDiscard.help" : "Se utiliza al seleccionar la acción de una unidad en combate JcJ. Se restablece al inicio del turno de cada unidad. Se pierde cualquier tiempo no utilizado.",
+
+	"vcmi.optionsTab.accumulate" : "Acumular",
+
+	"vcmi.optionsTab.simturnsTitle" : "Turnos simultáneos",
+	"vcmi.optionsTab.simturnsMin.hover" : "Al menos por",
+	"vcmi.optionsTab.simturnsMax.hover" : "Como máximo por",
+	"vcmi.optionsTab.simturnsAI.hover" : "(Experimental) Turnos simultáneos de la IA",
+	"vcmi.optionsTab.simturnsMin.help" : "Jugar simultáneamente durante el número especificado de días. Los contactos entre jugadores durante este período están bloqueados.",
+	"vcmi.optionsTab.simturnsMax.help" : "Jugar simultáneamente durante el número especificado de días o hasta el contacto con otro jugador.",
+	"vcmi.optionsTab.simturnsAI.help" : "{Turnos Simultáneos de IA}\nOpción experimental. Permite que los jugadores controlados por la IA actúen al mismo tiempo que el jugador humano cuando los turnos simultáneos están habilitados.",
+
+	"vcmi.optionsTab.turnTime.select"     : "Configuración del Cronómetro de turno",
+	"vcmi.optionsTab.turnTime.unlimited"  : "Tiempo de turno ilimitado",
+	"vcmi.optionsTab.turnTime.classic.1"  : "Cronómetro clásico: 1 minuto",
+	"vcmi.optionsTab.turnTime.classic.2"  : "Cronómetro clásico: 2 minutos",
+	"vcmi.optionsTab.turnTime.classic.5"  : "Cronómetro clásico: 5 minutos",
+	"vcmi.optionsTab.turnTime.classic.10" : "Cronómetro clásico: 10 minutos",
+	"vcmi.optionsTab.turnTime.classic.20" : "Cronómetro clásico: 20 minutos",
+	"vcmi.optionsTab.turnTime.classic.30" : "Cronómetro clásico: 30 minutos",
+	"vcmi.optionsTab.turnTime.chess.20"   : "Ajedrez: 20:00 + 10:00 + 02:00 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.16"   : "Ajedrez: 16:00 + 08:00 + 01:30 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.8"    : "Ajedrez: 08:00 + 04:00 + 01:00 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.4"    : "Ajedrez: 04:00 + 02:00 + 00:30 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.2"    : "Ajedrez: 02:00 + 01:00 + 00:15 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.1"    : "Ajedrez: 01:00 + 01:00 + 00:00 + 00:00",
+
+	"vcmi.optionsTab.simturns.select"         : "Configuración de turnos simultáneos",
+	"vcmi.optionsTab.simturns.none"           : "Sin turnos simultáneos",
+	"vcmi.optionsTab.simturns.tillContactMax" : "Turnos simultáneos: Hasta contactar",
+	"vcmi.optionsTab.simturns.tillContact1"   : "Turnos simultáneos: 1 semana, interrupción al contactar",
+	"vcmi.optionsTab.simturns.tillContact2"   : "Turnos simultáneos: 2 semanas, interrupción al contactar",
+	"vcmi.optionsTab.simturns.tillContact4"   : "Turnos simultáneos: 1 mes, interrupción al contactar",
+	"vcmi.optionsTab.simturns.blocked1"       : "Turnos simultáneos: 1 semana, contactos bloqueados",
+	"vcmi.optionsTab.simturns.blocked2"       : "Turnos simultáneos: 2 semanas, contactos bloqueados",
+	"vcmi.optionsTab.simturns.blocked4"       : "Turnos simultáneos: 1 mes, contactos bloqueados",
+
+	// Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language
+	// Using this information, VCMI will automatically select correct plural form for every possible amount
+	"vcmi.optionsTab.simturns.days.0" : " %d días",
+	"vcmi.optionsTab.simturns.days.1" : " %d día",
+	"vcmi.optionsTab.simturns.days.2" : " %d días",
+	"vcmi.optionsTab.simturns.weeks.0" : " %d semanas",
+	"vcmi.optionsTab.simturns.weeks.1" : " %d semana",
+	"vcmi.optionsTab.simturns.weeks.2" : " %d semanas",
+	"vcmi.optionsTab.simturns.months.0" : " %d meses",
+	"vcmi.optionsTab.simturns.months.1" : " %d mes",
+	"vcmi.optionsTab.simturns.months.2" : " %d meses",
+
+	"vcmi.optionsTab.extraOptions.hover" : "Opciones extra",
+	"vcmi.optionsTab.extraOptions.help" : "Opciones adicionales para el juego",
+
+	"vcmi.optionsTab.cheatAllowed.hover" : "Permitir trampas",
+	"vcmi.optionsTab.unlimitedReplay.hover" : "Repetición de batalla ilimitada",
+	"vcmi.optionsTab.cheatAllowed.help" : "{Permitir trampas}\nPermite la introducción de trucos durante el juego.",
+	"vcmi.optionsTab.unlimitedReplay.help" : "{Repetición de batalla ilimitada}\nSin límite de repetición de batallas.",
+
+	// Custom victory conditions for H3 campaigns and HotA maps
+	"vcmi.map.victoryCondition.daysPassed.toOthers" : "El enemigo ha logrado sobrevivir hasta hoy. ¡La victoria es suya!",
+	"vcmi.map.victoryCondition.daysPassed.toSelf" : "¡Felicidades! Has logrado sobrevivir. ¡La victoria es tuya!",
+	"vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "El enemigo ha derrotado a todas las criaturas que asolaban esta tierra y reclama la victoria.",
+	"vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "¡Felicidades! Has derrotado a todas las criaturas que asolaban esta tierra y puedes reclamar la victoria.",
+	"vcmi.map.victoryCondition.collectArtifacts.message" : "Adquirir tres artefactos",
+	"vcmi.map.victoryCondition.angelicAlliance.toSelf" : "¡Felicidades! Todos tus enemigos han sido derrotados y tienes la Alianza Angelical. ¡La victoria es tuya!",
+	"vcmi.map.victoryCondition.angelicAlliance.message" : "Derrota a todos los enemigos y crea la Alianza Angelical",
+
 	// few strings from WoG used by vcmi
 	"vcmi.stackExperience.description" : "» D e t a l l e s  d e  E x p e r i e n c i a  d e l  G r u p o «\n\nTipo de Criatura ................ : %s\nRango de Experiencia ............ : %s (%i)\nPuntos de Experiencia ............ : %i\nPuntos de Experiencia para el\nSiguiente Rango ............... : %i\nExperiencia Máxima por Batalla .. : %i%% (%i)\nNúmero de Criaturas en el grupo .. : %i\nMáximo de Nuevos Reclutas sin\nPerder el Rango Actual ......... : %i\nMultiplicador de Experiencia .... : %.2f\nMultiplicador de Actualización .. : %.2f\nExperiencia después del Rango 10 : %i\nMáximo de Nuevos Reclutas para\nMantener el Rango 10 si\nEstá en la Experiencia Máxima : %i",
 	"vcmi.stackExperience.rank.0" : "Básico",
@@ -297,8 +472,8 @@
 	"core.bonus.TRANSMUTATION.description": "${val}% de probabilidad de transformar la unidad atacada en otro tipo",
 	"core.bonus.UNDEAD.name": "No muerto",
 	"core.bonus.UNDEAD.description": "La criatura es un no muerto",
-	"core.bonus.UNLIMITED_RETALIATIONS.name": "Retaliaciones ilimitadas",
-	"core.bonus.UNLIMITED_RETALIATIONS.description": "Puede realizar un número ilimitado de ataques de represalia",
+	"core.bonus.UNLIMITED_RETALIATIONS.name": "Contraataques ilimitados",
+	"core.bonus.UNLIMITED_RETALIATIONS.description": "Puede realizar un número ilimitado de contraataques",
 	"core.bonus.WATER_IMMUNITY.name": "Inmunidad al agua",
 	"core.bonus.WATER_IMMUNITY.description": "Inmune a todos los hechizos de la escuela del agua",
 	"core.bonus.WIDE_BREATH.name": "Aliento amplio",

+ 7 - 0
Mods/vcmi/config/vcmi/ukrainian.json

@@ -70,6 +70,7 @@
 	"vcmi.lobby.mapPreview" : "Огляд мапи",
 	"vcmi.lobby.noPreview" : "огляд недоступний",
 	"vcmi.lobby.noUnderground" : "немає підземелля",
+	"vcmi.lobby.sortDate" : "Сортувати мапи за датою зміни",
 
 	"vcmi.client.errors.missingCampaigns" : "{Не вистачає файлів даних}\n\nФайли даних кампаній не знайдено! Можливо, ви використовуєте неповні або пошкоджені файли даних Heroes 3. Будь ласка, перевстановіть дані гри.",
 	"vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його",
@@ -144,6 +145,8 @@
 	"vcmi.adventureOptions.mapScrollSpeed1.help": "Встановити швидкість розгортання мапи - дуже повільно",
 	"vcmi.adventureOptions.mapScrollSpeed5.help": "Встановити швидкість розгортання мапи - дуже швидко",
 	"vcmi.adventureOptions.mapScrollSpeed6.help": "Встановити швидкість розгортання мапи - миттєво",
+	"vcmi.adventureOptions.hideBackground.hover" : "Приховувати тло",
+	"vcmi.adventureOptions.hideBackground.help" : "{Приховувати тло}\n\nПриховати мапу пригод на задньому тлі і показати замість неї текстуру.",
 
 	"vcmi.battleOptions.queueSizeLabel.hover": "Вигляд черги ходу істот",
 	"vcmi.battleOptions.queueSizeNoneButton.hover": "ВИМК",
@@ -183,6 +186,10 @@
 	"vcmi.battleWindow.damageEstimation.damage.1" : "%d одиниця пошкодження",
 	"vcmi.battleWindow.damageEstimation.kills" : "%d загинуть",
 	"vcmi.battleWindow.damageEstimation.kills.1" : "%d загине",
+	"vcmi.battleWindow.killed" : "Загинуло",
+	"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s було вбито влучними пострілами!",
+	"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s було вбито влучним пострілом!",
+	"vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s було вбито влучними пострілами!",
 
 	"vcmi.battleResultsWindow.applyResultsLabel" : "Прийняти результат бою",
 

+ 2 - 2
android/vcmi-app/build.gradle

@@ -10,8 +10,8 @@ android {
 		applicationId "is.xyz.vcmi"
 		minSdk 19
 		targetSdk 33
-		versionCode 1430
-		versionName "1.4.3"
+		versionCode 1500
+		versionName "1.5.0"
 		setProperty("archivesBaseName", "vcmi")
 	}
 

+ 71 - 0
android/vcmi-app/src/main/res/values-es/strings.xml

@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="url_project_page" translatable="false">https://vcmi.eu</string>
+    <string name="url_project_repo" translatable="false">https://github.com/vcmi/vcmi</string>
+    <string name="url_launcher_repo" translatable="false">https://github.com/vcmi/vcmi-android</string>
+    <string name="url_launcher_privacy" translatable="false">https://github.com/vcmi/vcmi/blob/master/docs/players/Privacy_Policy.md</string>
+
+    <string name="app_name">VCMI</string>
+    <string name="server_name">VCMI Servidor</string>
+    <string name="launcher_title">VCMI Lanzador</string>
+    <string name="launcher_btn_scale_title">Escalado de resolución</string>
+    <string name="launcher_btn_scale_subtitle_unknown">Seleccionado: desconocido</string>
+    <string name="launcher_btn_scale_subtitle">Seleccionado: %1$d%%</string>
+    <string name="launcher_btn_start_title">Iniciar VCMI</string>
+    <string name="launcher_btn_start_subtitle">version actual VCMI: %1$s</string>
+    <string name="launcher_btn_mods_title">Mods</string>
+    <string name="launcher_btn_mods_subtitle">Instalar nuevas facciones, Objectos, extras</string>
+    <string name="launcher_btn_language_title">Idioma</string>
+    <string name="launcher_btn_language_subtitle_unknown">Seleccionado: desconocido</string>
+    <string name="launcher_btn_language_subtitle">Seleccionado: %1$s</string>
+    <string name="launcher_btn_pointermode_title">Cambiar modo del puntero</string>
+    <string name="launcher_btn_pointermode_subtitle">Seleccionado: %1$s</string>
+    <string name="launcher_btn_pointermulti_title">Múltiplo de velocidad relativa del puntero</string>
+    <string name="launcher_btn_pointermulti_subtitle">Seleccionado: %1$s</string>
+    <string name="launcher_btn_sound_title">Sonido</string>
+    <string name="launcher_btn_music_title">Música</string>
+    <string name="launcher_btn_adventure_ai">Adventure AI</string>
+    <string name="launcher_btn_adventure_ai_title">Change adventure AI</string>
+    <string name="launcher_btn_import_title">Importar datos VCMI</string>
+    <string name="launcher_btn_import_description">Copiar ficheros VCMI al almacenamiento interno. Puedes importar datos antiguos a partir de la version 0.99 de VCMI o los ficheros de HOMM3.</string>
+    <string name="launcher_btn_export_title">Exportar datos VCMI</string>
+    <string name="launcher_btn_export_description">Hacer una copia interna de los datos VCMI antes de una desinstalación o sincronizar las partidas de la versión de escritorio. Tambien puedes acceder al almacenamiento interno directamente.</string>
+    <string name="launcher_progress_copy">Copiando %1$s</string>
+    <string name="launcher_version">Version del lanzador: %1$s</string>
+    <string name="launcher_error_vcmi_data_root_failed">No se puede crear la carpeta de datos VCMI en %1$s.</string>
+    <string name="launcher_error_h3_data_missing">No se encuentra la carpeta de datos en \'%1$s\'. Coloca tus ficheros de datos de HoMM3 en la carpeta o utiliza el boton de abajo para que se haga automáticamente. Quizás sea necesario reiniciar la aplicación.</string>
+    <string name="launcher_error_vcmi_data_internal_missing">No se pudieron encontrar ni extraer datos vcmi de los recursos de la aplicación. Intente reinstalar la aplicación.</string>
+    <string name="launcher_error_vcmi_data_internal_update">No se pudieron actualizar los datos de vcmi desde los recursos de la aplicación. Intente reinstalar la aplicación.</string>
+    <string name="launcher_error_permissions">Esta aplicación necesita permisos de escritura para utilizar el contenido almacenado en un almacenamiento externo.</string>
+    <string name="launcher_error_permission_broken">Los permisos no se pueden resolver correctamente</string>
+    <string name="mods_item_author_template">por %1$s</string>
+    <string name="misc_try_again">Volver a intentar</string>
+    <string name="launcher_section_init">Iniciar juego</string>
+    <string name="launcher_section_settings">Configuración</string>
+    <string name="menu_mods_download_repo">Descargar datos del repositorio</string>
+
+    <string name="misc_pointermode_normal">Normal</string>
+    <string name="misc_pointermode_relative">Relativo</string>
+    <string name="menu_launcher_about">Acerca</string>
+
+    <string name="mods_title">Mods detectados</string>
+    <string name="mods_failed_mod_loading">No se puede cargar el Mod en la carpeta \'%1$s\' </string>
+    <string name="mods_removal_title">Eliminando %1$s</string>
+    <string name="mods_removal_confirmation">Quieres eliminar %1$s</string>
+
+    <string name="about_title">Acerca de la applicación</string>
+    <string name="about_version_app">App versión: %1$s</string>
+    <string name="about_version_launcher">Versión del lanzador: %1$s</string>
+    <string name="about_section_project">Proyecto</string>
+    <string name="about_section_legal">Legal</string>
+    <string name="about_links_main">Página principal: %1$s</string>
+    <string name="about_links_repo">Repositorio del Proyecto: %1$s</string>
+    <string name="about_links_repo_launcher">Repositorio del lanzador: %1$s</string>
+    <string name="about_btn_authors">Autores</string>
+    <string name="about_btn_privacy">Política de privacidad: %1$s</string>
+    <string name="about_error_opening_url">No se pudo abrir la página web (no se encontró una aplicacion adecuada)</string>
+
+    <string name="dialog_authors_vcmi">VCMI autores</string>
+    <string name="dialog_authors_launcher">Lanzador autores</string>
+    <string name="launcher_error_config_saving_failed">No se puede guardar la configuración de VCMI; motivo: %1$s</string>
+</resources>

+ 1 - 4
client/CGameInfo.cpp

@@ -12,16 +12,13 @@
 
 #include "../lib/VCMI_Lib.h"
 
-const CGameInfo * CGI;
+CGameInfo * CGI;
 CClientState * CCS = nullptr;
 CServerHandler * CSH;
 
 
 CGameInfo::CGameInfo()
 {
-	generaltexth = nullptr;
-	mh = nullptr;
-	townh = nullptr;
 	globalServices = nullptr;
 }
 

+ 16 - 15
client/CGameInfo.h

@@ -56,7 +56,7 @@ extern CClientState * CCS;
 
 /// CGameInfo class
 /// for allowing different functions for accessing game informations
-class CGameInfo : public Services
+class CGameInfo final : public Services
 {
 public:
 	const ArtifactService * artifacts() const override;
@@ -78,19 +78,20 @@ public:
 	const spells::effects::Registry * spellEffects() const override;
 	spells::effects::Registry * spellEffects() override;
 
-	ConstTransitivePtr<CModHandler> modh; //public?
-	ConstTransitivePtr<BattleFieldHandler> battleFieldHandler;
-	ConstTransitivePtr<CHeroHandler> heroh;
-	ConstTransitivePtr<CCreatureHandler> creh;
-	ConstTransitivePtr<CSpellHandler> spellh;
-	ConstTransitivePtr<CSkillHandler> skillh;
-	ConstTransitivePtr<CObjectHandler> objh;
-	ConstTransitivePtr<TerrainTypeHandler> terrainTypeHandler;
-	ConstTransitivePtr<CObjectClassesHandler> objtypeh;
-	ConstTransitivePtr<ObstacleHandler> obstacleHandler;
-	CGeneralTextHandler * generaltexth;
-	CMapHandler * mh;
-	CTownHandler * townh;
+	std::shared_ptr<const CModHandler> modh;
+	std::shared_ptr<const BattleFieldHandler> battleFieldHandler;
+	std::shared_ptr<const CHeroHandler> heroh;
+	std::shared_ptr<const CCreatureHandler> creh;
+	std::shared_ptr<const CSpellHandler> spellh;
+	std::shared_ptr<const CSkillHandler> skillh;
+	std::shared_ptr<const CObjectHandler> objh;
+	std::shared_ptr<const TerrainTypeHandler> terrainTypeHandler;
+	std::shared_ptr<const CObjectClassesHandler> objtypeh;
+	std::shared_ptr<const ObstacleHandler> obstacleHandler;
+	std::shared_ptr<const CGeneralTextHandler> generaltexth;
+	std::shared_ptr<const CTownHandler> townh;
+
+	std::shared_ptr<CMapHandler> mh;
 
 	void setFromLib();
 
@@ -98,4 +99,4 @@ public:
 private:
 	const Services * globalServices;
 };
-extern const CGameInfo* CGI;
+extern CGameInfo* CGI;

+ 18 - 18
client/CMT.cpp

@@ -72,7 +72,7 @@ void init()
 	CStopWatch tmh;
 
 	loadDLLClasses();
-	const_cast<CGameInfo*>(CGI)->setFromLib();
+	CGI->setFromLib();
 
 	logGlobal->info("Initializing VCMI_Lib: %d ms", tmh.getDiff());
 
@@ -182,7 +182,8 @@ int main(int argc, char * argv[])
 	}
 
 	// Init old logging system and new (temporary) logging system
-	CStopWatch total, pomtime;
+	CStopWatch total;
+	CStopWatch pomtime;
 	std::cout.flags(std::ios::unitbuf);
 #ifndef VCMI_IOS
 	console = new CConsoleHandler();
@@ -429,15 +430,15 @@ void playIntro()
 {
 	auto audioData = CCS->videoh->getAudio(VideoPath::builtin("3DOLOGO.SMK"));
 	int sound = CCS->soundh->playSound(audioData);
-	if(CCS->videoh->openAndPlayVideo(VideoPath::builtin("3DOLOGO.SMK"), 0, 1, true, true))
+	if(CCS->videoh->openAndPlayVideo(VideoPath::builtin("3DOLOGO.SMK"), 0, 1, EVideoType::INTRO))
 	{
 		audioData = CCS->videoh->getAudio(VideoPath::builtin("NWCLOGO.SMK"));
 		sound = CCS->soundh->playSound(audioData);
-		if (CCS->videoh->openAndPlayVideo(VideoPath::builtin("NWCLOGO.SMK"), 0, 1, true, true))
+		if (CCS->videoh->openAndPlayVideo(VideoPath::builtin("NWCLOGO.SMK"), 0, 1, EVideoType::INTRO))
 		{
 			audioData = CCS->videoh->getAudio(VideoPath::builtin("H3INTRO.SMK"));
 			sound = CCS->soundh->playSound(audioData);
-			CCS->videoh->openAndPlayVideo(VideoPath::builtin("H3INTRO.SMK"), 0, 1, true, true);
+			CCS->videoh->openAndPlayVideo(VideoPath::builtin("H3INTRO.SMK"), 0, 1, EVideoType::INTRO);
 		}
 	}
 	CCS->soundh->stopSound(sound);
@@ -461,9 +462,9 @@ static void mainLoop()
 	{
 		if(CSH->client)
 			CSH->endGameplay();
-	}
 
-	GH.windows().clear();
+		GH.windows().clear();
+	}
 
 	CMM.reset();
 
@@ -523,25 +524,24 @@ void handleQuit(bool ask)
 	// FIXME: avoids crash if player attempts to close game while opening is still playing
 	// use cursor handler as indicator that loading is not done yet
 	// proper solution would be to abort init thread (or wait for it to finish)
+	if(!ask)
+	{
+		quitApplication();
+		return;
+	}
+
 	if (!CCS->curh)
 	{
 		quitRequestedDuringOpeningPlayback = true;
 		return;
 	}
 
-	if(ask)
-	{
-		CCS->curh->set(Cursor::Map::POINTER);
+	CCS->curh->set(Cursor::Map::POINTER);
 
-		if (LOCPLINT)
-			LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], quitApplication, nullptr);
-		else
-			CInfoWindow::showYesNoDialog(CGI->generaltexth->allTexts[69], {}, quitApplication, {}, PlayerColor(1));
-	}
+	if (LOCPLINT)
+		LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], quitApplication, nullptr);
 	else
-	{
-		quitApplication();
-	}
+		CInfoWindow::showYesNoDialog(CGI->generaltexth->allTexts[69], {}, quitApplication, {}, PlayerColor(1));
 }
 
 void handleFatalError(const std::string & message, bool terminate)

+ 10 - 4
client/CMakeLists.txt

@@ -52,6 +52,7 @@ set(client_SRCS
 	lobby/CScenarioInfoScreen.cpp
 	lobby/CSelectionBase.cpp
 	lobby/TurnOptionsTab.cpp
+	lobby/ExtraOptionsTab.cpp
 	lobby/OptionsTab.cpp
 	lobby/OptionsTabBase.cpp
 	lobby/RandomMapTab.cpp
@@ -95,14 +96,12 @@ set(client_SRCS
 	renderSDL/SDL_Extensions.cpp
 
 	widgets/Buttons.cpp
-	widgets/CAltar.cpp
 	widgets/CArtifactHolder.cpp
 	widgets/CComponent.cpp
 	widgets/CExchangeController.cpp
 	widgets/CGarrisonInt.cpp
 	widgets/CreatureCostBox.cpp
 	widgets/ComboBox.cpp
-	widgets/CTradeBase.cpp
 	widgets/Images.cpp
 	widgets/MiscWidgets.cpp
 	widgets/ObjectLists.cpp
@@ -117,6 +116,10 @@ set(client_SRCS
 	widgets/CArtifactsOfHeroBackpack.cpp
 	widgets/CWindowWithArtifacts.cpp
 	widgets/RadialMenu.cpp
+	widgets/markets/CAltarArtifacts.cpp
+	widgets/markets/CAltarCreatures.cpp
+	widgets/markets/CTradeBase.cpp
+	widgets/markets/TradePanels.cpp
 
 	windows/CAltarWindow.cpp
 	windows/CCastleInterface.cpp
@@ -215,6 +218,7 @@ set(client_HEADERS
 	lobby/CScenarioInfoScreen.h
 	lobby/CSelectionBase.h
 	lobby/TurnOptionsTab.h
+	lobby/ExtraOptionsTab.h
 	lobby/OptionsTab.h
 	lobby/OptionsTabBase.h
 	lobby/RandomMapTab.h
@@ -267,14 +271,12 @@ set(client_HEADERS
 	renderSDL/SDL_PixelAccess.h
 
 	widgets/Buttons.h
-	widgets/CAltar.h
 	widgets/CArtifactHolder.h
 	widgets/CComponent.h
 	widgets/CExchangeController.h
 	widgets/CGarrisonInt.h
 	widgets/CreatureCostBox.h
 	widgets/ComboBox.h
-	widgets/CTradeBase.h
 	widgets/Images.h
 	widgets/MiscWidgets.h
 	widgets/ObjectLists.h
@@ -289,6 +291,10 @@ set(client_HEADERS
 	widgets/CArtifactsOfHeroBackpack.h
 	widgets/CWindowWithArtifacts.h
 	widgets/RadialMenu.h
+	widgets/markets/CAltarArtifacts.h
+	widgets/markets/CAltarCreatures.h
+	widgets/markets/CTradeBase.h
+	widgets/markets/TradePanels.h
 
 	windows/CAltarWindow.h
 	windows/CCastleInterface.h

+ 29 - 0
client/CMusicHandler.cpp

@@ -183,6 +183,35 @@ void CSoundHandler::ambientStopSound(const AudioPath & soundId)
 	setChannelVolume(ambientChannels[soundId], volume);
 }
 
+uint32_t CSoundHandler::getSoundDurationMilliseconds(const AudioPath & sound)
+{
+	if (!initialized || sound.empty())
+		return 0;
+
+	auto resourcePath = sound.addPrefix("SOUNDS/");
+
+	if (!CResourceHandler::get()->existsResource(resourcePath))
+		return 0;
+
+	auto data = CResourceHandler::get()->load(resourcePath)->readAll();
+
+	SDL_AudioSpec spec;
+	uint32_t audioLen;
+	uint8_t *audioBuf;
+	uint32_t miliseconds = 0;
+
+	if(SDL_LoadWAV_RW(SDL_RWFromMem(data.first.get(), (int)data.second), 1, &spec, &audioBuf, &audioLen) != nullptr)
+	{
+		SDL_FreeWAV(audioBuf);
+		uint32_t sampleSize = SDL_AUDIO_BITSIZE(spec.format) / 8;
+		uint32_t sampleCount = audioLen / sampleSize;
+		uint32_t sampleLen = sampleCount / spec.channels;
+		miliseconds = 1000 * sampleLen / spec.freq;
+	}
+
+	return miliseconds ;
+}
+
 // Plays a sound, and return its channel so we can fade it out later
 int CSoundHandler::playSound(soundBase::soundID soundID, int repeats)
 {

+ 1 - 0
client/CMusicHandler.h

@@ -77,6 +77,7 @@ public:
 	void setChannelVolume(int channel, ui32 percent);
 
 	// Sounds
+	uint32_t getSoundDurationMilliseconds(const AudioPath & sound);
 	int playSound(soundBase::soundID soundID, int repeats=0);
 	int playSound(const AudioPath & sound, int repeats=0, bool cache=false);
 	int playSound(std::pair<std::unique_ptr<ui8 []>, si64> & data, int repeats=0, bool cache=false);

+ 46 - 16
client/CPlayerInterface.cpp

@@ -43,6 +43,7 @@
 
 #include "render/CAnimation.h"
 #include "render/IImage.h"
+#include "render/IRenderHandler.h"
 
 #include "widgets/Buttons.h"
 #include "widgets/CComponent.h"
@@ -82,7 +83,6 @@
 #include "../lib/UnlockGuard.h"
 #include "../lib/VCMIDirs.h"
 
-#include "../lib/bonuses/CBonusSystemNode.h"
 #include "../lib/bonuses/Limiters.h"
 #include "../lib/bonuses/Propagators.h"
 #include "../lib/bonuses/Updaters.h"
@@ -111,11 +111,7 @@
 // They all assume that interface mutex is locked.
 #define EVENT_HANDLER_CALLED_BY_CLIENT
 
-#define BATTLE_EVENT_POSSIBLE_RETURN	\
-	if (LOCPLINT != this)				\
-		return;							\
-	if (isAutoFightOn && !battleInt)	\
-		return;
+#define BATTLE_EVENT_POSSIBLE_RETURN	if (LOCPLINT != this) return; if (isAutoFightOn && !battleInt) return
 
 CPlayerInterface * LOCPLINT;
 
@@ -150,6 +146,7 @@ CPlayerInterface::CPlayerInterface(PlayerColor Player):
 	firstCall = 1; //if loading will be overwritten in serialize
 	autosaveCount = 0;
 	isAutoFightOn = false;
+	isAutoFightEndBattle = false;
 	ignoreEvents = false;
 	numOfMovedArts = 0;
 }
@@ -787,17 +784,20 @@ void CPlayerInterface::battleEnd(const BattleID & battleID, const BattleResult *
 
 		if(!battleInt)
 		{
-			bool allowManualReplay = queryID != QueryID::NONE;
+			bool allowManualReplay = queryID != QueryID::NONE && !isAutoFightEndBattle;
 
 			auto wnd = std::make_shared<BattleResultWindow>(*br, *this, allowManualReplay);
 
-			if (allowManualReplay)
+			if (allowManualReplay || isAutoFightEndBattle)
 			{
 				wnd->resultCallback = [=](ui32 selection)
 				{
 					cb->selectionMade(selection, queryID);
 				};
 			}
+			
+			isAutoFightEndBattle = false;
+
 			GH.windows().pushWindow(wnd);
 			// #1490 - during AI turn when quick combat is on, we need to display the message and wait for user to close it.
 			// Otherwise NewTurn causes freeze.
@@ -1069,6 +1069,19 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 
+	std::vector<ObjectInstanceID> tmpObjects;
+	if(objects.size() && dynamic_cast<const CGTownInstance *>(cb->getObj(objects[0])))
+	{
+		// sorting towns (like in client)
+		std::vector <const CGTownInstance*> Towns = LOCPLINT->localState->getOwnedTowns();
+		for(auto town : Towns)
+			for(auto item : objects)
+				if(town == cb->getObj(item))
+					tmpObjects.push_back(item);
+	}
+	else // other object list than town
+		tmpObjects = objects;
+
 	auto selectCallback = [=](int selection)
 	{
 		cb->sendQueryReply(selection, askID);
@@ -1083,9 +1096,9 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component
 	const std::string localDescription = description.toString();
 
 	std::vector<int> tempList;
-	tempList.reserve(objects.size());
+	tempList.reserve(tmpObjects.size());
 
-	for(auto item : objects)
+	for(auto item : tmpObjects)
 		tempList.push_back(item.getNum());
 
 	CComponent localIconC(icon);
@@ -1093,8 +1106,24 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component
 	std::shared_ptr<CIntObject> localIcon = localIconC.image;
 	localIconC.removeChild(localIcon.get(), false);
 
-	std::shared_ptr<CObjectListWindow> wnd = std::make_shared<CObjectListWindow>(tempList, localIcon, localTitle, localDescription, selectCallback);
+	std::vector<std::shared_ptr<IImage>> images;
+	for(auto & obj : tmpObjects)
+	{
+		if(!settings["general"]["enableUiEnhancements"].Bool())
+			break;
+		const CGTownInstance * t = dynamic_cast<const CGTownInstance *>(cb->getObj(obj));
+		if(t)
+		{
+			std::shared_ptr<CAnimation> a = GH.renderHandler().loadAnimation(AnimationPath::builtin("ITPA"));
+			a->preload();
+			images.push_back(a->getImage(t->town->clientInfo.icons[t->hasFort()][false] + 2)->scaleFast(Point(35, 23)));
+		}
+	}
+
+	auto wnd = std::make_shared<CObjectListWindow>(tempList, localIcon, localTitle, localDescription, selectCallback, 0, images);
 	wnd->onExit = cancelCallback;
+	wnd->onPopup = [this, tmpObjects](int index) { CRClickPopup::createAndPush(cb->getObj(tmpObjects[index]), GH.getCursorPosition()); };
+	wnd->onClicked = [this, tmpObjects](int index) { adventureInt->centerOnObject(cb->getObj(tmpObjects[index])); GH.windows().totalRedraw(); };
 	GH.windows().pushWindow(wnd);
 }
 
@@ -1155,16 +1184,16 @@ void CPlayerInterface::heroBonusChanged( const CGHeroInstance *hero, const Bonus
 	}
 }
 
-void CPlayerInterface::saveGame( BinarySerializer & h, const int version )
+void CPlayerInterface::saveGame( BinarySerializer & h )
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	localState->serialize(h, version);
+	localState->serialize(h);
 }
 
-void CPlayerInterface::loadGame( BinaryDeserializer & h, const int version )
+void CPlayerInterface::loadGame( BinaryDeserializer & h )
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	localState->serialize(h, version);
+	localState->serialize(h);
 	firstCall = -1;
 }
 
@@ -1676,7 +1705,8 @@ void CPlayerInterface::showTavernWindow(const CGObjectInstance * object, const C
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	auto onWindowClosed = [this, queryID](){
-		cb->selectionMade(0, queryID);
+		if (queryID != QueryID::NONE)
+			cb->selectionMade(0, queryID);
 	};
 	GH.windows().createAndPushWindow<CTavernWindow>(object, onWindowClosed);
 }

+ 3 - 2
client/CPlayerInterface.h

@@ -87,6 +87,7 @@ public: // TODO: make private
 	//During battle is quick combat mode is used
 	std::shared_ptr<CBattleGameInterface> autofightingAI; //AI that makes decisions
 	bool isAutoFightOn; //Flag, switch it to stop quick combat. Don't touch if there is no battle interface.
+	bool isAutoFightEndBattle; //Flag, if battle forced to end with autocombat
 
 protected: // Call-ins from server, should not be called directly, but only via GameInterface
 
@@ -145,8 +146,8 @@ protected: // Call-ins from server, should not be called directly, but only via
 	void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) override;
 	void playerStartsTurn(PlayerColor player) override; //called before yourTurn on active itnerface
 	void playerEndsTurn(PlayerColor player) override;
-	void saveGame(BinarySerializer & h, const int version) override; //saving
-	void loadGame(BinaryDeserializer & h, const int version) override; //loading
+	void saveGame(BinarySerializer & h) override; //saving
+	void loadGame(BinaryDeserializer & h) override; //loading
 	void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
 
 	//for battles

+ 22 - 4
client/CServerHandler.cpp

@@ -142,6 +142,8 @@ CServerHandler::CServerHandler()
 	registerTypesLobbyPacks(*applier);
 }
 
+CServerHandler::~CServerHandler() = default;
+
 void CServerHandler::resetStateForLobby(const StartInfo::EMode mode, const std::vector<std::string> * names)
 {
 	hostClientId = -1;
@@ -260,6 +262,9 @@ void CServerHandler::justConnectToServer(const std::string & addr, const ui16 po
 					addr.size() ? addr : getHostAddress(),
 					port ? port : getHostPort(),
 					NAME, uuid);
+
+			nextClient = std::make_unique<CClient>();
+			c->iser.cb = nextClient.get();
 		}
 		catch(std::runtime_error & error)
 		{
@@ -507,6 +512,13 @@ void CServerHandler::setTurnTimerInfo(const TurnTimerInfo & info) const
 	sendLobbyPack(lstt);
 }
 
+void CServerHandler::setExtraOptionsInfo(const ExtraOptionsInfo & info) const
+{
+	LobbySetExtraOptions lseo;
+	lseo.extraOptionsInfo = info;
+	sendLobbyPack(lseo);
+}
+
 void CServerHandler::sendMessage(const std::string & txt) const
 {
 	std::istringstream readed;
@@ -526,7 +538,8 @@ void CServerHandler::sendMessage(const std::string & txt) const
 	}
 	else if(command == "!forcep")
 	{
-		std::string connectedId, playerColorId;
+		std::string connectedId;
+		std::string playerColorId;
 		readed >> connectedId;
 		readed >> playerColorId;
 		if(connectedId.length() && playerColorId.length())
@@ -604,7 +617,9 @@ bool CServerHandler::validateGameStart(bool allowOnlyAI) const
 void CServerHandler::sendStartGame(bool allowOnlyAI) const
 {
 	verifyStateBeforeStart(allowOnlyAI ? true : settings["session"]["onlyai"].Bool());
-	GH.windows().createAndPushWindow<CLoadingScreen>();
+
+	if(!settings["session"]["headless"].Bool())
+		GH.windows().createAndPushWindow<CLoadingScreen>();
 	
 	LobbyStartGame lsg;
 	if(client)
@@ -628,7 +643,8 @@ void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameSta
 {
 	if(CMM)
 		CMM->disable();
-	client = new CClient();
+
+	std::swap(client, nextClient);
 
 	highScoreCalc = nullptr;
 
@@ -679,7 +695,7 @@ void CServerHandler::endGameplay(bool closeConnection, bool restart)
 	}
 
 	client->endGame();
-	vstd::clear_pointer(client);
+	client.reset();
 
 	if(!restart)
 	{
@@ -696,6 +712,8 @@ void CServerHandler::endGameplay(bool closeConnection, bool restart)
 	
 	if(c)
 	{
+		nextClient = std::make_unique<CClient>();
+		c->iser.cb = nextClient.get();
 		c->enterLobbyConnectionMode();
 		c->disableStackSendingByID();
 	}

+ 8 - 1
client/CServerHandler.h

@@ -72,6 +72,7 @@ public:
 	virtual void setDifficulty(int to) const = 0;
 	virtual void setTurnTimerInfo(const TurnTimerInfo &) const = 0;
 	virtual void setSimturnsInfo(const SimturnsInfo &) const = 0;
+	virtual void setExtraOptionsInfo(const ExtraOptionsInfo & info) const = 0;
 	virtual void sendMessage(const std::string & txt) const = 0;
 	virtual void sendGuiAction(ui8 action) const = 0; // TODO: possibly get rid of it?
 	virtual void sendStartGame(bool allowOnlyAI = false) const = 0;
@@ -94,6 +95,10 @@ class CServerHandler : public IServerAPI, public LobbyInfo
 
 	std::shared_ptr<HighScoreCalculation> highScoreCalc;
 
+	/// temporary helper member that exists while game in lobby mode
+	/// required to correctly deserialize gamestate using client-side game callback
+	std::unique_ptr<CClient> nextClient;
+
 	void threadHandleConnection();
 	void threadRunServer();
 	void onServerFinished();
@@ -115,13 +120,14 @@ public:
 	std::shared_ptr<boost::thread> threadRunLocalServer;
 
 	std::shared_ptr<CConnection> c;
-	CClient * client;
+	std::unique_ptr<CClient> client;
 
 	CondSh<bool> campaignServerRestartLock;
 
 	static const std::string localhostAddress;
 
 	CServerHandler();
+	~CServerHandler();
 	
 	std::string getHostAddress() const;
 	ui16 getHostPort() const;
@@ -158,6 +164,7 @@ public:
 	void setDifficulty(int to) const override;
 	void setTurnTimerInfo(const TurnTimerInfo &) const override;
 	void setSimturnsInfo(const SimturnsInfo &) const override;
+	void setExtraOptionsInfo(const ExtraOptionsInfo &) const override;
 	void sendMessage(const std::string & txt) const override;
 	void sendGuiAction(ui8 action) const override;
 	void sendRestartGame() const override;

+ 31 - 7
client/CVideoHandler.cpp

@@ -101,8 +101,8 @@ bool CVideoPlayer::open(const VideoPath & fname, bool scale)
 }
 
 // loop = to loop through the video
-// useOverlay = directly write to the screen.
-bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverlay, bool scale)
+// overlay = directly write to the screen.
+bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool overlay, bool scale)
 {
 	close();
 
@@ -199,7 +199,7 @@ bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverla
 	}
 
 	// Allocate a place to put our YUV image on that screen
-	if (useOverlay)
+	if (overlay)
 	{
 		texture = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STATIC, pos.w, pos.h);
 	}
@@ -624,7 +624,7 @@ Point CVideoPlayer::size()
 }
 
 // Plays a video. Only works for overlays.
-bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey)
+bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey, bool overlay)
 {
 	// Note: either the windows player or the linux player is
 	// broken. Compensate here until the bug is found.
@@ -647,7 +647,14 @@ bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey)
 
 		SDL_Rect rect = CSDL_Ext::toSDL(pos);
 
-		SDL_RenderFillRect(mainRenderer, &rect);
+		if(overlay)
+		{
+			SDL_RenderFillRect(mainRenderer, &rect);
+		}
+		else
+		{
+			SDL_RenderClear(mainRenderer);
+		}
 		SDL_RenderCopy(mainRenderer, texture, nullptr, &rect);
 		SDL_RenderPresent(mainRenderer);
 
@@ -672,10 +679,27 @@ bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey)
 	return true;
 }
 
-bool CVideoPlayer::openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey, bool scale)
+bool CVideoPlayer::openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType)
 {
+	bool scale;
+	bool stopOnKey;
+	bool overlay;
+
+	switch(videoType)
+	{
+		case EVideoType::INTRO:
+			stopOnKey = true;
+			scale = true;
+			overlay = false;
+			break;
+		case EVideoType::SPELLBOOK:
+		default:
+			stopOnKey = false;
+			scale = false;
+			overlay = true;
+	}
 	open(name, false, true, scale);
-	bool ret = playVideo(x, y,  stopOnKey);
+	bool ret = playVideo(x, y,  stopOnKey, overlay);
 	close();
 	return ret;
 }

+ 9 - 3
client/CVideoHandler.h

@@ -15,6 +15,12 @@
 struct SDL_Surface;
 struct SDL_Texture;
 
+enum class EVideoType : ui8
+{
+	INTRO = 0, // use entire window: stopOnKey = true, scale = true, overlay = false
+	SPELLBOOK  // overlay video: stopOnKey = false, scale = false, overlay = true
+};
+
 class IVideoPlayer : boost::noncopyable
 {
 public:
@@ -33,7 +39,7 @@ class IMainVideoPlayer : public IVideoPlayer
 public:
 	virtual ~IMainVideoPlayer() = default;
 	virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function<void()> restart = nullptr){}
-	virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false)
+	virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType)
 	{
 		return false;
 	}
@@ -90,7 +96,7 @@ class CVideoPlayer final : public IMainVideoPlayer
 	double frameTime;
 	bool doLoop;				// loop through video
 
-	bool playVideo(int x, int y, bool stopOnKey);
+	bool playVideo(int x, int y, bool stopOnKey, bool overlay);
 	bool open(const VideoPath & fname, bool loop, bool useOverlay = false, bool scale = false);
 public:
 	CVideoPlayer();
@@ -106,7 +112,7 @@ public:
 	void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function<void()> onVideoRestart = nullptr) override; //moves to next frame if appropriate, and blits it or blits only if redraw parameter is set true
 
 	// Opens video, calls playVideo, closes video; returns playVideo result (if whole video has been played)
-	bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) override;
+	bool openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType) override;
 
 	std::pair<std::unique_ptr<ui8 []>, si64> getAudio(const VideoPath & videoToOpen) override;
 

+ 38 - 31
client/Client.cpp

@@ -139,14 +139,10 @@ CClient::CClient()
 	waitingRequest.clear();
 	applier = std::make_shared<CApplier<CBaseForCLApply>>();
 	registerTypesClientPacks(*applier);
-	IObjectInterface::cb = this;
 	gs = nullptr;
 }
 
-CClient::~CClient()
-{
-	IObjectInterface::cb = nullptr;
-}
+CClient::~CClient() = default;
 
 const Services * CClient::services() const
 {
@@ -177,8 +173,9 @@ void CClient::newGame(CGameState * initializedGameState)
 {
 	CSH->th->update();
 	CMapService mapService;
-	gs = initializedGameState ? initializedGameState : new CGameState();
-	gs->preInit(VLC);
+	assert(initializedGameState);
+	gs = initializedGameState;
+	gs->preInit(VLC, this);
 	logNetwork->trace("\tCreating gamestate: %i", CSH->th->getDiff());
 	if(!initializedGameState)
 	{
@@ -200,7 +197,7 @@ void CClient::loadGame(CGameState * initializedGameState)
 	logNetwork->info("Game state was transferred over network, loading.");
 	gs = initializedGameState;
 
-	gs->preInit(VLC);
+	gs->preInit(VLC, this);
 	gs->updateOnLoad(CSH->si.get());
 	logNetwork->info("Game loaded, initialize interfaces.");
 
@@ -228,7 +225,7 @@ void CClient::loadGame(CGameState * initializedGameState)
 			throw std::runtime_error("Cannot open client part of " + CSH->si->mapname);
 
 		std::unique_ptr<CLoadFile> loader (new CLoadFile(clientSaveName));
-		serialize(loader->serializer, loader->serializer.fileVersion);
+		serialize(loader->serializer, loader->serializer.version);
 
 		logNetwork->info("Client data loaded.");
 	}
@@ -241,7 +238,7 @@ void CClient::loadGame(CGameState * initializedGameState)
 	initPlayerInterfaces();
 }
 
-void CClient::serialize(BinarySerializer & h, const int version)
+void CClient::serialize(BinarySerializer & h)
 {
 	assert(h.saving);
 	ui8 players = static_cast<ui8>(playerint.size());
@@ -254,20 +251,17 @@ void CClient::serialize(BinarySerializer & h, const int version)
 		h & i->first;
 		h & i->second->dllName;
 		h & i->second->human;
-		i->second->saveGame(h, version);
+		i->second->saveGame(h);
 	}
 
 #if SCRIPTING_ENABLED
-	if(version >= 800)
-	{
-		JsonNode scriptsState;
-		clientScripts->serializeState(h.saving, scriptsState);
-		h & scriptsState;
-	}
+	JsonNode scriptsState;
+	clientScripts->serializeState(h.saving, scriptsState);
+	h & scriptsState;
 #endif
 }
 
-void CClient::serialize(BinaryDeserializer & h, const int version)
+void CClient::serialize(BinaryDeserializer & h)
 {
 	assert(!h.saving);
 	ui8 players = 0;
@@ -323,7 +317,7 @@ void CClient::serialize(BinaryDeserializer & h, const int version)
 
 		// loadGame needs to be called after initGameInterface to load paths correctly
 		// initGameInterface is called in installNewPlayerInterface
-		nInt->loadGame(h, version);
+		nInt->loadGame(h);
 
 		if (shouldResetInterface)
 		{
@@ -370,7 +364,7 @@ void CClient::endGame()
 		logNetwork->info("Ending current game!");
 		removeGUI();
 
-		vstd::clear_pointer(const_cast<CGameInfo *>(CGI)->mh);
+		CGI->mh.reset();
 		vstd::clear_pointer(gs);
 
 		logNetwork->info("Deleted mapHandler and gameState.");
@@ -392,7 +386,7 @@ void CClient::initMapHandler()
 	// During loading CPlayerInterface from serialized state it's depend on MH
 	if(!settings["session"]["headless"].Bool())
 	{
-		const_cast<CGameInfo *>(CGI)->mh = new CMapHandler(gs->map);
+		CGI->mh = std::make_shared<CMapHandler>(gs->map);
 		logNetwork->trace("Creating mapHandler: %d ms", CSH->th->getDiff());
 	}
 
@@ -414,7 +408,7 @@ void CClient::initPlayerEnvironments()
 			hasHumanPlayer = true;
 	}
 
-	if(!hasHumanPlayer)
+	if(!hasHumanPlayer && !settings["session"]["headless"].Bool())
 	{
 		Settings session = settings.write["session"];
 		session["spectate"].Bool() = true;
@@ -439,7 +433,7 @@ void CClient::initPlayerInterfaces()
 		if(!vstd::contains(playerint, color))
 		{
 			logNetwork->info("Preparing interface for player %s", color.toString());
-			if(playerInfo.second.isControlledByAI())
+			if(playerInfo.second.isControlledByAI() || settings["session"]["onlyai"].Bool())
 			{
 				bool alliedToHuman = false;
 				for(auto & allyInfo : gs->scenarioOps->playerInfos)
@@ -560,7 +554,8 @@ int CClient::sendRequest(const CPackForServer * request, PlayerColor player)
 
 void CClient::battleStarted(const BattleInfo * info)
 {
-	std::shared_ptr<CPlayerInterface> att, def;
+	std::shared_ptr<CPlayerInterface> att;
+	std::shared_ptr<CPlayerInterface> def;
 	auto & leftSide = info->sides[0];
 	auto & rightSide = info->sides[1];
 
@@ -590,14 +585,26 @@ void CClient::battleStarted(const BattleInfo * info)
 		def = std::dynamic_pointer_cast<CPlayerInterface>(playerint[rightSide.color]);
 	
 	//Remove player interfaces for auto battle (quickCombat option)
-	if(att && att->isAutoFightOn)
+	if((att && att->isAutoFightOn) || (def && def->isAutoFightOn))
 	{
-		if (att->cb->getBattle(info->battleID)->battleGetTacticDist())
+		auto endTacticPhaseIfEligible = [info](const CPlayerInterface * interface)
 		{
-			auto side = att->cb->getBattle(info->battleID)->playerToSide(att->playerID);
-			auto action = BattleAction::makeEndOFTacticPhase(*side);
-			att->cb->battleMakeTacticAction(info->battleID, action);
-		}
+			if (interface->cb->getBattle(info->battleID)->battleGetTacticDist())
+			{
+				auto side = interface->cb->getBattle(info->battleID)->playerToSide(interface->playerID);
+
+				if(interface->playerID == info->sides[info->tacticsSide].color)
+				{
+					auto action = BattleAction::makeEndOFTacticPhase(*side);
+					interface->cb->battleMakeTacticAction(info->battleID, action);
+				}
+			}
+		};
+
+		if(att && att->isAutoFightOn)
+			endTacticPhaseIfEligible(att.get());
+		else // def && def->isAutoFightOn
+			endTacticPhaseIfEligible(def.get());
 
 		att.reset();
 		def.reset();
@@ -671,7 +678,7 @@ std::shared_ptr<const CPathsInfo> CClient::getPathsInfo(const CGHeroInstance * h
 
 	if(iter == std::end(pathCache))
 	{
-		std::shared_ptr<CPathsInfo> paths = std::make_shared<CPathsInfo>(getMapSize(), h);
+		auto paths = std::make_shared<CPathsInfo>(getMapSize(), h);
 
 		gs->calculatePaths(h, *paths.get());
 

+ 2 - 2
client/Client.h

@@ -131,8 +131,8 @@ public:
 
 	void newGame(CGameState * gameState);
 	void loadGame(CGameState * gameState);
-	void serialize(BinarySerializer & h, const int version);
-	void serialize(BinaryDeserializer & h, const int version);
+	void serialize(BinarySerializer & h);
+	void serialize(BinaryDeserializer & h);
 
 	void save(const std::string & fname);
 	void endGame();

+ 7 - 5
client/ClientCommandManager.cpp

@@ -203,7 +203,7 @@ void ClientCommandManager::handleConvertTextCommand()
 		try
 		{
 			// load and drop loaded map - we only need loader to run over all maps
-			mapService.loadMap(mapName);
+			mapService.loadMap(mapName, nullptr);
 		}
 		catch(std::exception & e)
 		{
@@ -216,7 +216,7 @@ void ClientCommandManager::handleConvertTextCommand()
 	{
 		auto state = CampaignHandler::getCampaign(campaignName.getName());
 		for (auto const & part : state->allScenarios())
-			state->getMap(part);
+			state->getMap(part, nullptr);
 	}
 
 	VLC->generaltexth->dumpAllTexts();
@@ -272,7 +272,7 @@ void ClientCommandManager::handleGetScriptsCommand()
 
 	boost::filesystem::create_directories(outPath);
 
-	for(auto & kv : VLC->scriptHandler->objects)
+	for(const auto & kv : VLC->scriptHandler->objects)
 	{
 		std::string name = kv.first;
 		boost::algorithm::replace_all(name,":","_");
@@ -379,7 +379,8 @@ void ClientCommandManager::handleBonusesCommand(std::istringstream & singleWordB
 void ClientCommandManager::handleTellCommand(std::istringstream& singleWordBuffer)
 {
 	std::string what;
-	int id1, id2;
+	int id1;
+	int id2;
 	singleWordBuffer >> what >> id1 >> id2;
 
 	if(what == "hs")
@@ -399,7 +400,8 @@ void ClientCommandManager::handleMpCommand()
 
 void ClientCommandManager::handleSetCommand(std::istringstream& singleWordBuffer)
 {
-	std::string what, value;
+	std::string what;
+	std::string value;
 	singleWordBuffer >> what;
 
 	Settings config = settings.write["session"][what];

+ 21 - 9
client/NetPacksClient.cpp

@@ -39,6 +39,7 @@
 #include "../lib/StartInfo.h"
 #include "../lib/CConfigHandler.h"
 #include "../lib/mapObjects/CGMarket.h"
+#include "../lib/mapObjects/CGTownInstance.h"
 #include "../lib/gameState/CGameState.h"
 #include "../lib/CStack.h"
 #include "../lib/battle/BattleInfo.h"
@@ -156,6 +157,9 @@ void ApplyClientNetPackVisitor::visitSetMana(SetMana & pack)
 	const CGHeroInstance *h = cl.getHero(pack.hid);
 	callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroManaPointsChanged, h);
 
+	if(settings["session"]["headless"].Bool())
+		return;
+
 	for (auto window : GH.windows().findWindows<BattleWindow>())
 		window->heroManaPointsChanged(h);
 }
@@ -466,7 +470,8 @@ void ApplyFirstClientNetPackVisitor::visitRemoveObject(RemoveObject & pack)
 			i->second->objectRemoved(o, pack.initiator);
 	}
 
-	CGI->mh->waitForOngoingAnimations();
+	if(CGI->mh)
+		CGI->mh->waitForOngoingAnimations();
 }
 
 void ApplyClientNetPackVisitor::visitRemoveObject(RemoveObject & pack)
@@ -552,9 +557,11 @@ void ApplyClientNetPackVisitor::visitNewStructures(NewStructures & pack)
 	}
 
 	// invalidate section of map view with our object and force an update
-	CGI->mh->onObjectInstantRemove(town, town->getOwner());
-	CGI->mh->onObjectInstantAdd(town, town->getOwner());
-
+	if(CGI->mh)
+	{
+		CGI->mh->onObjectInstantRemove(town, town->getOwner());
+		CGI->mh->onObjectInstantAdd(town, town->getOwner());
+	}
 }
 void ApplyClientNetPackVisitor::visitRazeStructures(RazeStructures & pack)
 {
@@ -565,8 +572,11 @@ void ApplyClientNetPackVisitor::visitRazeStructures(RazeStructures & pack)
 	}
 
 	// invalidate section of map view with our object and force an update
-	CGI->mh->onObjectInstantRemove(town, town->getOwner());
-	CGI->mh->onObjectInstantAdd(town, town->getOwner());
+	if(CGI->mh)
+	{
+		CGI->mh->onObjectInstantRemove(town, town->getOwner());
+		CGI->mh->onObjectInstantAdd(town, town->getOwner());
+	}
 }
 
 void ApplyClientNetPackVisitor::visitSetAvailableCreatures(SetAvailableCreatures & pack)
@@ -650,7 +660,7 @@ void ApplyFirstClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty &
 	}
 
 	// invalidate section of map view with our object and force an update with new flag color
-	if (pack.what == ObjProperty::OWNER)
+	if (pack.what == ObjProperty::OWNER && CGI->mh)
 	{
 		auto object = gs.getObjInstance(pack.id);
 		CGI->mh->onObjectInstantRemove(object, object->getOwner());
@@ -667,7 +677,7 @@ void ApplyClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack)
 	}
 
 	// invalidate section of map view with our object and force an update with new flag color
-	if (pack.what == ObjProperty::OWNER)
+	if (pack.what == ObjProperty::OWNER && CGI->mh)
 	{
 		auto object = gs.getObjInstance(pack.id);
 		CGI->mh->onObjectInstantAdd(object, object->getOwner());
@@ -1022,7 +1032,9 @@ void ApplyClientNetPackVisitor::visitNewObject(NewObject & pack)
 		if(gs.isVisible(obj, i->first))
 			i->second->newObject(obj);
 	}
-	CGI->mh->waitForOngoingAnimations();
+
+	if(CGI->mh)
+		CGI->mh->waitForOngoingAnimations();
 }
 
 void ApplyClientNetPackVisitor::visitSetAvailableArtifacts(SetAvailableArtifacts & pack)

+ 4 - 0
client/NetPacksLobbyClient.cpp

@@ -16,6 +16,7 @@
 #include "lobby/OptionsTab.h"
 #include "lobby/RandomMapTab.h"
 #include "lobby/TurnOptionsTab.h"
+#include "lobby/ExtraOptionsTab.h"
 #include "lobby/SelectionTab.h"
 #include "lobby/CBonusSelection.h"
 
@@ -99,6 +100,9 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyGuiAction(LobbyGuiAction & pack
 	case LobbyGuiAction::OPEN_TURN_OPTIONS:
 		lobby->toggleTab(lobby->tabTurnOptions);
 		break;
+	case LobbyGuiAction::OPEN_EXTRA_OPTIONS:
+		lobby->toggleTab(lobby->tabExtraOptions);
+		break;
 	}
 }
 

+ 2 - 2
client/PlayerLocalState.h

@@ -47,7 +47,7 @@ public:
 		int spellbookLastTabAdvmap = 4;
 
 		template<typename Handler>
-		void serialize(Handler & h, const int version)
+		void serialize(Handler & h)
 		{
 			h & spellbookLastPageBattle;
 			h & spellbookLastPageAdvmap;
@@ -94,7 +94,7 @@ public:
 	void setSelection(const CArmedInstance *sel);
 
 	template<typename Handler>
-	void serialize(Handler & h, int version)
+	void serialize(Handler & h)
 	{
 		//WARNING: this code is broken and not used. See CClient::loadGame
 		std::map<const CGHeroInstance *, int3> pathsMap; //hero -> dest

+ 1 - 1
client/adventureMap/AdventureMapInterface.cpp

@@ -314,7 +314,7 @@ void AdventureMapInterface::onSelectionChanged(const CArmedInstance *sel)
 		auto town = dynamic_cast<const CGTownInstance*>(sel);
 
 		widget->getInfoBar()->showTownSelection(town);
-		widget->getTownList()->updateWidget();;
+		widget->getTownList()->updateWidget();
 		widget->getTownList()->select(town);
 		widget->getHeroList()->select(nullptr);
 		onHeroChanged(nullptr);

+ 10 - 6
client/adventureMap/AdventureOptions.cpp

@@ -23,6 +23,7 @@
 
 #include "../../CCallback.h"
 #include "../../lib/StartInfo.h"
+#include "../../lib/CGeneralTextHandler.h"
 
 AdventureOptions::AdventureOptions()
 	: CWindowObject(PLAYER_COLORED, ImagePath::builtin("ADVOPTS"))
@@ -30,12 +31,7 @@ AdventureOptions::AdventureOptions()
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 
 	viewWorld = std::make_shared<CButton>(Point(24, 23), AnimationPath::builtin("ADVVIEW.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_WORLD);
-	viewWorld->addCallback( [] { LOCPLINT->viewWorldMap(); });
-
-	exit = std::make_shared<CButton>(Point(204, 313), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(), std::bind(&AdventureOptions::close, this), EShortcut::GLOBAL_RETURN);
-
-	scenInfo = std::make_shared<CButton>(Point(24, 198), AnimationPath::builtin("ADVINFO.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_SCENARIO);
-	scenInfo->addCallback(AdventureOptions::showScenarioInfo);
+	viewWorld->addCallback([] { LOCPLINT->viewWorldMap(); });
 
 	puzzle = std::make_shared<CButton>(Point(24, 81), AnimationPath::builtin("ADVPUZ.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_PUZZLE);
 	puzzle->addCallback(std::bind(&CPlayerInterface::showPuzzleMap, LOCPLINT));
@@ -45,6 +41,14 @@ AdventureOptions::AdventureOptions()
 		dig->addCallback(std::bind(&CPlayerInterface::tryDigging, LOCPLINT, h));
 	else
 		dig->block(true);
+
+	scenInfo = std::make_shared<CButton>(Point(24, 198), AnimationPath::builtin("ADVINFO.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_SCENARIO);
+	scenInfo->addCallback(AdventureOptions::showScenarioInfo);
+	
+	replay = std::make_shared<CButton>(Point(24, 257), AnimationPath::builtin("ADVTURN.DEF"), CButton::tooltip(), [&](){ close(); });
+	replay->addCallback([]{ LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.replayOpponentTurnNotImplemented")); });
+
+	exit = std::make_shared<CButton>(Point(203, 313), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(), std::bind(&AdventureOptions::close, this), EShortcut::GLOBAL_RETURN);
 }
 
 void AdventureOptions::showScenarioInfo()

+ 1 - 1
client/adventureMap/AdventureOptions.h

@@ -21,7 +21,7 @@ class AdventureOptions : public CWindowObject
 	std::shared_ptr<CButton> puzzle;
 	std::shared_ptr<CButton> dig;
 	std::shared_ptr<CButton> scenInfo;
-	/*std::shared_ptr<CButton> replay*/
+	std::shared_ptr<CButton> replay;
 
 public:
 	AdventureOptions();

+ 2 - 1
client/adventureMap/CInfoBar.cpp

@@ -134,7 +134,8 @@ CInfoBar::VisibleGameStatusInfo::VisibleGameStatusInfo()
 			halls.at(hallLevel)++;
 	}
 
-	std::vector<PlayerColor> allies, enemies;
+	std::vector<PlayerColor> allies;
+	std::vector<PlayerColor> enemies;
 
 	//generate list of allies and enemies
 	for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; i++)

+ 1 - 1
client/adventureMap/CList.cpp

@@ -263,7 +263,7 @@ void CHeroList::CHeroItem::showTooltip()
 
 std::string CHeroList::CHeroItem::getHoverText()
 {
-	return boost::str(boost::format(CGI->generaltexth->allTexts[15]) % hero->getNameTranslated() % hero->type->heroClass->getNameTranslated());
+	return boost::str(boost::format(CGI->generaltexth->allTexts[15]) % hero->getNameTranslated() % hero->getClassNameTranslated());
 }
 
 void CHeroList::CHeroItem::gesture(bool on, const Point & initialPosition, const Point & finalPosition)

+ 6 - 5
client/battle/BattleActionsController.cpp

@@ -29,6 +29,7 @@
 #include "../../CCallback.h"
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/CRandomGenerator.h"
 #include "../../lib/CStack.h"
 #include "../../lib/battle/BattleAction.h"
 #include "../../lib/spells/CSpellHandler.h"
@@ -750,7 +751,7 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, B
 
 			if (!spellcastingModeActive())
 			{
-				if (action.spell().toSpell())
+				if (action.spell().hasValue())
 				{
 					owner.giveCommand(EActionType::MONSTER_SPELL, targetHex, action.spell());
 				}
@@ -887,17 +888,17 @@ void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterS
 	{
 		// faerie dragon can cast only one, randomly selected spell until their next move
 		//TODO: faerie dragon type spell should be selected by server
-		const auto * spellToCast = owner.getBattle()->getRandomCastedSpell(CRandomGenerator::getDefault(), casterStack).toSpell();
+		const auto spellToCast = owner.getBattle()->getRandomCastedSpell(CRandomGenerator::getDefault(), casterStack);
 
-		if (spellToCast)
-			creatureSpells.push_back(spellToCast);
+		if (spellToCast.hasValue())
+			creatureSpells.push_back(spellToCast.toSpell());
 	}
 
 	TConstBonusListPtr bl = casterStack->getBonuses(Selector::type()(BonusType::SPELLCASTER));
 
 	for(const auto & bonus : *bl)
 	{
-		if (bonus->additionalInfo[0] <= 0)
+		if (bonus->additionalInfo[0] <= 0 && bonus->subtype.as<SpellID>().hasValue())
 			creatureSpells.push_back(bonus->subtype.as<SpellID>().toSpell());
 	}
 }

+ 4 - 4
client/battle/BattleInterface.cpp

@@ -352,13 +352,13 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 	CCS->curh->set(Cursor::Combat::BLOCKED);
 
 	const SpellID spellID = sc->spellID;
-	const CSpell * spell = spellID.toSpell();
-	auto targetedTile = sc->tile;
 
-	assert(spell);
-	if(!spell)
+	if(!spellID.hasValue())
 		return;
 
+	const CSpell * spell = spellID.toSpell();
+	auto targetedTile = sc->tile;
+
 	const AudioPath & castSoundPath = spell->getCastSound();
 
 	if (!castSoundPath.empty())

+ 3 - 5
client/battle/BattleInterfaceClasses.cpp

@@ -449,12 +449,10 @@ void HeroInfoBasicPanel::show(Canvas & to)
 }
 
 
-StackInfoBasicPanel::StackInfoBasicPanel(const CStack * stack, Point * position, bool initializeBackground)
+StackInfoBasicPanel::StackInfoBasicPanel(const CStack * stack, bool initializeBackground)
 	: CIntObject(0)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-	if (position != nullptr)
-		moveTo(*position);
 
 	if(initializeBackground)
 	{
@@ -530,7 +528,7 @@ void StackInfoBasicPanel::initializeData(const CStack * stack)
 		if (hasGraphics)
 		{
 			//FIXME: support permanent duration
-			int duration = stack->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(effect)))->turnsRemain;
+			int duration = stack->getFirstBonus(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(effect)))->turnsRemain;
 
 			icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("SpellInt"), effect + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed));
 			if(settings["general"]["enableUiEnhancements"].Bool())
@@ -585,7 +583,7 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface
 	exit = std::make_shared<CButton>(Point(384, 505), AnimationPath::builtin("iok6432.def"), std::make_pair("", ""), [&](){ bExitf();}, EShortcut::GLOBAL_ACCEPT);
 	exit->setBorderColor(Colors::METALLIC_GOLD);
 	
-	if(allowReplay)
+	if(allowReplay || owner.cb->getStartInfo()->extraOptionsInfo.unlimitedReplay)
 	{
 		repeat = std::make_shared<CButton>(Point(24, 505), AnimationPath::builtin("icn6432.def"), std::make_pair("", ""), [&](){ bRepeatf();}, EShortcut::GLOBAL_CANCEL);
 		repeat->setBorderColor(Colors::METALLIC_GOLD);

+ 1 - 1
client/battle/BattleInterfaceClasses.h

@@ -155,7 +155,7 @@ private:
 	std::vector<std::shared_ptr<CMultiLineLabel>> labelsMultiline;
 	std::vector<std::shared_ptr<CAnimImage>> icons;
 public:
-	StackInfoBasicPanel(const CStack * stack, Point * position, bool initializeBackground = true);
+	StackInfoBasicPanel(const CStack * stack, bool initializeBackground = true);
 
 	void show(Canvas & to) override;
 

+ 5 - 2
client/battle/BattleStacksController.cpp

@@ -30,11 +30,14 @@
 #include "../render/Colors.h"
 #include "../render/Canvas.h"
 #include "../render/IRenderHandler.h"
+#include "../render/Graphics.h"
+#include "../render/IFont.h"
 
 #include "../../CCallback.h"
 #include "../../lib/spells/ISpellMechanics.h"
 #include "../../lib/battle/BattleAction.h"
 #include "../../lib/battle/BattleHex.h"
+#include "../../lib/CRandomGenerator.h"
 #include "../../lib/CStack.h"
 #include "../../lib/CondSh.h"
 #include "../../lib/TextOperations.h"
@@ -327,10 +330,10 @@ void BattleStacksController::showStackAmountBox(Canvas & canvas, const CStack *
 			boxPosition = owner.fieldController->hexPositionLocal(frontPos).center() + Point(-8, -14);
 	}
 
-	Point textPosition = amountBG->dimensions()/2 + boxPosition + Point(0, 1);
+	Point textPosition = Point(amountBG->dimensions().x/2 + boxPosition.x, boxPosition.y + graphics->fonts[EFonts::FONT_TINY]->getLineHeight() - 6);
 
 	canvas.draw(amountBG, boxPosition);
-	canvas.drawText(textPosition, EFonts::FONT_TINY, Colors::WHITE, ETextAlignment::CENTER, TextOperations::formatMetric(stack->getCount(), 4));
+	canvas.drawText(textPosition, EFonts::FONT_TINY, Colors::WHITE, ETextAlignment::TOPCENTER, TextOperations::formatMetric(stack->getCount(), 4));
 }
 
 void BattleStacksController::showStack(Canvas & canvas, const CStack * stack)

+ 94 - 19
client/battle/BattleWindow.cpp

@@ -41,6 +41,8 @@
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/filesystem/ResourcePath.h"
 #include "../../lib/StartInfo.h"
+#include "../../lib/battle/BattleInfo.h"
+#include "../../lib/CPlayerState.h"
 #include "../windows/settings/SettingsMainWindow.h"
 
 BattleWindow::BattleWindow(BattleInterface & owner):
@@ -52,6 +54,12 @@ BattleWindow::BattleWindow(BattleInterface & owner):
 	pos.h = 600;
 	pos = center();
 
+	PlayerColor defenderColor = owner.getBattle()->getBattle()->getSidePlayer(BattleSide::DEFENDER);
+	PlayerColor attackerColor = owner.getBattle()->getBattle()->getSidePlayer(BattleSide::ATTACKER);
+	bool isDefenderHuman = defenderColor.isValidPlayer() && LOCPLINT->cb->getStartInfo()->playerInfos.at(defenderColor).isControlledByHuman();
+	bool isAttackerHuman = attackerColor.isValidPlayer() && LOCPLINT->cb->getStartInfo()->playerInfos.at(attackerColor).isControlledByHuman();
+	onlyOnePlayerHuman = isDefenderHuman != isAttackerHuman;
+
 	REGISTER_BUILDER("battleConsole", &BattleWindow::buildBattleConsole);
 	
 	const JsonNode config(JsonPath::builtin("config/widgets/BattleWindow2.json"));
@@ -60,6 +68,7 @@ BattleWindow::BattleWindow(BattleInterface & owner):
 	addShortcut(EShortcut::BATTLE_SURRENDER, std::bind(&BattleWindow::bSurrenderf, this));
 	addShortcut(EShortcut::BATTLE_RETREAT, std::bind(&BattleWindow::bFleef, this));
 	addShortcut(EShortcut::BATTLE_AUTOCOMBAT, std::bind(&BattleWindow::bAutofightf, this));
+	addShortcut(EShortcut::BATTLE_END_WITH_AUTOCOMBAT, std::bind(&BattleWindow::endWithAutocombat, this));
 	addShortcut(EShortcut::BATTLE_CAST_SPELL, std::bind(&BattleWindow::bSpellf, this));
 	addShortcut(EShortcut::BATTLE_WAIT, std::bind(&BattleWindow::bWaitf, this));
 	addShortcut(EShortcut::BATTLE_DEFEND, std::bind(&BattleWindow::bDefencef, this));
@@ -130,19 +139,13 @@ void BattleWindow::createStickyHeroInfoWindows()
 	{
 		InfoAboutHero info;
 		info.initFromHero(owner.defendingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE);
-		Point position = (GH.screenDimensions().x >= 1000)
-				? Point(pos.x + pos.w + 15, pos.y + 60)
-				: Point(pos.x + pos.w -79, pos.y + 195);
-		defenderHeroWindow = std::make_shared<HeroInfoBasicPanel>(info, &position);
+		defenderHeroWindow = std::make_shared<HeroInfoBasicPanel>(info, nullptr);
 	}
 	if(owner.attackingHeroInstance)
 	{
 		InfoAboutHero info;
 		info.initFromHero(owner.attackingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE);
-		Point position = (GH.screenDimensions().x >= 1000)
-				? Point(pos.x - 93, pos.y + 60)
-				: Point(pos.x + 1, pos.y + 195);
-		attackerHeroWindow = std::make_shared<HeroInfoBasicPanel>(info, &position);
+		attackerHeroWindow = std::make_shared<HeroInfoBasicPanel>(info, nullptr);
 	}
 
 	bool showInfoWindows = settings["battle"]["stickyHeroInfoWindows"].Bool();
@@ -155,6 +158,8 @@ void BattleWindow::createStickyHeroInfoWindows()
 		if(defenderHeroWindow)
 			defenderHeroWindow->disable();
 	}
+
+	setPositionInfoWindow();
 }
 
 void BattleWindow::createTimerInfoWindows()
@@ -222,6 +227,7 @@ void BattleWindow::hideQueue()
 		pos.h -= queue->pos.h;
 		pos = center();
 	}
+	setPositionInfoWindow();
 	GH.windows().totalRedraw();
 }
 
@@ -235,6 +241,7 @@ void BattleWindow::showQueue()
 
 	createQueue();
 	updateQueue();
+	setPositionInfoWindow();
 	GH.windows().totalRedraw();
 }
 
@@ -281,6 +288,38 @@ void BattleWindow::updateQueue()
 	queue->update();
 }
 
+void BattleWindow::setPositionInfoWindow()
+{
+	if(defenderHeroWindow)
+	{
+		Point position = (GH.screenDimensions().x >= 1000)
+				? Point(pos.x + pos.w + 15, pos.y + 60)
+				: Point(pos.x + pos.w -79, pos.y + 195);
+		defenderHeroWindow->moveTo(position);
+	}
+	if(attackerHeroWindow)
+	{
+		Point position = (GH.screenDimensions().x >= 1000)
+				? Point(pos.x - 93, pos.y + 60)
+				: Point(pos.x + 1, pos.y + 195);
+		attackerHeroWindow->moveTo(position);
+	}
+	if(defenderStackWindow)
+	{
+		Point position = (GH.screenDimensions().x >= 1000)
+				? Point(pos.x + pos.w + 15, defenderHeroWindow ? defenderHeroWindow->pos.y + 210 : pos.y + 60)
+				: Point(pos.x + pos.w -79, defenderHeroWindow ? defenderHeroWindow->pos.y : pos.y + 195);
+		defenderStackWindow->moveTo(position);
+	}
+	if(attackerStackWindow)
+	{
+		Point position = (GH.screenDimensions().x >= 1000)
+				? Point(pos.x - 93, attackerHeroWindow ? attackerHeroWindow->pos.y + 210 : pos.y + 60)
+				: Point(pos.x + 1, attackerHeroWindow ? attackerHeroWindow->pos.y : pos.y + 195);
+		attackerStackWindow->moveTo(position);
+	}
+}
+
 void BattleWindow::updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero)
 {
 	std::shared_ptr<HeroInfoBasicPanel> panelToUpdate = side == 0 ? attackerHeroWindow : defenderHeroWindow;
@@ -295,10 +334,7 @@ void BattleWindow::updateStackInfoWindow(const CStack * stack)
 
 	if(stack && stack->unitSide() == BattleSide::DEFENDER)
 	{
-		Point position = (GH.screenDimensions().x >= 1000)
-				? Point(pos.x + pos.w + 15, defenderHeroWindow ? defenderHeroWindow->pos.y + 210 : pos.y)
-				: Point(pos.x + pos.w -79, defenderHeroWindow ? defenderHeroWindow->pos.y : pos.y + 135);
-		defenderStackWindow = std::make_shared<StackInfoBasicPanel>(stack, &position);
+		defenderStackWindow = std::make_shared<StackInfoBasicPanel>(stack);
 		defenderStackWindow->setEnabled(showInfoWindows);
 	}
 	else
@@ -306,14 +342,13 @@ void BattleWindow::updateStackInfoWindow(const CStack * stack)
 	
 	if(stack && stack->unitSide() == BattleSide::ATTACKER)
 	{
-		Point position = (GH.screenDimensions().x >= 1000)
-				? Point(pos.x - 93, attackerHeroWindow ? attackerHeroWindow->pos.y + 210 : pos.y)
-				: Point(pos.x + 1, attackerHeroWindow ? attackerHeroWindow->pos.y : pos.y + 135);
-		attackerStackWindow = std::make_shared<StackInfoBasicPanel>(stack, &position);
+		attackerStackWindow = std::make_shared<StackInfoBasicPanel>(stack);
 		attackerStackWindow->setEnabled(showInfoWindows);
 	}
 	else
 		attackerStackWindow = nullptr;
+	
+	setPositionInfoWindow();
 }
 
 void BattleWindow::heroManaPointsChanged(const CGHeroInstance * hero)
@@ -449,7 +484,7 @@ void BattleWindow::bFleef()
 
 	if ( owner.getBattle()->battleCanFlee() )
 	{
-		CFunctionList<void()> ony = std::bind(&BattleWindow::reallyFlee,this);
+		auto ony = std::bind(&BattleWindow::reallyFlee,this);
 		owner.curInt->showYesNoDialog(CGI->generaltexth->allTexts[28], ony, nullptr); //Are you sure you want to retreat?
 	}
 	else
@@ -551,6 +586,12 @@ void BattleWindow::bAutofightf()
 	if (owner.actionsController->spellcastingModeActive())
 		return;
 
+	if(settings["battle"]["endWithAutocombat"].Bool() && onlyOnePlayerHuman)
+	{
+		endWithAutocombat();
+		return;
+	}
+
 	//Stop auto-fight mode
 	if(owner.curInt->isAutoFightOn)
 	{
@@ -601,7 +642,7 @@ void BattleWindow::bSpellf()
 	{
 		//TODO: move to spell mechanics, add more information to spell cast problem
 		//Handle Orb of Inhibition-like effects -> we want to display dialog with info, why casting is impossible
-		auto blockingBonus = owner.currentHero()->getBonusLocalFirst(Selector::type()(BonusType::BLOCK_ALL_MAGIC));
+		auto blockingBonus = owner.currentHero()->getFirstBonus(Selector::type()(BonusType::BLOCK_ALL_MAGIC));
 		if (!blockingBonus)
 			return;
 
@@ -721,7 +762,8 @@ void BattleWindow::blockUI(bool on)
 	setShortcutBlocked(EShortcut::BATTLE_WAIT, on || owner.tacticsMode || !canWait);
 	setShortcutBlocked(EShortcut::BATTLE_DEFEND, on || owner.tacticsMode);
 	setShortcutBlocked(EShortcut::BATTLE_SELECT_ACTION, on || owner.tacticsMode);
-	setShortcutBlocked(EShortcut::BATTLE_AUTOCOMBAT, owner.actionsController->spellcastingModeActive());
+	setShortcutBlocked(EShortcut::BATTLE_AUTOCOMBAT, (settings["battle"]["endWithAutocombat"].Bool() && onlyOnePlayerHuman) ? on || owner.tacticsMode || owner.actionsController->spellcastingModeActive() : owner.actionsController->spellcastingModeActive());
+	setShortcutBlocked(EShortcut::BATTLE_END_WITH_AUTOCOMBAT, on || owner.tacticsMode || !onlyOnePlayerHuman || owner.actionsController->spellcastingModeActive());
 	setShortcutBlocked(EShortcut::BATTLE_TACTICS_END, on && owner.tacticsMode);
 	setShortcutBlocked(EShortcut::BATTLE_TACTICS_NEXT, on && owner.tacticsMode);
 	setShortcutBlocked(EShortcut::BATTLE_CONSOLE_DOWN, on && !owner.tacticsMode);
@@ -733,6 +775,39 @@ std::optional<uint32_t> BattleWindow::getQueueHoveredUnitId()
 	return queue->getHoveredUnitIdIfAny();
 }
 
+void BattleWindow::endWithAutocombat() 
+{
+	if(!owner.makingTurn() || owner.tacticsMode)
+		return;
+
+	LOCPLINT->showYesNoDialog(
+		VLC->generaltexth->translate("vcmi.battleWindow.endWithAutocombat"),
+		[this]()
+		{
+			owner.curInt->isAutoFightEndBattle = true;
+
+			auto ai = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String());
+
+			AutocombatPreferences autocombatPreferences = AutocombatPreferences();
+			autocombatPreferences.enableSpellsUsage = settings["battle"]["enableAutocombatSpells"].Bool();
+
+			ai->initBattleInterface(owner.curInt->env, owner.curInt->cb, autocombatPreferences);
+			ai->battleStart(owner.getBattleID(), owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.getBattle()->battleGetMySide(), false);
+
+			owner.curInt->isAutoFightOn = true;
+			owner.curInt->cb->registerBattleInterface(ai);
+			owner.curInt->autofightingAI = ai;
+
+			owner.requestAutofightingAIToTakeAction();
+
+			close();
+
+			owner.curInt->battleInt.reset();
+		},
+		nullptr
+	);
+}
+
 void BattleWindow::showAll(Canvas & to)
 {
 	CIntObject::showAll(to);

+ 8 - 0
client/battle/BattleWindow.h

@@ -76,6 +76,8 @@ class BattleWindow : public InterfaceObjectConfigurable
 
 	std::shared_ptr<BattleConsole> buildBattleConsole(const JsonNode &) const;
 
+	bool onlyOnePlayerHuman;
+
 public:
 	BattleWindow(BattleInterface & owner );
 	~BattleWindow();
@@ -100,6 +102,9 @@ public:
 	/// Refresh queue after turn order changes
 	void updateQueue();
 
+	// Set positions for hero & stack info window
+	void setPositionInfoWindow();
+
 	/// Refresh sticky variant of hero info window after spellcast, side same as in BattleSpellCast::side
 	void updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero);
 
@@ -125,5 +130,8 @@ public:
 
 	/// Set possible alternative options. If more than 1 - the last will be considered as default option
 	void setAlternativeActions(const std::list<PossiblePlayerBattleAction> &);
+
+	/// ends battle with autocombat
+	void endWithAutocombat();
 };
 

+ 14 - 29
client/eventsSDL/InputSourceText.cpp

@@ -21,10 +21,6 @@
 
 #include <SDL_events.h>
 
-#ifdef VCMI_APPLE
-#	include <dispatch/dispatch.h>
-#endif
-
 void InputSourceText::handleEventTextInput(const SDL_TextInputEvent & text)
 {
 	GH.events().dispatchTextInput(text.text);
@@ -37,38 +33,27 @@ void InputSourceText::handleEventTextEditing(const SDL_TextEditingEvent & text)
 
 void InputSourceText::startTextInput(const Rect & whereInput)
 {
-
-#ifdef VCMI_APPLE
-	dispatch_async(dispatch_get_main_queue(), ^{
-#endif
-
-	Rect rectInScreenCoordinates = GH.screenHandler().convertLogicalPointsToWindow(whereInput);
-	SDL_Rect textInputRect = CSDL_Ext::toSDL(rectInScreenCoordinates);
-
-	SDL_SetTextInputRect(&textInputRect);
-
-	if (SDL_IsTextInputActive() == SDL_FALSE)
+	GH.dispatchMainThread([whereInput]()
 	{
-		SDL_StartTextInput();
-	}
+		Rect rectInScreenCoordinates = GH.screenHandler().convertLogicalPointsToWindow(whereInput);
+		SDL_Rect textInputRect = CSDL_Ext::toSDL(rectInScreenCoordinates);
+
+		SDL_SetTextInputRect(&textInputRect);
 
-#ifdef VCMI_APPLE
+		if (SDL_IsTextInputActive() == SDL_FALSE)
+		{
+			SDL_StartTextInput();
+		}
 	});
-#endif
 }
 
 void InputSourceText::stopTextInput()
 {
-#ifdef VCMI_APPLE
-	dispatch_async(dispatch_get_main_queue(), ^{
-#endif
-
-	if (SDL_IsTextInputActive() == SDL_TRUE)
+	GH.dispatchMainThread([]()
 	{
-		SDL_StopTextInput();
-	}
-
-#ifdef VCMI_APPLE
+		if (SDL_IsTextInputActive() == SDL_TRUE)
+		{
+			SDL_StopTextInput();
+		}
 	});
-#endif
 }

+ 1 - 1
client/gui/InterfaceObjectConfigurable.cpp

@@ -301,7 +301,7 @@ EShortcut InterfaceObjectConfigurable::readHotkey(const JsonNode & config) const
 	EShortcut result = GH.shortcuts().findShortcut(config.String());
 	if (result == EShortcut::NONE)
 		logGlobal->error("Invalid hotkey '%s' in interface configuration!", config.String());
-	return result;;
+	return result;
 }
 
 std::shared_ptr<CPicture> InterfaceObjectConfigurable::buildPicture(const JsonNode & config) const

+ 1 - 0
client/gui/Shortcut.h

@@ -125,6 +125,7 @@ enum class EShortcut
 	BATTLE_SURRENDER,
 	BATTLE_RETREAT,
 	BATTLE_AUTOCOMBAT,
+	BATTLE_END_WITH_AUTOCOMBAT,
 	BATTLE_CAST_SPELL,
 	BATTLE_WAIT,
 	BATTLE_DEFEND,

+ 2 - 0
client/gui/ShortcutHandler.cpp

@@ -124,6 +124,7 @@ std::vector<EShortcut> ShortcutHandler::translateKeycode(SDL_Keycode key) const
 		{SDLK_s,         EShortcut::BATTLE_SURRENDER          },
 		{SDLK_r,         EShortcut::BATTLE_RETREAT            },
 		{SDLK_a,         EShortcut::BATTLE_AUTOCOMBAT         },
+		{SDLK_e,         EShortcut::BATTLE_END_WITH_AUTOCOMBAT},
 		{SDLK_c,         EShortcut::BATTLE_CAST_SPELL         },
 		{SDLK_w,         EShortcut::BATTLE_WAIT               },
 		{SDLK_d,         EShortcut::BATTLE_DEFEND             },
@@ -265,6 +266,7 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		{"battleSurrender",          EShortcut::BATTLE_SURRENDER          },
 		{"battleRetreat",            EShortcut::BATTLE_RETREAT            },
 		{"battleAutocombat",         EShortcut::BATTLE_AUTOCOMBAT         },
+		{"battleAutocombatEnd",      EShortcut::BATTLE_END_WITH_AUTOCOMBAT},
 		{"battleCastSpell",          EShortcut::BATTLE_CAST_SPELL         },
 		{"battleWait",               EShortcut::BATTLE_WAIT               },
 		{"battleDefend",             EShortcut::BATTLE_DEFEND             },

+ 4 - 0
client/lobby/CBonusSelection.cpp

@@ -72,6 +72,7 @@ CBonusSelection::CBonusSelection()
 
 	buttonStart = std::make_shared<CButton>(Point(475, 536), AnimationPath::builtin("CBBEGIB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::startMap, this), EShortcut::GLOBAL_ACCEPT);
 	buttonRestart = std::make_shared<CButton>(Point(475, 536), AnimationPath::builtin("CBRESTB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::restartMap, this), EShortcut::GLOBAL_ACCEPT);
+	buttonVideo = std::make_shared<CButton>(Point(705, 214), AnimationPath::builtin("CBVIDEB.DEF"), CButton::tooltip(), [this](){ GH.windows().createAndPushWindow<CPrologEpilogVideo>(getCampaign()->scenario(CSH->campaignMap).prolog, [this](){ redraw(); }); });
 	buttonBack = std::make_shared<CButton>(Point(624, 536), AnimationPath::builtin("CBCANCB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::goBack, this), EShortcut::GLOBAL_CANCEL);
 
 	campaignName = std::make_shared<CLabel>(481, 28, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->si->getCampaignName());
@@ -309,6 +310,7 @@ void CBonusSelection::updateAfterStateChange()
 	if(CSH->state != EClientState::GAMEPLAY)
 	{
 		buttonRestart->disable();
+		buttonVideo->disable();
 		buttonStart->enable();
 		if(!getCampaign()->conqueredScenarios().empty())
 			buttonBack->block(true);
@@ -319,6 +321,7 @@ void CBonusSelection::updateAfterStateChange()
 	{
 		buttonStart->disable();
 		buttonRestart->enable();
+		buttonVideo->enable();
 		buttonBack->block(false);
 		if(buttonDifficultyLeft)
 			buttonDifficultyLeft->disable();
@@ -401,6 +404,7 @@ void CBonusSelection::startMap()
 	//block buttons immediately
 	buttonStart->block(true);
 	buttonRestart->block(true);
+	buttonVideo->block(true);
 	buttonBack->block(true);
 
 	if(LOCPLINT) // we're currently ingame, so ask for starting new map and end game

+ 1 - 0
client/lobby/CBonusSelection.h

@@ -65,6 +65,7 @@ public:
 	std::shared_ptr<CButton> buttonStart;
 	std::shared_ptr<CButton> buttonRestart;
 	std::shared_ptr<CButton> buttonBack;
+	std::shared_ptr<CButton> buttonVideo;
 	std::shared_ptr<CLabel> campaignName;
 	std::shared_ptr<CLabel> labelCampaignDescription;
 	std::shared_ptr<CTextBox> campaignDescription;

+ 33 - 4
client/lobby/CLobbyScreen.cpp

@@ -12,6 +12,7 @@
 
 #include "CBonusSelection.h"
 #include "TurnOptionsTab.h"
+#include "ExtraOptionsTab.h"
 #include "OptionsTab.h"
 #include "RandomMapTab.h"
 #include "SelectionTab.h"
@@ -53,7 +54,10 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType)
 
 		buttonOptions = std::make_shared<CButton>(Point(411, 510), AnimationPath::builtin("GSPBUTT.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabOpt), EShortcut::LOBBY_ADDITIONAL_OPTIONS);
 		if(settings["general"]["enableUiEnhancements"].Bool())
-			buttonTurnOptions = std::make_shared<CButton>(Point(619, 510), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabTurnOptions), EShortcut::NONE);
+		{
+			buttonTurnOptions = std::make_shared<CButton>(Point(619, 105), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabTurnOptions), EShortcut::NONE);
+			buttonExtraOptions = std::make_shared<CButton>(Point(619, 510), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabExtraOptions), EShortcut::NONE);
+		}
 	};
 
 	buttonChat = std::make_shared<CButton>(Point(619, 80), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), EShortcut::LOBBY_HIDE_CHAT);
@@ -65,6 +69,7 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType)
 	{
 		tabOpt = std::make_shared<OptionsTab>();
 		tabTurnOptions = std::make_shared<TurnOptionsTab>();
+		tabExtraOptions = std::make_shared<ExtraOptionsTab>();
 		tabRand = std::make_shared<RandomMapTab>();
 		tabRand->mapInfoChanged += std::bind(&IServerAPI::setMapInfo, CSH, _1, _2);
 		buttonRMG = std::make_shared<CButton>(Point(411, 105), AnimationPath::builtin("GSPBUTT.DEF"), CGI->generaltexth->zelp[47], 0, EShortcut::LOBBY_RANDOM_MAP);
@@ -84,6 +89,7 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType)
 	{
 		tabOpt = std::make_shared<OptionsTab>();
 		tabTurnOptions = std::make_shared<TurnOptionsTab>();
+		tabExtraOptions = std::make_shared<ExtraOptionsTab>();
 		buttonStart = std::make_shared<CButton>(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, false), EShortcut::LOBBY_LOAD_GAME);
 		initLobby();
 		break;
@@ -122,16 +128,30 @@ void CLobbyScreen::toggleTab(std::shared_ptr<CIntObject> tab)
 		CSH->sendGuiAction(LobbyGuiAction::OPEN_RANDOM_MAP_OPTIONS);
 	else if(tab == tabTurnOptions)
 		CSH->sendGuiAction(LobbyGuiAction::OPEN_TURN_OPTIONS);
+	else if(tab == tabExtraOptions)
+		CSH->sendGuiAction(LobbyGuiAction::OPEN_EXTRA_OPTIONS);
 	CSelectionBase::toggleTab(tab);
 }
 
 void CLobbyScreen::startCampaign()
 {
-	if(CSH->mi)
-	{
+	if(!CSH->mi)
+		return;
+
+	try {
 		auto ourCampaign = CampaignHandler::getCampaign(CSH->mi->fileURI);
 		CSH->setCampaignState(ourCampaign);
 	}
+	catch (const std::runtime_error &e)
+	{
+		// handle possible exception on map loading. For example campaign that contains map in unsupported format
+		// for example, wog campaigns or hota campaigns without hota map support mod
+		MetaString message;
+		message.appendTextID("vcmi.client.errors.invalidMap");
+		message.replaceRawString(e.what());
+
+		CInfoWindow::showInfoDialog(message.toString(), {});
+	}
 }
 
 void CLobbyScreen::startScenario(bool allowOnlyAI)
@@ -157,6 +177,9 @@ void CLobbyScreen::toggleMode(bool host)
 	if (buttonTurnOptions)
 		buttonTurnOptions->addTextOverlay(CGI->generaltexth->translate("vcmi.optionsTab.turnOptions.hover"), FONT_SMALL, buttonColor);
 
+	if (buttonExtraOptions)
+		buttonExtraOptions->addTextOverlay(CGI->generaltexth->translate("vcmi.optionsTab.extraOptions.hover"), FONT_SMALL, buttonColor);
+
 	if(buttonRMG)
 	{
 		buttonRMG->addTextOverlay(CGI->generaltexth->allTexts[740], FONT_SMALL, buttonColor);
@@ -168,10 +191,14 @@ void CLobbyScreen::toggleMode(bool host)
 	if (buttonTurnOptions)
 		buttonTurnOptions->block(!host);
 
+	if (buttonExtraOptions)
+		buttonExtraOptions->block(!host);
+
 	if(CSH->mi)
 	{
 		tabOpt->recreate();
 		tabTurnOptions->recreate();
+		tabExtraOptions->recreate();
 	}
 }
 
@@ -191,7 +218,9 @@ void CLobbyScreen::updateAfterStateChange()
 		if (tabOpt)
 			tabOpt->recreate();
 		if (tabTurnOptions)
-		tabTurnOptions->recreate();
+			tabTurnOptions->recreate();
+		if (tabExtraOptions)
+			tabExtraOptions->recreate();
 	}
 
 	buttonStart->block(CSH->mi == nullptr || CSH->isGuest());

+ 1 - 1
client/lobby/CSelectionBase.cpp

@@ -22,7 +22,6 @@
 #include "../CPlayerInterface.h"
 #include "../CMusicHandler.h"
 #include "../CVideoHandler.h"
-#include "../CPlayerInterface.h"
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/Shortcut.h"
@@ -43,6 +42,7 @@
 
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/CHeroHandler.h"
+#include "../../lib/CRandomGenerator.h"
 #include "../../lib/CThreadHelper.h"
 #include "../../lib/filesystem/Filesystem.h"
 #include "../../lib/mapping/CMapInfo.h"

+ 3 - 0
client/lobby/CSelectionBase.h

@@ -27,6 +27,7 @@ class CToggleGroup;
 class RandomMapTab;
 class OptionsTab;
 class TurnOptionsTab;
+class ExtraOptionsTab;
 class SelectionTab;
 class InfoCard;
 class CChatBox;
@@ -60,6 +61,7 @@ public:
 	std::shared_ptr<CButton> buttonRMG;
 	std::shared_ptr<CButton> buttonOptions;
 	std::shared_ptr<CButton> buttonTurnOptions;
+	std::shared_ptr<CButton> buttonExtraOptions;
 	std::shared_ptr<CButton> buttonStart;
 	std::shared_ptr<CButton> buttonBack;
 	std::shared_ptr<CButton> buttonSimturns;
@@ -67,6 +69,7 @@ public:
 	std::shared_ptr<SelectionTab> tabSel;
 	std::shared_ptr<OptionsTab> tabOpt;
 	std::shared_ptr<TurnOptionsTab> tabTurnOptions;
+	std::shared_ptr<ExtraOptionsTab> tabExtraOptions;
 	std::shared_ptr<RandomMapTab> tabRand;
 	std::shared_ptr<CIntObject> curTab;
 

+ 18 - 0
client/lobby/ExtraOptionsTab.cpp

@@ -0,0 +1,18 @@
+/*
+ * ExtraOptionsTab.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#include "StdInc.h"
+#include "ExtraOptionsTab.h"
+
+ExtraOptionsTab::ExtraOptionsTab()
+	: OptionsTabBase(JsonPath::builtin("config/widgets/extraOptionsTab.json"))
+{
+
+}

+ 18 - 0
client/lobby/ExtraOptionsTab.h

@@ -0,0 +1,18 @@
+/*
+ * ExtraOptionsTab.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "OptionsTabBase.h"
+
+class ExtraOptionsTab : public OptionsTabBase
+{
+public:
+	ExtraOptionsTab();
+};

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