Explorar o código

Merge remote-tracking branch 'upstream/develop' into cpp-map-editor

# Conflicts:
#	CMakeLists.txt
nordsoft %!s(int64=3) %!d(string=hai) anos
pai
achega
d32898b39b
Modificáronse 100 ficheiros con 1138 adicións e 213 borrados
  1. 8 0
      .github/workflows/github.yml
  2. 2 0
      .gitignore
  3. 5 0
      AI/BattleAI/BattleAI.h
  4. 1 1
      AI/BattleAI/CMakeLists.txt
  5. 4 0
      AI/BattleAI/EnemyInfo.h
  6. 4 0
      AI/BattleAI/PossibleSpellcast.h
  7. 6 1
      AI/BattleAI/StackWithBonuses.h
  8. 2 0
      AI/BattleAI/StdInc.h
  9. 2 2
      AI/EmptyAI/CMakeLists.txt
  10. 4 0
      AI/Nullkiller/AIGateway.h
  11. 1 2
      AI/Nullkiller/Analyzers/ArmyManager.cpp
  12. 1 0
      AI/Nullkiller/Analyzers/ArmyManager.h
  13. 1 3
      AI/Nullkiller/Analyzers/BuildAnalyzer.cpp
  14. 4 4
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp
  15. 4 3
      AI/Nullkiller/Analyzers/HeroManager.h
  16. 1 1
      AI/Nullkiller/Analyzers/ObjectClusterizer.cpp
  17. 0 2
      AI/Nullkiller/Behaviors/BuildingBehavior.cpp
  18. 6 6
      AI/Nullkiller/Behaviors/DefenceBehavior.cpp
  19. 4 4
      AI/Nullkiller/Behaviors/StartupBehavior.cpp
  20. 4 1
      AI/Nullkiller/CMakeLists.txt
  21. 4 0
      AI/Nullkiller/Engine/FuzzyEngines.h
  22. 5 0
      AI/Nullkiller/Engine/FuzzyHelper.h
  23. 2 10
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  24. 7 1
      AI/Nullkiller/Engine/PriorityEvaluator.h
  25. 1 1
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  26. 1 4
      AI/Nullkiller/Pathfinding/Actions/BoatActions.h
  27. 3 1
      AI/Nullkiller/Pathfinding/Actions/SpecialAction.h
  28. 3 12
      AI/Nullkiller/Pathfinding/Actors.cpp
  29. 3 2
      AI/Nullkiller/Pathfinding/Actors.h
  30. 0 3
      AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp
  31. 1 0
      AI/Nullkiller/StdInc.h
  32. 1 1
      AI/StupidAI/CMakeLists.txt
  33. 3 1
      AI/StupidAI/StdInc.h
  34. 1 5
      AI/VCAI/AIhelper.cpp
  35. 0 1
      AI/VCAI/AIhelper.h
  36. 1 0
      AI/VCAI/ArmyManager.h
  37. 1 1
      AI/VCAI/CMakeLists.txt
  38. 4 0
      AI/VCAI/FuzzyEngines.h
  39. 4 0
      AI/VCAI/FuzzyHelper.h
  40. 3 8
      AI/VCAI/ResourceManager.cpp
  41. 2 0
      AI/VCAI/StdInc.h
  42. 4 0
      AI/VCAI/VCAI.cpp
  43. 4 0
      AI/VCAI/VCAI.h
  44. 6 3
      AUTHORS
  45. 10 4
      CCallback.h
  46. 7 0
      CI/ios/before_install.sh
  47. 3 0
      CI/ios/post_pack.sh
  48. 4 1
      CI/msvc/before_install.sh
  49. 103 19
      CMakeLists.txt
  50. 62 5
      CMakePresets.json
  51. 19 1
      ChangeLog
  52. 27 5
      Global.h
  53. 2 0
      README.md
  54. 4 0
      Version.cpp.in
  55. 6 0
      Version.h
  56. 21 0
      client/CFocusableHelper.cpp
  57. 11 0
      client/CFocusableHelper.h
  58. 10 6
      client/CGameInfo.h
  59. 44 17
      client/CMT.cpp
  60. 76 3
      client/CMakeLists.txt
  61. 18 13
      client/CPlayerInterface.h
  62. 68 14
      client/CServerHandler.cpp
  63. 10 2
      client/CServerHandler.h
  64. 11 5
      client/Client.h
  65. 8 3
      client/Graphics.h
  66. 6 1
      client/SDLRWwrapper.h
  67. 2 0
      client/StdInc.h
  68. 6 1
      client/battle/CBattleAnimations.h
  69. 1 2
      client/battle/CBattleInterface.cpp
  70. 16 11
      client/battle/CBattleInterface.h
  71. 13 7
      client/battle/CBattleInterfaceClasses.h
  72. 3 2
      client/gui/CAnimation.cpp
  73. 6 1
      client/gui/CAnimation.h
  74. 6 1
      client/gui/CGuiHandler.h
  75. 1 0
      client/gui/CIntObject.h
  76. 4 0
      client/gui/Fonts.h
  77. 31 5
      client/gui/SDL_Extensions.cpp
  78. 1 0
      client/gui/SDL_Extensions.h
  79. 23 0
      client/ios/GameChatKeyboardHanlder.h
  80. 107 0
      client/ios/GameChatKeyboardHanlder.m
  81. 121 0
      client/ios/Images.xcassets/AppIcon.appiconset/Contents.json
  82. BIN=BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  83. BIN=BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  84. BIN=BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  85. BIN=BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  86. BIN=BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  87. BIN=BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  88. BIN=BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  89. BIN=BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  90. BIN=BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  91. BIN=BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  92. BIN=BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  93. BIN=BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  94. BIN=BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  95. BIN=BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  96. 6 0
      client/ios/Images.xcassets/Contents.json
  97. 60 0
      client/ios/Info.plist
  98. 39 0
      client/ios/LaunchScreen.storyboard
  99. 31 0
      client/ios/Settings.bundle/Root.plist
  100. 3 0
      client/ios/Settings.bundle/en.lproj/Root.strings

+ 8 - 0
.github/workflows/github.yml

@@ -90,6 +90,12 @@ jobs:
             preset: macos-arm-conan-ninja-release
             conan_profile: macos-arm
             artifact_platform: arm
+          - platform: ios
+            os: macos-12
+            test: 0
+            pack: 1
+            extension: ipa
+            preset: ios-release
           - platform: mxe
             os: ubuntu-20.04
             mxe: i686-w64-mingw32.shared
@@ -189,6 +195,8 @@ jobs:
         cd '${{github.workspace}}/out/build/${{matrix.preset}}'
         CPACK_PATH=`which -a cpack | grep -m1 -v -i chocolatey`
         "$CPACK_PATH" -C ${{env.BUILD_TYPE}} ${{ matrix.cpack_args }}
+        test -f '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' \
+          && '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' '${{github.workspace}}' "$(ls '${{ env.VCMI_PACKAGE_FILE_NAME }}'.*)"
         rm -rf _CPack_Packages
 
     - name: Additional logs

+ 2 - 0
.gitignore

@@ -39,6 +39,8 @@ doc/*
 VCMI_VS11.sdf
 *.ipch
 VCMI_VS11.opensdf
+.DS_Store
+CMakeUserPresets.json
 
 # Visual Studio
 *.suo

+ 5 - 0
AI/BattleAI/BattleAI.h

@@ -13,7 +13,12 @@
 #include "PossibleSpellcast.h"
 #include "PotentialTargets.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CSpell;
+
+VCMI_LIB_NAMESPACE_END
+
 class EnemyInfo;
 
 /*

+ 1 - 1
AI/BattleAI/CMakeLists.txt

@@ -33,7 +33,7 @@ endif()
 
 add_library(BattleAI SHARED ${battleAI_SRCS} ${battleAI_HEADERS})
 target_include_directories(BattleAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
-target_link_libraries(BattleAI PRIVATE vcmi)
+target_link_libraries(BattleAI PRIVATE ${VCMI_LIB_TARGET})
 
 vcmi_set_output_dir(BattleAI "AI")
 enable_pch(BattleAI)

+ 4 - 0
AI/BattleAI/EnemyInfo.h

@@ -9,11 +9,15 @@
  */
 #pragma once
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 namespace battle
 {
 	class Unit;
 }
 
+VCMI_LIB_NAMESPACE_END
+
 class EnemyInfo
 {
 public:

+ 4 - 0
AI/BattleAI/PossibleSpellcast.h

@@ -14,8 +14,12 @@
 
 #include "../../lib/battle/Destination.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CSpell;
 
+VCMI_LIB_NAMESPACE_END
+
 class PossibleSpellcast
 {
 public:

+ 6 - 1
AI/BattleAI/StackWithBonuses.h

@@ -18,9 +18,14 @@
 #include "../../lib/battle/BattleProxy.h"
 #include "../../lib/battle/CUnitState.h"
 
-class HypotheticBattle;
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CStack;
 
+VCMI_LIB_NAMESPACE_END
+
+class HypotheticBattle;
+
 ///Fake random generator, used by AI to evaluate random server behavior
 class RNGStub : public vstd::RNG
 {

+ 2 - 0
AI/BattleAI/StdInc.h

@@ -13,3 +13,5 @@
 // This header should be treated as a pre compiled header file(PCH) in the compiler building settings.
 
 // Here you can add specific libraries and macros which are specific to this project.
+
+VCMI_LIB_USING_NAMESPACE

+ 2 - 2
AI/EmptyAI/CMakeLists.txt

@@ -15,9 +15,9 @@ assign_source_group(${emptyAI_SRCS} ${emptyAI_HEADERS})
 
 add_library(EmptyAI SHARED ${emptyAI_SRCS} ${emptyAI_HEADERS})
 target_include_directories(EmptyAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
-target_link_libraries(EmptyAI PRIVATE vcmi)
+target_link_libraries(EmptyAI PRIVATE ${VCMI_LIB_TARGET})
 
 vcmi_set_output_dir(EmptyAI "AI")
 enable_pch(EmptyAI)
 
-install(TARGETS EmptyAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR})
+install(TARGETS EmptyAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR} OPTIONAL)

+ 4 - 0
AI/Nullkiller/AIGateway.h

@@ -25,8 +25,12 @@
 #include "Pathfinding/AIPathfinder.h"
 #include "Engine/Nullkiller.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 struct QuestInfo;
 
+VCMI_LIB_NAMESPACE_END
+
 class AIStatus
 {
 	boost::mutex mx;

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

@@ -204,7 +204,6 @@ std::shared_ptr<CCreatureSet> ArmyManager::getArmyAvailableToBuyAsCCreatureSet(
 	TResources availableRes) const
 {
 	std::vector<creInfo> creaturesInDwellings;
-	int freeHeroSlots = GameConstants::ARMY_SIZE;
 	auto army = std::make_shared<TemporaryArmy>();
 
 	for(int i = dwelling->creatures.size() - 1; i >= 0; i--)
@@ -497,4 +496,4 @@ ArmyUpgradeInfo ArmyManager::calculateCreaturesUpgrade(
 	}
 
 	return result;
-}
+}

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

@@ -41,6 +41,7 @@ struct ArmyUpgradeInfo
 class DLL_EXPORT IArmyManager //: public: IAbstractManager
 {
 public:
+	virtual ~IArmyManager() = default;
 	virtual void update() = 0;
 	virtual ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const = 0;
 	virtual	ui64 howManyReinforcementsCanBuy(

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

@@ -129,8 +129,6 @@ void BuildAnalyzer::update()
 	{
 		logAi->trace("Checking town %s", town->name);
 
-		auto townInfo = town->town;
-
 		developmentInfos.push_back(TownDevelopmentInfo(town));
 		TownDevelopmentInfo & developmentInfo = developmentInfos.back();
 
@@ -399,4 +397,4 @@ std::string BuildingInfo::toString() const
 		+ ", creature: " + std::to_string(creatureGrows) + " x " + std::to_string(creatureLevel)
 		+ " x " + creatureCost.toString()
 		+ ", daily: " + dailyIncome.toString();
-}
+}

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

@@ -63,7 +63,7 @@ void DangerHitMapAnalyzer::updateHitMap()
 				auto & node = hitMap[pos.x][pos.y][pos.z];
 
 				if(tileDanger > node.maximumDanger.danger
-					|| tileDanger == node.maximumDanger.danger && node.maximumDanger.turn > turn)
+					|| (tileDanger == node.maximumDanger.danger && node.maximumDanger.turn > turn))
 				{
 					node.maximumDanger.danger = tileDanger;
 					node.maximumDanger.turn = turn;
@@ -71,7 +71,7 @@ void DangerHitMapAnalyzer::updateHitMap()
 				}
 
 				if(turn < node.fastestDanger.turn
-					|| turn == node.fastestDanger.turn && node.fastestDanger.danger < tileDanger)
+					|| (turn == node.fastestDanger.turn && node.fastestDanger.danger < tileDanger))
 				{
 					node.fastestDanger.danger = tileDanger;
 					node.fastestDanger.turn = turn;
@@ -101,8 +101,8 @@ uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath &
 	int turn = path.turn();
 	const HitMapNode & info = hitMap[tile.x][tile.y][tile.z];
 
-	return info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger)
-		|| info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger);
+	return (info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger))
+		|| (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger));
 }
 
 const HitMapNode & DangerHitMapAnalyzer::getObjectTreat(const CGObjectInstance * obj) const

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

@@ -20,6 +20,7 @@
 class DLL_EXPORT IHeroManager //: public: IAbstractManager
 {
 public:
+	virtual ~IHeroManager() = default;
 	virtual const std::map<HeroPtr, HeroRole> & getHeroRoles() const = 0;
 	virtual int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const = 0;
 	virtual HeroRole getHeroRole(const HeroPtr & hero) const = 0;
@@ -31,6 +32,7 @@ public:
 class DLL_EXPORT ISecondarySkillRule
 {
 public:
+	virtual ~ISecondarySkillRule() = default;
 	virtual void evaluateScore(const CGHeroInstance * hero, SecondarySkill skill, float & score) const = 0;
 };
 
@@ -52,11 +54,10 @@ private:
 	static SecondarySkillEvaluator scountSkillsScores;
 
 	CCallback * cb; //this is enough, but we downcast from CCallback
-	const Nullkiller * ai;
 	std::map<HeroPtr, HeroRole> heroRoles;
 
 public:
-	HeroManager(CCallback * CB, const Nullkiller * ai) : cb(CB), ai(ai) {}
+	HeroManager(CCallback * CB, const Nullkiller * ai) : cb(CB) {}
 	const std::map<HeroPtr, HeroRole> & getHeroRoles() const override;
 	HeroRole getHeroRole(const HeroPtr & hero) const override;
 	int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const override;
@@ -102,4 +103,4 @@ private:
 
 public:
 	void evaluateScore(const CGHeroInstance * hero, SecondarySkill skill, float & score) const override;
-};
+};

+ 1 - 1
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

@@ -149,7 +149,7 @@ bool ObjectClusterizer::shouldVisitObject(const CGObjectInstance * obj) const
 
 	const int3 pos = obj->visitablePos();
 
-	if(obj->ID != Obj::CREATURE_GENERATOR1 && vstd::contains(ai->memory->alreadyVisited, obj)
+	if((obj->ID != Obj::CREATURE_GENERATOR1 && vstd::contains(ai->memory->alreadyVisited, obj))
 		|| obj->wasVisited(ai->playerID))
 	{
 		return false;

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

@@ -53,8 +53,6 @@ Goals::TGoalVec BuildingBehavior::decompose() const
 
 	for(auto & developmentInfo : developmentInfos)
 	{
-		auto town = developmentInfo.town;
-
 		for(auto & buildingInfo : developmentInfo.toBuild)
 		{
 			if(goldPreasure < MAX_GOLD_PEASURE || buildingInfo.dailyIncome[Res::GOLD] > 0)

+ 6 - 6
AI/Nullkiller/Behaviors/DefenceBehavior.cpp

@@ -106,10 +106,10 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 		{
 			if(path.getHeroStrength() > treat.danger)
 			{
-				if(path.turn() <= treat.turn && dayOfWeek + treat.turn < 6 && isSafeToVisit(path.targetHero, path.heroArmy, treat.danger)
-					|| path.exchangeCount == 1 && path.turn() < treat.turn
+				if((path.turn() <= treat.turn && dayOfWeek + treat.turn < 6 && isSafeToVisit(path.targetHero, path.heroArmy, treat.danger))
+					|| (path.exchangeCount == 1 && path.turn() < treat.turn)
 					|| path.turn() < treat.turn - 1
-					|| path.turn() < treat.turn && treat.turn >= 2)
+					|| (path.turn() < treat.turn && treat.turn >= 2))
 				{
 					logAi->debug(
 						"Hero %s can eliminate danger for town %s using path %s.",
@@ -217,7 +217,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				// dismiss creatures we are not able to pick to be able to hide in garrison
 				if(town->garrisonHero
 					|| town->getUpperArmy()->stacksCount() == 0
-					|| town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL)
+					|| (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL))
 				{
 					tasks.push_back(
 						Goals::sptr(Composition()
@@ -228,7 +228,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				continue;
 			}
 				
-			if(treat.turn == 0 || path.turn() <= treat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= treat.danger)
+			if(treat.turn == 0 || (path.turn() <= treat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= treat.danger))
 			{
 				if(ai->nullkiller->arePathHeroesLocked(path))
 				{
@@ -294,4 +294,4 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 	}
 
 	logAi->debug("Found %d tasks", tasks.size());
-}
+}

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

@@ -55,7 +55,7 @@ const CGHeroInstance * getNearestHero(const CGTownInstance * town)
 	if(shortestPath.nodes.size() > 1
 		|| shortestPath.turn() != 0
 		|| shortestPath.targetHero->visitablePos().dist2dSQ(town->visitablePos()) > 4
-		|| town->garrisonHero && shortestPath.targetHero == town->garrisonHero.get())
+		|| (town->garrisonHero && shortestPath.targetHero == town->garrisonHero.get()))
 		return nullptr;
 
 	return shortestPath.targetHero;
@@ -76,13 +76,13 @@ bool needToRecruitHero(const CGTownInstance * startupTown)
 
 	for(auto obj : ai->nullkiller->objectClusterizer->getNearbyObjects())
 	{
-		if(obj->ID == Obj::RESOURCE && obj->subID == Res::GOLD
+		if((obj->ID == Obj::RESOURCE && obj->subID == Res::GOLD)
 			|| obj->ID == Obj::TREASURE_CHEST
 			|| obj->ID == Obj::CAMPFIRE
 			|| obj->ID == Obj::WATER_WHEEL)
 		{
 			auto path = paths->getPathInfo(obj->visitablePos());
-			if((path->accessible == CGPathNode::BLOCKVIS || path->accessible == CGPathNode::VISIT) 
+			if((path->accessible == CGPathNode::BLOCKVIS || path->accessible == CGPathNode::VISITABLE) 
 				&& path->reachable())
 			{
 				treasureSourcesCount++;
@@ -162,7 +162,7 @@ Goals::TGoalVec StartupBehavior::decompose() const
 				auto garrisonHeroScore = ai->nullkiller->heroManager->evaluateHero(garrisonHero);
 
 				if(visitingHeroScore > garrisonHeroScore
-					|| ai->nullkiller->heroManager->getHeroRole(garrisonHero) == HeroRole::SCOUT && ai->nullkiller->heroManager->getHeroRole(visitingHero) == HeroRole::MAIN)
+					|| (ai->nullkiller->heroManager->getHeroRole(garrisonHero) == HeroRole::SCOUT && ai->nullkiller->heroManager->getHeroRole(visitingHero) == HeroRole::MAIN))
 				{
 					if(canRecruitHero || ai->nullkiller->armyManager->howManyReinforcementsCanGet(visitingHero, garrisonHero) > 200)
 					{

+ 4 - 1
AI/Nullkiller/CMakeLists.txt

@@ -130,7 +130,7 @@ add_library(Nullkiller SHARED ${Nullkiller_SRCS} ${Nullkiller_HEADERS})
 
 target_include_directories(Nullkiller PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
 
-target_link_libraries(Nullkiller PRIVATE vcmi fuzzylite::fuzzylite)
+target_link_libraries(Nullkiller PRIVATE ${VCMI_LIB_TARGET} fuzzylite::fuzzylite)
 
 target_link_libraries(Nullkiller PRIVATE TBB::tbb)
 
@@ -138,3 +138,6 @@ vcmi_set_output_dir(Nullkiller "AI")
 enable_pch(Nullkiller)
 
 install(TARGETS Nullkiller RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR})
+if(APPLE_IOS)
+	install(IMPORTED_RUNTIME_ARTIFACTS TBB::tbb LIBRARY DESTINATION ${LIB_DIR}) # CMake 3.21+
+endif()

+ 4 - 0
AI/Nullkiller/Engine/FuzzyEngines.h

@@ -11,8 +11,12 @@
 #include <fl/Headers.h>
 #include "../Goals/AbstractGoal.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CArmedInstance;
 
+VCMI_LIB_NAMESPACE_END
+
 class engineBase //subclasses create fuzzylite variables with "new" that are not freed - this is desired as fl::Engine wants to destroy these...
 {
 protected:

+ 5 - 0
AI/Nullkiller/Engine/FuzzyHelper.h

@@ -10,7 +10,12 @@
 #pragma once
 #include "FuzzyEngines.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CBank;
+
+VCMI_LIB_NAMESPACE_END
+
 class Nullkiller;
 
 class DLL_EXPORT FuzzyHelper

+ 2 - 10
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -122,7 +122,7 @@ uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHero
 	{
 		//No free slot, we might discard our weakest stack
 		weakestStackPower = std::numeric_limits<ui64>().max();
-		for (const auto stack : slots)
+		for (const auto & stack : slots)
 		{
 			vstd::amin(weakestStackPower, stack.second->getPower());
 		}
@@ -645,7 +645,6 @@ public:
 		}
 
 		auto heroPtr = task->hero;
-		auto day = ai->cb->getDate(Date::DAY);
 		auto hero = heroPtr.get(ai->cb.get());
 		bool checkGold = evaluationContext.danger == 0;
 		auto army = path.heroArmy;
@@ -670,11 +669,8 @@ public:
 
 class ClusterEvaluationContextBuilder : public IEvaluationContextBuilder
 {
-private:
-	const Nullkiller * ai;
-
 public:
-	ClusterEvaluationContextBuilder(const Nullkiller * ai) : ai(ai) {}
+	ClusterEvaluationContextBuilder(const Nullkiller * ai) {}
 
 	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	{
@@ -699,7 +695,6 @@ public:
 		for(auto objInfo : objects)
 		{
 			auto target = objInfo.first;
-			auto day = ai->cb->getDate(Date::DAY);
 			bool checkGold = objInfo.second.danger == 0;
 			auto army = hero;
 
@@ -718,9 +713,6 @@ public:
 			if(boost > 8)
 				break;
 		}
-
-		const AIPath & pathToCenter = clusterGoal.getPathToCenter();
-
 	}
 };
 

+ 7 - 1
AI/Nullkiller/Engine/PriorityEvaluator.h

@@ -12,9 +12,14 @@
 #include "../Goals/CGoal.h"
 #include "../Pathfinding/AIPathfinder.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
+class CGWitchHut;
+
+VCMI_LIB_NAMESPACE_END
+
 class BuildingInfo;
 class Nullkiller;
-class CGWitchHut;
 
 class RewardEvaluator
 {
@@ -61,6 +66,7 @@ struct DLL_EXPORT EvaluationContext
 class IEvaluationContextBuilder
 {
 public:
+	virtual ~IEvaluationContextBuilder() = default;
 	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal goal) const = 0;
 };
 

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

@@ -558,7 +558,7 @@ bool AINodeStorage::selectNextActor()
 	for(auto actor = actors.begin(); actor != actors.end(); actor++)
 	{
 		if(actor->get()->armyValue > currentActor->get()->armyValue
-			|| actor->get()->armyValue == currentActor->get()->armyValue && actor <= currentActor)
+			|| (actor->get()->armyValue == currentActor->get()->armyValue && actor <= currentActor))
 		{
 			continue;
 		}

+ 1 - 4
AI/Nullkiller/Pathfinding/Actions/BoatActions.h

@@ -24,9 +24,6 @@ namespace AIPathfinding
 	
 	class SummonBoatAction : public VirtualBoatAction
 	{
-	private:
-		const CGHeroInstance * hero;
-
 	public:
 		virtual void execute(const CGHeroInstance * hero) const override;
 
@@ -71,4 +68,4 @@ namespace AIPathfinding
 
 		virtual const CGObjectInstance * targetObject() const override;
 	};
-}
+}

+ 3 - 1
AI/Nullkiller/Pathfinding/Actions/SpecialAction.h

@@ -18,6 +18,8 @@ struct AIPathNode;
 class SpecialAction
 {
 public:
+	virtual ~SpecialAction() = default;
+
 	virtual bool canAct(const AIPathNode * source) const
 	{
 		return true;
@@ -39,4 +41,4 @@ public:
 	virtual std::string toString() const = 0;
 
 	virtual const CGObjectInstance * targetObject() const { return nullptr; }
-};
+};

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

@@ -164,6 +164,7 @@ ExchangeResult ChainActor::tryExchangeNoLock(const ChainActor * specialActor, co
 	return baseActor->tryExchangeNoLock(specialActor, other);
 }
 
+VCMI_LIB_NAMESPACE_BEGIN
 namespace vstd
 {
 	template <class M, class Key, class F>
@@ -180,6 +181,7 @@ namespace vstd
 		return v;
 	}
 }
+VCMI_LIB_NAMESPACE_END
 
 ExchangeResult HeroActor::tryExchangeNoLock(const ChainActor * specialActor, const ChainActor * other) const
 {
@@ -269,8 +271,6 @@ ExchangeResult HeroExchangeMap::tryExchangeNoLock(const ChainActor * other)
 			return result; // already inserted
 		}
 
-		auto position = inserted.first;
-
 		auto differentMasks = (actor->chainMask & other->chainMask) == 0;
 
 		if(!differentMasks) return result;
@@ -461,15 +461,6 @@ CCreatureSet * DwellingActor::getDwellingCreatures(const CGDwelling * dwelling,
 			continue;
 
 		auto creature = creatureInfo.second.back().toCreature();
-		auto count = creatureInfo.first;
-
-		if(waitForGrowth)
-		{
-			const CGTownInstance * town = dynamic_cast<const CGTownInstance *>(dwelling);
-
-			count += town ? town->creatureGrowth(creature->level) : creature->growth;
-		}
-
 		dwellingCreatures->addToSlot(
 			dwellingCreatures->getSlotFor(creature),
 			creature->idNumber,
@@ -487,4 +478,4 @@ TownGarrisonActor::TownGarrisonActor(const CGTownInstance * town, uint64_t chain
 std::string TownGarrisonActor::toString() const
 {
 	return town->name;
-}
+}

+ 3 - 2
AI/Nullkiller/Pathfinding/Actors.h

@@ -75,7 +75,8 @@ public:
 	TResources armyCost;
 	std::shared_ptr<TurnInfo> tiCache;
 
-	ChainActor(){}
+	ChainActor() = default;
+	virtual ~ChainActor() = default;
 
 	virtual std::string toString() const;
 	ExchangeResult tryExchangeNoLock(const ChainActor * other) const { return tryExchangeNoLock(this, other); }
@@ -168,4 +169,4 @@ private:
 public:
 	TownGarrisonActor(const CGTownInstance * town, uint64_t chainMask);
 	virtual std::string toString() const override;
-};
+};

+ 0 - 3
AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp

@@ -126,7 +126,6 @@ namespace AIPathfinding
 		const AIPathNode * destinationNode = nodeStorage->getAINode(destination.node);
 		auto questObj = dynamic_cast<const IQuestObject *>(destination.nodeObject);
 		auto questInfo = QuestInfo(questObj->quest, destination.nodeObject, destination.coord);
-		auto nodeHero = pathfinderHelper->hero;
 		QuestAction questAction(questInfo);
 
 		if(destination.nodeObject->ID == Obj::QUEST_GUARD && questObj->quest->missionType == CQuest::MISSION_NONE)
@@ -157,8 +156,6 @@ namespace AIPathfinding
 
 			nodeStorage->updateAINode(destination.node, [&](AIPathNode * node)
 			{
-				auto questInfo = QuestInfo(questObj->quest, destination.nodeObject, destination.coord);
-
 				node->specialAction.reset(new QuestAction(questAction));
 			});
 		}

+ 1 - 0
AI/Nullkiller/StdInc.h

@@ -1,3 +1,4 @@
 #pragma  once
 #include "../../Global.h"
+VCMI_LIB_USING_NAMESPACE
 #include "../../CCallback.h"

+ 1 - 1
AI/StupidAI/CMakeLists.txt

@@ -14,7 +14,7 @@ set(stupidAI_HEADERS
 assign_source_group(${stupidAI_SRCS} ${stupidAI_HEADERS})
 
 add_library(StupidAI SHARED ${stupidAI_SRCS} ${stupidAI_HEADERS})
-target_link_libraries(StupidAI PRIVATE vcmi)
+target_link_libraries(StupidAI PRIVATE ${VCMI_LIB_TARGET})
 target_include_directories(StupidAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
 
 vcmi_set_output_dir(StupidAI "AI")

+ 3 - 1
AI/StupidAI/StdInc.h

@@ -4,4 +4,6 @@
 
 // This header should be treated as a pre compiled header file(PCH) in the compiler building settings.
 
-// Here you can add specific libraries and macros which are specific to this project.
+// Here you can add specific libraries and macros which are specific to this project.
+
+VCMI_LIB_USING_NAMESPACE

+ 1 - 5
AI/VCAI/AIhelper.cpp

@@ -19,10 +19,6 @@ AIhelper::AIhelper()
 	armyManager.reset(new ArmyManager());
 }
 
-AIhelper::~AIhelper()
-{
-}
-
 bool AIhelper::notifyGoalCompleted(Goals::TSubgoal goal)
 {
 	return resourceManager->notifyGoalCompleted(goal);
@@ -182,4 +178,4 @@ std::vector<SlotInfo>::iterator AIhelper::getWeakestCreature(std::vector<SlotInf
 std::vector<SlotInfo> AIhelper::getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const
 {
 	return armyManager->getSortedSlots(target, source);
-}
+}

+ 0 - 1
AI/VCAI/AIhelper.h

@@ -36,7 +36,6 @@ class DLL_EXPORT AIhelper : public IResourceManager, public IBuildingManager, pu
 	//TODO: vector<IAbstractManager>
 public:
 	AIhelper();
-	~AIhelper();
 
 	bool canAfford(const TResources & cost) const;
 	TResources reservedResources() const override;

+ 1 - 0
AI/VCAI/ArmyManager.h

@@ -28,6 +28,7 @@ struct SlotInfo
 class DLL_EXPORT IArmyManager //: public: IAbstractManager
 {
 public:
+	virtual ~IArmyManager() = default;
 	virtual void init(CPlayerSpecificInfoCallback * CB) = 0;
 	virtual void setAI(VCAI * AI) = 0;
 	virtual bool canGetArmy(const CArmedInstance * target, const CArmedInstance * source) const = 0;

+ 1 - 1
AI/VCAI/CMakeLists.txt

@@ -107,7 +107,7 @@ add_library(VCAI SHARED ${VCAI_SRCS} ${VCAI_HEADERS})
 
 target_include_directories(VCAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
 
-target_link_libraries(VCAI PRIVATE vcmi fuzzylite::fuzzylite)
+target_link_libraries(VCAI PRIVATE ${VCMI_LIB_TARGET} fuzzylite::fuzzylite)
 
 vcmi_set_output_dir(VCAI "AI")
 enable_pch(VCAI)

+ 4 - 0
AI/VCAI/FuzzyEngines.h

@@ -11,8 +11,12 @@
 #include <fl/Headers.h>
 #include "Goals/AbstractGoal.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CArmedInstance;
 
+VCMI_LIB_NAMESPACE_END
+
 class engineBase //subclasses create fuzzylite variables with "new" that are not freed - this is desired as fl::Engine wants to destroy these...
 {
 protected:

+ 4 - 0
AI/VCAI/FuzzyHelper.h

@@ -10,8 +10,12 @@
 #pragma once
 #include "FuzzyEngines.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CBank;
 
+VCMI_LIB_NAMESPACE_END
+
 class DLL_EXPORT FuzzyHelper
 {
 public:

+ 3 - 8
AI/VCAI/ResourceManager.cpp

@@ -120,14 +120,12 @@ Goals::TSubgoal ResourceManager::collectResourcesForOurGoal(ResourceObjective &o
 		return o.goal;
 	}
 
-	float goalPriority = 10; //arbitrary, will be divided
-	for (const resPair & p : missingResources)
+	for (const resPair p : missingResources)
 	{
 		if (!income[p.first]) //prioritize resources with 0 income
 		{
 			resourceType = p.first;
 			amountToCollect = p.second;
-			goalPriority /= amountToCollect; //need more resources -> lower priority
 			break;
 		}
 	}
@@ -138,7 +136,7 @@ Goals::TSubgoal ResourceManager::collectResourcesForOurGoal(ResourceObjective &o
 		std::map<Res::ERes, float> daysToEarn;
 		for (auto it : missingResources)
 			daysToEarn[it.first] = (float)missingResources[it.first] / income[it.first];
-		auto incomeComparer = [&income](const timePair & lhs, const timePair & rhs) -> bool
+		auto incomeComparer = [](const timePair & lhs, const timePair & rhs) -> bool
 		{
 			//theoretically income can be negative, but that falls into this comparison
 			return lhs.second < rhs.second;
@@ -146,12 +144,9 @@ Goals::TSubgoal ResourceManager::collectResourcesForOurGoal(ResourceObjective &o
 
 		resourceType = boost::max_element(daysToEarn, incomeComparer)->first;
 		amountToCollect = missingResources[resourceType];
-		goalPriority /= daysToEarn[resourceType]; //more days - lower priority
 	}
-	if (resourceType == Res::GOLD)
-		goalPriority *= 1000;
 
-	//this is abstract goal and might take soem time to complete
+	//this is abstract goal and might take some time to complete
 	return Goals::sptr(Goals::CollectRes(resourceType, amountToCollect).setisAbstract(true));
 }
 

+ 2 - 0
AI/VCAI/StdInc.h

@@ -1,2 +1,4 @@
 #pragma  once
 #include "../../Global.h"
+
+VCMI_LIB_USING_NAMESPACE

+ 4 - 0
AI/VCAI/VCAI.cpp

@@ -29,8 +29,12 @@
 
 extern FuzzyHelper * fh;
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CGVisitableOPW;
 
+VCMI_LIB_NAMESPACE_END
+
 const double SAFE_ATTACK_CONSTANT = 1.5;
 
 //one thread may be turn of AI and another will be handling a side effect for AI2

+ 4 - 0
AI/VCAI/VCAI.h

@@ -26,8 +26,12 @@
 #include "../../lib/CondSh.h"
 #include "Pathfinding/AIPathfinder.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 struct QuestInfo;
 
+VCMI_LIB_NAMESPACE_END
+
 class AIhelper;
 
 class AIStatus

+ 6 - 3
AUTHORS

@@ -78,6 +78,9 @@ Andrii Danylchenko
 
 Dmitry Orlov, <[email protected]>
    * special buildings support in fan towns, new features and bug fixes
-
-Andrey Cherkas aka nordsoft, <[email protected]>
-   * new terrain support, random map generator features and various bug fixes
+
+Andrey Cherkas aka nordsoft, <[email protected]>
+   * new terrain support, random map generator features and various bug fixes
+
+Andrey Filipenkov aka kambala-decapitator, <[email protected]>
+   * iOS support, macOS improvements, various bug fixes

+ 10 - 4
CCallback.h

@@ -13,6 +13,8 @@
 #include "lib/battle/CPlayerBattleCallback.h"
 #include "lib/int3.h" // for int3
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CGHeroInstance;
 class CGameState;
 struct CPath;
@@ -20,21 +22,27 @@ class CGObjectInstance;
 class CArmedInstance;
 class BattleAction;
 class CGTownInstance;
-struct lua_State;
-class CClient;
 class IShipyard;
 struct CGPathNode;
 struct CGPath;
 struct CPathsInfo;
 class PathfinderConfig;
 struct CPack;
+struct CPackForServer;
 class IBattleEventsReceiver;
 class IGameEventsReceiver;
 struct ArtifactLocation;
 
+VCMI_LIB_NAMESPACE_END
+
+class CClient;
+struct lua_State;
+
 class IBattleCallback
 {
 public:
+	virtual ~IBattleCallback() = default;
+
 	bool waitTillRealize; //if true, request functions will return after they are realized by server
 	bool unlockGsWhenWaiting;//if true after sending each request, gs mutex will be unlocked so the changes can be applied; NOTICE caller must have gs mx locked prior to any call to actiob callback!
 	//battle
@@ -86,8 +94,6 @@ public:
 	virtual int bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) = 0;
 };
 
-struct CPackForServer;
-
 class CBattleCallback : public IBattleCallback, public CPlayerBattleCallback
 {
 protected:

+ 7 - 0
CI/ios/before_install.sh

@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+echo DEVELOPER_DIR=/Applications/Xcode_13.4.1.app >> $GITHUB_ENV
+
+curl -L 'https://github.com/vcmi/vcmi-ios-deps/releases/latest/download/vcmi-ios-depends-xc13.2.1.txz' \
+	| tar -xf -
+build/fix_install_paths.command

+ 3 - 0
CI/ios/post_pack.sh

@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+"$1/ios/zip2ipa.sh" "$2"

+ 4 - 1
CI/msvc/before_install.sh

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

+ 103 - 19
CMakeLists.txt

@@ -26,6 +26,18 @@ project(VCMI)
 # It's used currently to make sure that 3rd-party dependencies in git submodules get proper FOLDER property
 # - Make FindFuzzyLite check for the right version and disable FORCE_BUNDLED_FL by default
 
+if(APPLE)
+	if(${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
+		set(APPLE_MACOS 1)
+	else()
+		set(APPLE_IOS 1)
+	endif()
+endif()
+
+if(APPLE_IOS)
+	set(BUILD_SINGLE_APP 1)
+endif()
+
 ############################################
 #        User-provided options             #
 ############################################
@@ -41,11 +53,21 @@ set(VCMI_VERSION_MAJOR 1)
 set(VCMI_VERSION_MINOR 0)
 set(VCMI_VERSION_PATCH 0)
 
+set(APP_SHORT_VERSION "${VCMI_VERSION_MAJOR}.${VCMI_VERSION_MINOR}")
+if(NOT VCMI_VERSION_PATCH EQUAL 0)
+	string(APPEND APP_SHORT_VERSION ".${VCMI_VERSION_PATCH}")
+endif()
+
 option(ENABLE_ERM "Enable compilation of ERM scripting module" OFF)
 option(ENABLE_LUA "Enable compilation of LUA scripting module" OFF)
 option(ENABLE_LAUNCHER "Enable compilation of launcher" ON)
 option(ENABLE_EDITOR "Enable compilation of map editor" ON)
 option(ENABLE_TEST "Enable compilation of unit tests" ON)
+if(APPLE_IOS)
+	set(BUNDLE_IDENTIFIER_PREFIX "" CACHE STRING "Bundle identifier prefix")
+else()
+	option(ENABLE_TEST "Enable compilation of unit tests" ON)
+endif()
 if(NOT ${CMAKE_VERSION} VERSION_LESS "3.16.0")
 	option(ENABLE_PCH "Enable compilation using precompiled headers" ON)
 endif(NOT ${CMAKE_VERSION} VERSION_LESS "3.16.0")
@@ -54,7 +76,9 @@ option(ENABLE_DEBUG_CONSOLE "Enable debug console for Windows builds" ON)
 option(ENABLE_MULTI_PROCESS_BUILDS "Enable /MP flag for MSVS solution" ON)
 
 # Used for Snap packages and also useful for debugging
-option(ENABLE_MONOLITHIC_INSTALL "Install everything in single directory on Linux and Mac" OFF)
+if(NOT APPLE_IOS)
+	option(ENABLE_MONOLITHIC_INSTALL "Install everything in single directory on Linux and Mac" OFF)
+endif()
 
 # Allow to pass package name from Travis CI
 set(PACKAGE_NAME_SUFFIX "" CACHE STRING "Suffix for CPack package name")
@@ -134,6 +158,34 @@ set(CMAKE_MAP_IMPORTED_CONFIG_MINSIZEREL MinSizeRel Release RelWithDebInfo "")
 # Release falls back to RelWithDebInfo, then MinSizeRel
 set(CMAKE_MAP_IMPORTED_CONFIG_RELEASE Release RelWithDebInfo MinSizeRel "")
 
+set(CMAKE_XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC YES)
+set(CMAKE_XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT[variant=Debug] dwarf)
+set(CMAKE_XCODE_ATTRIBUTE_ENABLE_BITCODE NO)
+set(CMAKE_XCODE_ATTRIBUTE_ENABLE_NS_ASSERTIONS NO)
+set(CMAKE_XCODE_ATTRIBUTE_ENABLE_NS_ASSERTIONS[variant=Debug] YES)
+set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_64_TO_32_BIT_CONVERSION NO)
+set(CMAKE_XCODE_ATTRIBUTE_MARKETING_VERSION ${APP_SHORT_VERSION})
+set(CMAKE_XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH NO)
+set(CMAKE_XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH[variant=Debug] YES)
+
+if(BUILD_SINGLE_APP)
+	add_compile_definitions(SINGLE_PROCESS_APP=1)
+endif()
+
+if(APPLE_IOS)
+	set(CMAKE_MACOSX_RPATH 1)
+	set(CMAKE_OSX_DEPLOYMENT_TARGET 10.0)
+
+	list(APPEND CMAKE_FIND_ROOT_PATH "${CMAKE_PREFIX_PATH}") # required for Boost
+	set(CMAKE_FIND_USE_CMAKE_ENVIRONMENT_PATH FALSE)
+	set(CMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH FALSE)
+
+	set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED NO)
+	set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED_FOR_APPS YES)
+	set(CMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${BUNDLE_IDENTIFIER_PREFIX}.$(PRODUCT_NAME)")
+	set(CMAKE_XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2")
+endif()
+
 if(MINGW OR MSVC)
 	# Windows Vista or newer for FuzzyLite 6 to compile
 	add_definitions(-D_WIN32_WINNT=0x0600)
@@ -198,6 +250,7 @@ if(CMAKE_COMPILER_IS_GNUCXX OR NOT WIN32) #so far all *nix compilers support suc
 	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-strict-aliasing -Wno-switch -Wno-sign-compare -Wno-unused-local-typedefs")
 	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-parameter -Wno-overloaded-virtual -Wno-type-limits -Wno-unknown-pragmas")
 	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-reorder")
+	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-varargs") # fuzzylite - Operation.h
 
 	if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
 		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-mismatched-tags -Wno-unknown-warning-option -Wno-missing-braces")
@@ -209,7 +262,7 @@ if(CMAKE_COMPILER_IS_GNUCXX OR NOT WIN32) #so far all *nix compilers support suc
 endif()
 
 # Check if some platform-specific libraries are needed for linking
-if(NOT WIN32)
+if(NOT WIN32 AND NOT APPLE_IOS)
 	include(CheckLibraryExists)
 
 	# Shared memory functions used by Boost.Interprocess
@@ -236,7 +289,12 @@ if(TARGET zlib::zlib)
 	add_library(ZLIB::ZLIB ALIAS zlib::zlib)
 endif()
 
-find_package(ffmpeg COMPONENTS avutil swscale avformat avcodec)
+set(FFMPEG_COMPONENTS avutil swscale avformat avcodec)
+if(APPLE_IOS)
+	list(APPEND FFMPEG_COMPONENTS swresample)
+endif()
+find_package(ffmpeg COMPONENTS ${FFMPEG_COMPONENTS})
+
 option(FORCE_BUNDLED_MINIZIP "Force bundled Minizip library" OFF)
 if(NOT FORCE_BUNDLED_MINIZIP)
 	find_package(minizip)
@@ -306,14 +364,19 @@ elseif(APPLE)
 		set(LIB_DIR "." CACHE STRING "Where to install main library")
 		set(DATA_DIR "." CACHE STRING "Where to install data files")
 	else()
-		set(APP_BUNDLE_DIR "${CMAKE_PROJECT_NAME}.app")
-		set(APP_BUNDLE_CONTENTS_DIR "${APP_BUNDLE_DIR}/Contents")
-		set(APP_BUNDLE_BINARY_DIR "${APP_BUNDLE_CONTENTS_DIR}/MacOS")
-		set(APP_BUNDLE_RESOURCES_DIR "${APP_BUNDLE_CONTENTS_DIR}/Resources")
-
-		set(BIN_DIR "${APP_BUNDLE_BINARY_DIR}" CACHE STRING "Where to install binaries")
-		set(LIB_DIR "${APP_BUNDLE_CONTENTS_DIR}/Frameworks" CACHE STRING "Where to install main library")
-		set(DATA_DIR "${APP_BUNDLE_RESOURCES_DIR}/Data" CACHE STRING "Where to install data files")
+		if(APPLE_MACOS)
+			set(APP_BUNDLE_DIR "${CMAKE_PROJECT_NAME}.app")
+			set(APP_BUNDLE_CONTENTS_DIR "${APP_BUNDLE_DIR}/Contents")
+			set(APP_BUNDLE_BINARY_DIR "${APP_BUNDLE_CONTENTS_DIR}/MacOS")
+			set(APP_BUNDLE_RESOURCES_DIR "${APP_BUNDLE_CONTENTS_DIR}/Resources")
+
+			set(BIN_DIR "${APP_BUNDLE_BINARY_DIR}" CACHE STRING "Where to install binaries")
+			set(LIB_DIR "${APP_BUNDLE_CONTENTS_DIR}/Frameworks" CACHE STRING "Where to install main library")
+			set(DATA_DIR "${APP_BUNDLE_RESOURCES_DIR}/Data" CACHE STRING "Where to install data files")
+		else()
+			set(LIB_DIR "Frameworks")
+			set(DATA_DIR ".")
+		endif()
 	endif()
 else()
 	# includes lib path which determines where to install shared libraries (either /lib or /lib64)
@@ -349,6 +412,20 @@ set(SCRIPTING_LIB_DIR "${LIB_DIR}/scripting")
 #        Add subdirectories           #
 #######################################
 
+if(APPLE_IOS)
+	add_subdirectory(ios)
+endif()
+
+include(VCMI_lib)
+if(BUILD_SINGLE_APP)
+	add_subdirectory(lib_client)
+	add_subdirectory(lib_server)
+	set(VCMI_LIB_TARGET vcmi_lib_client)
+else()
+	add_subdirectory(lib)
+	set(VCMI_LIB_TARGET vcmi)
+endif()
+
 if(ENABLE_ERM)
 	add_subdirectory(scripting/erm)
 endif()
@@ -359,16 +436,16 @@ if(NOT TARGET minizip::minizip)
 	add_subdirectory_with_folder("3rdparty" lib/minizip)
 	add_library(minizip::minizip ALIAS minizip)
 endif()
-add_subdirectory(lib)
-add_subdirectory(client)
-add_subdirectory(server)
-add_subdirectory_with_folder("AI" AI)
+
 if(ENABLE_LAUNCHER)
 	add_subdirectory(launcher)
 endif()
 if(ENABLE_EDITOR)
 	add_subdirectory(mapeditor)
 endif()
+add_subdirectory(client)
+add_subdirectory(server)
+add_subdirectory_with_folder("AI" AI)
 if(ENABLE_TEST)
 	enable_testing()
 	add_subdirectory(test)
@@ -379,11 +456,13 @@ endif()
 #######################################
 
 install(DIRECTORY config DESTINATION ${DATA_DIR})
-install(DIRECTORY scripts DESTINATION ${DATA_DIR})
 install(DIRECTORY Mods DESTINATION ${DATA_DIR})
+if(ENABLE_LUA)
+	install(DIRECTORY scripts DESTINATION ${DATA_DIR})
+endif()
 
-# that script is useless for Windows
-if(NOT WIN32)
+# that script is useless for Windows and iOS
+if(NOT WIN32 AND NOT APPLE_IOS)
 	install(FILES vcmibuilder DESTINATION ${BIN_DIR} PERMISSIONS
 		OWNER_WRITE OWNER_READ OWNER_EXECUTE
 					GROUP_READ GROUP_EXECUTE
@@ -491,8 +570,9 @@ if(WIN32)
 	# Use BundleUtilities to fix build when Vcpkg is used and disable it for MXE
 	if(NOT (${CMAKE_CROSSCOMPILING}))
 		add_subdirectory(osx)
+		add_subdirectory(win)
 	endif()
-elseif(APPLE AND NOT ENABLE_MONOLITHIC_INSTALL)
+elseif(APPLE_MACOS AND NOT ENABLE_MONOLITHIC_INSTALL)
 	set(CPACK_MONOLITHIC_INSTALL 1)
 	set(CPACK_GENERATOR "DragNDrop")
 	set(CPACK_DMG_BACKGROUND_IMAGE "${CMAKE_SOURCE_DIR}/osx/dmg_background.png")
@@ -522,6 +602,10 @@ elseif(APPLE AND NOT ENABLE_MONOLITHIC_INSTALL)
 
 	# Bundle fixing code must be in separate directory to be executed after all other install code
 	add_subdirectory(osx)
+elseif(APPLE_IOS)
+	set(CPACK_GENERATOR ZIP)
+	set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY OFF)
+	set(CPACK_INSTALL_CMAKE_PROJECTS "${CMAKE_CURRENT_BINARY_DIR};${CMAKE_PROJECT_NAME};app;/")
 else()
 	set(CPACK_GENERATOR TGZ)
 endif()

+ 62 - 5
CMakePresets.json

@@ -2,19 +2,31 @@
     "version": 2,
     "configurePresets": [
         {
-            "name": "default-release",
+            "name": "release-binary-dir",
+            "hidden": true,
+            "binaryDir": "${sourceDir}/out/build/${presetName}"
+        },
+        {
+            "name": "base-release",
+            "inherits": "release-binary-dir",
             "hidden": true,
-            "binaryDir": "${sourceDir}/out/build/${presetName}",
-            "generator": "Ninja",
             "cacheVariables": {
                 "CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}",
                 "PACKAGE_FILE_NAME" : "$env{VCMI_PACKAGE_FILE_NAME}",
                 "PACKAGE_NAME_SUFFIX" : "$env{VCMI_PACKAGE_NAME_SUFFIX}",
                 "CMAKE_BUILD_TYPE": "RelWithDebInfo",
-                "FORCE_BUNDLED_FL": "OFF",
                 "ENABLE_TEST": "OFF"
             }
         },
+        {
+            "name": "default-release",
+            "inherits": "base-release",
+            "hidden": true,
+            "generator": "Ninja",
+            "cacheVariables": {
+                "FORCE_BUNDLED_FL": "OFF"
+            }
+        },
         {
             "name" : "linux-release",
             "inherits" : "default-release",
@@ -87,6 +99,41 @@
             "description": "VCMI MacOS Xcode",
             "inherits": "default-release",
             "generator": "Xcode"
+        },
+        {
+            "name": "ios-device",
+            "displayName": "Base iOS device",
+            "description": "Base VCMI preset for iOS device",
+            "generator": "Xcode",
+            "binaryDir": "../build-${presetName}",
+            "cacheVariables": {
+                "CMAKE_SYSTEM_NAME": "iOS",
+                "FORCE_BUNDLED_FL": "ON",
+                "FORCE_BUNDLED_MINIZIP": "ON"
+            }
+        },
+        {
+            "name": "ios-simulator",
+            "displayName": "Base iOS simulator",
+            "description": "Base VCMI preset for iOS simulator",
+            "inherits": "ios-device",
+            "cacheVariables": {
+                "CMAKE_OSX_SYSROOT": "iphonesimulator"
+            }
+        },
+        {
+            "name": "ios-release",
+            "displayName": "iOS release",
+            "description": "VCMI iOS release",
+            "inherits": [
+                "base-release",
+                "ios-device",
+                "release-binary-dir"
+            ],
+            "cacheVariables": {
+                "BUNDLE_IDENTIFIER_PREFIX": "eu.vcmi",
+                "CMAKE_PREFIX_PATH": "${sourceDir}/build/iphoneos"
+            }
         }
     ],
     "buildPresets": [
@@ -135,6 +182,16 @@
             "name": "windows-msvc-relwithdebinfo",
             "configurePreset": "windows-msvc-release",
             "inherits": "default-release"
+        },
+        {
+            "name": "ios-release",
+            "configurePreset": "ios-release",
+            "inherits": "default-release",
+            "configuration": "Release",
+            "targets": ["vcmiclient"],
+            "nativeToolOptions": [
+                "CODE_SIGNING_ALLOWED_FOR_APPS=NO"
+            ]
         }
     ],
     "testPresets": [
@@ -172,4 +229,4 @@
             "inherits": "default-release"
         }
     ]
-}
+}

+ 19 - 1
ChangeLog

@@ -1,4 +1,22 @@
-0.99 -> 1.0
+1.0.0 -> 1.1.0
+
+GENERAL:
+* Mods and their versions and serialized into save files. Game checks mod compatibility before loading
+* Logs are stored in system default logs directory
+* LUA/ERM libs are not compiled by default
+* FFMpeg dependency is optional now
+
+MODS:
+* Supported rewardable objects customization
+* Battleground obstacles are extendable now with VLC mechanism
+* Introduced "compatibility" section into mods settings
+
+LAUNCHER:
+* Fixed problem with duplicated mods in the list
+* Launcher shows compatible mods only
+* Uninstall button was moved to the left of layout
+
+0.99 -> 1.0.0
 
 GENERAL:
 * Spectator mode was implemented through command-line options

+ 27 - 5
Global.h

@@ -68,7 +68,7 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 #  define VCMI_UNIX
 #  define VCMI_APPLE
 #  include "TargetConditionals.h"
-#  if TARGET_IPHONE_SIMULATOR
+#  if TARGET_OS_SIMULATOR || TARGET_IPHONE_SIMULATOR
 #    define VCMI_IOS
 #    define VCMI_IOS_SIM
 #  elif TARGET_OS_IPHONE
@@ -82,10 +82,6 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 #  error "VCMI supports only Windows, OSX, Linux and Android targets"
 #endif
 
-#ifdef VCMI_IOS
-#  error "iOS system isn't yet supported."
-#endif
-
 // Each compiler uses own way to supress fall through warning. Try to find it.
 #ifdef __has_cpp_attribute
 #  if __has_cpp_attribute(fallthrough)
@@ -268,11 +264,35 @@ template<typename T, size_t N> char (&_ArrayCountObj(const T (&)[N]))[N];
 // should be used for variables that becomes unused in release builds (e.g. only used for assert checks)
 #define UNUSED(VAR) ((void)VAR)
 
+// old iOS SDKs compatibility
+#ifdef VCMI_IOS
+#include <AvailabilityVersions.h>
+
+#ifndef __IPHONE_13_0
+#define __IPHONE_13_0 130000
+#endif
+#endif // VCMI_IOS
+
+// single-process build makes 2 copies of the main lib by wrapping it in a namespace
+#ifdef VCMI_LIB_NAMESPACE
+#define VCMI_LIB_NAMESPACE_BEGIN namespace VCMI_LIB_NAMESPACE {
+#define VCMI_LIB_NAMESPACE_END }
+#define VCMI_LIB_USING_NAMESPACE using namespace VCMI_LIB_NAMESPACE;
+#define VCMI_LIB_WRAP_NAMESPACE(x) VCMI_LIB_NAMESPACE::x
+#else
+#define VCMI_LIB_NAMESPACE_BEGIN
+#define VCMI_LIB_NAMESPACE_END
+#define VCMI_LIB_USING_NAMESPACE
+#define VCMI_LIB_WRAP_NAMESPACE(x) x
+#endif
+
 /* ---------------------------------------------------------------------------- */
 /* VCMI standard library */
 /* ---------------------------------------------------------------------------- */
 #include <vstd/CLoggerBase.h>
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 void inline handleException()
 {
 	try
@@ -743,3 +763,5 @@ namespace std
 	}
 }
 #endif // NO_STD_TOSTRING
+
+VCMI_LIB_NAMESPACE_END

+ 2 - 0
README.md

@@ -20,6 +20,7 @@ To use VCMI you need to own original data files.
  * [Linux](https://wiki.vcmi.eu/Installation_on_Linux)
  * [macOS](https://wiki.vcmi.eu/Installation_on_macOS)
  * [Windows](https://wiki.vcmi.eu/Installation_on_Windows)
+ * [iOS](https://wiki.vcmi.eu/Installation_on_iOS)
 
 ## Building from source
 
@@ -29,6 +30,7 @@ Platform support is constantly tested by continuous integration and CMake config
  * [On Linux for Windows with MXE](https://wiki.vcmi.eu/How_to_build_VCMI_(Linux/Cmake/MXE))
  * [On macOS](https://wiki.vcmi.eu/How_to_build_VCMI_(macOS))
  * [On Windows using MSVC and Vcpkg](https://wiki.vcmi.eu/How_to_build_VCMI_(Windows/Vcpkg))
+ * [iOS on macOS](https://wiki.vcmi.eu/How_to_build_VCMI_(iOS))
 
 ## Copyright and license
 

+ 4 - 0
Version.cpp.in

@@ -9,7 +9,11 @@
  */
 #include "Version.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 namespace GameConstants
 {
 const char GIT_SHA1[] = "@GIT_SHA1@";
 }
+
+VCMI_LIB_NAMESPACE_END

+ 6 - 0
Version.h

@@ -1,6 +1,12 @@
 #pragma once
 
+#include "StdInc.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
 namespace GameConstants
 {
 extern const char GIT_SHA1[];
 }
+
+VCMI_LIB_NAMESPACE_END

+ 21 - 0
client/CFocusableHelper.cpp

@@ -0,0 +1,21 @@
+/*
+ * CFocusableHelper.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "CFocusableHelper.h"
+#include "../Global.h"
+#include "widgets/TextControls.h"
+
+void removeFocusFromActiveInput()
+{
+    if(CFocusable::inputWithFocus == nullptr)
+        return;
+    CFocusable::inputWithFocus->focus = false;
+    CFocusable::inputWithFocus->redraw();
+    CFocusable::inputWithFocus = nullptr;
+}

+ 11 - 0
client/CFocusableHelper.h

@@ -0,0 +1,11 @@
+/*
+ * CFocusableHelper.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+void removeFocusFromActiveInput();

+ 10 - 6
client/CGameInfo.h

@@ -13,29 +13,33 @@
 
 #include "../lib/ConstTransitivePtr.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CModHandler;
-class CMapHandler;
 class CHeroHandler;
 class CCreatureHandler;
 class CSpellHandler;
 class CSkillHandler;
 class CBuildingHandler;
 class CObjectHandler;
-class CSoundHandler;
-class CMusicHandler;
 class CObjectClassesHandler;
 class CTownHandler;
 class CGeneralTextHandler;
 class CConsoleHandler;
-class CCursorHandler;
 class CGameState;
-class IMainVideoPlayer;
-class CServerHandler;
 class BattleFieldHandler;
 class ObstacleHandler;
 
 class CMap;
 
+VCMI_LIB_NAMESPACE_END
+
+class CMapHandler;
+class CSoundHandler;
+class CMusicHandler;
+class CCursorHandler;
+class IMainVideoPlayer;
+class CServerHandler;
 
 //a class for non-mechanical client GUI classes
 class CClientState

+ 44 - 17
client/CMT.cpp

@@ -159,7 +159,7 @@ static void SDLLogCallback(void*           userdata,
 
 #if defined(VCMI_WINDOWS) && !defined(__GNUC__) && defined(VCMI_WITH_DEBUG_CONSOLE)
 int wmain(int argc, wchar_t* argv[])
-#elif defined(VCMI_ANDROID)
+#elif defined(VCMI_IOS) || defined(VCMI_ANDROID)
 int SDL_main(int argc, char *argv[])
 #else
 int main(int argc, char * argv[])
@@ -170,7 +170,7 @@ int main(int argc, char * argv[])
 	setenv("LANG", "C", 1);
 #endif
 
-#ifndef VCMI_ANDROID
+#if !defined(VCMI_ANDROID) && !defined(VCMI_IOS)
 	// Correct working dir executable folder (not bundle folder) so we can use executable relative paths
 	boost::filesystem::current_path(boost::filesystem::system_complete(argv[0]).parent_path());
 #endif
@@ -217,12 +217,20 @@ int main(int argc, char * argv[])
 	if(vm.count("help"))
 	{
 		prog_help(opts);
+#ifdef VCMI_IOS
+		exit(0);
+#else
 		return 0;
+#endif
 	}
 	if(vm.count("version"))
 	{
 		prog_version();
+#ifdef VCMI_IOS
+		exit(0);
+#else
 		return 0;
+#endif
 	}
 
 	// Init old logging system and new (temporary) logging system
@@ -410,7 +418,7 @@ int main(int argc, char * argv[])
 		CCS->musich->setVolume((ui32)settings["general"]["music"].Float());
 		logGlobal->info("Initializing screen and sound handling: %d ms", pomtime.getDiff());
 	}
-#ifdef __APPLE__
+#ifdef VCMI_MAC
 	// Ctrl+click should be treated as a right click on Mac OS X
 	SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, "1");
 #endif
@@ -1027,11 +1035,15 @@ static bool recreateWindow(int w, int h, int bpp, bool fullscreen, int displayIn
 		if (displayIndex < 0)
 			displayIndex = 0;
 	}
+#ifdef VCMI_IOS
+	SDL_GetWindowSize(mainWindow, &w, &h);
+#else
 	if(!checkVideoMode(displayIndex, w, h))
 	{
 		logGlobal->error("Error: SDL says that %dx%d resolution is not available!", w, h);
 		return false;
 	}
+#endif
 
 	bool bufOnScreen = (screenBuf == screen);
 	bool realFullscreen = settings["video"]["realFullscreen"].Bool();
@@ -1088,26 +1100,40 @@ static bool recreateWindow(int w, int h, int bpp, bool fullscreen, int displayIn
 
 	if(nullptr == mainWindow)
 	{
+#if defined(VCMI_ANDROID) || defined(VCMI_IOS)
+		auto createWindow = [displayIndex](Uint32 extraFlags) -> bool {
+			mainWindow = SDL_CreateWindow(NAME.c_str(), SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex), SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex), 0, 0, SDL_WINDOW_FULLSCREEN | extraFlags);
+			return mainWindow != nullptr;
+		};
 
-	#ifdef VCMI_ANDROID
-		mainWindow = SDL_CreateWindow(NAME.c_str(), SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex),SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex), 0, 0, SDL_WINDOW_FULLSCREEN);
+# ifdef VCMI_IOS
+		SDL_SetHint(SDL_HINT_IOS_HIDE_HOME_INDICATOR, "1");
+		SDL_SetHint(SDL_HINT_RETURN_KEY_HIDES_IME, "1");
+		SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best");
 
-		// SDL on Android doesn't do proper letterboxing, and will show an annoying flickering in the blank space in case you're not using the full screen estate
+		Uint32 windowFlags = SDL_WINDOW_BORDERLESS | SDL_WINDOW_ALLOW_HIGHDPI;
+		if(!createWindow(windowFlags | SDL_WINDOW_METAL))
+		{
+			logGlobal->warn("Metal unavailable, using OpenGLES");
+			createWindow(windowFlags);
+		}
+# else
+		createWindow(0);
+# endif // VCMI_IOS
+
+		// SDL on mobile doesn't do proper letterboxing, and will show an annoying flickering in the blank space in case you're not using the full screen estate
 		// That's why we need to make sure our width and height we'll use below have the same aspect ratio as the screen itself to ensure we fill the full screen estate
 
 		SDL_Rect screenRect;
 
 		if(SDL_GetDisplayBounds(0, &screenRect) == 0)
 		{
-			int screenWidth, screenHeight;
-			double aspect;
+			const auto screenWidth = screenRect.w;
+			const auto screenHeight = screenRect.h;
 
-			screenWidth = screenRect.w;
-			screenHeight = screenRect.h;
+			const auto aspect = static_cast<double>(screenWidth) / screenHeight;
 
-			aspect = (double)screenWidth / (double)screenHeight;
-
-			logGlobal->info("Screen size and aspect ration: %dx%d (%lf)", screenWidth, screenHeight, aspect);
+			logGlobal->info("Screen size and aspect ratio: %dx%d (%lf)", screenWidth, screenHeight, aspect);
 
 			if((double)w / aspect > (double)h)
 			{
@@ -1124,8 +1150,7 @@ static bool recreateWindow(int w, int h, int bpp, bool fullscreen, int displayIn
 		{
 			logGlobal->error("Can't fix aspect ratio for screen");
 		}
-	#else
-
+#else
 		if(fullscreen)
 		{
 			if(realFullscreen)
@@ -1138,7 +1163,7 @@ static bool recreateWindow(int w, int h, int bpp, bool fullscreen, int displayIn
 		{
 			mainWindow = SDL_CreateWindow(NAME.c_str(), SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex),SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex), w, h, 0);
 		}
-	#endif
+#endif // defined(VCMI_ANDROID) || defined(VCMI_IOS)
 
 		if(nullptr == mainWindow)
 		{
@@ -1161,7 +1186,7 @@ static bool recreateWindow(int w, int h, int bpp, bool fullscreen, int displayIn
 	}
 	else
 	{
-#ifndef VCMI_ANDROID
+#if !defined(VCMI_ANDROID) && !defined(VCMI_IOS)
 
 		if(fullscreen)
 		{
@@ -1384,7 +1409,9 @@ static void handleEvent(SDL_Event & ev)
 	{
 		switch (ev.window.event) {
 		case SDL_WINDOWEVENT_RESTORED:
+#ifndef VCMI_IOS
 			fullScreenChanged();
+#endif
 			break;
 		}
 		return;

+ 76 - 3
client/CMakeLists.txt

@@ -145,6 +145,22 @@ set(client_HEADERS
 		SDLRWwrapper.h
 )
 
+if(APPLE_IOS)
+	set(client_SRCS ${client_SRCS}
+		CFocusableHelper.cpp
+		ios/GameChatKeyboardHanlder.m
+		ios/main.m
+		ios/startSDL.mm
+		ios/utils.mm
+	)
+	set(client_HEADERS ${client_HEADERS}
+		CFocusableHelper.h
+		ios/GameChatKeyboardHanlder.h
+		ios/startSDL.h
+		ios/utils.h
+	)
+endif()
+
 assign_source_group(${client_SRCS} ${client_HEADERS} VCMI_client.rc)
 
 if(ANDROID) # android needs client/server to be libraries, not executables, so we can't reuse the build part of this script
@@ -160,7 +176,16 @@ if(ENABLE_DEBUG_CONSOLE)
 else()
 	add_executable(vcmiclient WIN32 ${client_SRCS} ${client_HEADERS} ${client_ICON})
 endif(ENABLE_DEBUG_CONSOLE)
+
 add_dependencies(vcmiclient vcmiserver BattleAI StupidAI VCAI Nullkiller)
+if(APPLE_IOS)
+	if(ENABLE_ERM)
+		add_dependencies(vcmiclient vcmiERM)
+	endif()
+	if(ENABLE_LUA)
+		add_dependencies(vcmiclient vcmiLua)
+	endif()
+endif()
 
 if(WIN32)
 	set_target_properties(vcmiclient
@@ -173,15 +198,55 @@ if(WIN32)
 		target_link_libraries(vcmiclient SDL2::SDL2main)
 	endif()
 	target_compile_definitions(vcmiclient PRIVATE WINDOWS_IGNORE_PACKING_MISMATCH)
+elseif(APPLE_IOS)
+	target_link_libraries(vcmiclient PRIVATE
+		iOS_utils
+
+		# FFmpeg
+		bz2
+		iconv
+		z
+		"-framework AudioToolbox"
+		"-framework AVFoundation"
+		"-framework CoreMedia"
+		"-framework VideoToolbox"
+	)
+
+	set_target_properties(vcmiclient PROPERTIES
+		MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_LIST_DIR}/ios/Info.plist"
+		XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/Frameworks"
+		XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "$(CODE_SIGNING_ALLOWED_FOR_APPS)"
+		XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME AppIcon
+	)
+
+	foreach(XCODE_RESOURCE LaunchScreen.storyboard Images.xcassets Settings.bundle vcmi_logo.png)
+		set(XCODE_RESOURCE_PATH ios/${XCODE_RESOURCE})
+		target_sources(vcmiclient PRIVATE ${XCODE_RESOURCE_PATH})
+		set_source_files_properties(${XCODE_RESOURCE_PATH} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
+
+		# workaround to prevent CMAKE_SKIP_PRECOMPILE_HEADERS being added as compile flag
+		# add max version condition when https://gitlab.kitware.com/cmake/cmake/-/merge_requests/7562 is merged
+		if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.22.0")
+			set_source_files_properties(${XCODE_RESOURCE_PATH} PROPERTIES LANGUAGE CXX)
+		endif()
+	endforeach()
+
+	set(CMAKE_EXE_LINKER_FLAGS "-Wl,-e,_client_main")
 endif()
 
+if(BUILD_SINGLE_APP)
+	target_link_libraries(vcmiclient PRIVATE vcmiserver)
+	if(ENABLE_LAUNCHER)
+		target_link_libraries(vcmiclient PRIVATE vcmilauncher)
+	endif()
+endif()
 target_link_libraries(vcmiclient PRIVATE
-		vcmi SDL2::SDL2 SDL2::Image SDL2::Mixer SDL2::TTF
+		${VCMI_LIB_TARGET} SDL2::SDL2 SDL2::Image SDL2::Mixer SDL2::TTF
 )
 
 if(ffmpeg_LIBRARIES)
 	target_link_libraries(vcmiclient PRIVATE
-		ffmpeg::swscale ffmpeg::avutil ffmpeg::avcodec ffmpeg::avformat
+		${ffmpeg_LIBRARIES}
 	)
 else()
 	target_compile_definitions(vcmiclient PRIVATE DISABLE_VIDEO)
@@ -193,7 +258,15 @@ target_include_directories(vcmiclient
 vcmi_set_output_dir(vcmiclient "")
 enable_pch(vcmiclient)
 
-install(TARGETS vcmiclient DESTINATION ${BIN_DIR})
+if(APPLE_IOS)
+	add_custom_command(TARGET vcmiclient POST_BUILD
+		COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" --component "${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}" --config "$<CONFIG>" --prefix "$<TARGET_BUNDLE_CONTENT_DIR:vcmiclient>"
+		COMMAND ${CMAKE_SOURCE_DIR}/ios/codesign.sh
+	)
+	install(TARGETS vcmiclient DESTINATION Payload COMPONENT app) # for ipa generation with cpack
+else()
+	install(TARGETS vcmiclient DESTINATION ${BIN_DIR})
+endif()
 
 #install icons and desktop file on Linux
 if(NOT WIN32 AND NOT APPLE)

+ 18 - 13
client/CPlayerInterface.h

@@ -19,30 +19,33 @@
 #define sprintf_s snprintf
 #endif
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 class Artifact;
 
-class CButton;
-class CToggleGroup;
 struct TryMoveHero;
 class CGHeroInstance;
-class CAdvMapInt;
-class CCastleInterface;
-class CBattleInterface;
 class CStack;
-class CComponent;
 class CCreature;
-struct SDL_Surface;
 struct CGPath;
-class CCreatureAnimation;
-class CSelectableComponent;
 class CCreatureSet;
 class CGObjectInstance;
-class CSlider;
 struct UpgradeInfo;
 template <typename T> struct CondSh;
+struct CPathsInfo;
+
+VCMI_LIB_NAMESPACE_END
+
+class CButton;
+class CToggleGroup;
+class CAdvMapInt;
+class CCastleInterface;
+class CBattleInterface;
+class CComponent;
+class CCreatureAnimation;
+class CSelectableComponent;
+class CSlider;
 class CInGameConsole;
-class CInGameConsole;
-union SDL_Event;
 class CInfoWindow;
 class IShowActivatable;
 class ClickableL;
@@ -52,7 +55,9 @@ class KeyInterested;
 class MotionInterested;
 class TimeInterested;
 class IShowable;
-struct CPathsInfo;
+
+struct SDL_Surface;
+union SDL_Event;
 
 namespace boost
 {

+ 68 - 14
client/CServerHandler.cpp

@@ -17,13 +17,19 @@
 
 #include "lobby/CSelectionBase.h"
 #include "lobby/CLobbyScreen.h"
+#include "windows/InfoWindows.h"
 
 #include "mainmenu/CMainMenu.h"
 
-#ifndef VCMI_ANDROID
-#include "../lib/Interprocess.h"
-#else
+#ifdef VCMI_ANDROID
 #include "../lib/CAndroidVMHelper.h"
+#elif defined(VCMI_IOS)
+#include "ios/utils.h"
+#include "../server/CVCMIServer.h"
+
+#include <dispatch/dispatch.h>
+#else
+#include "../lib/Interprocess.h"
 #endif
 #include "../lib/CConfigHandler.h"
 #include "../lib/CGeneralTextHandler.h"
@@ -75,7 +81,7 @@ public:
 	bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const override
 	{
 		T * ptr = static_cast<T *>(pack);
-		logNetwork->trace("\tImmidiately apply on lobby: %s", typeList.getTypeInfo(ptr)->name());
+		logNetwork->trace("\tImmediately apply on lobby: %s", typeList.getTypeInfo(ptr)->name());
 		return ptr->applyOnLobbyHandler(handler);
 	}
 
@@ -131,7 +137,7 @@ void CServerHandler::resetStateForLobby(const StartInfo::EMode mode, const std::
 	else
 		myNames.push_back(settings["general"]["playerName"].String());
 
-#ifndef VCMI_ANDROID
+#if !defined(VCMI_ANDROID) && !defined(VCMI_IOS)
 	shm.reset();
 
 	if(!settings["session"]["disable-shm"].Bool())
@@ -161,11 +167,33 @@ void CServerHandler::startLocalServerAndConnect()
 		threadRunLocalServer->join();
 
 	th->update();
+	
+	auto errorMsg = CGI->generaltexth->localizedTexts["server"]["errors"]["existingProcess"].String();
+	try
+	{
+		CConnection testConnection(settings["server"]["server"].String(), getDefaultPort(), NAME, uuid);
+		logNetwork->error("Port is busy, check if another instance of vcmiserver is working");
+		CInfoWindow::showInfoDialog(errorMsg, {});
+		return;
+	}
+	catch(...)
+	{
+		//no connection means that port is not busy and we can start local server
+	}
+	
 #ifdef VCMI_ANDROID
 	{
 		CAndroidVMHelper envHelper;
 		envHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "startServer", true);
 	}
+#elif defined(SINGLE_PROCESS_APP)
+	boost::condition_variable cond;
+	threadRunLocalServer = std::make_shared<boost::thread>([&cond, this] {
+		setThreadName("CVCMIServer");
+		CVCMIServer::create(&cond, uuid);
+		onServerFinished();
+	});
+	threadRunLocalServer->detach();
 #else
 	threadRunLocalServer = std::make_shared<boost::thread>(&CServerHandler::threadRunServer, this); //runs server executable;
 #endif
@@ -173,10 +201,7 @@ void CServerHandler::startLocalServerAndConnect()
 
 	th->update();
 
-#ifndef VCMI_ANDROID
-	if(shm)
-		shm->sr->waitTillReady();
-#else
+#ifdef VCMI_ANDROID
 	logNetwork->info("waiting for server");
 	while(!androidTestServerReadyFlag.load())
 	{
@@ -185,16 +210,40 @@ void CServerHandler::startLocalServerAndConnect()
 	}
 	logNetwork->info("waiting for server finished...");
 	androidTestServerReadyFlag = false;
+#elif defined(SINGLE_PROCESS_APP)
+	{
+#ifdef VCMI_IOS
+		dispatch_sync(dispatch_get_main_queue(), ^{
+			iOS_utils::showLoadingIndicator();
+		});
+#endif
+
+		boost::mutex m;
+		boost::unique_lock<boost::mutex> lock{m};
+		logNetwork->info("waiting for server");
+		cond.wait(lock);
+		logNetwork->info("server is ready");
+
+#ifdef VCMI_IOS
+		dispatch_sync(dispatch_get_main_queue(), ^{
+			iOS_utils::hideLoadingIndicator();
+		});
+#endif
+	}
+#else
+	if(shm)
+		shm->sr->waitTillReady();
 #endif
 	logNetwork->trace("Waiting for server: %d ms", th->getDiff());
 
 	th->update(); //put breakpoint here to attach to server before it does something stupid
 
-#ifndef VCMI_ANDROID
-	justConnectToServer(settings["server"]["server"].String(), shm ? shm->sr->port : 0);
+#if !defined(VCMI_ANDROID) && !defined(VCMI_IOS)
+	const ui16 port = shm ? shm->sr->port : 0;
 #else
-	justConnectToServer(settings["server"]["server"].String());
+	const ui16 port = 0;
 #endif
+	justConnectToServer(settings["server"]["server"].String(), port);
 
 	logNetwork->trace("\tConnecting to the server: %d ms", th->getDiff());
 }
@@ -683,7 +732,7 @@ void CServerHandler::threadHandleConnection()
 
 void CServerHandler::threadRunServer()
 {
-#ifndef VCMI_ANDROID
+#if !defined(VCMI_ANDROID) && !defined(VCMI_IOS)
 	setThreadName("CServerHandler::threadRunServer");
 	const std::string logName = (VCMIDirs::get().userLogsPath() / "server_log.txt").string();
 	std::string comm = VCMIDirs::get().serverPath().string()
@@ -724,9 +773,14 @@ void CServerHandler::threadRunServer()
 		logNetwork->error("Error: server failed to close correctly or crashed!");
 		logNetwork->error("Check %s for more info", logName);
 	}
+	onServerFinished();
+#endif
+}
+
+void CServerHandler::onServerFinished()
+{
 	threadRunLocalServer.reset();
 	CSH->campaignServerRestartLock.setn(false);
-#endif
 }
 
 void CServerHandler::sendLobbyPack(const CPackForLobby & pack) const

+ 10 - 2
client/CServerHandler.h

@@ -14,7 +14,8 @@
 #include "../lib/StartInfo.h"
 #include "../lib/CondSh.h"
 
-struct SharedMemory;
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CConnection;
 class PlayerColor;
 struct StartInfo;
@@ -23,9 +24,15 @@ class CMapInfo;
 struct ClientPlayer;
 struct CPack;
 struct CPackForLobby;
-class CClient;
 
 template<typename T> class CApplier;
+
+VCMI_LIB_NAMESPACE_END
+
+struct SharedMemory;
+
+class CClient;
+
 class CBaseForLobbyApply;
 
 // TODO: Add mutex so we can't set CONNECTION_CANCELLED if client already connected, but thread not setup yet
@@ -76,6 +83,7 @@ class CServerHandler : public IServerAPI, public LobbyInfo
 
 	void threadHandleConnection();
 	void threadRunServer();
+	void onServerFinished();
 	void sendLobbyPack(const CPackForLobby & pack) const override;
 
 public:

+ 11 - 5
client/Client.h

@@ -19,25 +19,22 @@
 #include "../lib/CondSh.h"
 #include "../lib/CPathfinder.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 struct CPack;
 struct CPackForServer;
 class CCampaignState;
-class CBattleCallback;
 class IGameEventsReceiver;
 class IBattleEventsReceiver;
 class CBattleGameInterface;
 class CGameState;
 class CGameInterface;
-class CCallback;
 class BattleAction;
-class CClient;
 struct CPathsInfo;
 class BinaryDeserializer;
 class BinarySerializer;
-namespace boost { class thread; }
 
 template<typename T> class CApplier;
-class CBaseForCLApply;
 
 #if SCRIPTING_ENABLED
 namespace scripting
@@ -51,6 +48,15 @@ namespace events
 	class EventBus;
 }
 
+VCMI_LIB_NAMESPACE_END
+
+class CBattleCallback;
+class CCallback;
+class CClient;
+class CBaseForCLApply;
+
+namespace boost { class thread; }
+
 template<typename T>
 class ThreadSafeVector
 {

+ 8 - 3
client/Graphics.h

@@ -13,18 +13,23 @@
 #include "../lib/GameConstants.h"
 #include "gui/Geometries.h"
 
-struct SDL_Surface;
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CGHeroInstance;
 class CGTownInstance;
 class CHeroClass;
-struct SDL_Color;
 struct InfoAboutHero;
 struct InfoAboutTown;
 class CGObjectInstance;
 class ObjectTemplate;
-class CAnimation;
 class EntityService;
 
+VCMI_LIB_NAMESPACE_END
+
+struct SDL_Surface;
+struct SDL_Color;
+class CAnimation;
+
 enum EFonts
 {
 	FONT_BIG, FONT_CALLI, FONT_CREDITS, FONT_HIGH_SCORE, FONT_MEDIUM, FONT_SMALL, FONT_TIMES, FONT_TINY, FONT_VERD

+ 6 - 1
client/SDLRWwrapper.h

@@ -9,7 +9,12 @@
  */
 #pragma once
 
-struct SDL_RWops;
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CInputStream;
 
+VCMI_LIB_NAMESPACE_END
+
+struct SDL_RWops;
+
 SDL_RWops* MakeSDLRWops(std::unique_ptr<CInputStream> in);

+ 2 - 0
client/StdInc.h

@@ -7,3 +7,5 @@
 // This header should be treated as a pre compiled header file(PCH) in the compiler building settings.
 
 // Here you can add specific libraries and macros which are specific to this project.
+
+VCMI_LIB_USING_NAMESPACE

+ 6 - 1
client/battle/CBattleAnimations.h

@@ -12,8 +12,13 @@
 #include "../../lib/battle/BattleHex.h"
 #include "../widgets/Images.h"
 
-class CBattleInterface;
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CStack;
+
+VCMI_LIB_NAMESPACE_END
+
+class CBattleInterface;
 class CCreatureAnimation;
 struct CatapultProjectileInfo;
 struct StackAttackedInfo;

+ 1 - 2
client/battle/CBattleInterface.cpp

@@ -1083,11 +1083,10 @@ void CBattleInterface::stacksAreAttacked(std::vector<StackAttackedInfo> attacked
 
 	std::array<int, 2> killedBySide = {0, 0};
 
-	int targets = 0, damage = 0;
+	int targets = 0;
 	for(const StackAttackedInfo & attackedInfo : attackedInfos)
 	{
 		++targets;
-		damage += (int)attackedInfo.dmg;
 
 		ui8 side = attackedInfo.defender->side;
 		killedBySide.at(side) += attackedInfo.amountKilled;

+ 16 - 11
client/battle/CBattleInterface.h

@@ -19,14 +19,11 @@
 #include "../../lib/spells/CSpellHandler.h" //CSpell::TAnimation
 #include "../../lib/battle/CBattleInfoCallback.h"
 
-class CLabel;
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CCreatureSet;
 class CGHeroInstance;
 class CStack;
-class CCallback;
-class CButton;
-class CToggleButton;
-class CToggleGroup;
 struct BattleResult;
 struct BattleSpellCast;
 struct CObstacleInstance;
@@ -35,8 +32,21 @@ struct SetStackEffect;
 class BattleAction;
 class CGTownInstance;
 struct CatapultAttack;
-struct CatapultProjectileInfo;
 struct BattleTriggerEffect;
+struct BattleHex;
+struct InfoAboutHero;
+class CBattleGameInterface;
+struct CustomEffectInfo;
+class CSpell;
+
+VCMI_LIB_NAMESPACE_END
+
+class CLabel;
+class CCallback;
+class CButton;
+class CToggleButton;
+class CToggleGroup;
+struct CatapultProjectileInfo;
 class CBattleAnimation;
 class CBattleHero;
 class CBattleConsole;
@@ -46,13 +56,8 @@ class CPlayerInterface;
 class CCreatureAnimation;
 struct ProjectileInfo;
 class CClickableHex;
-struct BattleHex;
-struct InfoAboutHero;
-class CBattleGameInterface;
-struct CustomEffectInfo;
 class CAnimation;
 class IImage;
-class CSpell;
 
 /// Small struct which contains information about the id of the attacked stack, the damage dealt,...
 struct StackAttackedInfo

+ 13 - 7
client/battle/CBattleInterfaceClasses.h

@@ -13,8 +13,20 @@
 #include "../../lib/battle/BattleHex.h"
 #include "../windows/CWindowObject.h"
 
-struct SDL_Surface;
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CGHeroInstance;
+struct BattleResult;
+class CStack;
+
+namespace battle
+{
+class Unit;
+}
+
+VCMI_LIB_NAMESPACE_END
+
+struct SDL_Surface;
 class CBattleInterface;
 class CPicture;
 class CFilledTexture;
@@ -23,12 +35,6 @@ class CToggleButton;
 class CToggleGroup;
 class CLabel;
 class CTextBox;
-struct BattleResult;
-class CStack;
-namespace battle
-{
-	class Unit;
-}
 class CAnimImage;
 class CPlayerInterface;
 

+ 3 - 2
client/gui/CAnimation.cpp

@@ -10,10 +10,11 @@
 #include "StdInc.h"
 #include "CAnimation.h"
 
+#include "SDL_Extensions.h"
+#include "SDL_Pixels.h"
+
 #include "../CBitmapHandler.h"
 #include "../Graphics.h"
-#include "../gui/SDL_Extensions.h"
-#include "../gui/SDL_Pixels.h"
 
 #include "../lib/filesystem/Filesystem.h"
 #include "../lib/filesystem/ISimpleResourceLoader.h"

+ 6 - 1
client/gui/CAnimation.h

@@ -21,8 +21,13 @@
 #undef OUT
 #endif
 
-struct SDL_Surface;
+VCMI_LIB_NAMESPACE_BEGIN
+
 class JsonNode;
+
+VCMI_LIB_NAMESPACE_END
+
+struct SDL_Surface;
 class CDefFile;
 class ColorShifter;
 

+ 6 - 1
client/gui/CGuiHandler.h

@@ -13,6 +13,12 @@
 #include "Geometries.h"
 #include "SDL_Extensions.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
+template <typename T> struct CondSh;
+
+VCMI_LIB_NAMESPACE_END
+
 class CFramerateManager;
 class CGStatusBar;
 class CIntObject;
@@ -20,7 +26,6 @@ class IUpdateable;
 class IShowActivatable;
 class IShowable;
 enum class EIntObjMouseBtnType;
-template <typename T> struct CondSh;
 
 // TODO: event handling need refactoring
 enum EUserEvent

+ 1 - 0
client/gui/CIntObject.h

@@ -140,6 +140,7 @@ public:
 	//double click
 	virtual void onDoubleClick(){}
 
+	// These are the arguments that can be used to determine what kind of input the CIntObject will receive
 	enum {LCLICK=1, RCLICK=2, HOVER=4, MOVE=8, KEYBOARD=16, TIME=32, GENERAL=64, WHEEL=128, DOUBLECLICK=256, TEXTINPUT=512, MCLICK=1024, ALL=0xffff};
 	const ui16 & active;
 	void addUsedEvents(ui16 newActions);

+ 4 - 0
client/gui/Fonts.h

@@ -9,8 +9,12 @@
  */
 #pragma once
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 class JsonNode;
 
+VCMI_LIB_NAMESPACE_END
+
 struct Point;
 struct SDL_Surface;
 struct SDL_Color;

+ 31 - 5
client/gui/SDL_Extensions.cpp

@@ -20,6 +20,10 @@
 #include <dispatch/dispatch.h>
 #endif
 
+#ifdef VCMI_IOS
+#include "ios/utils.h"
+#endif
+
 const SDL_Color Colors::YELLOW = { 229, 215, 123, 0 };
 const SDL_Color Colors::WHITE = { 255, 243, 222, 0 };
 const SDL_Color Colors::METALLIC_GOLD = { 173, 142, 66, 0 };
@@ -790,15 +794,37 @@ SDL_Color CSDL_Ext::makeColor(ui8 r, ui8 g, ui8 b, ui8 a)
 
 void CSDL_Ext::startTextInput(SDL_Rect * where)
 {
+	auto impl = [](SDL_Rect * where)
+	{
+		if (SDL_IsTextInputActive() == SDL_FALSE)
+		{
+			SDL_StartTextInput();
+		}
+		SDL_SetTextInputRect(where);
+	};
+
 #ifdef VCMI_APPLE
 	dispatch_async(dispatch_get_main_queue(), ^{
 #endif
 
-	if (SDL_IsTextInputActive() == SDL_FALSE)
-	{
-		SDL_StartTextInput();
-	}
-	SDL_SetTextInputRect(where);
+#ifdef VCMI_IOS
+	// TODO ios: looks like SDL bug actually, try fixing there
+	auto renderer = SDL_GetRenderer(mainWindow);
+	float scaleX, scaleY;
+	SDL_Rect viewport;
+	SDL_RenderGetScale(renderer, &scaleX, &scaleY);
+	SDL_RenderGetViewport(renderer, &viewport);
+
+	const auto nativeScale = iOS_utils::screenScale();
+	auto rectInScreenCoordinates = *where;
+	rectInScreenCoordinates.x = (viewport.x + rectInScreenCoordinates.x) * scaleX / nativeScale;
+	rectInScreenCoordinates.y = (viewport.y + rectInScreenCoordinates.y) * scaleY / nativeScale;
+	rectInScreenCoordinates.w = rectInScreenCoordinates.w * scaleX / nativeScale;
+	rectInScreenCoordinates.h = rectInScreenCoordinates.h * scaleY / nativeScale;
+	impl(&rectInScreenCoordinates);
+#else
+	impl(where);
+#endif
 
 #ifdef VCMI_APPLE
 	});

+ 1 - 0
client/gui/SDL_Extensions.h

@@ -155,6 +155,7 @@ typedef void (*BlitterWithRotationVal)(SDL_Surface *src,SDL_Rect srcRect, SDL_Su
 class ColorShifter
 {
 public:
+	virtual ~ColorShifter() = default;
 	virtual SDL_Color shiftColor(SDL_Color clr) const = 0;
 };
 

+ 23 - 0
client/ios/GameChatKeyboardHanlder.h

@@ -0,0 +1,23 @@
+/*
+ * GameChatKeyboardHanlder.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface GameChatKeyboardHanlder : NSObject
+
+@property (nonatomic, weak) UITextField * textFieldSDL;
+
+- (void)triggerInput;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 107 - 0
client/ios/GameChatKeyboardHanlder.m

@@ -0,0 +1,107 @@
+/*
+ * GameChatKeyboardHanlder.m, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#import "GameChatKeyboardHanlder.h"
+
+#include <SDL_events.h>
+
+static int watchReturnKey(void * userdata, SDL_Event * event);
+
+static void sendKeyEvent(SDL_KeyCode keyCode)
+{
+	SDL_Event keyEvent;
+	keyEvent.key = (SDL_KeyboardEvent){
+		.type = SDL_KEYDOWN,
+		.keysym.sym = keyCode,
+	};
+	SDL_PushEvent(&keyEvent);
+}
+
+static CGRect keyboardFrame(NSNotification * n, NSString * userInfoKey)
+{
+	return [n.userInfo[userInfoKey] CGRectValue];
+}
+static CGRect keyboardFrameBegin(NSNotification * n) { return keyboardFrame(n, UIKeyboardFrameBeginUserInfoKey); }
+static CGRect keyboardFrameEnd  (NSNotification * n) { return keyboardFrame(n, UIKeyboardFrameEndUserInfoKey); }
+
+
+@interface GameChatKeyboardHanlder ()
+@property (nonatomic) BOOL wasChatMessageSent;
+@end
+
+@implementation GameChatKeyboardHanlder
+
+- (void)triggerInput {
+	__auto_type notificationCenter = NSNotificationCenter.defaultCenter;
+	[notificationCenter addObserver:self selector:@selector(textDidBeginEditing:) name:UITextFieldTextDidBeginEditingNotification object:nil];
+	[notificationCenter addObserver:self selector:@selector(textDidEndEditing:) name:UITextFieldTextDidEndEditingNotification object:nil];
+	[notificationCenter addObserver:self selector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil];
+	[notificationCenter addObserver:self selector:@selector(keyboardDidChangeFrame:) name:UIKeyboardDidChangeFrameNotification object:nil];
+
+	self.wasChatMessageSent = NO;
+	sendKeyEvent(SDLK_TAB);
+}
+
+- (void)positionTextFieldAboveKeyboardRect:(CGRect)kbFrame {
+	__auto_type r = kbFrame;
+	r.size.height = CGRectGetHeight(self.textFieldSDL.frame);
+	r.origin.y -= r.size.height;
+	self.textFieldSDL.frame = r;
+}
+
+#pragma mark - Notifications
+
+- (void)textDidBeginEditing:(NSNotification *)n {
+	self.textFieldSDL.hidden = NO;
+	self.textFieldSDL.text = nil;
+
+	// watch for pressing Return to ignore sending Escape key after keyboard is closed
+	SDL_AddEventWatch(watchReturnKey, (__bridge void *)self);
+}
+
+- (void)textDidEndEditing:(NSNotification *)n {
+	[NSNotificationCenter.defaultCenter removeObserver:self];
+	self.textFieldSDL.hidden = YES;
+
+	// discard chat message
+	if(!self.wasChatMessageSent)
+		sendKeyEvent(SDLK_ESCAPE);
+}
+
+- (void)keyboardWillChangeFrame:(NSNotification *)n {
+	// animate textfield together with keyboard
+	[UIView performWithoutAnimation:^{
+		[self positionTextFieldAboveKeyboardRect:keyboardFrameBegin(n)];
+	}];
+
+	NSTimeInterval kbAnimationDuration = [n.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
+	NSUInteger kbAnimationCurve = [n.userInfo[UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue];
+	[UIView animateWithDuration:kbAnimationDuration delay:0 options:(kbAnimationCurve << 16) animations:^{
+		[self positionTextFieldAboveKeyboardRect:keyboardFrameEnd(n)];
+	} completion:nil];
+}
+
+- (void)keyboardDidChangeFrame:(NSNotification *)n {
+	[self positionTextFieldAboveKeyboardRect:keyboardFrameEnd(n)];
+}
+
+@end
+
+
+static int watchReturnKey(void * userdata, SDL_Event * event)
+{
+	if(event->type == SDL_KEYDOWN && event->key.keysym.scancode == SDL_SCANCODE_RETURN)
+	{
+		__auto_type self = (__bridge GameChatKeyboardHanlder *)userdata;
+		self.wasChatMessageSent = YES;
+		SDL_DelEventWatch(watchReturnKey, userdata);
+	}
+	return 1;
+}

+ 121 - 0
client/ios/Images.xcassets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,121 @@
+{
+  "images" : [
+    {
+      "filename" : "[email protected]",
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "size" : "20x20"
+    },
+    {
+      "filename" : "[email protected]",
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "size" : "20x20"
+    },
+    {
+      "filename" : "[email protected]",
+      "idiom" : "iphone",
+      "scale" : "1x",
+      "size" : "29x29"
+    },
+    {
+      "filename" : "[email protected]",
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "size" : "29x29"
+    },
+    {
+      "filename" : "[email protected]",
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "size" : "29x29"
+    },
+    {
+      "filename" : "[email protected]",
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "size" : "40x40"
+    },
+    {
+      "filename" : "[email protected]",
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "size" : "40x40"
+    },
+    {
+      "filename" : "[email protected]",
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "size" : "60x60"
+    },
+    {
+      "filename" : "[email protected]",
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "size" : "60x60"
+    },
+    {
+      "filename" : "[email protected]",
+      "idiom" : "ipad",
+      "scale" : "1x",
+      "size" : "20x20"
+    },
+    {
+      "filename" : "[email protected]",
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "20x20"
+    },
+    {
+      "filename" : "[email protected]",
+      "idiom" : "ipad",
+      "scale" : "1x",
+      "size" : "29x29"
+    },
+    {
+      "filename" : "[email protected]",
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "29x29"
+    },
+    {
+      "filename" : "[email protected]",
+      "idiom" : "ipad",
+      "scale" : "1x",
+      "size" : "40x40"
+    },
+    {
+      "filename" : "[email protected]",
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "40x40"
+    },
+    {
+      "filename" : "[email protected]",
+      "idiom" : "ipad",
+      "scale" : "1x",
+      "size" : "76x76"
+    },
+    {
+      "filename" : "[email protected]",
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "76x76"
+    },
+    {
+      "filename" : "[email protected]",
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "83.5x83.5"
+    },
+    {
+      "idiom" : "ios-marketing",
+      "scale" : "1x",
+      "size" : "1024x1024"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN=BIN
client/ios/Images.xcassets/AppIcon.appiconset/[email protected]


BIN=BIN
client/ios/Images.xcassets/AppIcon.appiconset/[email protected]


BIN=BIN
client/ios/Images.xcassets/AppIcon.appiconset/[email protected]


BIN=BIN
client/ios/Images.xcassets/AppIcon.appiconset/[email protected]


BIN=BIN
client/ios/Images.xcassets/AppIcon.appiconset/[email protected]


BIN=BIN
client/ios/Images.xcassets/AppIcon.appiconset/[email protected]


BIN=BIN
client/ios/Images.xcassets/AppIcon.appiconset/[email protected]


BIN=BIN
client/ios/Images.xcassets/AppIcon.appiconset/[email protected]


BIN=BIN
client/ios/Images.xcassets/AppIcon.appiconset/[email protected]


BIN=BIN
client/ios/Images.xcassets/AppIcon.appiconset/[email protected]


BIN=BIN
client/ios/Images.xcassets/AppIcon.appiconset/[email protected]


BIN=BIN
client/ios/Images.xcassets/AppIcon.appiconset/[email protected]


BIN=BIN
client/ios/Images.xcassets/AppIcon.appiconset/[email protected]


BIN=BIN
client/ios/Images.xcassets/AppIcon.appiconset/[email protected]


+ 6 - 0
client/ios/Images.xcassets/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 60 - 0
client/ios/Info.plist

@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>VCMI</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>$(MARKETING_VERSION)</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleURLTypes</key>
+	<array>
+		<dict>
+			<key>CFBundleURLSchemes</key>
+			<array>
+				<string>vcmi</string>
+			</array>
+		</dict>
+	</array>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+	<key>LSRequiresIPhoneOS</key>
+	<true/>
+	<key>NSAppTransportSecurity</key>
+	<dict>
+		<key>NSAllowsArbitraryLoads</key>
+		<true/>
+	</dict>
+	<key>UIApplicationSupportsIndirectInputEvents</key>
+	<true/>
+	<key>UIFileSharingEnabled</key>
+	<true/>
+	<key>UILaunchStoryboardName</key>
+	<string>LaunchScreen</string>
+	<key>UIRequiredDeviceCapabilities</key>
+	<array>
+		<string>armv7</string>
+		<string>opengles-2</string>
+	</array>
+	<key>UIRequiresFullScreen</key>
+	<true/>
+	<key>UIStatusBarHidden</key>
+	<true/>
+	<key>UISupportedInterfaceOrientations</key>
+	<array>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+</dict>
+</plist>

+ 39 - 0
client/ios/LaunchScreen.storyboard

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+    <device id="retina4_7" orientation="portrait" appearance="light"/>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
+        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="EHf-IW-A2E">
+            <objects>
+                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <subviews>
+                            <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="vcmi_logo.png" translatesAutoresizingMaskIntoConstraints="NO" id="xBx-rS-h4V">
+                                <rect key="frame" x="87.5" y="233.5" width="200" height="200"/>
+                            </imageView>
+                        </subviews>
+                        <viewLayoutGuide key="safeArea" id="Jnx-JV-IwA"/>
+                        <color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                        <constraints>
+                            <constraint firstItem="xBx-rS-h4V" firstAttribute="centerY" secondItem="Jnx-JV-IwA" secondAttribute="centerY" id="OgR-oG-SHt"/>
+                            <constraint firstItem="xBx-rS-h4V" firstAttribute="centerX" secondItem="Jnx-JV-IwA" secondAttribute="centerX" id="eTp-at-qVT"/>
+                        </constraints>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="53" y="375"/>
+        </scene>
+    </scenes>
+    <resources>
+        <image name="vcmi_logo.png" width="200" height="200"/>
+    </resources>
+</document>

+ 31 - 0
client/ios/Settings.bundle/Root.plist

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>StringsTable</key>
+	<string>Root</string>
+	<key>PreferenceSpecifiers</key>
+	<array>
+		<dict>
+			<key>Type</key>
+			<string>PSRadioGroupSpecifier</string>
+			<key>Title</key>
+			<string>LaunchType</string>
+			<key>Key</key>
+			<string>LaunchType</string>
+			<key>DefaultValue</key>
+			<integer>0</integer>
+			<key>Values</key>
+			<array>
+				<integer>0</integer>
+				<integer>1</integer>
+			</array>
+			<key>Titles</key>
+			<array>
+				<string>Launcher</string>
+				<string>Game</string>
+			</array>
+		</dict>
+	</array>
+</dict>
+</plist>

+ 3 - 0
client/ios/Settings.bundle/en.lproj/Root.strings

@@ -0,0 +1,3 @@
+"LaunchType" = "App start type";
+"Launcher" = "Launcher";
+"Game" = "Game";

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio