Selaa lähdekoodia

Merge remote-tracking branch 'upstream/develop' into adv_search

Laserlicht 1 vuosi sitten
vanhempi
sitoutus
9ed4dbaeb4
100 muutettua tiedostoa jossa 817 lisäystä ja 565 poistoa
  1. 57 36
      .github/workflows/github.yml
  2. 1 1
      AI/BattleAI/AttackPossibility.cpp
  3. 15 15
      AI/BattleAI/StackWithBonuses.cpp
  4. 9 9
      AI/BattleAI/StackWithBonuses.h
  5. 5 6
      AI/Nullkiller/AIGateway.cpp
  6. 0 1
      AI/Nullkiller/AIUtility.cpp
  7. 2 2
      AI/Nullkiller/Analyzers/ArmyManager.cpp
  8. 5 5
      AI/Nullkiller/Analyzers/BuildAnalyzer.cpp
  9. 1 2
      AI/Nullkiller/Analyzers/HeroManager.cpp
  10. 1 1
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  11. 2 2
      AI/Nullkiller/Goals/BuildThis.cpp
  12. 1 1
      AI/Nullkiller/Goals/CaptureObject.h
  13. 1 1
      AI/Nullkiller/Goals/ExecuteHeroChain.cpp
  14. 1 1
      AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp
  15. 2 2
      AI/Nullkiller/Pathfinding/Actors.cpp
  16. 1 1
      AI/Nullkiller/Pathfinding/Actors.h
  17. 0 1
      AI/VCAI/AIUtility.cpp
  18. 4 4
      AI/VCAI/BuildingManager.cpp
  19. 1 1
      AI/VCAI/Goals/BuildThis.cpp
  20. 1 1
      AI/VCAI/Goals/FindObj.cpp
  21. 4 4
      AI/VCAI/Goals/GatherTroops.cpp
  22. 1 2
      AI/VCAI/MapObjectsEvaluator.cpp
  23. 7 8
      AI/VCAI/VCAI.cpp
  24. 73 40
      CCallback.cpp
  25. 11 1
      CCallback.h
  26. 0 4
      CI/android-32/before_install.sh
  27. 0 4
      CI/android-64/before_install.sh
  28. 0 7
      CI/android/before_install.sh
  29. 4 0
      CI/before_install/android.sh
  30. 2 3
      CI/before_install/linux_qt5.sh
  31. 3 1
      CI/before_install/linux_qt6.sh
  32. 0 2
      CI/before_install/macos.sh
  33. 7 0
      CI/before_install/mingw.sh
  34. 0 0
      CI/before_install/msvc.sh
  35. 1 1
      CI/conan/base/cross-macro.j2
  36. 1 1
      CI/install_conan_dependencies.sh
  37. 0 5
      CI/ios/before_install.sh
  38. 0 1
      CI/linux-qt6/upload_package.sh
  39. 0 1
      CI/linux/upload_package.sh
  40. 0 4
      CI/mac-arm/before_install.sh
  41. 0 4
      CI/mac-intel/before_install.sh
  42. 0 14
      CI/mingw-32/before_install.sh
  43. 0 14
      CI/mingw/before_install.sh
  44. 0 6
      CI/msvc/build_script.bat
  45. 0 5
      CI/msvc/coverity_build_script.bat
  46. 0 17
      CI/msvc/coverity_upload_script.ps
  47. 0 0
      CI/validate_json.py
  48. 17 8
      CMakeLists.txt
  49. BIN
      Mods/vcmi/Data/spellResearch/accept.png
  50. BIN
      Mods/vcmi/Data/spellResearch/close.png
  51. BIN
      Mods/vcmi/Data/spellResearch/reroll.png
  52. 1 1
      Mods/vcmi/config/vcmi/chinese.json
  53. 13 0
      Mods/vcmi/config/vcmi/english.json
  54. 7 0
      Mods/vcmi/config/vcmi/german.json
  55. 23 2
      Mods/vcmi/config/vcmi/polish.json
  56. 17 1
      Mods/vcmi/config/vcmi/portuguese.json
  57. 154 138
      Mods/vcmi/config/vcmi/swedish.json
  58. 3 2
      client/CPlayerInterface.cpp
  59. 9 4
      client/CServerHandler.cpp
  60. 0 1
      client/CServerHandler.h
  61. 12 14
      client/Client.cpp
  62. 4 4
      client/Client.h
  63. 1 2
      client/ClientCommandManager.cpp
  64. 1 0
      client/ClientNetPackVisitors.h
  65. 1 1
      client/HeroMovementController.cpp
  66. 8 2
      client/NetPacksClient.cpp
  67. 142 20
      client/PlayerLocalState.cpp
  68. 18 10
      client/PlayerLocalState.h
  69. 1 2
      client/adventureMap/CList.cpp
  70. 3 3
      client/adventureMap/MapAudioPlayer.cpp
  71. 0 1
      client/battle/BattleInterface.cpp
  72. 7 6
      client/battle/BattleInterfaceClasses.cpp
  73. 8 8
      client/battle/BattleSiegeController.cpp
  74. 2 2
      client/gui/CGuiHandler.cpp
  75. 5 0
      client/gui/CursorHandler.cpp
  76. 1 0
      client/gui/CursorHandler.h
  77. 1 1
      client/lobby/CBonusSelection.cpp
  78. 0 1
      client/lobby/CSelectionBase.cpp
  79. 7 7
      client/lobby/OptionsTab.cpp
  80. 9 7
      client/lobby/SelectionTab.cpp
  81. 1 0
      client/lobby/SelectionTab.h
  82. 0 1
      client/mainmenu/CCampaignScreen.cpp
  83. 2 2
      client/mapView/MapRenderer.cpp
  84. 2 2
      client/mapView/MapRendererContext.cpp
  85. 3 3
      client/mapView/MapRendererContextState.cpp
  86. 1 1
      client/mapView/MapViewController.cpp
  87. 7 7
      client/mapView/mapHandler.cpp
  88. 11 3
      client/media/CVideoHandler.cpp
  89. 1 1
      client/render/AssetGenerator.cpp
  90. 0 1
      client/render/Graphics.cpp
  91. 2 0
      client/render/IScreenHandler.h
  92. 8 8
      client/renderSDL/CTrueTypeFont.cpp
  93. 15 3
      client/renderSDL/CursorHardware.cpp
  94. 41 24
      client/renderSDL/ScreenHandler.cpp
  95. 2 0
      client/renderSDL/ScreenHandler.h
  96. 8 5
      client/widgets/CComponent.cpp
  97. 1 0
      client/widgets/CComponent.h
  98. 4 5
      client/widgets/MiscWidgets.cpp
  99. 8 8
      client/widgets/TextControls.cpp
  100. 1 1
      client/widgets/markets/CMarketBase.cpp

+ 57 - 36
.github/workflows/github.yml

@@ -22,14 +22,17 @@ jobs:
           - platform: linux-qt6
             os: ubuntu-24.04
             test: 0
+            before_install: linux_qt6.sh
             preset: linux-clang-test
           - platform: linux
             os: ubuntu-24.04
             test: 1
+            before_install: linux_qt5.sh
             preset: linux-gcc-test
           - platform: linux
             os: ubuntu-20.04
             test: 0
+            before_install: linux_qt5.sh
             preset: linux-gcc-debug
           - platform: mac-intel
             os: macos-13
@@ -37,8 +40,10 @@ jobs:
             pack: 1
             pack_type: Release
             extension: dmg
+            before_install: macos.sh
             preset: macos-conan-ninja-release
             conan_profile: macos-intel
+            conan_prebuilts: dependencies-mac-intel
             conan_options: --options with_apple_system_libs=True
             artifact_platform: intel
           - platform: mac-arm
@@ -47,8 +52,10 @@ jobs:
             pack: 1
             pack_type: Release
             extension: dmg
+            before_install: macos.sh
             preset: macos-arm-conan-ninja-release
             conan_profile: macos-arm
+            conan_prebuilts: dependencies-mac-arm
             conan_options: --options with_apple_system_libs=True
             artifact_platform: arm
           - platform: ios
@@ -57,8 +64,10 @@ jobs:
             pack: 1
             pack_type: Release
             extension: ipa
+            before_install: macos.sh
             preset: ios-release-conan-ccache
             conan_profile: ios-arm64
+            conan_prebuilts: dependencies-ios
             conan_options: --options with_apple_system_libs=True
           - platform: msvc
             os: windows-latest
@@ -66,40 +75,45 @@ jobs:
             pack: 1
             pack_type: RelWithDebInfo
             extension: exe
+            before_install: msvc.sh
             preset: windows-msvc-release
-          - platform: mingw
-            os: ubuntu-22.04
+          - platform: mingw_x86_64
+            os: ubuntu-24.04
             test: 0
             pack: 1
             pack_type: Release
             extension: exe
-            cpack_args: -D CPACK_NSIS_EXECUTABLE=`which makensis`
             cmake_args: -G Ninja
+            before_install: mingw.sh
             preset: windows-mingw-conan-linux
             conan_profile: mingw64-linux.jinja
-          - platform: mingw-32
-            os: ubuntu-22.04
+            conan_prebuilts: dependencies-mingw-x86-64
+          - platform: mingw_x86
+            os: ubuntu-24.04
             test: 0
             pack: 1
             pack_type: Release
             extension: exe
-            cpack_args: -D CPACK_NSIS_EXECUTABLE=`which makensis`
             cmake_args: -G Ninja
+            before_install: mingw.sh
             preset: windows-mingw-conan-linux
             conan_profile: mingw32-linux.jinja
+            conan_prebuilts: dependencies-mingw-x86
           - platform: android-32
-            os: macos-14
+            os: ubuntu-24.04
             extension: apk
             preset: android-conan-ninja-release
-            conan_profile: android-32
-            conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT
+            before_install: android.sh
+            conan_profile: android-32-ndk
+            conan_prebuilts: dependencies-android-armeabi-v7a
             artifact_platform: armeabi-v7a
           - platform: android-64
-            os: macos-14
+            os: ubuntu-24.04
             extension: apk
             preset: android-conan-ninja-release
-            conan_profile: android-64
-            conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT
+            before_install: android.sh
+            conan_profile: android-64-ndk
+            conan_prebuilts: dependencies-android-arm64-v8a
             artifact_platform: arm64-v8a
     runs-on: ${{ matrix.os }}
     defaults:
@@ -107,15 +121,21 @@ jobs:
         shell: bash
 
     steps:
-    - uses: actions/checkout@v4
+    - name: Checkout repository
+      uses: actions/checkout@v4
       with:
         submodules: recursive
 
-    - name: Dependencies
-      run: source '${{github.workspace}}/CI/${{matrix.platform}}/before_install.sh'
+    - name: Prepare CI
+      if: "${{ matrix.before_install != '' }}"
+      run: source '${{github.workspace}}/CI/before_install/${{matrix.before_install}}'
       env:
         VCMI_BUILD_PLATFORM: x64
 
+    - name: Install Conan Dependencies
+      if: "${{ matrix.conan_prebuilts != '' }}"
+      run: source '${{github.workspace}}/CI/install_conan_dependencies.sh' '${{matrix.conan_prebuilts}}'
+
     # ensure the ccache for each PR is separate so they don't interfere with each other
     # fall back to ccache of the vcmi/vcmi repo if no PR-specific ccache is found
     - name: ccache for PRs
@@ -157,15 +177,13 @@ jobs:
         mkdir -p ~/.local/share/vcmi/
         mv h3_assets/* ~/.local/share/vcmi/
 
-    - uses: actions/setup-python@v5
+    - name: Install Conan
       if: "${{ matrix.conan_profile != '' }}"
-      with:
-        python-version: '3.10'
+      run: pipx install 'conan<2.0'
 
-    - name: Conan setup
+    - name: Install Conan profile
       if: "${{ matrix.conan_profile != '' }}"
       run: |
-        pip3 install 'conan<2.0'
         conan profile new default --detect
         conan install . \
           --install-folder=conan-generated \
@@ -177,7 +195,13 @@ jobs:
       env:
         GENERATE_ONLY_BUILT_CONFIG: 1
 
-    - uses: actions/setup-java@v4
+    # Workaround for gradle not discovering SDK that was installed via conan
+    - name: Find Android NDK
+      if: ${{ startsWith(matrix.platform, 'android') }}
+      run: sudo ln -s -T /home/runner/.conan/data/android-ndk/r25c/_/_/package/4db1be536558d833e52e862fd84d64d75c2b3656/bin /usr/local/lib/android/sdk/ndk/25.2.9519653
+
+    - name: Install Java
+      uses: actions/setup-java@v4
       if: ${{ startsWith(matrix.platform, 'android') }}
       with:
         distribution: 'temurin'
@@ -242,11 +266,13 @@ jobs:
       if: ${{ matrix.pack == 1 }}
       run: |
         cd '${{github.workspace}}/out/build/${{matrix.preset}}'
-        CPACK_PATH=`which -a cpack | grep -m1 -v -i chocolatey`
-        counter=0; until "$CPACK_PATH" -C ${{matrix.pack_type}} ${{ matrix.cpack_args }} || ((counter > 20)); do sleep 3; ((counter++)); done
-        test -f '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' \
-          && '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' '${{github.workspace}}' "$(ls '${{ env.VCMI_PACKAGE_FILE_NAME }}'.*)"
-        rm -rf _CPack_Packages
+        
+        # Workaround for CPack bug on macOS 13
+        counter=0
+        until cpack -C ${{matrix.pack_type}} || ((counter > 20)); do
+            sleep 3
+            ((counter++))
+        done
 
     - name: Artifacts
       if: ${{ matrix.pack == 1 }}
@@ -268,7 +294,7 @@ jobs:
         echo "ANDROID_APK_PATH=$ANDROID_APK_PATH" >> $GITHUB_ENV
         echo "ANDROID_AAB_PATH=$ANDROID_AAB_PATH" >> $GITHUB_ENV
 
-    - name: Android apk artifacts
+    - name: Upload android apk artifacts
       if: ${{ startsWith(matrix.platform, 'android') }}
       uses: actions/upload-artifact@v4
       with:
@@ -276,15 +302,15 @@ jobs:
         path: |
           ${{ env.ANDROID_APK_PATH }}
 
-    - name: Android aab artifacts
-      if: ${{ startsWith(matrix.platform, 'android') }}
+    - name: Upload Android aab artifacts
+      if: ${{ startsWith(matrix.platform, 'android') && github.ref == 'refs/heads/master' }}
       uses: actions/upload-artifact@v4
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - aab
         path: |
           ${{ env.ANDROID_AAB_PATH }}
 
-    - name: Symbols
+    - name: Upload debug symbols
       if: ${{ matrix.platform == 'msvc' }}
       uses: actions/upload-artifact@v4
       with:
@@ -343,11 +369,6 @@ jobs:
     steps:
         - uses: actions/checkout@v4
 
-        - uses: actions/setup-python@v5
-          if: "${{ matrix.conan_profile != '' }}"
-          with:
-            python-version: '3.10'
-
         - name: Ensure LF line endings
           run: |
             find . -path ./.git -prune -o -path ./AI/FuzzyLite -prune -o -path ./test/googletest \
@@ -358,4 +379,4 @@ jobs:
         - name: Validate JSON
           run: |
             sudo apt install python3-jstyleson
-            python3 CI/linux-qt6/validate_json.py
+            python3 CI/validate_json.py

+ 1 - 1
AI/BattleAI/AttackPossibility.cpp

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

+ 15 - 15
AI/BattleAI/StackWithBonuses.cpp

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

+ 9 - 9
AI/BattleAI/StackWithBonuses.h

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

+ 5 - 6
AI/Nullkiller/AIGateway.cpp

@@ -17,7 +17,6 @@
 #include "../../lib/mapObjects/ObjectTemplate.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/CConfigHandler.h"
-#include "../../lib/CHeroHandler.h"
 #include "../../lib/IGameSettings.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/serializer/CTypeList.h"
@@ -649,12 +648,12 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
 				auto danger = nullkiller->dangerEvaluator->evaluateDanger(target, hero.get());
 				auto ratio = static_cast<float>(danger) / hero->getTotalStrength();
 
-				answer = 1;
+				answer = true;
 				
 				if(topObj->id != goalObjectID && nullkiller->dangerEvaluator->evaluateDanger(topObj) > 0)
 				{
 					// no if we do not aim to visit this object
-					answer = 0;
+					answer = false;
 				}
 				
 				logAi->trace("Query hook: %s(%s) by %s danger ratio %f", target.toString(), topObj->getObjectName(), hero.name(), ratio);
@@ -864,7 +863,7 @@ void AIGateway::makeTurn()
 
 void AIGateway::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h)
 {
-	LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->pos.toString());
+	LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->anchorPos().toString());
 	switch(obj->ID)
 	{
 	case Obj::TOWN:
@@ -1454,8 +1453,8 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 
 void AIGateway::buildStructure(const CGTownInstance * t, BuildingID building)
 {
-	auto name = t->town->buildings.at(building)->getNameTranslated();
-	logAi->debug("Player %d will build %s in town of %s at %s", ai->playerID, name, t->getNameTranslated(), t->pos.toString());
+	auto name = t->getTown()->buildings.at(building)->getNameTranslated();
+	logAi->debug("Player %d will build %s in town of %s at %s", ai->playerID, name, t->getNameTranslated(), t->anchorPos().toString());
 	cb->buildBuilding(t, building); //just do this;
 }
 

+ 0 - 1
AI/Nullkiller/AIUtility.cpp

@@ -14,7 +14,6 @@
 
 #include "../../lib/UnlockGuard.h"
 #include "../../lib/CConfigHandler.h"
-#include "../../lib/CHeroHandler.h"
 #include "../../lib/mapObjects/MapObjects.h"
 #include "../../lib/mapping/CMapDefines.h"
 #include "../../lib/gameState/QuestInfo.h"

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

@@ -144,7 +144,7 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
 
 	for(auto & slot : sortedSlots)
 	{
-		alignmentMap[slot.creature->getFaction()] += slot.power;
+		alignmentMap[slot.creature->getFactionID()] += slot.power;
 	}
 
 	std::set<FactionID> allowedFactions;
@@ -178,7 +178,7 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
 
 		for(auto & slot : sortedSlots)
 		{
-			if(vstd::contains(allowedFactions, slot.creature->getFaction()))
+			if(vstd::contains(allowedFactions, slot.creature->getFactionID()))
 			{
 				auto slotID = newArmyInstance.getSlotFor(slot.creature->getId());
 

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

@@ -17,7 +17,7 @@ namespace NKAI
 
 void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
 {
-	auto townInfo = developmentInfo.town->town;
+	auto townInfo = developmentInfo.town->getTown();
 	auto creatures = townInfo->creatures;
 	auto buildings = townInfo->getAllBuildings();
 
@@ -31,7 +31,7 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
 		}
 	}
 
-	for(int level = 0; level < developmentInfo.town->town->creatures.size(); level++)
+	for(int level = 0; level < developmentInfo.town->getTown()->creatures.size(); level++)
 	{
 		logAi->trace("Checking dwelling level %d", level);
 		BuildingInfo nextToBuild = BuildingInfo();
@@ -82,7 +82,7 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo)
 	{
 		for(auto & buildingID : buildingSet)
 		{
-			if(!developmentInfo.town->hasBuilt(buildingID) && developmentInfo.town->town->buildings.count(buildingID))
+			if(!developmentInfo.town->hasBuilt(buildingID) && developmentInfo.town->getTown()->buildings.count(buildingID))
 			{
 				developmentInfo.addBuildingToBuild(getBuildingOrPrerequisite(developmentInfo.town, buildingID));
 
@@ -198,7 +198,7 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 	bool excludeDwellingDependencies) const
 {
 	BuildingID building = toBuild;
-	auto townInfo = town->town;
+	auto townInfo = town->getTown();
 
 	const CBuilding * buildPtr = townInfo->buildings.at(building);
 	const CCreature * creature = nullptr;
@@ -327,7 +327,7 @@ bool BuildAnalyzer::hasAnyBuilding(int32_t alignment, BuildingID bid) const
 {
 	for(auto tdi : developmentInfos)
 	{
-		if(tdi.town->getFaction() == alignment && tdi.town->hasBuilt(bid))
+		if(tdi.town->getFactionID() == alignment && tdi.town->hasBuilt(bid))
 			return true;
 	}
 

+ 1 - 2
AI/Nullkiller/Analyzers/HeroManager.cpp

@@ -11,7 +11,6 @@
 #include "../StdInc.h"
 #include "../Engine/Nullkiller.h"
 #include "../../../lib/mapObjects/MapObjects.h"
-#include "../../../lib/CHeroHandler.h"
 #include "../../../lib/IGameSettings.h"
 
 namespace NKAI
@@ -71,7 +70,7 @@ float HeroManager::evaluateSecSkill(SecondarySkill skill, const CGHeroInstance *
 
 float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const
 {
-	auto heroSpecial = Selector::source(BonusSource::HERO_SPECIAL, BonusSourceID(hero->type->getId()));
+	auto heroSpecial = Selector::source(BonusSource::HERO_SPECIAL, BonusSourceID(hero->getHeroTypeID()));
 	auto secondarySkillBonus = Selector::targetSourceType()(BonusSource::SECONDARY_SKILL);
 	auto specialSecondarySkillBonuses = hero->getBonuses(heroSpecial.And(secondarySkillBonus));
 	auto secondarySkillBonuses = hero->getBonuses(Selector::sourceTypeSel(BonusSource::SECONDARY_SKILL));

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

@@ -1120,7 +1120,7 @@ public:
 
 uint64_t RewardEvaluator::getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const
 {
-	if(ai->buildAnalyzer->hasAnyBuilding(town->getFaction(), bi.id))
+	if(ai->buildAnalyzer->hasAnyBuilding(town->getFactionID(), bi.id))
 		return 0;
 
 	auto creaturesToUpgrade = ai->armyManager->getTotalCreaturesAvailable(bi.baseCreatureID);

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

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

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

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

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

@@ -30,7 +30,7 @@ ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance *
 #if NKAI_TRACE_LEVEL >= 1
 		targetName = obj->getObjectName() + tile.toString();
 #else
-		targetName = obj->typeName + tile.toString();
+		targetName = obj->getTypeName() + tile.toString();
 #endif
 	}
 	else

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

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

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

@@ -182,7 +182,7 @@ ExchangeResult HeroActor::tryExchangeNoLock(const ChainActor * specialActor, con
 		return &actor == specialActor;
 	});
 
-	result.actor = &(dynamic_cast<HeroActor *>(result.actor)->specialActors[index]);
+	result.actor = &(dynamic_cast<HeroActor *>(result.actor)->specialActors.at(index));
 
 	return result;
 }
@@ -440,7 +440,7 @@ int DwellingActor::getInitialTurn(bool waitForGrowth, int dayOfWeek)
 
 std::string DwellingActor::toString() const
 {
-	return dwelling->typeName + dwelling->visitablePos().toString();
+	return dwelling->getTypeName() + dwelling->visitablePos().toString();
 }
 
 CCreatureSet * DwellingActor::getDwellingCreatures(const CGDwelling * dwelling, bool waitForGrowth)

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

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

+ 0 - 1
AI/VCAI/AIUtility.cpp

@@ -15,7 +15,6 @@
 
 #include "../../lib/UnlockGuard.h"
 #include "../../lib/CConfigHandler.h"
-#include "../../lib/CHeroHandler.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/mapObjects/CQuest.h"
 #include "../../lib/mapping/CMapDefines.h"

+ 4 - 4
AI/VCAI/BuildingManager.cpp

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

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

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

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

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

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

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

+ 1 - 2
AI/VCAI/MapObjectsEvaluator.cpp

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

+ 7 - 8
AI/VCAI/VCAI.cpp

@@ -20,7 +20,6 @@
 #include "../../lib/mapObjects/MapObjects.h"
 #include "../../lib/mapObjects/ObjectTemplate.h"
 #include "../../lib/CConfigHandler.h"
-#include "../../lib/CHeroHandler.h"
 #include "../../lib/IGameSettings.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/bonuses/Limiters.h"
@@ -1032,7 +1031,7 @@ void VCAI::mainLoop()
 
 void VCAI::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h)
 {
-	LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->pos.toString());
+	LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->anchorPos().toString());
 	switch(obj->ID)
 	{
 	case Obj::TOWN:
@@ -1417,11 +1416,11 @@ void VCAI::wander(HeroPtr h)
 				//TODO pick the truly best
 				const CGTownInstance * t = *boost::max_element(townsNotReachable, compareReinforcements);
 				logAi->debug("%s can't reach any town, we'll try to make our way to %s at %s", h->getNameTranslated(), t->getNameTranslated(), t->visitablePos().toString());
-				int3 pos1 = h->pos;
+				int3 posBefore = h->visitablePos();
 				striveToGoal(sptr(Goals::ClearWayTo(t->visitablePos()).sethero(h))); //TODO: drop "strive", add to mainLoop
 				//if out hero is stuck, we may need to request another hero to clear the way we see
 
-				if(pos1 == h->pos && h == primaryHero()) //hero can't move
+				if(posBefore == h->visitablePos() && h == primaryHero()) //hero can't move
 				{
 					if(canRecruitAnyHero(t))
 						recruitHero(t);
@@ -1471,7 +1470,7 @@ void VCAI::wander(HeroPtr h)
 				{
 					auto chosenObject = cb->getObjInstance(ObjectInstanceID(bestObjectGoal->objid));
 					if(chosenObject != nullptr)
-						logAi->debug("Of all %d destinations, object %s at pos=%s seems nice", dests.size(), chosenObject->getObjectName(), chosenObject->pos.toString());
+						logAi->debug("Of all %d destinations, object %s at pos=%s seems nice", dests.size(), chosenObject->getObjectName(), chosenObject->anchorPos().toString());
 				}
 				else
 					logAi->debug("Trying to realize goal of type %s as part of wandering.", bestObjectGoal->name());
@@ -1994,8 +1993,8 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
 
 void VCAI::buildStructure(const CGTownInstance * t, BuildingID building)
 {
-	auto name = t->town->buildings.at(building)->getNameTranslated();
-	logAi->debug("Player %d will build %s in town of %s at %s", ai->playerID, name, t->getNameTranslated(), t->pos.toString());
+	auto name = t->getTown()->buildings.at(building)->getNameTranslated();
+	logAi->debug("Player %d will build %s in town of %s at %s", ai->playerID, name, t->getNameTranslated(), t->anchorPos().toString());
 	cb->buildBuilding(t, building); //just do this;
 }
 
@@ -2081,7 +2080,7 @@ void VCAI::tryRealize(Goals::BuildThis & g)
 		if (cb->canBuildStructure(t, b) == EBuildingState::ALLOWED)
 		{
 			logAi->debug("Player %d will build %s in town of %s at %s",
-				playerID, t->town->buildings.at(b)->getNameTranslated(), t->getNameTranslated(), t->pos.toString());
+				playerID, t->getTown()->buildings.at(b)->getNameTranslated(), t->getNameTranslated(), t->anchorPos().toString());
 			cb->buildBuilding(t, b);
 			throw goalFulfilledException(sptr(g));
 		}

+ 73 - 40
CCallback.cpp

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

+ 11 - 1
CCallback.h

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

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

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

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

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

+ 0 - 7
CI/android/before_install.sh

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

+ 4 - 0
CI/before_install/android.sh

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

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

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

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

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

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

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

+ 7 - 0
CI/before_install/mingw.sh

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

+ 0 - 0
CI/msvc/before_install.sh → CI/before_install/msvc.sh


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

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

+ 1 - 1
CI/install_conan_dependencies.sh

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

+ 0 - 5
CI/ios/before_install.sh

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

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

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

+ 0 - 1
CI/linux/upload_package.sh

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

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

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

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

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

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

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

+ 0 - 14
CI/mingw/before_install.sh

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

+ 0 - 6
CI/msvc/build_script.bat

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

+ 0 - 5
CI/msvc/coverity_build_script.bat

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

+ 0 - 17
CI/msvc/coverity_upload_script.ps

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

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


+ 17 - 8
CMakeLists.txt

@@ -180,11 +180,6 @@ else()
 	add_definitions(-DVCMI_NO_EXTRA_VERSION)
 endif(ENABLE_GITVERSION)
 
-# Precompiled header configuration
-if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 6.0 )
-	set(ENABLE_PCH OFF) # broken
-endif()
-
 if(ENABLE_PCH)
 	macro(enable_pch name)
 		target_precompile_headers(${name} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:<StdInc.h$<ANGLE-R>>)
@@ -328,7 +323,6 @@ if(MINGW OR MSVC)
 		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4244") # 4244: conversion from 'xxx' to 'yyy', possible loss of data
 		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4267") # 4267: conversion from 'xxx' to 'yyy', possible loss of data
 		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4275") # 4275: non dll-interface class 'xxx' used as base for dll-interface class
-		#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4800") # 4800: implicit conversion from 'xxx' to bool. Possible information loss
 
 		if(ENABLE_STRICT_COMPILATION)
 			set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /WX") # Treats all compiler warnings as errors
@@ -493,14 +487,23 @@ if (ENABLE_CLIENT)
 	if(TARGET SDL2_image::SDL2_image)
 		add_library(SDL2::Image ALIAS SDL2_image::SDL2_image)
 	endif()
+	if(TARGET SDL2_image::SDL2_image-static)
+		add_library(SDL2::Image ALIAS SDL2_image::SDL2_image-static)
+	endif()
 	find_package(SDL2_mixer REQUIRED)
 	if(TARGET SDL2_mixer::SDL2_mixer)
 		add_library(SDL2::Mixer ALIAS SDL2_mixer::SDL2_mixer)
 	endif()
+	if(TARGET SDL2_mixer::SDL2_mixer-static)
+		add_library(SDL2::Mixer ALIAS SDL2_mixer::SDL2_mixer-static)
+	endif()
 	find_package(SDL2_ttf REQUIRED)
 	if(TARGET SDL2_ttf::SDL2_ttf)
 		add_library(SDL2::TTF ALIAS SDL2_ttf::SDL2_ttf)
 	endif()
+	if(TARGET SDL2_ttf::SDL2_ttf-static)
+		add_library(SDL2::TTF ALIAS SDL2_ttf::SDL2_ttf-static)
+	endif()
 endif()
 
 if(ENABLE_LOBBY)
@@ -662,6 +665,10 @@ if(NOT TARGET minizip::minizip)
 	add_library(minizip::minizip ALIAS minizip)
 endif()
 
+if(ENABLE_LAUNCHER OR ENABLE_EDITOR)
+	add_subdirectory(vcmiqt)
+endif()
+
 if(ENABLE_LAUNCHER)
 	add_subdirectory(launcher)
 endif()
@@ -723,7 +730,7 @@ endif()
 
 if(WIN32)
 	if(TBB_FOUND AND MSVC)
-		   install_vcpkg_imported_tgt(TBB::tbb)
+		install_vcpkg_imported_tgt(TBB::tbb)
 	endif()
 
 	if(USING_CONAN)
@@ -733,7 +740,9 @@ if(WIN32)
 				${dep_files}
 				"${CMAKE_SYSROOT}/bin/*.dll" 
 				"${CMAKE_SYSROOT}/lib/*.dll" 
-				"${CONAN_SYSTEM_LIBRARY_LOCATION}/*.dll")
+				"${CONAN_SYSTEM_LIBRARY_LOCATION}/libgcc_s_dw2-1.dll" # for 32-bit only?
+				"${CONAN_SYSTEM_LIBRARY_LOCATION}/libgcc_s_seh-1.dll" # for 64-bit only?
+				"${CONAN_SYSTEM_LIBRARY_LOCATION}/libstdc++-6.dll")
 	else()
 		file(GLOB dep_files
 				${dep_files}

BIN
Mods/vcmi/Data/spellResearch/accept.png


BIN
Mods/vcmi/Data/spellResearch/close.png


BIN
Mods/vcmi/Data/spellResearch/reroll.png


+ 1 - 1
Mods/vcmi/config/vcmi/chinese.json

@@ -581,7 +581,7 @@
 	"core.bonus.GARGOYLE.description": "不能被复活或治疗",
 	"core.bonus.GENERAL_DAMAGE_REDUCTION.name": "减少伤害 (${val}%)",
 	"core.bonus.GENERAL_DAMAGE_REDUCTION.description": "减少从远程和近战中遭受的物理伤害",
-	"core.bonus.HATE.name": "${subtype.creature}的死敌",
+	"core.bonus.HATE.name": "憎恨${subtype.creature}",
 	"core.bonus.HATE.description": "对${subtype.creature}造成额外${val}%伤害",
 	"core.bonus.HEALER.name": "治疗者",
 	"core.bonus.HEALER.description": "可以治疗友军单位",

+ 13 - 0
Mods/vcmi/config/vcmi/english.json

@@ -63,6 +63,13 @@
 
 	"vcmi.spellBook.search" : "search...",
 
+	"vcmi.spellResearch.canNotAfford" : "You can't afford to replace {%SPELL1} with {%SPELL2}. But you can still discard this spell and continue spell research.",
+	"vcmi.spellResearch.comeAgain" : "Research has already been done today. Come back tomorrow.",
+	"vcmi.spellResearch.pay" : "Would you like to replace {%SPELL1} with {%SPELL2}? Or discard this spell and continue spell research?",
+	"vcmi.spellResearch.research" : "Research this Spell",
+	"vcmi.spellResearch.skip" : "Skip this Spell",
+	"vcmi.spellResearch.abort" : "Abort",
+
 	"vcmi.mainMenu.serverConnecting" : "Connecting...",
 	"vcmi.mainMenu.serverAddressEnter" : "Enter address:",
 	"vcmi.mainMenu.serverConnectionFailed" : "Failed to connect",
@@ -345,6 +352,12 @@
 	"vcmi.heroWindow.openCommander.help"  : "Shows details about the commander of this hero.",
 	"vcmi.heroWindow.openBackpack.hover" : "Open artifact backpack window",
 	"vcmi.heroWindow.openBackpack.help"  : "Opens window that allows easier artifact backpack management.",
+	"vcmi.heroWindow.sortBackpackByCost.hover"  : "Sort by cost",
+	"vcmi.heroWindow.sortBackpackByCost.help"  : "Sort artifacts in backpack by cost.",
+	"vcmi.heroWindow.sortBackpackBySlot.hover"  : "Sort by slot",
+	"vcmi.heroWindow.sortBackpackBySlot.help"  : "Sort artifacts in backpack by equipped slot.",
+	"vcmi.heroWindow.sortBackpackByClass.hover"  : "Sort by class",
+	"vcmi.heroWindow.sortBackpackByClass.help"  : "Sort artifacts in backpack by artifact class. Treasure, Minor, Major, Relic",
 
 	"vcmi.tavernWindow.inviteHero"  : "Invite hero",
 

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

@@ -62,6 +62,13 @@
 
 	"vcmi.spellBook.search" : "suchen...",
 
+	"vcmi.spellResearch.canNotAfford" : "Ihr könnt es Euch nicht leisten, {%SPELL1} durch {%SPELL2} zu ersetzen. Aber Ihr könnt diesen Zauberspruch trotzdem verwerfen und die Zauberspruchforschung fortsetzen.",
+	"vcmi.spellResearch.comeAgain" : "Die Forschung wurde heute bereits abgeschlossen. Kommt morgen wieder.",
+	"vcmi.spellResearch.pay" : "Möchtet Ihr {%SPELL1} durch {%SPELL2} ersetzen? Oder diesen Zauberspruch verwerfen und die Zauberspruchforschung fortsetzen?",
+	"vcmi.spellResearch.research" : "Erforsche diesen Zauberspruch",
+	"vcmi.spellResearch.skip" : "Überspringe diesen Zauberspruch",
+	"vcmi.spellResearch.abort" : "Abbruch",
+
 	"vcmi.mainMenu.serverConnecting" : "Verbinde...",
 	"vcmi.mainMenu.serverAddressEnter" : "Addresse eingeben:",
 	"vcmi.mainMenu.serverConnectionFailed" : "Verbindung fehlgeschlagen",

+ 23 - 2
Mods/vcmi/config/vcmi/polish.json

@@ -12,6 +12,9 @@
 	"vcmi.adventureMap.monsterThreat.levels.9"  : "Przytłaczający",
 	"vcmi.adventureMap.monsterThreat.levels.10" : "Śmiertelny",
 	"vcmi.adventureMap.monsterThreat.levels.11" : "Nie do pokonania",
+	"vcmi.adventureMap.monsterLevel"            : "\n\n%Jednostka %ATTACK_TYPE %LEVEL poziomu z miasta %TOWN",
+	"vcmi.adventureMap.monsterMeleeType"        : "Walcząca wręcz",
+	"vcmi.adventureMap.monsterRangedType"       : "Dystansowa",
 
 	"vcmi.adventureMap.confirmRestartGame"     : "Czy na pewno chcesz zrestartować grę?",
 	"vcmi.adventureMap.noTownWithMarket"       : "Brak dostępnego targowiska!",
@@ -58,6 +61,13 @@
 
 	"vcmi.spellBook.search" : "szukaj...",
 
+	"vcmi.spellResearch.canNotAfford" : "Nie stać Cię na zastąpienie {%SPELL1} przez {%SPELL2}, ale za to możesz odrzucić ten czar i kontynuować badania.",
+	"vcmi.spellResearch.comeAgain" : "Badania zostały już przeprowadzone dzisiaj. Wróć jutro.",
+	"vcmi.spellResearch.pay" : "Czy chcesz zastąpić {%SPELL1} czarem {%SPELL2}? Czy odrzucić ten czar i kontynuować badania?",
+	"vcmi.spellResearch.research" : "Zamień zaklęcia",
+	"vcmi.spellResearch.skip" : "Kontynuuj badania",
+	"vcmi.spellResearch.abort" : "Anuluj",
+
 	"vcmi.mainMenu.serverConnecting" : "Łączenie...",
 	"vcmi.mainMenu.serverAddressEnter" : "Wprowadź adres:",
 	"vcmi.mainMenu.serverConnectionFailed" : "Połączenie nie powiodło się",
@@ -142,6 +152,7 @@
 	"vcmi.client.errors.invalidMap" : "{Błędna mapa lub kampania}\n\nNie udało się stworzyć gry! Wybrana mapa lub kampania jest niepoprawna lub uszkodzona. Powód:\n%s",
 	"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.disconnected" : "{Błąd sieciowy}\n\nUtracono połączenie z serwerem!",
+	"vcmi.server.errors.playerLeft" : "{Rozłączenie z graczem}\n\n%s opuścił rozgrywkę!", //%s -> player color
 	"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}",
@@ -235,8 +246,10 @@
 	"vcmi.adventureOptions.borderScroll.help" : "{Przewijanie na brzegu mapy}\n\nPrzewijanie mapy przygody gdy kursor najeżdża na brzeg okna gry. Może być wyłączone poprzez przytrzymanie klawisza CTRL.",
 	"vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Zarządzanie armią w panelu informacyjnym",
 	"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Zarządzanie armią w panelu informacyjnym}\n\nPozwala zarządzać jednostkami w panelu informacyjnym, zamiast przełączać między domyślnymi informacjami.",
-	"vcmi.adventureOptions.leftButtonDrag.hover" : "Przeciąganie mapy lewym kliknięciem",
+	"vcmi.adventureOptions.leftButtonDrag.hover" : "Przeciąganie lewym",
 	"vcmi.adventureOptions.leftButtonDrag.help" : "{Przeciąganie mapy lewym kliknięciem}\n\nUmożliwia przesuwanie mapy przygody poprzez przeciąganie myszy z wciśniętym lewym przyciskiem.",
+	"vcmi.adventureOptions.rightButtonDrag.hover" : "Przeciąganie prawym",
+	"vcmi.adventureOptions.rightButtonDrag.help" : "{Przeciąganie mapy prawym kliknięciem}\n\nUmożliwia przesuwanie mapy przygody poprzez przeciąganie myszy z wciśniętym prawym przyciskiem.",
 	"vcmi.adventureOptions.smoothDragging.hover" : "'Pływające' przeciąganie mapy",
 	"vcmi.adventureOptions.smoothDragging.help" : "{'Pływające' przeciąganie mapy}\n\nPrzeciąganie mapy następuje ze stopniowo zanikającym przyspieszeniem.",
 	"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Pomiń efekty zanikania",
@@ -337,6 +350,12 @@
 	"vcmi.heroWindow.openCommander.help"  : "Wyświetla informacje o dowódcy przynależącym do tego bohatera",
 	"vcmi.heroWindow.openBackpack.hover" : "Otwórz okno sakwy",
 	"vcmi.heroWindow.openBackpack.help"  : "Otwiera okno pozwalające łatwiej zarządzać artefaktami w sakwie",
+	"vcmi.heroWindow.sortBackpackByCost.hover"  : "Sortuj wg. wartości",
+	"vcmi.heroWindow.sortBackpackByCost.help"  : "Sortuj artefakty w sakwie według wartości",
+	"vcmi.heroWindow.sortBackpackBySlot.hover"  : "Sortuj wg. miejsc",
+	"vcmi.heroWindow.sortBackpackBySlot.help"  : "Sortuj artefakty w sakwie według umiejscowienia na ciele",
+	"vcmi.heroWindow.sortBackpackByClass.hover"  : "Sortuj wg. jakości",
+	"vcmi.heroWindow.sortBackpackByClass.help"  : "Sortuj artefakty w sakwie według jakości: Skarb, Pomniejszy, Potężny, Relikt",
 
 	"vcmi.tavernWindow.inviteHero"  : "Zaproś bohatera",
 
@@ -663,5 +682,7 @@
 	"core.bonus.WIDE_BREATH.name": "Szerokie zionięcie",
 	"core.bonus.WIDE_BREATH.description": "Szeroki atak zionięciem (wiele heksów)",
 	"core.bonus.DISINTEGRATE.name": "Rozpadanie",
-	"core.bonus.DISINTEGRATE.description": "Po śmierci nie pozostaje żaden trup"
+	"core.bonus.DISINTEGRATE.description": "Po śmierci nie pozostaje żaden trup",
+	"core.bonus.INVINCIBLE.name": "Niezwyciężony",
+	"core.bonus.INVINCIBLE.description": "Nic nie może mieć na niego wpływu"
 }

+ 17 - 1
Mods/vcmi/config/vcmi/portuguese.json

@@ -12,7 +12,9 @@
 	"vcmi.adventureMap.monsterThreat.levels.9"  : "Avassaladora",
 	"vcmi.adventureMap.monsterThreat.levels.10" : "Mortal",
 	"vcmi.adventureMap.monsterThreat.levels.11" : "Impossível",
-	"vcmi.adventureMap.monsterLevel" : "\n\nNível %LEVEL, unidade de %TOWN",
+	"vcmi.adventureMap.monsterLevel"            : "\n\nNível %LEVEL, unidade %TOWN de ataque %ATTACK_TYPE",
+	"vcmi.adventureMap.monsterMeleeType"        : "corpo a corpo",
+	"vcmi.adventureMap.monsterRangedType"       : "à distância",
 
 	"vcmi.adventureMap.confirmRestartGame"               : "Tem certeza de que deseja reiniciar o jogo?",
 	"vcmi.adventureMap.noTownWithMarket"                 : "Não há mercados disponíveis!",
@@ -59,6 +61,13 @@
 
 	"vcmi.spellBook.search" : "Procurar...",
 
+	"vcmi.spellResearch.canNotAfford" : "Você não pode se dar ao luxo de substituir {%SPELL1} por {%SPELL2}. Mas você ainda pode descartar este feitiço e continuar a pesquisa de feitiços.",
+	"vcmi.spellResearch.comeAgain" : "A pesquisa já foi realizada hoje. Volte amanhã.",
+	"vcmi.spellResearch.pay" : "Gostaria de substituir {%SPELL1} por {%SPELL2}? Ou descartar este feitiço e continuar a pesquisa de feitiços?",
+	"vcmi.spellResearch.research" : "Pesquisar este Feitiço",
+	"vcmi.spellResearch.skip" : "Pular este Feitiço",
+	"vcmi.spellResearch.abort" : "Abortar",
+
 	"vcmi.mainMenu.serverConnecting" : "Conectando...",
 	"vcmi.mainMenu.serverAddressEnter" : "Insira o endereço:",
 	"vcmi.mainMenu.serverConnectionFailed" : "Falha ao conectar",
@@ -143,6 +152,7 @@
 	"vcmi.client.errors.invalidMap" : "{Mapa ou campanha inválido}\n\nFalha ao iniciar o jogo! O mapa ou campanha selecionado pode ser inválido ou corrompido. Motivo:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{Arquivos de dados ausentes}\n\nOs arquivos de dados das campanhas não foram encontrados! Você pode estar usando arquivos de dados incompletos ou corrompidos do Heroes 3. Por favor, reinstale os dados do jogo.",
 	"vcmi.server.errors.disconnected" : "{Erro de Rede}\n\nA conexão com o servidor do jogo foi perdida!",
+	"vcmi.server.errors.playerLeft" : "{Jogador Saiu}\n\nO jogador %s desconectou-se do jogo!", //%s -> player color
 	"vcmi.server.errors.existingProcess" : "Outro processo do servidor VCMI está em execução. Por favor, termine-o antes de iniciar um novo jogo.",
 	"vcmi.server.errors.modsToEnable"    : "{Os seguintes mods são necessários}",
 	"vcmi.server.errors.modsToDisable"   : "{Os seguintes mods devem ser desativados}",
@@ -340,6 +350,12 @@
 	"vcmi.heroWindow.openCommander.help" : "Mostra detalhes sobre o comandante deste herói.",
 	"vcmi.heroWindow.openBackpack.hover" : "Abrir janela da mochila de artefatos",
 	"vcmi.heroWindow.openBackpack.help" : "Abre a janela que facilita o gerenciamento da mochila de artefatos.",
+	"vcmi.heroWindow.sortBackpackByCost.hover"  : "Ordenar por custo",
+	"vcmi.heroWindow.sortBackpackByCost.help"   : "Ordenar artefatos na mochila por custo.",
+	"vcmi.heroWindow.sortBackpackBySlot.hover"  : "Ordenar por espaço",
+	"vcmi.heroWindow.sortBackpackBySlot.help"   : "Ordenar artefatos na mochila por espaço equipado.",
+	"vcmi.heroWindow.sortBackpackByClass.hover" : "Ordenar por classe",
+	"vcmi.heroWindow.sortBackpackByClass.help"  : "Ordenar artefatos na mochila por classe de artefato. Tesouro, Menor, Maior, Relíquia",
 
 	"vcmi.tavernWindow.inviteHero" : "Convidar herói",
 

+ 154 - 138
Mods/vcmi/config/vcmi/swedish.json

@@ -13,6 +13,8 @@
 	"vcmi.adventureMap.monsterThreat.levels.10" : "Dödlig",
 	"vcmi.adventureMap.monsterThreat.levels.11" : "Omöjlig",
 	"vcmi.adventureMap.monsterLevel"            : "\n\nNivå: %LEVEL - Faktion: %TOWN",
+	"vcmi.adventureMap.monsterMeleeType"        : "närstrid",
+	"vcmi.adventureMap.monsterRangedType"       : "fjärrstrid",
 
 	"vcmi.adventureMap.confirmRestartGame"               : "Är du säker på att du vill starta om spelet?",
 	"vcmi.adventureMap.noTownWithMarket"                 : "Det finns inga tillgängliga marknadsplatser!",
@@ -21,7 +23,7 @@
 	"vcmi.adventureMap.playerAttacked"                   : "Spelare har blivit attackerad: %s",
 	"vcmi.adventureMap.moveCostDetails"                  : "Förflyttningspoängs-kostnad: %TURNS tur(er) + %POINTS poäng - Återstående poäng: %REMAINING",
 	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Förflyttningspoängs-kostnad: %POINTS poäng - Återstående poäng: %REMAINING",
-	"vcmi.adventureMap.movementPointsHeroInfo" 			 : "(Förflyttningspoäng: %REMAINING / %POINTS)",
+	"vcmi.adventureMap.movementPointsHeroInfo"           : "(Förflyttningspoäng: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Tyvärr, att spela om motståndarens tur är inte implementerat ännu!",
 
 	"vcmi.capitalColors.0" : "Röd",
@@ -38,12 +40,12 @@
 	"vcmi.heroOverview.secondarySkills" : "Sekundärförmågor",
 	"vcmi.heroOverview.spells"          : "Trollformler",
 
-	"vcmi.radialWheel.mergeSameUnit"    : "Slå samman samma varelser",
+	"vcmi.radialWheel.mergeSameUnit"    : "Slå samman varelser av samma sort",
 	"vcmi.radialWheel.fillSingleUnit"   : "Fyll på med enstaka varelser",
 	"vcmi.radialWheel.splitSingleUnit"  : "Dela av en enda varelse",
 	"vcmi.radialWheel.splitUnitEqually" : "Dela upp varelser lika",
 	"vcmi.radialWheel.moveUnit"         : "Flytta varelser till en annan armé",
-	"vcmi.radialWheel.splitUnit"        : "Dela upp varelse till en annan ruta",
+	"vcmi.radialWheel.splitUnit"        : "Dela upp varelseförband till en annan ruta",
 
 	"vcmi.radialWheel.heroGetArmy"       : "Hämta armé från annan hjälte",
 	"vcmi.radialWheel.heroSwapArmy"      : "Byt armé med annan hjälte",
@@ -52,13 +54,20 @@
 	"vcmi.radialWheel.heroSwapArtifacts" : "Byt artefakter med annan hjälte",
 	"vcmi.radialWheel.heroDismiss"       : "Avfärda hjälten",
 
-	"vcmi.radialWheel.moveTop"    : "Flytta till toppen",
+	"vcmi.radialWheel.moveTop"    : "Flytta längst upp",
 	"vcmi.radialWheel.moveUp"     : "Flytta upp",
 	"vcmi.radialWheel.moveDown"   : "Flytta nedåt",
-	"vcmi.radialWheel.moveBottom" : "Flytta till botten",
+	"vcmi.radialWheel.moveBottom" : "Flytta längst ner",
 
 	"vcmi.spellBook.search" : "sök...",
 
+	"vcmi.spellResearch.canNotAfford" : "Du har inte råd att byta ut '{%SPELL1}' med '{%SPELL2}'. Du kan fortfarande göra dig av med den här trollformeln och forska vidare.",
+	"vcmi.spellResearch.comeAgain"    : "Forskningen har redan gjorts idag. Kom tillbaka imorgon.",
+	"vcmi.spellResearch.pay"          : "Vill du byta ut '{%SPELL1}' med '{%SPELL2}'? Eller vill du göra dig av med den valda trollformeln och forska vidare?",
+	"vcmi.spellResearch.research"     : "Forska fram denna trollformel",
+	"vcmi.spellResearch.skip"         : "Strunta i denna trollformel",
+	"vcmi.spellResearch.abort"        : "Avbryt",
+
 	"vcmi.mainMenu.serverConnecting"       : "Ansluter...",
 	"vcmi.mainMenu.serverAddressEnter"     : "Ange adress:",
 	"vcmi.mainMenu.serverConnectionFailed" : "Misslyckades med att ansluta",
@@ -135,14 +144,15 @@
 	"vcmi.lobby.pvp.coin.hover"           : "Mynt",
 	"vcmi.lobby.pvp.coin.help"            : "Singla slant",
 	"vcmi.lobby.pvp.randomTown.hover"     : "Slumpmässig stad",
-	"vcmi.lobby.pvp.randomTown.help"      : "Skriv en slumpmässig stad i chatten",
-	"vcmi.lobby.pvp.randomTownVs.hover"   : "Slumpmässig stad vs.",
-	"vcmi.lobby.pvp.randomTownVs.help"    : "Skriv två slumpmässiga städer i chatten",
+	"vcmi.lobby.pvp.randomTown.help"      : "Skriv en slumpad stad i chatten",
+	"vcmi.lobby.pvp.randomTownVs.hover"   : "Slumpad stad vs.",
+	"vcmi.lobby.pvp.randomTownVs.help"    : "Skriv två slumpade städer i chatten",
 	"vcmi.lobby.pvp.versus"               : "vs.",
 
 	"vcmi.client.errors.invalidMap"       : "{Ogiltig karta eller kampanj}\n\nStartade inte spelet! Vald karta eller kampanj kan vara ogiltig eller skadad. Orsak:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{Saknade datafiler}\n\nKampanjernas datafiler hittades inte! Du kanske använder ofullständiga eller skadade Heroes 3-datafiler. Vänligen installera om speldata.",
 	"vcmi.server.errors.disconnected"     : "{Nätverksfel}\n\nAnslutningen till spelservern har förlorats!",
+	"vcmi.server.errors.playerLeft"       : "{Spelare har lämnat}\n\n%s spelaren har kopplat bort sig från spelet!", //%s -> spelarens färg
 	"vcmi.server.errors.existingProcess"  : "En annan VCMI-serverprocess är igång. Vänligen avsluta den innan du startar ett nytt spel.",
 	"vcmi.server.errors.modsToEnable"     : "{Följande modd(ar) krävs}",
 	"vcmi.server.errors.modsToDisable"    : "{Följande modd(ar) måste inaktiveras}",
@@ -221,8 +231,8 @@
 	"vcmi.systemOptions.enableUiEnhancementsButton.help"   : "{Gränssnittsförbättringar}\n\nVälj mellan olika förbättringar av användargränssnittet. Till exempel en lättåtkomlig ryggsäcksknapp med mera. Avaktivera för att få en mer klassisk spelupplevelse.",
 	"vcmi.systemOptions.enableLargeSpellbookButton.hover"  : "Stor trollformelsbok",
 	"vcmi.systemOptions.enableLargeSpellbookButton.help"   : "{Stor trollformelsbok}\n\nAktiverar en större trollformelsbok som rymmer fler trollformler per sida (animeringen av sidbyte i den större trollformelsboken fungerar inte).",
-	"vcmi.systemOptions.audioMuteFocus.hover"              : "Stänger av ljudet vid inaktivitet",
-	"vcmi.systemOptions.audioMuteFocus.help"               : "{Stäng av ljud vid inaktivitet}\n\nStänger av ljudet i spelet vid inaktivitet. Undantag är meddelanden i spelet och ljudet för ny tur/omgång.",
+	"vcmi.systemOptions.audioMuteFocus.hover"              : "Tyst vid inaktivitet",
+	"vcmi.systemOptions.audioMuteFocus.help"               : "{Tyst vid inaktivitet}\n\nStänger av ljudet i spelet vid inaktivitet. Undantag är meddelanden i spelet och ljudet för ny turomgång.",
 
 	"vcmi.adventureOptions.infoBarPick.hover"                : "Visar textmeddelanden i infopanelen",
 	"vcmi.adventureOptions.infoBarPick.help"                 : "{Infopanelsmeddelanden}\n\nNär det är möjligt kommer spelmeddelanden från besökande kartobjekt att visas i infopanelen istället för att dyka upp i ett separat fönster.",
@@ -234,11 +244,11 @@
 	"vcmi.adventureOptions.showGrid.help"                    : "{Visa rutnät}\n\nVisa rutnätsöverlägget som markerar gränserna mellan äventyrskartans brickor/rutor.",
 	"vcmi.adventureOptions.borderScroll.hover"               : "Kantrullning",
 	"vcmi.adventureOptions.borderScroll.help"                : "{Kantrullning}\n\nRullar äventyrskartan när markören är angränsande till fönsterkanten. Kan inaktiveras genom att hålla ned CTRL-tangenten.",
-	"vcmi.adventureOptions.infoBarCreatureManagement.hover"  : "Hantering av varelser i infopanelen i nedre högra hörnet",
+	"vcmi.adventureOptions.infoBarCreatureManagement.hover"  : "Hantera armén i nedre högra hörnet",
 	"vcmi.adventureOptions.infoBarCreatureManagement.help"   : "{Varelsehantering i infopanelen}\n\nTillåter omarrangering av varelser i infopanelen längst ner till höger på äventyrskartan istället för att bläddra mellan olika infopaneler.",
-	"vcmi.adventureOptions.leftButtonDrag.hover"             : "Dra kartan med vänster musknapp",
+	"vcmi.adventureOptions.leftButtonDrag.hover"             : "V.klicksdragning",
 	"vcmi.adventureOptions.leftButtonDrag.help"              : "{Vänsterklicksdragning}\n\nVid aktivering kan äventyrskartans kartvy dras genom att flytta musen med vänster musknapp nedtryckt.",
-	"vcmi.adventureOptions.rightButtonDrag.hover"            : "Dra kartan med höger musknapp",
+	"vcmi.adventureOptions.rightButtonDrag.hover"            : "H.klicksdragning",
 	"vcmi.adventureOptions.rightButtonDrag.help"             : "{Högerklicksdragning}\n\nVid aktivering kan äventyrskartans kartvy dras genom att flytta musen med höger musknapp nedtryckt.",
 	"vcmi.adventureOptions.smoothDragging.hover"             : "Mjuk kartdragning",
 	"vcmi.adventureOptions.smoothDragging.help"              : "{Mjuk kartdragning}\n\nVid aktivering så har kartdragningen en modern rullningseffekt.",
@@ -268,16 +278,16 @@
 	"vcmi.battleOptions.animationsSpeed1.help"           : "Ställ in animationshastigheten till mycket långsam.",
 	"vcmi.battleOptions.animationsSpeed5.help"           : "Ställ in animationshastigheten till mycket snabb.",
 	"vcmi.battleOptions.animationsSpeed6.help"           : "Ställ in animationshastigheten till omedelbar.",
-	"vcmi.battleOptions.movementHighlightOnHover.hover"  : "Muspeka (hovra) för att avslöja förflyttningsräckvidd",
-	"vcmi.battleOptions.movementHighlightOnHover.help"   : "{Muspeka för att avslöja förflyttningsräckvidd}\n\nVisar enheters potentiella förflyttningsräckvidd över slagfältet när du håller muspekaren över dem.",
+	"vcmi.battleOptions.movementHighlightOnHover.hover"  : "Avslöja förflyttningsräckvidd",
+	"vcmi.battleOptions.movementHighlightOnHover.help"   : "{Muspeka (hovra) för att avslöja förflyttningsräckvidd}\n\nVisar enheters potentiella förflyttningsräckvidd över slagfältet när du håller muspekaren över dem.",
 	"vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Avslöja skyttars räckvidd",
 	"vcmi.battleOptions.rangeLimitHighlightOnHover.help" : "{Muspeka för att avslöja skyttars räckvidd}\n\nVisar hur långt en enhets distansattack sträcker sig över slagfältet när du håller muspekaren över dem.",
 	"vcmi.battleOptions.showStickyHeroInfoWindows.hover" : "Visa fönster med hjältars primärförmågor",
 	"vcmi.battleOptions.showStickyHeroInfoWindows.help"  : "{Visa fönster med hjältars primärförmågor}\n\nKommer alltid att visa ett fönster där du kan se dina hjältars primärförmågor (anfall, försvar, trollkonst, kunskap och trollformelpoäng).",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover"      : "Hoppa över intromusik",
 	"vcmi.battleOptions.skipBattleIntroMusic.help"       : "{Hoppa över intromusik}\n\nTillåt åtgärder under intromusiken som spelas i början av varje strid.",
-	"vcmi.battleOptions.endWithAutocombat.hover"         : "Slutför striden så fort som möjligt",
-	"vcmi.battleOptions.endWithAutocombat.help"          : "{Slutför strid}\n\nAuto-strid spelar striden åt dig för att striden ska slutföras så fort som möjligt.",
+	"vcmi.battleOptions.endWithAutocombat.hover"         : "Snabbstrid (AI)",
+	"vcmi.battleOptions.endWithAutocombat.help"          : "{Slutför striden så fort som möjligt}\n\nAI för auto-strid spelar striden åt dig för att striden ska slutföras så fort som möjligt.",
 	"vcmi.battleOptions.showQuickSpell.hover"            : "Snabb åtkomst till dina trollformler",
 	"vcmi.battleOptions.showQuickSpell.help"             : "{Visa snabbtrollformels-panelen}\n\nVisar en snabbvalspanel vid sidan av stridsfönstret där du har snabb åtkomst till några av dina trollformler",
 
@@ -340,6 +350,12 @@
 	"vcmi.heroWindow.openCommander.help"  : "Visar detaljer om befälhavaren för den här hjälten.",
 	"vcmi.heroWindow.openBackpack.hover"  : "Öppna artefaktryggsäcksfönster",
 	"vcmi.heroWindow.openBackpack.help"   : "Öppnar fönster som gör det enklare att hantera artefaktryggsäcken.",
+	"vcmi.heroWindow.sortBackpackByCost.hover"  : "Sortera efter kostnad",
+	"vcmi.heroWindow.sortBackpackByCost.help"   : "Sorterar artefakter i ryggsäcken efter kostnad.",
+	"vcmi.heroWindow.sortBackpackBySlot.hover"  : "Sortera efter plats",
+	"vcmi.heroWindow.sortBackpackBySlot.help"   : "Sorterar artefakter i ryggsäcken efter utrustad plats.",
+	"vcmi.heroWindow.sortBackpackByClass.hover" : "Sortera efter klass",
+	"vcmi.heroWindow.sortBackpackByClass.help"  : "Sorterar artefakter i ryggsäcken efter artefaktklass (skatt, mindre, större, relik)",
 
 	"vcmi.tavernWindow.inviteHero"  : "Bjud in hjälte",
 
@@ -352,8 +368,8 @@
 	"vcmi.creatureWindow.returnArtifact.hover" : "Återlämna artefakt",
 	"vcmi.creatureWindow.returnArtifact.help"  : "Klicka på den här knappen för att lägga tillbaka artefakten i hjältens ryggsäck.",
 
-	"vcmi.questLog.hideComplete.hover" : "Gömmer alla slutförda uppdrag",
-	"vcmi.questLog.hideComplete.help"  : "Dölj alla slutförda uppdrag.",
+	"vcmi.questLog.hideComplete.hover" : "Dölj alla slutförda uppdrag",
+	"vcmi.questLog.hideComplete.help"  : "Gömmer undan alla slutförda uppdrag.",
 
 	"vcmi.randomMapTab.widgets.randomTemplate"       : "(Slumpmässig)",
 	"vcmi.randomMapTab.widgets.templateLabel"        : "Mall",
@@ -362,22 +378,22 @@
 	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Vägtyper",
 
 	"vcmi.optionsTab.turnOptions.hover" : "Turomgångsalternativ",
-	"vcmi.optionsTab.turnOptions.help"  : "Välj alternativ för turomgångs-timer och simultana turer",
+	"vcmi.optionsTab.turnOptions.help"  : "Turomgångs-timer och samtidiga turomgångar (förinställningar)",
 
 	"vcmi.optionsTab.chessFieldBase.hover"          : "Bas-timern",
-	"vcmi.optionsTab.chessFieldTurn.hover"          : "Turomgångs-timern",
+	"vcmi.optionsTab.chessFieldTurn.hover"          : "Tur-timern",
 	"vcmi.optionsTab.chessFieldBattle.hover"        : "Strids-timern",
 	"vcmi.optionsTab.chessFieldUnit.hover"          : "Enhets-timern",
-	"vcmi.optionsTab.chessFieldBase.help"           : "Används när {Turomgångs-timern} når '0'. Ställs in en gång i början av spelet. När den når '0' avslutas den aktuella turomgången (pågående strid avslutas med förlust).",
-	"vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Används utanför strid eller när {Strids-timern} tar slut. Återställs varje turomgång. Outnyttjad tid läggs till i {Bas-timern} till nästa turomgång.",
+	"vcmi.optionsTab.chessFieldBase.help"           : "Används när {Tur-timern} når 0. Ställs in i början av spelet. Vid 0 avslutas turomgången (pågående strid avslutas med förlust).",
+	"vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Används utanför strid eller när {Strids-timern} tar slut. Återställs varje turomgång. Outnyttjad tid läggs till i {Bas-timern}.",
 	"vcmi.optionsTab.chessFieldTurnDiscard.help"    : "Används utanför strid eller när {Strids-timern} tar slut. Återställs varje turomgång. Outnyttjad tid går förlorad.",
-	"vcmi.optionsTab.chessFieldBattle.help"         : "Används i strider med AI eller i PVP-strid när {Enhets-timern} tar slut. Återställs i början av varje strid.",
-	"vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Används när du styr din enhet i PVP-strid. Outnyttjad tid läggs till i {Strids-timern} när enheten har avslutat sin turomgång.",
-	"vcmi.optionsTab.chessFieldUnitDiscard.help"    : "Används när du styr din enhet i PVP-strid. Återställs i början av varje enhets turomgång. Outnyttjad tid går förlorad.",
+	"vcmi.optionsTab.chessFieldBattle.help"         : "Används i strider med AI eller i PvP-strid när {Enhets-timern} tar slut. Återställs i början av varje strid.",
+	"vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Används när du styr dina enheter i PvP-strid. Outnyttjad tid läggs till i {Strids-timern} när enheten har avslutat sin turomgång.",
+	"vcmi.optionsTab.chessFieldUnitDiscard.help"    : "Används när du styr dina enheter i PvP-strid. Återställs i början av varje enhets turomgång. Outnyttjad tid går förlorad.",
 
 	"vcmi.optionsTab.accumulate" : "Ackumulera",
 
-	"vcmi.optionsTab.simturnsTitle"     : "Simultana turomgångar",
+	"vcmi.optionsTab.simturnsTitle"     : "Samtidiga turomgångar",
 	"vcmi.optionsTab.simturnsMin.hover" : "Åtminstone i",
 	"vcmi.optionsTab.simturnsMax.hover" : "Som mest i",
 	"vcmi.optionsTab.simturnsAI.hover"  : "(Experimentell) Simultana AI-turomgångar",
@@ -385,7 +401,7 @@
 	"vcmi.optionsTab.simturnsMax.help"  : "Spela samtidigt som andra spelare under ett angivet antal dagar eller tills en tillräckligt nära kontakt inträffar med en annan spelare",
 	"vcmi.optionsTab.simturnsAI.help"   : "{Simultana AI-turomgångar}\nExperimentellt alternativ. Tillåter AI-spelare att agera samtidigt som den mänskliga spelaren när simultana turomgångar är aktiverade.",
 
-	"vcmi.optionsTab.turnTime.select"     : "Turtids-förinställningar",
+	"vcmi.optionsTab.turnTime.select"     : "Ställ in turomgångs-timer",
 	"vcmi.optionsTab.turnTime.unlimited"  : "Obegränsat med tid",
 	"vcmi.optionsTab.turnTime.classic.1"  : "Klassisk timer: 1 minut",
 	"vcmi.optionsTab.turnTime.classic.2"  : "Klassisk timer: 2 minuter",
@@ -393,22 +409,22 @@
 	"vcmi.optionsTab.turnTime.classic.10" : "Klassisk timer: 10 minuter",
 	"vcmi.optionsTab.turnTime.classic.20" : "Klassisk timer: 20 minuter",
 	"vcmi.optionsTab.turnTime.classic.30" : "Klassisk timer: 30 minuter",
-	"vcmi.optionsTab.turnTime.chess.20"   : "Schack-timer: 20:00 + 10:00 + 02:00 + 00:00",
-	"vcmi.optionsTab.turnTime.chess.16"   : "Schack-timer: 16:00 + 08:00 + 01:30 + 00:00",
-	"vcmi.optionsTab.turnTime.chess.8"    : "Schack-timer: 08:00 + 04:00 + 01:00 + 00:00",
-	"vcmi.optionsTab.turnTime.chess.4"    : "Schack-timer: 04:00 + 02:00 + 00:30 + 00:00",
-	"vcmi.optionsTab.turnTime.chess.2"    : "Schack-timer: 02:00 + 01:00 + 00:15 + 00:00",
-	"vcmi.optionsTab.turnTime.chess.1"    : "Schack-timer: 01:00 + 01:00 + 00:00 + 00:00",
-
-	"vcmi.optionsTab.simturns.select"         : "Välj förinställning för simultana/samtidiga turer",
-	"vcmi.optionsTab.simturns.none"           : "Inga simultana/samtidiga turer",
-	"vcmi.optionsTab.simturns.tillContactMax" : "Simultantur: Fram till kontakt",
-	"vcmi.optionsTab.simturns.tillContact1"   : "Simultantur: 1 vecka, bryt vid kontakt",
-	"vcmi.optionsTab.simturns.tillContact2"   : "Simultantur: 2 veckor, bryt vid kontakt",
-	"vcmi.optionsTab.simturns.tillContact4"   : "Simultantur: 1 månad, bryt vid kontakt",
-	"vcmi.optionsTab.simturns.blocked1"       : "Simultantur: 1 vecka, kontakter blockerade",
-	"vcmi.optionsTab.simturns.blocked2"       : "Simultantur: 2 veckor, kontakter blockerade",
-	"vcmi.optionsTab.simturns.blocked4"       : "Simultantur: 1 månad, kontakter blockerade",
+	"vcmi.optionsTab.turnTime.chess.20"   : "Schack: 20:00 + 10:00 + 02:00 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.16"   : "Schack: 16:00 + 08:00 + 01:30 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.8"    : "Schack: 08:00 + 04:00 + 01:00 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.4"    : "Schack: 04:00 + 02:00 + 00:30 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.2"    : "Schack: 02:00 + 01:00 + 00:15 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.1"    : "Schack: 01:00 + 01:00 + 00:00 + 00:00",
+
+	"vcmi.optionsTab.simturns.select"         : "Ställ in samtidiga turomgångar",
+	"vcmi.optionsTab.simturns.none"           : "Inga samtidiga turomgångar",
+	"vcmi.optionsTab.simturns.tillContactMax" : "Samtur: Fram till närkontakt",
+	"vcmi.optionsTab.simturns.tillContact1"   : "Samtur: 1 vecka (bryts vid närkontakt)",
+	"vcmi.optionsTab.simturns.tillContact2"   : "Samtur: 2 veckor (bryts vid närkontakt)",
+	"vcmi.optionsTab.simturns.tillContact4"   : "Samtur: 1 månad (bryts vid närkontakt)",
+	"vcmi.optionsTab.simturns.blocked1"       : "Samtur: 1 vecka (närkontakt blockerad)",
+	"vcmi.optionsTab.simturns.blocked2"       : "Samtur: 2 veckor (närkontakt blockerad)",
+	"vcmi.optionsTab.simturns.blocked4"       : "Samtur: 1 månad (närkontakt blockerad)",
 
 	// 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
@@ -518,155 +534,155 @@
 	"core.seerhut.quest.reachDate.visit.5"       : "Stängt fram till %s.",
 
 	"core.bonus.ADDITIONAL_ATTACK.name"                  : "Dubbelslag",
-	"core.bonus.ADDITIONAL_ATTACK.description"           : "Attackerar två gånger",
+	"core.bonus.ADDITIONAL_ATTACK.description"           : "Attackerar två gånger.",
 	"core.bonus.ADDITIONAL_RETALIATION.name"             : "Ytterligare motattacker",
-	"core.bonus.ADDITIONAL_RETALIATION.description"      : "Kan slå tillbaka ${val} extra gånger",
+	"core.bonus.ADDITIONAL_RETALIATION.description"      : "Kan slå tillbaka ${val} extra gång(er).",
 	"core.bonus.AIR_IMMUNITY.name"                       : "Luft-immunitet",
-	"core.bonus.AIR_IMMUNITY.description"                : "Immun mot alla trollformler från skolan för luftmagi",
+	"core.bonus.AIR_IMMUNITY.description"                : "Immun mot alla luftmagi-trollformler.",
 	"core.bonus.ATTACKS_ALL_ADJACENT.name"               : "Attackera runtomkring",
-	"core.bonus.ATTACKS_ALL_ADJACENT.description"        : "Attackerar alla angränsande fiender",
-	"core.bonus.BLOCKS_RETALIATION.name"                 : "Ingen motattack",
-	"core.bonus.BLOCKS_RETALIATION.description"          : "Fienden kan inte slå tillbaka/retaliera",
-	"core.bonus.BLOCKS_RANGED_RETALIATION.name"          : "Ingen motattack på avstånd",
-	"core.bonus.BLOCKS_RANGED_RETALIATION.description"   : "Fienden kan inte göra en motattack/retaliering på avstånd genom att använda en distansattack",
-	"core.bonus.CATAPULT.name"                           : "Katapult",
-	"core.bonus.CATAPULT.description"                    : "Attackerar belägringsmurar",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name"        : "Minska trollformelkostnaden (${val})",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description" : "Minskar trollformelkostnaden för hjälten med ${val}",
+	"core.bonus.ATTACKS_ALL_ADJACENT.description"        : "Attackerar alla angränsande fiender.",
+	"core.bonus.BLOCKS_RETALIATION.name"                 : "Retaliera ej i närstrid",
+	"core.bonus.BLOCKS_RETALIATION.description"          : "Fienden kan inte slå tillbaka/retaliera.",
+	"core.bonus.BLOCKS_RANGED_RETALIATION.name"          : "Retaliera ej på avstånd",
+	"core.bonus.BLOCKS_RANGED_RETALIATION.description"   : "Fienden kan inte retaliera på avstånd.",
+	"core.bonus.CATAPULT.name"                           : "Katapult-attack",
+	"core.bonus.CATAPULT.description"                    : "Attackerar belägringsmurar.",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name"        : "Minska magikostnad (${val})",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description" : "Minskar magikostnaden för hjälten med ${val}.",
 	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name"       : "Magisk dämpare (${val})",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Ökar trollformelkostnaden för fiendens trollformler med ${val}",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Ökar fiendens magikostnad med ${val}.",
 	"core.bonus.CHARGE_IMMUNITY.name"                    : "Galoppanfalls-immunitet",
-	"core.bonus.CHARGE_IMMUNITY.description"             : "Immun mot ryttares och tornerares galopperande ridanfall",
+	"core.bonus.CHARGE_IMMUNITY.description"             : "Immun mot ryttares galopperande ridanfall.",
 	"core.bonus.DARKNESS.name"                           : "I skydd av mörkret",
-	"core.bonus.DARKNESS.description"                    : "Skapar ett hölje av mörker med en ${val}-rutorsradie",
+	"core.bonus.DARKNESS.description"                    : "Skapar ett mörkerhölje med rutradien ${val}.",
 	"core.bonus.DEATH_STARE.name"                        : "Dödsblick (${val}%)",
-	"core.bonus.DEATH_STARE.description"                 : "Varje förbandsenhet med 'Dödsblick' har ${val}% chans att döda den översta enheten i ett fiendeförband",
+	"core.bonus.DEATH_STARE.description"                 : "Varje dödsblick har ${val}% chans att döda.",
 	"core.bonus.DEFENSIVE_STANCE.name"                   : "Försvarshållning",
-	"core.bonus.DEFENSIVE_STANCE.description"            : "Ger ytterligare +${val} till enhetens försvarsförmåga när du väljer att försvarar dig",
+	"core.bonus.DEFENSIVE_STANCE.description"            : "+${val} extra försvar när du försvarar dig.",
 	"core.bonus.DESTRUCTION.name"                        : "Förintelse",
-	"core.bonus.DESTRUCTION.description"                 : "Har ${val}% chans att döda extra enheter efter attack",
+	"core.bonus.DESTRUCTION.description"                 : "${val}% chans att ta död på fler efter attack.",
 	"core.bonus.DOUBLE_DAMAGE_CHANCE.name"               : "Dödsstöt",
-	"core.bonus.DOUBLE_DAMAGE_CHANCE.description"        : "Har ${val}% chans att ge dubbel basskada vid attack",
+	"core.bonus.DOUBLE_DAMAGE_CHANCE.description"        : "${val}% chans till dubbel basskada vid attack.",
 	"core.bonus.DRAGON_NATURE.name"                      : "Drake",
-	"core.bonus.DRAGON_NATURE.description"               : "Varelsen har en draknatur",
+	"core.bonus.DRAGON_NATURE.description"               : "Varelsen har en draknatur.",
 	"core.bonus.EARTH_IMMUNITY.name"                     : "Jord-immunitet",
-	"core.bonus.EARTH_IMMUNITY.description"              : "Immun mot alla trollformler från skolan för jordmagi",
+	"core.bonus.EARTH_IMMUNITY.description"              : "Immun mot alla jordmagi-trollformler.",
 	"core.bonus.ENCHANTER.name"                          : "Förtrollare",
-	"core.bonus.ENCHANTER.description"                   : "Kan kasta ${subtyp.spell} på alla varje tur/omgång",
+	"core.bonus.ENCHANTER.description"                   : "Kastar mass-${subtype.spell} varje turomgång.",
 	"core.bonus.ENCHANTED.name"                          : "Förtrollad",
-	"core.bonus.ENCHANTED.description"                   : "Påverkas av permanent ${subtype.spell}",
+	"core.bonus.ENCHANTED.description"                   : "Påverkas av permanent ${subtype.spell}.",
 	"core.bonus.ENEMY_ATTACK_REDUCTION.name"             : "Avfärda attack (${val}%)",
-	"core.bonus.ENEMY_ATTACK_REDUCTION.description"      : "När du blir attackerad ignoreras ${val}% av angriparens attack",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.description"      : "Ignorerar ${val}% av angriparens attack.",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.name"            : "Förbigå försvar (${val}%)",
-	"core.bonus.ENEMY_DEFENCE_REDUCTION.description"     : "När du attackerar ignoreras ${val}% av försvararens försvar",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.description"     : "Din attack ignorerar ${val}% av fiendens försvar.",
 	"core.bonus.FIRE_IMMUNITY.name"                      : "Eld-immunitet",
-	"core.bonus.FIRE_IMMUNITY.description"               : "Immun mot alla trollformler från skolan för eldmagi",
+	"core.bonus.FIRE_IMMUNITY.description"               : "Immun mot alla eldmagi-trollformler.",
 	"core.bonus.FIRE_SHIELD.name"                        : "Eldsköld (${val}%)",
-	"core.bonus.FIRE_SHIELD.description"                 : "Reflekterar en del av närstridsskadorna",
+	"core.bonus.FIRE_SHIELD.description"                 : "Reflekterar en del av närstridsskadorna.",
 	"core.bonus.FIRST_STRIKE.name"                       : "Första slaget",
-	"core.bonus.FIRST_STRIKE.description"                : "Denna varelse gör en motattack innan den blir attackerad",
+	"core.bonus.FIRST_STRIKE.description"                : "Retalierar innan den blir attackerad.",
 	"core.bonus.FEAR.name"                               : "Rädsla",
-	"core.bonus.FEAR.description"                        : "Orsakar rädsla på ett fiendeförband",
+	"core.bonus.FEAR.description"                        : "Orsakar rädsla på ett fiendeförband.",
 	"core.bonus.FEARLESS.name"                           : "Orädd",
-	"core.bonus.FEARLESS.description"                    : "Immun mot rädsla",
+	"core.bonus.FEARLESS.description"                    : "Immun mot rädsla.",
 	"core.bonus.FEROCITY.name"                           : "Vildsint",
-	"core.bonus.FEROCITY.description"                    : "Attackerar ${val} extra gång(er) om någon dödas",
+	"core.bonus.FEROCITY.description"                    : "+${val} extra attack(er) om någon dödas.",
 	"core.bonus.FLYING.name"                             : "Flygande",
-	"core.bonus.FLYING.description"                      : "Flyger vid förflyttning (ignorerar hinder)",
+	"core.bonus.FLYING.description"                      : "Flyger vid förflyttning (ignorerar hinder).",
 	"core.bonus.FREE_SHOOTING.name"                      : "Skjut på nära håll",
-	"core.bonus.FREE_SHOOTING.description"               : "Kan använda distansattacker på närstridsavstånd",
+	"core.bonus.FREE_SHOOTING.description"               : "Använd distansattacker på närstridsavstånd.",
 	"core.bonus.GARGOYLE.name"                           : "Stenfigur",
-	"core.bonus.GARGOYLE.description"                    : "Kan varken upplivas eller läkas",
+	"core.bonus.GARGOYLE.description"                    : "Kan varken upplivas eller läkas.",
 	"core.bonus.GENERAL_DAMAGE_REDUCTION.name"           : "Minska skada (${val}%)",
-	"core.bonus.GENERAL_DAMAGE_REDUCTION.description"    : "Reducerar fysisk skada från både distans- och närstridsattacker",
-	"core.bonus.HATE.name"                               : "Hatar ${subtyp.varelse}",
-	"core.bonus.HATE.description"                        : "Gör ${val}% mer skada mot ${subtyp.varelse}",
+	"core.bonus.GENERAL_DAMAGE_REDUCTION.description"    : "Reducerar skadan från inkommande attacker.",
+	"core.bonus.HATE.name"                               : "Hatar: ${subtype.creature}",
+	"core.bonus.HATE.description"                        : "Gör ${val}% mer skada mot ${subtype.creature}.",
 	"core.bonus.HEALER.name"                             : "Helare",
-	"core.bonus.HEALER.description"                      : "Helar/läker allierade enheter",
+	"core.bonus.HEALER.description"                      : "Helar/läker allierade enheter.",
 	"core.bonus.HP_REGENERATION.name"                    : "Självläkande",
-	"core.bonus.HP_REGENERATION.description"             : "Får tillbaka ${val} träffpoäng (hälsa) varje runda",
+	"core.bonus.HP_REGENERATION.description"             : "Återfår ${val} hälsa (träffpoäng) varje runda.",
 	"core.bonus.JOUSTING.name"                           : "Galopperande ridanfall",
-	"core.bonus.JOUSTING.description"                    : "Orsakar +${val}% extra skada för varje ruta som enheten förflyttas innan attack",
+	"core.bonus.JOUSTING.description"                    : "+${val}% skada per rutförflyttning före attack.",
 	"core.bonus.KING.name"                               : "Kung",
-	"core.bonus.KING.description"                        : "Sårbar för 'Dräpar'-nivå ${val} eller högre",
-	"core.bonus.LEVEL_SPELL_IMMUNITY.name"               : "Förtrollningsimmunitet 1-${val}",
-	"core.bonus.LEVEL_SPELL_IMMUNITY.description"        : "Immun mot trollformler på nivå 1-${val}",
-	"core.bonus.LIMITED_SHOOTING_RANGE.name"             : "Begränsad räckvidd för skjutning",
-	"core.bonus.LIMITED_SHOOTING_RANGE.description"      : "Kan inte sikta på enheter längre bort än ${val} rutor",
-	"core.bonus.LIFE_DRAIN.name"                         : "Dränerar livskraft (${val}%)",
-	"core.bonus.LIFE_DRAIN.description"                  : "Dränerar ${val}% träffpoäng (hälsa) av utdelad skada",
-	"core.bonus.MANA_CHANNELING.name"                    : "Kanalisera trollformelspoäng ${val}%",
-	"core.bonus.MANA_CHANNELING.description"             : "Ger din hjälte ${val}% av den mängd trollformelspoäng som fienden spenderar per trollformel i strid",
-	"core.bonus.MANA_DRAIN.name"                         : "Dränera trollformelspoäng",
-	"core.bonus.MANA_DRAIN.description"                  : "Dränerar ${val} trollformelspoäng varje tur",
+	"core.bonus.KING.description"                        : "Sårbar för Dräpar-nivå ${val} eller högre.",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.name"               : "Trolldomsimmunitet 1-${val}",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.description"        : "Immun mot nivå 1-${val}-trollformler.",
+	"core.bonus.LIMITED_SHOOTING_RANGE.name"             : "Begränsad skjuträckvidd",
+	"core.bonus.LIMITED_SHOOTING_RANGE.description"      : "Skjuträckvidd: ${val} rutor.",
+	"core.bonus.LIFE_DRAIN.name"                         : "Dränera livskraft (${val}%)",
+	"core.bonus.LIFE_DRAIN.description"                  : "Dränera ${val}% hälsa av den vållade skadan.",
+	"core.bonus.MANA_CHANNELING.name"                    : "Kanalisera magi (${val}%)",
+	"core.bonus.MANA_CHANNELING.description"             : "Ger din hjälte ${val}% av fiendens spenderade mana.",
+	"core.bonus.MANA_DRAIN.name"                         : "Dränera mana",
+	"core.bonus.MANA_DRAIN.description"                  : "Dränerar ${val} mana från fienden varje tur.",
 	"core.bonus.MAGIC_MIRROR.name"                       : "Magisk spegel (${val}%)",
-	"core.bonus.MAGIC_MIRROR.description"                : "Har ${val}% chans att reflektera (omdirigera) en offensiv trollformel på en fiendeenhet",
+	"core.bonus.MAGIC_MIRROR.description"                : "${val}% chans att reflektera skadliga trollformler.",
 	"core.bonus.MAGIC_RESISTANCE.name"                   : "Magiskt motstånd (${val}%)",
-	"core.bonus.MAGIC_RESISTANCE.description"            : "Har en ${val}% chans att motstå en skadlig trollformel",
-	"core.bonus.MIND_IMMUNITY.name"                      : "Immunitet mot sinnesförtrollningar",
-	"core.bonus.MIND_IMMUNITY.description"               : "Immun mot förtrollningar som påverkar dina sinnen",
-	"core.bonus.NO_DISTANCE_PENALTY.name"                : "Ingen avståndsbestraffning",
-	"core.bonus.NO_DISTANCE_PENALTY.description"         : "Gör full skada på vilket avstånd som helst i strid",
-	"core.bonus.NO_MELEE_PENALTY.name"                   : "Ingen närstridsbestraffning",
-	"core.bonus.NO_MELEE_PENALTY.description"            : "Varelsen har ingen närstridsbestraffning",
-	"core.bonus.NO_MORALE.name"                          : "Ingen Moralpåverkan",
-	"core.bonus.NO_MORALE.description"                   : "Varelsen är immun mot moraliska effekter och har alltid neutral moral",
+	"core.bonus.MAGIC_RESISTANCE.description"            : "${val}% chans att motstå en skadlig trollformel.",
+	"core.bonus.MIND_IMMUNITY.name"                      : "Immun mot sinnesmagi",
+	"core.bonus.MIND_IMMUNITY.description"               : "Immun mot magi som påverkar dina sinnen.",
+	"core.bonus.NO_DISTANCE_PENALTY.name"                : "Långdistansskytt",
+	"core.bonus.NO_DISTANCE_PENALTY.description"         : "Gör full skada på alla avstånd i strid.",
+	"core.bonus.NO_MELEE_PENALTY.name"                   : "Närstridsspecialist",
+	"core.bonus.NO_MELEE_PENALTY.description"            : "Ingen närstridsbestraffning.",
+	"core.bonus.NO_MORALE.name"                          : "Ingen moralpåverkan",
+	"core.bonus.NO_MORALE.description"                   : "Immun mot moral-effekter (neutral moral).",
 	"core.bonus.NO_WALL_PENALTY.name"                    : "Ingen murbestraffning",
-	"core.bonus.NO_WALL_PENALTY.description"             : "Orsakar full skada mot fiender bakom en mur",
+	"core.bonus.NO_WALL_PENALTY.description"             : "Gör full skada mot fiender bakom en mur.",
 	"core.bonus.NON_LIVING.name"                         : "Icke levande",
-	"core.bonus.NON_LIVING.description"                  : "Immunitet mot många effekter som annars bara påverkar levande och odöda varelser",
+	"core.bonus.NON_LIVING.description"                  : "Immunitet mot många effekter.",
 	"core.bonus.RANDOM_SPELLCASTER.name"                 : "Slumpmässig besvärjare",
-	"core.bonus.RANDOM_SPELLCASTER.description"          : "Kan kasta trollformler som väljs slumpmässigt",
+	"core.bonus.RANDOM_SPELLCASTER.description"          : "Kastar trollformler som väljs slumpmässigt.",
 	"core.bonus.RANGED_RETALIATION.name"                 : "Motattacker på avstånd",
-	"core.bonus.RANGED_RETALIATION.description"          : "Kan retaliera/motattackera på avstånd",
-	"core.bonus.RECEPTIVE.name"                          : "Mottaglig",
-	"core.bonus.RECEPTIVE.description"                   : "Ingen immunitet mot vänliga besvärjelser",
+	"core.bonus.RANGED_RETALIATION.description"          : "Kan retaliera/motattackera på avstånd.",
+	"core.bonus.RECEPTIVE.name"                          : "Magiskt mottaglig",
+	"core.bonus.RECEPTIVE.description"                   : "Ingen immunitet mot vänliga trollformler.",
 	"core.bonus.REBIRTH.name"                            : "Återfödelse (${val}%)",
-	"core.bonus.REBIRTH.description"                     : "${val}% av enheterna kommer att återuppväckas efter döden",
+	"core.bonus.REBIRTH.description"                     : "${val}% återuppväcks efter döden.",
 	"core.bonus.RETURN_AFTER_STRIKE.name"                : "Återvänder efter närstrid",
-	"core.bonus.RETURN_AFTER_STRIKE.description"         : "Efter att ha attackerat en fiendeenhet i närstrid återvänder enheten till rutan som den var placerad på innan den utförde sin närstridsattack",
-	"core.bonus.REVENGE.name"                            : "Hämnd",
-	"core.bonus.REVENGE.description"                     : "Orsakar extra skada baserat på angriparens förlorade träffpoäng (hälsa) i strid",
+	"core.bonus.RETURN_AFTER_STRIKE.description"         : "Återvänder till sin ruta efter attack.",
+	"core.bonus.REVENGE.name"                            : "Hämndlysten",
+	"core.bonus.REVENGE.description"                     : "Vållar mer skada om den själv blivit skadad.",
 	"core.bonus.SHOOTER.name"                            : "Distans-attack",
-	"core.bonus.SHOOTER.description"                     : "Varelsen kan skjuta/attackera på avstånd",
+	"core.bonus.SHOOTER.description"                     : "Varelsen kan skjuta/attackera på avstånd.",
 	"core.bonus.SHOOTS_ALL_ADJACENT.name"                : "Skjuter alla i närheten",
-	"core.bonus.SHOOTS_ALL_ADJACENT.description"         : "Denna varelses distans-attacker drabbar alla mål i ett litet område",
+	"core.bonus.SHOOTS_ALL_ADJACENT.description"         : "Distans-attack drabbar alla inom räckhåll.",
 	"core.bonus.SOUL_STEAL.name"                         : "Själtjuv",
-	"core.bonus.SOUL_STEAL.description"                  : "Återuppväcker ${val} av sina egna enheter för varje dödad fiendeenhet",
+	"core.bonus.SOUL_STEAL.description"                  : "För varje dödad fiende återuppväcks: ${val}.",
 	"core.bonus.SPELLCASTER.name"                        : "Besvärjare",
-	"core.bonus.SPELLCASTER.description"                 : "Kan kasta ${subtype.spell}",
+	"core.bonus.SPELLCASTER.description"                 : "Kan kasta: ${subtype.spell}.",
 	"core.bonus.SPELL_AFTER_ATTACK.name"                 : "Besvärja efter attack",
-	"core.bonus.SPELL_AFTER_ATTACK.description"          : "Har en ${val}% chans att kasta ${subtype.spell} efter att den har attackerat",
+	"core.bonus.SPELL_AFTER_ATTACK.description"          : "${val}% chans för '${subtype.spell}' efter attack.",
 	"core.bonus.SPELL_BEFORE_ATTACK.name"                : "Besvärja före attack",
-	"core.bonus.SPELL_BEFORE_ATTACK.description"         : "Har en ${val}% chans att kasta ${subtype.spell} innan den attackerar",
+	"core.bonus.SPELL_BEFORE_ATTACK.description"         : "${val}% chans för '${subtype.spell}' före attack.",
 	"core.bonus.SPELL_DAMAGE_REDUCTION.name"             : "Trolldoms-resistens",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description"      : "Skadan från trollformler är reducet med ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description"      : "Reducerar magisk-skada med ${val}%.",
 	"core.bonus.SPELL_IMMUNITY.name"                     : "Trolldoms-immunitet",
-	"core.bonus.SPELL_IMMUNITY.description"              : "Immun mot ${subtype.spell}",
-	"core.bonus.SPELL_LIKE_ATTACK.name"                  : "Trolldomsliknande attack",
-	"core.bonus.SPELL_LIKE_ATTACK.description"           : "Attackerar med ${subtype.spell}",
+	"core.bonus.SPELL_IMMUNITY.description"              : "Immun mot '${subtype.spell}'.",
+	"core.bonus.SPELL_LIKE_ATTACK.name"                  : "Magisk attack",
+	"core.bonus.SPELL_LIKE_ATTACK.description"           : "Attackerar med '${subtype.spell}'.",
 	"core.bonus.SPELL_RESISTANCE_AURA.name"              : "Motståndsaura",
-	"core.bonus.SPELL_RESISTANCE_AURA.description"       : "Närbelägna förband får ${val}% magi-resistens",
+	"core.bonus.SPELL_RESISTANCE_AURA.description"       : "Angränsande förband får ${val}% magi-resistens.",
 	"core.bonus.SUMMON_GUARDIANS.name"                   : "Åkalla väktare",
-	"core.bonus.SUMMON_GUARDIANS.description"            : "I början av striden åkallas ${subtype.creature} (${val}%)",
+	"core.bonus.SUMMON_GUARDIANS.description"            : "Vid strid åkallas: ${subtype.creature} ${val}%.",
 	"core.bonus.SYNERGY_TARGET.name"                     : "Synergibar",
-	"core.bonus.SYNERGY_TARGET.description"              : "Denna varelse är sårbar för synergieffekt",
+	"core.bonus.SYNERGY_TARGET.description"              : "Denna varelse är sårbar för synergieffekt.",
 	"core.bonus.TWO_HEX_ATTACK_BREATH.name"              : "Dödlig andedräkt",
-	"core.bonus.TWO_HEX_ATTACK_BREATH.description"       : "Andningsattack (2 rutors räckvidd)",
+	"core.bonus.TWO_HEX_ATTACK_BREATH.description"       : "Andningsattack (2 rutors räckvidd).",
 	"core.bonus.THREE_HEADED_ATTACK.name"                : "Trehövdad attack",
-	"core.bonus.THREE_HEADED_ATTACK.description"         : "Attackerar tre angränsande enheter",
+	"core.bonus.THREE_HEADED_ATTACK.description"         : "Attackerar upp till tre enheter framför sig.",
 	"core.bonus.TRANSMUTATION.name"                      : "Transmutation",
-	"core.bonus.TRANSMUTATION.description"               : "${val}% chans att förvandla angripen enhet till en annan typ",
+	"core.bonus.TRANSMUTATION.description"               : "${val}% chans att förvandla angripen enhet.",
 	"core.bonus.UNDEAD.name"                             : "Odöd",
-	"core.bonus.UNDEAD.description"                      : "Varelsen är odöd",
-	"core.bonus.UNLIMITED_RETALIATIONS.name"             : "Obegränsat antal motattacker",
-	"core.bonus.UNLIMITED_RETALIATIONS.description"      : "Kan slå tillbaka mot ett obegränsat antal attacker varje omgång",
+	"core.bonus.UNDEAD.description"                      : "Varelsen är odöd.",
+	"core.bonus.UNLIMITED_RETALIATIONS.name"             : "Slår tillbaka varje gång",
+	"core.bonus.UNLIMITED_RETALIATIONS.description"      : "Obegränsat antal motattacker.",
 	"core.bonus.WATER_IMMUNITY.name"                     : "Vatten-immunitet",
-	"core.bonus.WATER_IMMUNITY.description"              : "Immun mot alla trollformler från vattenmagi-skolan",
+	"core.bonus.WATER_IMMUNITY.description"              : "Immun mot alla vattenmagi-trollformler.",
 	"core.bonus.WIDE_BREATH.name"                        : "Bred dödlig andedräkt",
-	"core.bonus.WIDE_BREATH.description"                 : "Bred andningsattack (flera rutor)",
+	"core.bonus.WIDE_BREATH.description"                 : "Bred andningsattack (flera rutor).",
 	"core.bonus.DISINTEGRATE.name"                       : "Desintegrerar",
-	"core.bonus.DISINTEGRATE.description"                : "Ingen fysisk kropp finns kvar efter att enheten blivit besegrad i strid",
+	"core.bonus.DISINTEGRATE.description"                : "Ingen kropp lämnas kvar på slagfältet.",
 	"core.bonus.INVINCIBLE.name"                         : "Oövervinnerlig",
-	"core.bonus.INVINCIBLE.description"                  : "Kan inte påverkas av någonting"
+	"core.bonus.INVINCIBLE.description"                  : "Kan inte påverkas av någonting."
 }

+ 3 - 2
client/CPlayerInterface.cpp

@@ -67,7 +67,6 @@
 
 #include "../lib/CConfigHandler.h"
 #include "../lib/texts/CGeneralTextHandler.h"
-#include "../lib/CHeroHandler.h"
 #include "../lib/CPlayerState.h"
 #include "../lib/CRandomGenerator.h"
 #include "../lib/CStack.h"
@@ -1138,7 +1137,7 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component
 		const CGTownInstance * t = dynamic_cast<const CGTownInstance *>(cb->getObj(obj));
 		if(t)
 		{
-			auto image = GH.renderHandler().loadImage(AnimationPath::builtin("ITPA"), t->town->clientInfo.icons[t->hasFort()][false] + 2, 0, EImageBlitMode::OPAQUE);
+			auto image = GH.renderHandler().loadImage(AnimationPath::builtin("ITPA"), t->getTown()->clientInfo.icons[t->hasFort()][false] + 2, 0, EImageBlitMode::OPAQUE);
 			image->scaleTo(Point(35, 23));
 			images.push_back(image);
 		}
@@ -1333,6 +1332,8 @@ void CPlayerInterface::initializeHeroTownList()
 			localState->addOwnedTown(town);
 	}
 
+	localState->deserialize(*cb->getPlayerState(playerID)->playerLocalSettings);
+
 	if(adventureInt)
 		adventureInt->onHeroChanged(nullptr);
 }

+ 9 - 4
client/CServerHandler.cpp

@@ -141,7 +141,12 @@ void CServerHandler::resetStateForLobby(EStartMode mode, ESelectionScreen screen
 	if(!playerNames.empty()) //if have custom set of player names - use it
 		localPlayerNames = playerNames;
 	else
-		localPlayerNames.push_back(settings["general"]["playerName"].String());
+	{
+		std::string playerName = settings["general"]["playerName"].String();
+		if(playerName == "Player")
+			playerName = CGI->generaltexth->translate("core.genrltxt.434");
+		localPlayerNames.push_back(playerName);
+	}
 
 	gameChat->resetMatchState();
 	lobbyClient->resetMatchState();
@@ -853,7 +858,7 @@ void CServerHandler::onPacketReceived(const std::shared_ptr<INetworkConnection>
 	if(getState() == EClientState::DISCONNECTING)
 		return;
 
-	CPack * pack = logicConnection->retrievePack(message);
+	auto pack = logicConnection->retrievePack(message);
 	ServerHandlerCPackVisitor visitor(*this);
 	pack->visit(visitor);
 }
@@ -938,14 +943,14 @@ void CServerHandler::visitForLobby(CPackForLobby & lobbyPack)
 
 void CServerHandler::visitForClient(CPackForClient & clientPack)
 {
-	client->handlePack(&clientPack);
+	client->handlePack(clientPack);
 }
 
 
 void CServerHandler::sendLobbyPack(const CPackForLobby & pack) const
 {
 	if(getState() != EClientState::STARTING)
-		logicConnection->sendPack(&pack);
+		logicConnection->sendPack(pack);
 }
 
 bool CServerHandler::inLobbyRoom() const

+ 0 - 1
client/CServerHandler.h

@@ -25,7 +25,6 @@ struct TurnTimerInfo;
 class CMapInfo;
 class CGameState;
 struct ClientPlayer;
-struct CPack;
 struct CPackForLobby;
 struct CPackForClient;
 

+ 12 - 14
client/Client.cpp

@@ -163,7 +163,7 @@ void CClient::save(const std::string & fname)
 	}
 
 	SaveGame save_game(fname);
-	sendRequest(&save_game, PlayerColor::NEUTRAL);
+	sendRequest(save_game, PlayerColor::NEUTRAL);
 }
 
 void CClient::endNetwork()
@@ -348,37 +348,35 @@ void CClient::installNewBattleInterface(std::shared_ptr<CBattleGameInterface> ba
 	}
 }
 
-void CClient::handlePack(CPackForClient * pack)
+void CClient::handlePack(CPackForClient & pack)
 {
 	ApplyClientNetPackVisitor afterVisitor(*this, *gameState());
 	ApplyFirstClientNetPackVisitor beforeVisitor(*this, *gameState());
 
-	pack->visit(beforeVisitor);
-	logNetwork->trace("\tMade first apply on cl: %s", typeid(*pack).name());
+	pack.visit(beforeVisitor);
+	logNetwork->trace("\tMade first apply on cl: %s", typeid(pack).name());
 	{
 		boost::unique_lock lock(CGameState::mutex);
 		gs->apply(pack);
 	}
-	logNetwork->trace("\tApplied on gs: %s", typeid(*pack).name());
-	pack->visit(afterVisitor);
-	logNetwork->trace("\tMade second apply on cl: %s", typeid(*pack).name());
-
-	delete pack;
+	logNetwork->trace("\tApplied on gs: %s", typeid(pack).name());
+	pack.visit(afterVisitor);
+	logNetwork->trace("\tMade second apply on cl: %s", typeid(pack).name());
 }
 
-int CClient::sendRequest(const CPackForServer * request, PlayerColor player)
+int CClient::sendRequest(const CPackForServer & request, PlayerColor player)
 {
 	static ui32 requestCounter = 1;
 
 	ui32 requestID = requestCounter++;
-	logNetwork->trace("Sending a request \"%s\". It'll have an ID=%d.", typeid(*request).name(), requestID);
+	logNetwork->trace("Sending a request \"%s\". It'll have an ID=%d.", typeid(request).name(), requestID);
 
 	waitingRequest.pushBack(requestID);
-	request->requestID = requestID;
-	request->player = player;
+	request.requestID = requestID;
+	request.player = player;
 	CSH->logicConnection->sendPack(request);
 	if(vstd::contains(playerint, player))
-		playerint[player]->requestSent(request, requestID);
+		playerint[player]->requestSent(&request, requestID);
 
 	return requestID;
 }

+ 4 - 4
client/Client.h

@@ -16,7 +16,6 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-struct CPack;
 struct CPackForServer;
 class IBattleEventsReceiver;
 class CBattleGameInterface;
@@ -143,8 +142,8 @@ public:
 
 	static ThreadSafeVector<int> waitingRequest; //FIXME: make this normal field (need to join all threads before client destruction)
 
-	void handlePack(CPackForClient * pack); //applies the given pack and deletes it
-	int sendRequest(const CPackForServer * request, PlayerColor player); //returns ID given to that request
+	void handlePack(CPackForClient & pack); //applies the given pack and deletes it
+	int sendRequest(const CPackForServer & request, PlayerColor player); //returns ID given to that request
 
 	void battleStarted(const BattleInfo * info);
 	void battleFinished(const BattleID & battleID);
@@ -159,6 +158,7 @@ public:
 	friend class CBattleCallback; //handling players actions
 
 	void changeSpells(const CGHeroInstance * hero, bool give, const std::set<SpellID> & spells) override {};
+	void setResearchedSpells(const CGTownInstance * town, int level, const std::vector<SpellID> & spells, bool accepted) override {};
 	bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override {return false;};
 	void createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) override {};
 	void setOwner(const CGObjectInstance * obj, PlayerColor owner) override {};
@@ -204,7 +204,7 @@ public:
 	void setManaPoints(ObjectInstanceID hid, int val) override {};
 	void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override {};
 	void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {};
-	void sendAndApply(CPackForClient * pack) override {};
+	void sendAndApply(CPackForClient & pack) override {};
 	void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {};
 	void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {};
 

+ 1 - 2
client/ClientCommandManager.cpp

@@ -36,7 +36,6 @@
 #include "../lib/modding/CModHandler.h"
 #include "../lib/modding/ContentTypeHandler.h"
 #include "../lib/modding/ModUtility.h"
-#include "../lib/CHeroHandler.h"
 #include "../lib/VCMIDirs.h"
 #include "../lib/logging/VisualLogger.h"
 #include "../lib/serializer/Connection.h"
@@ -453,7 +452,7 @@ void ClientCommandManager::handleTellCommand(std::istringstream& singleWordBuffe
 	if(what == "hs")
 	{
 		for(const CGHeroInstance* h : LOCPLINT->cb->getHeroesInfo())
-			if(h->type->getIndex() == id1)
+			if(h->getHeroTypeID().getNum() == id1)
 				if(const CArtifactInstance* a = h->getArt(ArtifactPosition(id2)))
 					printCommandMessage(a->nodeName());
 	}

+ 1 - 0
client/ClientNetPackVisitors.h

@@ -37,6 +37,7 @@ public:
 	void visitHeroVisitCastle(HeroVisitCastle & pack) override;
 	void visitSetMana(SetMana & pack) override;
 	void visitSetMovePoints(SetMovePoints & pack) override;
+	void visitSetResearchedSpells(SetResearchedSpells & pack) override;
 	void visitFoWChange(FoWChange & pack) override;
 	void visitChangeStackCount(ChangeStackCount & pack) override;
 	void visitSetStackType(SetStackType & pack) override;

+ 1 - 1
client/HeroMovementController.cpp

@@ -375,7 +375,7 @@ void HeroMovementController::sendMovementRequest(const CGHeroInstance * h, const
 	{
 		updateMovementSound(h, currNode.coord, nextNode.coord, nextNode.action);
 
-		assert(h->pos.z == nextNode.coord.z); // Z should change only if it's movement via teleporter and in this case this code shouldn't be executed at all
+		assert(h->anchorPos().z == nextNode.coord.z); // Z should change only if it's movement via teleporter and in this case this code shouldn't be executed at all
 
 		logGlobal->trace("Requesting hero movement to %s", nextNode.coord.toString());
 

+ 8 - 2
client/NetPacksClient.cpp

@@ -14,6 +14,7 @@
 #include "CPlayerInterface.h"
 #include "CGameInfo.h"
 #include "windows/GUIClasses.h"
+#include "windows/CCastleInterface.h"
 #include "mapView/mapHandler.h"
 #include "adventureMap/AdventureMapInterface.h"
 #include "adventureMap/CInGameConsole.h"
@@ -31,7 +32,6 @@
 #include "../lib/filesystem/FileInfo.h"
 #include "../lib/serializer/Connection.h"
 #include "../lib/texts/CGeneralTextHandler.h"
-#include "../lib/CHeroHandler.h"
 #include "../lib/VCMI_Lib.h"
 #include "../lib/mapping/CMap.h"
 #include "../lib/VCMIDirs.h"
@@ -172,6 +172,12 @@ void ApplyClientNetPackVisitor::visitSetMovePoints(SetMovePoints & pack)
 	callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroMovePointsChanged, h);
 }
 
+void ApplyClientNetPackVisitor::visitSetResearchedSpells(SetResearchedSpells & pack)
+{
+	for(const auto & win : GH.windows().findWindows<CMageGuildScreen>())
+		win->updateSpells(pack.tid);
+}
+
 void ApplyClientNetPackVisitor::visitFoWChange(FoWChange & pack)
 {
 	for(auto &i : cl.playerint)
@@ -664,7 +670,7 @@ void ApplyClientNetPackVisitor::visitSetHeroesInTown(SetHeroesInTown & pack)
 void ApplyClientNetPackVisitor::visitHeroRecruited(HeroRecruited & pack)
 {
 	CGHeroInstance *h = gs.map->heroesOnMap.back();
-	if(h->getHeroType() != pack.hid)
+	if(h->getHeroTypeID() != pack.hid)
 	{
 		logNetwork->error("Something wrong with hero recruited!");
 	}

+ 142 - 20
client/PlayerLocalState.cpp

@@ -11,6 +11,7 @@
 #include "PlayerLocalState.h"
 
 #include "../CCallback.h"
+#include "../lib/json/JsonNode.h"
 #include "../lib/mapObjects/CGHeroInstance.h"
 #include "../lib/mapObjects/CGTownInstance.h"
 #include "../lib/pathfinder/CGPathNode.h"
@@ -23,34 +24,20 @@ PlayerLocalState::PlayerLocalState(CPlayerInterface & owner)
 {
 }
 
-void PlayerLocalState::saveHeroPaths(std::map<const CGHeroInstance *, int3> & pathsMap)
+const PlayerSpellbookSetting & PlayerLocalState::getSpellbookSettings() const
 {
-	for(auto & p : paths)
-	{
-		if(p.second.nodes.size())
-			pathsMap[p.first] = p.second.endPos();
-		else
-			logGlobal->debug("%s has assigned an empty path! Ignoring it...", p.first->getNameTranslated());
-	}
+	return spellbookSettings;
 }
 
-void PlayerLocalState::loadHeroPaths(std::map<const CGHeroInstance *, int3> & pathsMap)
+void PlayerLocalState::setSpellbookSettings(const PlayerSpellbookSetting & newSettings)
 {
-	if(owner.cb)
-	{
-		for(auto & p : pathsMap)
-		{
-			CGPath path;
-			owner.cb->getPathsInfo(p.first)->getPath(path, p.second);
-			paths[p.first] = path;
-			logGlobal->trace("Restored path for hero %s leading to %s with %d nodes", p.first->nodeName(), p.second.toString(), path.nodes.size());
-		}
-	}
+	spellbookSettings = newSettings;
 }
 
 void PlayerLocalState::setPath(const CGHeroInstance * h, const CGPath & path)
 {
 	paths[h] = path;
+	syncronizeState();
 }
 
 const CGPath & PlayerLocalState::getPath(const CGHeroInstance * h) const
@@ -70,6 +57,7 @@ bool PlayerLocalState::setPath(const CGHeroInstance * h, const int3 & destinatio
 	if(!owner.cb->getPathsInfo(h)->getPath(path, destination))
 	{
 		paths.erase(h); //invalidate previously possible path if selected (before other hero blocked only path / fly spell expired)
+		syncronizeState();
 		return false;
 	}
 
@@ -93,6 +81,7 @@ void PlayerLocalState::erasePath(const CGHeroInstance * h)
 {
 	paths.erase(h);
 	adventureInt->onHeroChanged(h);
+	syncronizeState();
 }
 
 void PlayerLocalState::verifyPath(const CGHeroInstance * h)
@@ -170,6 +159,7 @@ void PlayerLocalState::setSelection(const CArmedInstance * selection)
 
 	if (adventureInt && selection)
 		adventureInt->onSelectionChanged(selection);
+	syncronizeState();
 }
 
 bool PlayerLocalState::isHeroSleeping(const CGHeroInstance * hero) const
@@ -184,6 +174,7 @@ void PlayerLocalState::setHeroAsleep(const CGHeroInstance * hero)
 	assert(!vstd::contains(sleepingHeroes, hero));
 
 	sleepingHeroes.push_back(hero);
+	syncronizeState();
 }
 
 void PlayerLocalState::setHeroAwaken(const CGHeroInstance * hero)
@@ -193,6 +184,7 @@ void PlayerLocalState::setHeroAwaken(const CGHeroInstance * hero)
 	assert(vstd::contains(sleepingHeroes, hero));
 
 	vstd::erase(sleepingHeroes, hero);
+	syncronizeState();
 }
 
 const std::vector<const CGHeroInstance *> & PlayerLocalState::getWanderingHeroes()
@@ -215,6 +207,8 @@ void PlayerLocalState::addWanderingHero(const CGHeroInstance * hero)
 
 	if (currentSelection == nullptr)
 		setSelection(hero);
+
+	syncronizeState();
 }
 
 void PlayerLocalState::removeWanderingHero(const CGHeroInstance * hero)
@@ -225,7 +219,12 @@ void PlayerLocalState::removeWanderingHero(const CGHeroInstance * hero)
 	if (hero == currentSelection)
 	{
 		auto const * nextHero = getNextWanderingHero(hero);
-		setSelection(nextHero);
+		if (nextHero)
+			setSelection(nextHero);
+		else if (!ownedTowns.empty())
+			setSelection(ownedTowns.front());
+		else
+			setSelection(nullptr);
 	}
 
 	vstd::erase(wanderingHeroes, hero);
@@ -236,6 +235,8 @@ void PlayerLocalState::removeWanderingHero(const CGHeroInstance * hero)
 
 	if (currentSelection == nullptr && !ownedTowns.empty())
 		setSelection(ownedTowns.front());
+
+	syncronizeState();
 }
 
 void PlayerLocalState::swapWanderingHero(size_t pos1, size_t pos2)
@@ -244,6 +245,8 @@ void PlayerLocalState::swapWanderingHero(size_t pos1, size_t pos2)
 	std::swap(wanderingHeroes.at(pos1), wanderingHeroes.at(pos2));
 
 	adventureInt->onHeroOrderChanged();
+
+	syncronizeState();
 }
 
 const std::vector<const CGTownInstance *> & PlayerLocalState::getOwnedTowns()
@@ -266,6 +269,8 @@ void PlayerLocalState::addOwnedTown(const CGTownInstance * town)
 
 	if (currentSelection == nullptr)
 		setSelection(town);
+
+	syncronizeState();
 }
 
 void PlayerLocalState::removeOwnedTown(const CGTownInstance * town)
@@ -282,6 +287,8 @@ void PlayerLocalState::removeOwnedTown(const CGTownInstance * town)
 
 	if (currentSelection == nullptr && !ownedTowns.empty())
 		setSelection(ownedTowns.front());
+
+	syncronizeState();
 }
 
 void PlayerLocalState::swapOwnedTowns(size_t pos1, size_t pos2)
@@ -289,5 +296,120 @@ void PlayerLocalState::swapOwnedTowns(size_t pos1, size_t pos2)
 	assert(ownedTowns[pos1] && ownedTowns[pos2]);
 	std::swap(ownedTowns.at(pos1), ownedTowns.at(pos2));
 
+	syncronizeState();
+
 	adventureInt->onTownOrderChanged();
 }
+
+void PlayerLocalState::syncronizeState()
+{
+	JsonNode data;
+	serialize(data);
+	owner.cb->saveLocalState(data);
+}
+
+void PlayerLocalState::serialize(JsonNode & dest) const
+{
+	dest.clear();
+
+	for (auto const * town : ownedTowns)
+	{
+		JsonNode record;
+		record["id"].Integer() = town->id;
+		dest["towns"].Vector().push_back(record);
+	}
+
+	for (auto const * hero : wanderingHeroes)
+	{
+		JsonNode record;
+		record["id"].Integer() = hero->id;
+		if (vstd::contains(sleepingHeroes, hero))
+			record["sleeping"].Bool() = true;
+
+		if (paths.count(hero))
+		{
+			record["path"]["x"].Integer() = paths.at(hero).lastNode().coord.x;
+			record["path"]["y"].Integer() = paths.at(hero).lastNode().coord.y;
+			record["path"]["z"].Integer() = paths.at(hero).lastNode().coord.z;
+		}
+		dest["heroes"].Vector().push_back(record);
+	}
+	dest["spellbook"]["pageBattle"].Integer() = spellbookSettings.spellbookLastPageBattle;
+	dest["spellbook"]["pageAdvmap"].Integer() = spellbookSettings.spellbookLastPageAdvmap;
+	dest["spellbook"]["tabBattle"].Integer() = spellbookSettings.spellbookLastTabBattle;
+	dest["spellbook"]["tabAdvmap"].Integer() = spellbookSettings.spellbookLastTabAdvmap;
+
+	if (currentSelection)
+		dest["currentSelection"].Integer() = currentSelection->id;
+}
+
+void PlayerLocalState::deserialize(const JsonNode & source)
+{
+	// this method must be called after player state has been initialized
+	assert(currentSelection != nullptr);
+	assert(!ownedTowns.empty() || !wanderingHeroes.empty());
+
+	auto oldHeroes = wanderingHeroes;
+	auto oldTowns = ownedTowns;
+
+	paths.clear();
+	sleepingHeroes.clear();
+	wanderingHeroes.clear();
+	ownedTowns.clear();
+
+	for (auto const & town : source["towns"].Vector())
+	{
+		ObjectInstanceID objID(town["id"].Integer());
+		const CGTownInstance * townPtr = owner.cb->getTown(objID);
+
+		if (!townPtr)
+			continue;
+
+		if (!vstd::contains(oldTowns, townPtr))
+			continue;
+
+		ownedTowns.push_back(townPtr);
+		vstd::erase(oldTowns, townPtr);
+	}
+
+	for (auto const & hero : source["heroes"].Vector())
+	{
+		ObjectInstanceID objID(hero["id"].Integer());
+		const CGHeroInstance * heroPtr = owner.cb->getHero(objID);
+
+		if (!heroPtr)
+			continue;
+
+		if (!vstd::contains(oldHeroes, heroPtr))
+			continue;
+
+		wanderingHeroes.push_back(heroPtr);
+		vstd::erase(oldHeroes, heroPtr);
+
+		if (hero["sleeping"].Bool())
+			sleepingHeroes.push_back(heroPtr);
+
+		if (hero["path"]["x"].isNumber() && hero["path"]["y"].isNumber() && hero["path"]["z"].isNumber())
+		{
+			int3 pathTarget(hero["path"]["x"].Integer(), hero["path"]["y"].Integer(), hero["path"]["z"].Integer());
+			setPath(heroPtr, pathTarget);
+		}
+	}
+
+	spellbookSettings.spellbookLastPageBattle = source["spellbook"]["pageBattle"].Integer();
+	spellbookSettings.spellbookLastPageAdvmap = source["spellbook"]["pageAdvmap"].Integer();
+	spellbookSettings.spellbookLastTabBattle = source["spellbook"]["tabBattle"].Integer();
+	spellbookSettings.spellbookLastTabAdvmap = source["spellbook"]["tabAdvmap"].Integer();
+
+	// append any owned heroes / towns that were not present in loaded state
+	wanderingHeroes.insert(wanderingHeroes.end(), oldHeroes.begin(), oldHeroes.end());
+	ownedTowns.insert(ownedTowns.end(), oldTowns.begin(), oldTowns.end());
+
+//FIXME: broken, anything that is selected in here will be overwritten on NewTurn pack
+//	ObjectInstanceID selectedObjectID(source["currentSelection"].Integer());
+//	const CGObjectInstance * objectPtr = owner.cb->getObjInstance(selectedObjectID);
+//	const CArmedInstance * armyPtr = dynamic_cast<const CArmedInstance*>(objectPtr);
+//
+//	if (armyPtr)
+//		setSelection(armyPtr);
+}

+ 18 - 10
client/PlayerLocalState.h

@@ -14,6 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 class CGHeroInstance;
 class CGTownInstance;
 class CArmedInstance;
+class JsonNode;
 struct CGPath;
 class int3;
 
@@ -21,6 +22,15 @@ VCMI_LIB_NAMESPACE_END
 
 class CPlayerInterface;
 
+struct PlayerSpellbookSetting
+{
+	//on which page we left spellbook
+	int spellbookLastPageBattle = 0;
+	int spellbookLastPageAdvmap = 0;
+	int spellbookLastTabBattle = 4;
+	int spellbookLastTabAdvmap = 4;
+};
+
 /// Class that contains potentially serializeable state of a local player
 class PlayerLocalState
 {
@@ -34,18 +44,10 @@ class PlayerLocalState
 	std::vector<const CGHeroInstance *> wanderingHeroes; //our heroes on the adventure map (not the garrisoned ones)
 	std::vector<const CGTownInstance *> ownedTowns; //our towns on the adventure map
 
-	void saveHeroPaths(std::map<const CGHeroInstance *, int3> & paths);
-	void loadHeroPaths(std::map<const CGHeroInstance *, int3> & paths);
+	PlayerSpellbookSetting spellbookSettings;
 
+	void syncronizeState();
 public:
-	struct SpellbookLastSetting
-	{
-		//on which page we left spellbook
-		int spellbookLastPageBattle = 0;
-		int spellbookLastPageAdvmap = 0;
-		int spellbookLastTabBattle = 4;
-		int spellbookLastTabAdvmap = 4;
-	} spellbookSettings;
 
 	explicit PlayerLocalState(CPlayerInterface & owner);
 
@@ -53,6 +55,9 @@ public:
 	void setHeroAsleep(const CGHeroInstance * hero);
 	void setHeroAwaken(const CGHeroInstance * hero);
 
+	const PlayerSpellbookSetting & getSpellbookSettings() const;
+	void setSpellbookSettings(const PlayerSpellbookSetting & newSettings);
+
 	const std::vector<const CGTownInstance *> & getOwnedTowns();
 	const CGTownInstance * getOwnedTown(size_t index);
 	void addOwnedTown(const CGTownInstance * hero);
@@ -81,6 +86,9 @@ public:
 	const CGTownInstance * getCurrentTown() const;
 	const CArmedInstance * getCurrentArmy() const;
 
+	void serialize(JsonNode & dest) const;
+	void deserialize(const JsonNode & source);
+
 	/// Changes currently selected object
 	void setSelection(const CArmedInstance *sel);
 };

+ 1 - 2
client/adventureMap/CList.cpp

@@ -29,7 +29,6 @@
 #include "../render/Colors.h"
 
 #include "../../lib/texts/CGeneralTextHandler.h"
-#include "../../lib/CHeroHandler.h"
 #include "../../lib/IGameSettings.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
@@ -432,7 +431,7 @@ std::shared_ptr<CIntObject> CTownList::CTownItem::genSelection()
 
 void CTownList::CTownItem::update()
 {
-	size_t iconIndex = town->town->clientInfo.icons[town->hasFort()][town->built >= LOCPLINT->cb->getSettings().getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)];
+	size_t iconIndex = town->getTown()->clientInfo.icons[town->hasFort()][town->built >= LOCPLINT->cb->getSettings().getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)];
 
 	picture->setFrame(iconIndex + 2);
 	redraw();

+ 3 - 3
client/adventureMap/MapAudioPlayer.cpp

@@ -81,9 +81,9 @@ void MapAudioPlayer::addObject(const CGObjectInstance * obj)
 		{
 			for(int fy = 0; fy < obj->getHeight(); ++fy)
 			{
-				int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z);
+				int3 currTile(obj->anchorPos().x - fx, obj->anchorPos().y - fy, obj->anchorPos().z);
 
-				if(LOCPLINT->cb->isInTheMap(currTile) && obj->coveringAt(currTile.x, currTile.y))
+				if(LOCPLINT->cb->isInTheMap(currTile) && obj->coveringAt(currTile))
 					objects[currTile.z][currTile.x][currTile.y].push_back(obj->id);
 			}
 		}
@@ -108,7 +108,7 @@ void MapAudioPlayer::addObject(const CGObjectInstance * obj)
 
 		for(const auto & tile : tiles)
 		{
-			int3 currTile = obj->pos + tile;
+			int3 currTile = obj->anchorPos() + tile;
 
 			if(LOCPLINT->cb->isInTheMap(currTile))
 				objects[currTile.z][currTile.x][currTile.y].push_back(obj->id);

+ 0 - 1
client/battle/BattleInterface.cpp

@@ -39,7 +39,6 @@
 #include "../../lib/CStack.h"
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
-#include "../../lib/CHeroHandler.h"
 #include "../../lib/gameState/InfoAboutArmy.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/networkPacks/PacksForClientBattle.h"

+ 7 - 6
client/battle/BattleInterfaceClasses.cpp

@@ -50,10 +50,11 @@
 #include "../../lib/CStack.h"
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CCreatureHandler.h"
+#include "../../lib/entities/hero/CHeroClass.h"
+#include "../../lib/entities/hero/CHero.h"
 #include "../../lib/gameState/InfoAboutArmy.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
 #include "../../lib/texts/TextOperations.h"
-#include "../../lib/CHeroHandler.h"
 #include "../../lib/StartInfo.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/networkPacks/PacksForClientBattle.h"
@@ -389,13 +390,13 @@ BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * her
 {
 	AnimationPath animationPath;
 
-	if(!hero->type->battleImage.empty())
-		animationPath = hero->type->battleImage;
+	if(!hero->getHeroType()->battleImage.empty())
+		animationPath = hero->getHeroType()->battleImage;
 	else
 	if(hero->gender == EHeroGender::FEMALE)
-		animationPath = hero->type->heroClass->imageBattleFemale;
+		animationPath = hero->getHeroClass()->imageBattleFemale;
 	else
-		animationPath = hero->type->heroClass->imageBattleMale;
+		animationPath = hero->getHeroClass()->imageBattleMale;
 
 	animation = GH.renderHandler().loadAnimation(animationPath, EImageBlitMode::ALPHA);
 
@@ -1027,7 +1028,7 @@ void StackQueue::update()
 
 int32_t StackQueue::getSiegeShooterIconID()
 {
-	return owner.siegeController->getSiegedTown()->town->faction->getIndex();
+	return owner.siegeController->getSiegedTown()->getFactionID().getNum();
 }
 
 std::optional<uint32_t> StackQueue::getHoveredUnitIdIfAny() const

+ 8 - 8
client/battle/BattleSiegeController.cpp

@@ -58,14 +58,14 @@ ImagePath BattleSiegeController::getWallPieceImageName(EWallVisual::EWallVisual
 		};
 	};
 
-	const std::string & prefix = town->town->clientInfo.siegePrefix;
+	const std::string & prefix = town->getTown()->clientInfo.siegePrefix;
 	std::string addit = std::to_string(getImageIndex());
 
 	switch(what)
 	{
 	case EWallVisual::BACKGROUND_WALL:
 		{
-			auto faction = town->town->faction->getIndex();
+			auto faction = town->getFactionID();
 
 			if (faction == ETownType::RAMPART || faction == ETownType::NECROPOLIS || faction == ETownType::DUNGEON || faction == ETownType::STRONGHOLD)
 				return ImagePath::builtinTODO(prefix + "TPW1.BMP");
@@ -111,7 +111,7 @@ ImagePath BattleSiegeController::getWallPieceImageName(EWallVisual::EWallVisual
 
 void BattleSiegeController::showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what)
 {
-	auto & ci = town->town->clientInfo;
+	auto & ci = town->getTown()->clientInfo;
 	auto const & pos = ci.siegePositions[what];
 
 	if ( wallPieceImages[what] && pos.isValid())
@@ -120,7 +120,7 @@ void BattleSiegeController::showWallPiece(Canvas & canvas, EWallVisual::EWallVis
 
 ImagePath BattleSiegeController::getBattleBackgroundName() const
 {
-	const std::string & prefix = town->town->clientInfo.siegePrefix;
+	const std::string & prefix = town->getTown()->clientInfo.siegePrefix;
 	return ImagePath::builtinTODO(prefix + "BACK.BMP");
 }
 
@@ -130,8 +130,8 @@ bool BattleSiegeController::getWallPieceExistence(EWallVisual::EWallVisual what)
 
 	switch (what)
 	{
-	case EWallVisual::MOAT:              return fortifications.hasMoat && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT).isValid();
-	case EWallVisual::MOAT_BANK:         return fortifications.hasMoat && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT_BANK).isValid();
+	case EWallVisual::MOAT:              return fortifications.hasMoat && town->getTown()->clientInfo.siegePositions.at(EWallVisual::MOAT).isValid();
+	case EWallVisual::MOAT_BANK:         return fortifications.hasMoat && town->getTown()->clientInfo.siegePositions.at(EWallVisual::MOAT_BANK).isValid();
 	case EWallVisual::KEEP_BATTLEMENT:   return fortifications.citadelHealth > 0 && owner.getBattle()->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED;
 	case EWallVisual::UPPER_BATTLEMENT:  return fortifications.upperTowerHealth > 0 && owner.getBattle()->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED;
 	case EWallVisual::BOTTOM_BATTLEMENT: return fortifications.lowerTowerHealth > 0 && owner.getBattle()->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED;
@@ -218,8 +218,8 @@ Point BattleSiegeController::getTurretCreaturePosition( BattleHex position ) con
 	if (posID != 0)
 	{
 		return {
-			town->town->clientInfo.siegePositions[posID].x,
-			town->town->clientInfo.siegePositions[posID].y
+			town->getTown()->clientInfo.siegePositions[posID].x,
+			town->getTown()->clientInfo.siegePositions[posID].y
 		};
 	}
 

+ 2 - 2
client/gui/CGuiHandler.cpp

@@ -250,8 +250,8 @@ void CGuiHandler::setStatusbar(std::shared_ptr<IStatusBar> newStatusBar)
 void CGuiHandler::onScreenResize(bool resolutionChanged)
 {
 	if(resolutionChanged)
-	{
 		screenHandler().onScreenResize();
-	}
+
 	windows().onScreenResize();
+	CCS->curh->onScreenResize();
 }

+ 5 - 0
client/gui/CursorHandler.cpp

@@ -312,3 +312,8 @@ void CursorHandler::changeCursor(Cursor::ShowType newShowType)
 			break;
 	}
 }
+
+void CursorHandler::onScreenResize()
+{
+	cursor->setImage(getCurrentImage(), getPivotOffset());
+}

+ 1 - 0
client/gui/CursorHandler.h

@@ -182,6 +182,7 @@ public:
 
 	void hide();
 	void show();
+	void onScreenResize();
 
 	/// change cursor's positions to (x, y)
 	void cursorMove(const int & x, const int & y);

+ 1 - 1
client/lobby/CBonusSelection.cpp

@@ -41,7 +41,6 @@
 
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CCreatureHandler.h"
-#include "../../lib/CHeroHandler.h"
 #include "../../lib/CSkillHandler.h"
 #include "../../lib/StartInfo.h"
 #include "../../lib/entities/building/CBuilding.h"
@@ -49,6 +48,7 @@
 #include "../../lib/entities/faction/CFaction.h"
 #include "../../lib/entities/faction/CTown.h"
 #include "../../lib/entities/faction/CTownHandler.h"
+#include "../../lib/entities/hero/CHeroHandler.h"
 #include "../../lib/filesystem/Filesystem.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
 

+ 0 - 1
client/lobby/CSelectionBase.cpp

@@ -43,7 +43,6 @@
 #include "../render/IFont.h"
 #include "../render/IRenderHandler.h"
 
-#include "../../lib/CHeroHandler.h"
 #include "../../lib/CRandomGenerator.h"
 #include "../../lib/CThreadHelper.h"
 #include "../../lib/filesystem/Filesystem.h"

+ 7 - 7
client/lobby/OptionsTab.cpp

@@ -39,12 +39,13 @@
 #include "../../lib/entities/faction/CFaction.h"
 #include "../../lib/entities/faction/CTown.h"
 #include "../../lib/entities/faction/CTownHandler.h"
+#include "../../lib/entities/hero/CHeroHandler.h"
+#include "../../lib/entities/hero/CHeroClass.h"
 #include "../../lib/filesystem/Filesystem.h"
 #include "../../lib/networkPacks/PacksForLobby.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
 #include "../../lib/CArtHandler.h"
 #include "../../lib/CConfigHandler.h"
-#include "../../lib/CHeroHandler.h"
 #include "../../lib/mapping/CMapInfo.h"
 #include "../../lib/mapping/CMapHeader.h"
 
@@ -835,9 +836,9 @@ OptionsTab::HandicapWindow::HandicapWindow()
 			if(i == 0)
 			{
 				if(isIncome)
-					labels.push_back(std::make_shared<CLabel>(xPos, 35, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("core.jktext.32")));
+					labels.push_back(std::make_shared<CLabel>(xPos, 38, FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("core.jktext.32")));
 				else if(isGrowth)
-					labels.push_back(std::make_shared<CLabel>(xPos, 35, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.194")));
+					labels.push_back(std::make_shared<CLabel>(xPos, 38, FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.194")));
 				else
 					anim.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("SMALRES"), GameResID(resource), 0, 15 + xPos + (j == 0 ? 10 : 0), 35));
 			}
@@ -1035,14 +1036,13 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con
 		labelPlayerNameEdit = std::make_shared<CTextInput>(Rect(6, 3, 95, 15), EFonts::FONT_SMALL, ETextAlignment::CENTER, false);
 		labelPlayerNameEdit->setText(name);
 	}
-	const auto & font = GH.renderHandler().loadFont(FONT_SMALL);
 
-	labelWhoCanPlay = std::make_shared<CMultiLineLabel>(Rect(6, 23, 45, font->getLineHeight()*2), EFonts::FONT_TINY, ETextAlignment::TOPCENTER, Colors::WHITE, CGI->generaltexth->arraytxt[206 + whoCanPlay]);
+	labelWhoCanPlay = std::make_shared<CMultiLineLabel>(Rect(6, 21, 45, 26), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->arraytxt[206 + whoCanPlay]);
 
 	auto hasHandicap = [this](){ return s->handicap.startBonus.empty() && s->handicap.percentIncome == 100 && s->handicap.percentGrowth == 100; };
 	std::string labelHandicapText = hasHandicap() ? CGI->generaltexth->arraytxt[210] : MetaString::createFromTextID("vcmi.lobby.handicap").toString();
-	labelHandicap = std::make_shared<CMultiLineLabel>(Rect(57, 24, 47, font->getLineHeight()*2), EFonts::FONT_TINY, ETextAlignment::TOPCENTER, Colors::WHITE, labelHandicapText);
-	handicap = std::make_shared<LRClickableArea>(Rect(56, 24, 49, font->getLineHeight()*2), [](){
+	labelHandicap = std::make_shared<CMultiLineLabel>(Rect(55, 23, 46, 24), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, labelHandicapText);
+	handicap = std::make_shared<LRClickableArea>(Rect(53, 23, 50, 24), [](){
 		if(!CSH->isHost())
 			return;
 		

+ 9 - 7
client/lobby/SelectionTab.cpp

@@ -480,11 +480,11 @@ void SelectionTab::filter(int size, bool selectFirst)
 		if((elem->mapHeader && (!size || elem->mapHeader->width == size)) || tabType == ESelectionScreen::campaignList)
 		{
 			if(showRandom)
-				curFolder = "RANDOMMAPS/";
+				curFolder = "RandomMaps/";
 
 			auto [folderName, baseFolder, parentExists, fileInFolder] = checkSubfolder(elem->originalFileURI);
 
-			if((showRandom && baseFolder != "RANDOMMAPS") || (!showRandom && baseFolder == "RANDOMMAPS"))
+			if((showRandom && baseFolder != "RandomMaps") || (!showRandom && baseFolder == "RandomMaps"))
 				continue;
 
 			if(parentExists && !showRandom)
@@ -715,7 +715,7 @@ void SelectionTab::selectFileName(std::string fname)
 	selectAbs(-1);
 
 	if(tabType == ESelectionScreen::saveGame && inputName->getText().empty())
-		inputName->setText("NEWGAME");
+		inputName->setText(CGI->generaltexth->translate("core.genrltxt.11"));
 }
 
 void SelectionTab::selectNewestFile()
@@ -808,7 +808,8 @@ void SelectionTab::parseMaps(const std::unordered_set<ResourcePath> & files)
 		try
 		{
 			auto mapInfo = std::make_shared<ElementInfo>();
-			mapInfo->mapInit(file.getName());
+			mapInfo->mapInit(file.getOriginalName());
+			mapInfo->name = mapInfo->getNameForList();
 
 			if (isMapSupported(*mapInfo))
 				allItems.push_back(mapInfo);
@@ -828,6 +829,7 @@ void SelectionTab::parseSaves(const std::unordered_set<ResourcePath> & files)
 		{
 			auto mapInfo = std::make_shared<ElementInfo>();
 			mapInfo->saveInit(file);
+			mapInfo->name = mapInfo->getNameForList();
 
 			// Filter out other game modes
 			bool isCampaign = mapInfo->scenarioOptionsOfSave->mode == EStartMode::CAMPAIGN;
@@ -872,9 +874,9 @@ void SelectionTab::parseCampaigns(const std::unordered_set<ResourcePath> & files
 	for(auto & file : files)
 	{
 		auto info = std::make_shared<ElementInfo>();
-		//allItems[i].date = std::asctime(std::localtime(&files[i].date));
-		info->fileURI = file.getName();
+		info->fileURI = file.getOriginalName();
 		info->campaignInit();
+		info->name = info->getNameForList();
 		if(info->campaign)
 			allItems.push_back(info);
 	}
@@ -988,6 +990,6 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr<ElementInfo> info, bool
 		iconLossCondition->setFrame(info->mapHeader->defeatIconIndex, 0);
 		labelName->setMaxWidth(185);
 	}
-	labelName->setText(info->getNameForList());
+	labelName->setText(info->name);
 	labelName->setColor(color);
 }

+ 1 - 0
client/lobby/SelectionTab.h

@@ -33,6 +33,7 @@ public:
 	ElementInfo() : CMapInfo() { }
 	~ElementInfo() { }
 	std::string folderName = "";
+	std::string name = "";
 	bool isFolder = false;
 };
 

+ 0 - 1
client/mainmenu/CCampaignScreen.cpp

@@ -37,7 +37,6 @@
 #include "../../lib/spells/CSpellHandler.h"
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CSkillHandler.h"
-#include "../../lib/CHeroHandler.h"
 #include "../../lib/CCreatureHandler.h"
 
 #include "../../lib/campaign/CampaignHandler.h"

+ 2 - 2
client/mapView/MapRenderer.cpp

@@ -591,8 +591,8 @@ void MapRendererOverlay::renderTile(IMapRendererContext & context, Canvas & targ
 
 			if(context.objectTransparency(objectID, coordinates) > 0 && !context.isActiveHero(object))
 			{
-				visitable |= object->visitableAt(coordinates.x, coordinates.y);
-				blocking |= object->blockingAt(coordinates.x, coordinates.y);
+				visitable |= object->visitableAt(coordinates);
+				blocking |= object->blockingAt(coordinates);
 			}
 		}
 

+ 2 - 2
client/mapView/MapRendererContext.cpp

@@ -120,7 +120,7 @@ size_t MapRendererBaseContext::objectGroupIndex(ObjectInstanceID objectID) const
 Point MapRendererBaseContext::objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const
 {
 	const CGObjectInstance * object = getObject(objectID);
-	int3 offsetTiles(object->getPosition() - coordinates);
+	int3 offsetTiles(object->anchorPos() - coordinates);
 	return Point(offsetTiles) * Point(32, 32);
 }
 
@@ -498,7 +498,7 @@ size_t MapRendererWorldViewContext::overlayImageIndex(const int3 & coordinates)
 	{
 		const auto * object = getObject(objectID);
 
-		if(!object->visitableAt(coordinates.x, coordinates.y))
+		if(!object->visitableAt(coordinates))
 			continue;
 
 		ObjectPosInfo info(object);

+ 3 - 3
client/mapView/MapRendererContextState.cpp

@@ -49,9 +49,9 @@ void MapRendererContextState::addObject(const CGObjectInstance * obj)
 	{
 		for(int fy = 0; fy < obj->getHeight(); ++fy)
 		{
-			int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z);
+			int3 currTile(obj->anchorPos().x - fx, obj->anchorPos().y - fy, obj->anchorPos().z);
 
-			if(LOCPLINT->cb->isInTheMap(currTile) && obj->coveringAt(currTile.x, currTile.y))
+			if(LOCPLINT->cb->isInTheMap(currTile) && obj->coveringAt(currTile))
 			{
 				auto & container = objects[currTile.z][currTile.x][currTile.y];
 
@@ -73,7 +73,7 @@ void MapRendererContextState::addMovingObject(const CGObjectInstance * object, c
 	{
 		for(int y = yFrom; y <= yDest; ++y)
 		{
-			int3 currTile(x, y, object->pos.z);
+			int3 currTile(x, y, object->anchorPos().z);
 
 			if(LOCPLINT->cb->isInTheMap(currTile))
 			{

+ 1 - 1
client/mapView/MapViewController.cpp

@@ -317,7 +317,7 @@ bool MapViewController::isEventVisible(const CGObjectInstance * obj, const Playe
 	if(obj->isVisitable())
 		return context->isVisible(obj->visitablePos());
 	else
-		return context->isVisible(obj->pos);
+		return context->isVisible(obj->anchorPos());
 }
 
 bool MapViewController::isEventVisible(const CGHeroInstance * obj, const int3 & from, const int3 & dest)

+ 7 - 7
client/mapView/mapHandler.cpp

@@ -59,7 +59,7 @@ std::string CMapHandler::getTerrainDescr(const int3 & pos, bool rightClick) cons
 
 	for(const auto & object : map->objects)
 	{
-		if(object && object->coveringAt(pos.x, pos.y) && object->pos.z == pos.z && object->isTile2Terrain())
+		if(object && object->coveringAt(pos) && object->isTile2Terrain())
 		{
 			result = object->getObjectName();
 			break;
@@ -103,15 +103,15 @@ bool CMapHandler::compareObjectBlitOrder(const CGObjectInstance * a, const CGObj
 
 	for(const auto & aOffset : a->getBlockedOffsets())
 	{
-		int3 testTarget = a->pos + aOffset + int3(0, 1, 0);
-		if(b->blockingAt(testTarget.x, testTarget.y))
+		int3 testTarget = a->anchorPos() + aOffset + int3(0, 1, 0);
+		if(b->blockingAt(testTarget))
 			bBlocksA += 1;
 	}
 
 	for(const auto & bOffset : b->getBlockedOffsets())
 	{
-		int3 testTarget = b->pos + bOffset + int3(0, 1, 0);
-		if(a->blockingAt(testTarget.x, testTarget.y))
+		int3 testTarget = b->anchorPos() + bOffset + int3(0, 1, 0);
+		if(a->blockingAt(testTarget))
 			aBlocksB += 1;
 	}
 
@@ -126,8 +126,8 @@ bool CMapHandler::compareObjectBlitOrder(const CGObjectInstance * a, const CGObj
 		return aBlocksB < bBlocksA;
 
 	// object that don't have clear priority via tile blocking will appear based on their row
-	if(a->pos.y != b->pos.y)
-		return a->pos.y < b->pos.y;
+	if(a->anchorPos().y != b->anchorPos().y)
+		return a->anchorPos().y < b->anchorPos().y;
 
 	// heroes should appear on top of objects on the same tile
 	if(b->ID==Obj::HERO && a->ID!=Obj::HERO)

+ 11 - 3
client/media/CVideoHandler.cpp

@@ -545,7 +545,7 @@ std::pair<std::unique_ptr<ui8 []>, si64> CAudioInstance::extractAudio(const Vide
 			frameSamplesBuffer.resize(std::max(frameSamplesBuffer.size(), bytesToRead));
 			uint8_t * frameSamplesPtr = frameSamplesBuffer.data();
 
-			int result = swr_convert(swr_ctx, &frameSamplesPtr, frame->nb_samples, (const uint8_t **)frame->data, frame->nb_samples);
+			int result = swr_convert(swr_ctx, &frameSamplesPtr, frame->nb_samples, const_cast<const uint8_t **>(frame->data), frame->nb_samples);
 
 			if (result < 0)
 				throwFFmpegError(result);
@@ -608,9 +608,8 @@ std::pair<std::unique_ptr<ui8 []>, si64> CAudioInstance::extractAudio(const Vide
 bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool stopOnKey)
 {
 	CVideoInstance instance;
-	CAudioInstance audio;
 
-	auto extractedAudio = audio.extractAudio(name);
+	auto extractedAudio = getAudio(name);
 	int audioHandle = CCS->soundh->playSound(extractedAudio);
 
 	if (!instance.openInput(name))
@@ -684,6 +683,15 @@ std::unique_ptr<IVideoInstance> CVideoPlayer::open(const VideoPath & name, float
 
 std::pair<std::unique_ptr<ui8[]>, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen)
 {
+	AudioPath audioPath = videoToOpen.toType<EResType::SOUND>();
+	AudioPath audioPathVideoDir = audioPath.addPrefix("VIDEO/");
+
+	if(CResourceHandler::get()->existsResource(audioPath))
+		return CResourceHandler::get()->load(audioPath)->readAll();
+
+	if(CResourceHandler::get()->existsResource(audioPathVideoDir))
+		return CResourceHandler::get()->load(audioPathVideoDir)->readAll();
+
 	CAudioInstance audio;
 	return audio.extractAudio(videoToOpen);
 }

+ 1 - 1
client/render/AssetGenerator.cpp

@@ -158,7 +158,7 @@ void AssetGenerator::createPlayerColoredBackground(const PlayerColor & player)
 	assert(player.isValidPlayer());
 	if (!player.isValidPlayer())
 	{
-		logGlobal->error("Unable to colorize to invalid player color %d!", static_cast<int>(player.getNum()));
+		logGlobal->error("Unable to colorize to invalid player color %d!", player.getNum());
 		return;
 	}
 

+ 0 - 1
client/render/Graphics.cpp

@@ -28,7 +28,6 @@
 #include "../lib/modding/CModHandler.h"
 #include "../lib/modding/ModScope.h"
 #include "../lib/VCMI_Lib.h"
-#include "../lib/CHeroHandler.h"
 
 #include <SDL_surface.h>
 

+ 2 - 0
client/render/IScreenHandler.h

@@ -44,6 +44,8 @@ public:
 	/// Dimensions of logical output. Can be different if scaling is used
 	virtual Point getLogicalResolution() const = 0;
 
+	virtual int getInterfaceScalingPercentage() const = 0;
+
 	virtual int getScalingFactor() const = 0;
 
 	/// Window has focus

+ 8 - 8
client/renderSDL/CTrueTypeFont.cpp

@@ -64,9 +64,9 @@ int CTrueTypeFont::getFontStyle(const JsonNode &config) const
 CTrueTypeFont::CTrueTypeFont(const JsonNode & fontConfig):
 	data(loadData(fontConfig)),
 	font(loadFont(fontConfig), TTF_CloseFont),
-	dropShadow(!fontConfig["noShadow"].Bool()),
+	blended(true),
 	outline(fontConfig["outline"].Bool()),
-	blended(true)
+	dropShadow(!fontConfig["noShadow"].Bool())
 {
 	assert(font);
 
@@ -95,14 +95,14 @@ size_t CTrueTypeFont::getLineHeightScaled() const
 	return TTF_FontHeight(font.get());
 }
 
-size_t CTrueTypeFont::getGlyphWidthScaled(const char *data) const
+size_t CTrueTypeFont::getGlyphWidthScaled(const char *text) const
 {
-	return getStringWidthScaled(std::string(data, TextOperations::getUnicodeCharacterSize(*data)));
+	return getStringWidthScaled(std::string(text, TextOperations::getUnicodeCharacterSize(*text)));
 }
 
-bool CTrueTypeFont::canRepresentCharacter(const char * data) const
+bool CTrueTypeFont::canRepresentCharacter(const char * text) const
 {
-	uint32_t codepoint = TextOperations::getUnicodeCodepoint(data, TextOperations::getUnicodeCharacterSize(*data));
+	uint32_t codepoint = TextOperations::getUnicodeCodepoint(text, TextOperations::getUnicodeCharacterSize(*text));
 #if SDL_TTF_VERSION_ATLEAST(2, 0, 18)
 	return TTF_GlyphIsProvided32(font.get(), codepoint);
 #elif SDL_TTF_VERSION_ATLEAST(2, 0, 12)
@@ -114,10 +114,10 @@ bool CTrueTypeFont::canRepresentCharacter(const char * data) const
 #endif
 }
 
-size_t CTrueTypeFont::getStringWidthScaled(const std::string & data) const
+size_t CTrueTypeFont::getStringWidthScaled(const std::string & text) const
 {
 	int width;
-	TTF_SizeUTF8(font.get(), data.c_str(), &width, nullptr);
+	TTF_SizeUTF8(font.get(), text.c_str(), &width, nullptr);
 	return width;
 }
 

+ 15 - 3
client/renderSDL/CursorHardware.cpp

@@ -11,11 +11,14 @@
 #include "StdInc.h"
 #include "CursorHardware.h"
 
+#include "SDL_Extensions.h"
+
 #include "../gui/CGuiHandler.h"
 #include "../render/IScreenHandler.h"
 #include "../render/Colors.h"
 #include "../render/IImage.h"
-#include "SDL_Extensions.h"
+
+#include "../../lib/CConfigHandler.h"
 
 #include <SDL_render.h>
 #include <SDL_events.h>
@@ -45,19 +48,28 @@ void CursorHardware::setVisible(bool on)
 
 void CursorHardware::setImage(std::shared_ptr<IImage> image, const Point & pivotOffset)
 {
-	auto cursorSurface = CSDL_Ext::newSurface(image->dimensions() * GH.screenHandler().getScalingFactor());
+	int videoScalingSettings = GH.screenHandler().getInterfaceScalingPercentage();
+	float cursorScalingSettings = settings["video"]["cursorScalingFactor"].Float();
+	int cursorScalingPercent = videoScalingSettings * cursorScalingSettings;
+	Point cursorDimensions = image->dimensions() * GH.screenHandler().getScalingFactor();
+	Point cursorDimensionsScaled = image->dimensions() * cursorScalingPercent / 100;
+	Point pivotOffsetScaled = pivotOffset * cursorScalingPercent / 100 / GH.screenHandler().getScalingFactor();
+
+	auto cursorSurface = CSDL_Ext::newSurface(cursorDimensions);
 
 	CSDL_Ext::fillSurface(cursorSurface, CSDL_Ext::toSDL(Colors::TRANSPARENCY));
 
 	image->draw(cursorSurface, Point(0,0));
+	auto cursorSurfaceScaled = CSDL_Ext::scaleSurface(cursorSurface, cursorDimensionsScaled.x, cursorDimensionsScaled.y );
 
 	auto oldCursor = cursor;
-	cursor = SDL_CreateColorCursor(cursorSurface, pivotOffset.x, pivotOffset.y);
+	cursor = SDL_CreateColorCursor(cursorSurfaceScaled, pivotOffsetScaled.x, pivotOffsetScaled.y);
 
 	if (!cursor)
 		logGlobal->error("Failed to set cursor! SDL says %s", SDL_GetError());
 
 	SDL_FreeSurface(cursorSurface);
+	SDL_FreeSurface(cursorSurfaceScaled);
 
 	GH.dispatchMainThread([this, oldCursor](){
 		SDL_SetCursor(cursor);

+ 41 - 24
client/renderSDL/ScreenHandler.cpp

@@ -84,19 +84,39 @@ Rect ScreenHandler::convertLogicalPointsToWindow(const Rect & input) const
 	return result;
 }
 
-Point ScreenHandler::getPreferredLogicalResolution() const
+int ScreenHandler::getInterfaceScalingPercentage() const
 {
-	Point renderResolution = getRenderResolution();
-	double reservedAreaWidth = settings["video"]["reservedWidth"].Float();
-	Point availableResolution = Point(renderResolution.x * (1 - reservedAreaWidth), renderResolution.y);
-
 	auto [minimalScaling, maximalScaling] = getSupportedScalingRange();
 
 	int userScaling = settings["video"]["resolution"]["scaling"].Integer();
+
+	if (userScaling == 0) // autodetection
+	{
+#ifdef VCMI_MOBILE
+		// for mobiles - stay at maximum scaling unless we have large screen
+		// might be better to check screen DPI / physical dimensions, but way more complex, and may result in different edge cases, e.g. chromebooks / tv's
+		int preferredMinimalScaling = 200;
+#else
+		// for PC - avoid downscaling if possible
+		int preferredMinimalScaling = 100;
+#endif
+		// prefer a little below maximum - to give space for extended UI
+		int preferredMaximalScaling = maximalScaling * 10 / 12;
+		userScaling = std::max(std::min(maximalScaling, preferredMinimalScaling), preferredMaximalScaling);
+	}
+
 	int scaling = std::clamp(userScaling, minimalScaling, maximalScaling);
+	return scaling;
+}
 
-	Point logicalResolution = availableResolution * 100.0 / scaling;
+Point ScreenHandler::getPreferredLogicalResolution() const
+{
+	Point renderResolution = getRenderResolution();
+	double reservedAreaWidth = settings["video"]["reservedWidth"].Float();
 
+	int scaling = getInterfaceScalingPercentage();
+	Point availableResolution = Point(renderResolution.x * (1 - reservedAreaWidth), renderResolution.y);
+	Point logicalResolution = availableResolution * 100.0 / scaling;
 	return logicalResolution;
 }
 
@@ -335,25 +355,22 @@ EUpscalingFilter ScreenHandler::loadUpscalingFilter() const
 	if (filter != EUpscalingFilter::AUTO)
 		return filter;
 
-	// for now - always fallback to no filter
-	return EUpscalingFilter::NONE;
-
 	// else - autoselect
-//	Point outputResolution = getRenderResolution();
-//	Point logicalResolution = getPreferredLogicalResolution();
-//
-//	float scaleX = static_cast<float>(outputResolution.x) / logicalResolution.x;
-//	float scaleY = static_cast<float>(outputResolution.x) / logicalResolution.x;
-//	float scaling = std::min(scaleX, scaleY);
-//
-//	if (scaling <= 1.0f)
-//		return EUpscalingFilter::NONE;
-//	if (scaling <= 2.0f)
-//		return EUpscalingFilter::XBRZ_2;
-//	if (scaling <= 3.0f)
-//		return EUpscalingFilter::XBRZ_3;
-//
-//	return EUpscalingFilter::XBRZ_4;
+	Point outputResolution = getRenderResolution();
+	Point logicalResolution = getPreferredLogicalResolution();
+
+	float scaleX = static_cast<float>(outputResolution.x) / logicalResolution.x;
+	float scaleY = static_cast<float>(outputResolution.x) / logicalResolution.x;
+	float scaling = std::min(scaleX, scaleY);
+
+	if (scaling <= 1.001f)
+		return EUpscalingFilter::NONE; // running at original resolution or even lower than that - no need for xbrz
+	if (scaling <= 2.001f)
+		return EUpscalingFilter::XBRZ_2; // resolutions below 1200p (including 1080p / FullHD)
+	if (scaling <= 3.001f)
+		return EUpscalingFilter::XBRZ_3; // resolutions below 2400p (including 1440p and 2160p / 4K)
+
+	return EUpscalingFilter::XBRZ_4; // Only for massive displays, e.g. 8K
 }
 
 void ScreenHandler::selectUpscalingFilter()

+ 2 - 0
client/renderSDL/ScreenHandler.h

@@ -112,6 +112,8 @@ public:
 
 	int getScalingFactor() const final;
 
+	int getInterfaceScalingPercentage() const final;
+
 	std::vector<Point> getSupportedResolutions() const final;
 	std::vector<Point> getSupportedResolutions(int displayIndex) const;
 	std::tuple<int, int> getSupportedScalingRange() const final;

+ 8 - 5
client/widgets/CComponent.cpp

@@ -12,9 +12,6 @@
 
 #include "Images.h"
 
-#include <vcmi/spells/Service.h>
-#include <vcmi/spells/Spell.h>
-
 #include "../gui/CGuiHandler.h"
 #include "../gui/CursorHandler.h"
 #include "../gui/TextAlignment.h"
@@ -29,7 +26,6 @@
 #include "../CGameInfo.h"
 
 #include "../../lib/ArtifactUtils.h"
-#include "../../lib/CHeroHandler.h"
 #include "../../lib/entities/building/CBuilding.h"
 #include "../../lib/entities/faction/CFaction.h"
 #include "../../lib/entities/faction/CTown.h"
@@ -42,6 +38,11 @@
 #include "../../lib/CArtHandler.h"
 #include "../../lib/CArtifactInstance.h"
 
+#include <vcmi/spells/Service.h>
+#include <vcmi/spells/Spell.h>
+#include <vcmi/HeroTypeService.h>
+#include <vcmi/HeroType.h>
+
 CComponent::CComponent(ComponentType Type, ComponentSubType Subtype, std::optional<int32_t> Val, ESize imageSize, EFonts font)
 {
 	init(Type, Subtype, Val, imageSize, font, "");
@@ -70,6 +71,7 @@ void CComponent::init(ComponentType Type, ComponentSubType Subtype, std::optiona
 	customSubtitle = ValText;
 	size = imageSize;
 	font = fnt;
+	newLine = false;
 
 	assert(size < sizeInvalid);
 
@@ -471,7 +473,8 @@ void CComponentBox::placeComponents(bool selectable)
 
 		//start next row
 		if ((pos.w != 0 && rows.back().width + comp->pos.w + distance > pos.w) // row is full
-			|| rows.back().comps >= componentsInRow)
+			|| rows.back().comps >= componentsInRow
+			|| (prevComp && prevComp->newLine))
 		{
 			prevComp = nullptr;
 			rows.push_back (RowData (0,0,0));

+ 1 - 0
client/widgets/CComponent.h

@@ -52,6 +52,7 @@ public:
 	std::string customSubtitle;
 	ESize size; //component size.
 	EFonts font; //Font size of label
+	bool newLine; //Line break after component
 
 	std::string getDescription() const;
 	std::string getSubtitle() const;

+ 4 - 5
client/widgets/MiscWidgets.cpp

@@ -468,8 +468,8 @@ void CInteractableTownTooltip::init(const CGTownInstance * town)
 				LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE);
 		}
 	}, [town]{
-		if(!town->town->faction->getDescriptionTranslated().empty())
-			CRClickPopup::createAndPush(town->town->faction->getDescriptionTranslated());
+		if(!town->getFaction()->getDescriptionTranslated().empty())
+			CRClickPopup::createAndPush(town->getFaction()->getDescriptionTranslated());
 	});
 	fastMarket = std::make_shared<LRClickableArea>(Rect(143, 31, 30, 34), []()
 	{
@@ -532,8 +532,7 @@ CreatureTooltip::CreatureTooltip(Point pos, const CGCreature * creature)
 {
 	OBJECT_CONSTRUCTION;
 
-	auto creatureID = creature->getCreature();
-	int32_t creatureIconIndex = CGI->creatures()->getById(creatureID)->getIconIndex();
+	int32_t creatureIconIndex = creature->getCreature()->getIconIndex();
 
 	creatureImage = std::make_shared<CAnimImage>(AnimationPath::builtin("TWCRPORT"), creatureIconIndex);
 	creatureImage->center(Point(parent->pos.x + parent->pos.w / 2, parent->pos.y + creatureImage->pos.h / 2 + 11));
@@ -633,7 +632,7 @@ CCreaturePic::CCreaturePic(int x, int y, const CCreature * cre, bool Big, bool A
 	pos.x+=x;
 	pos.y+=y;
 
-	auto faction = cre->getFaction();
+	auto faction = cre->getFactionID();
 
 	assert(CGI->townh->size() > faction);
 

+ 8 - 8
client/widgets/TextControls.cpp

@@ -309,7 +309,7 @@ void CMultiLineLabel::splitText(const std::string & Txt, bool redrawAfter)
 	lines.clear();
 
 	const auto & fontPtr = GH.renderHandler().loadFont(font);
-	int lineHeight = static_cast<int>(fontPtr->getLineHeight());
+	int lineHeight = fontPtr->getLineHeight();
 
 	lines = CMessage::breakText(Txt, pos.w, font);
 
@@ -330,16 +330,16 @@ Rect CMultiLineLabel::getTextLocation()
 		return pos;
 
 	const auto & fontPtr = GH.renderHandler().loadFont(font);
-	Point textSize(pos.w, fontPtr->getLineHeight() * (int)lines.size());
-	Point textOffset(pos.w - textSize.x, pos.h - textSize.y);
+	Point textSizeComputed(pos.w, fontPtr->getLineHeight() * lines.size()); //FIXME: how is this different from textSize member?
+	Point textOffset(pos.w - textSizeComputed.x, pos.h - textSizeComputed.y);
 
 	switch(alignment)
 	{
-	case ETextAlignment::TOPLEFT:     return Rect(pos.topLeft(), textSize);
-	case ETextAlignment::TOPCENTER:   return Rect(pos.topLeft(), textSize);
-	case ETextAlignment::CENTER:      return Rect(pos.topLeft() + textOffset / 2, textSize);
-	case ETextAlignment::CENTERRIGHT: return Rect(pos.topLeft() + Point(textOffset.x, textOffset.y / 2), textSize);
-	case ETextAlignment::BOTTOMRIGHT: return Rect(pos.topLeft() + textOffset, textSize);
+	case ETextAlignment::TOPLEFT:     return Rect(pos.topLeft(), textSizeComputed);
+	case ETextAlignment::TOPCENTER:   return Rect(pos.topLeft(), textSizeComputed);
+	case ETextAlignment::CENTER:      return Rect(pos.topLeft() + textOffset / 2, textSizeComputed);
+	case ETextAlignment::CENTERRIGHT: return Rect(pos.topLeft() + Point(textOffset.x, textOffset.y / 2), textSizeComputed);
+	case ETextAlignment::BOTTOMRIGHT: return Rect(pos.topLeft() + textOffset, textSizeComputed);
 	}
 	assert(0);
 	return Rect();

+ 1 - 1
client/widgets/markets/CMarketBase.cpp

@@ -23,9 +23,9 @@
 
 #include "../../../CCallback.h"
 
+#include "../../../lib/entities/hero/CHeroHandler.h"
 #include "../../../lib/texts/CGeneralTextHandler.h"
 #include "../../../lib/mapObjects/CGHeroInstance.h"
-#include "../../../lib/CHeroHandler.h"
 #include "../../../lib/mapObjects/CGMarket.h"
 
 CMarketBase::CMarketBase(const IMarket * market, const CGHeroInstance * hero)

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä