Ver código fonte

Merge remote-tracking branch 'origin/develop' into terrain-rewrite

# Conflicts:
#	lib/Terrain.cpp
#	lib/Terrain.h
#	lib/battle/CBattleInfoEssentials.cpp
#	lib/rmg/ObstaclePlacer.cpp
#	lib/rmg/RiverPlacer.cpp
Tomasz Zieliński 3 anos atrás
pai
commit
f386f42166
100 arquivos alterados com 1483 adições e 168 exclusões
  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. 4 1
      AI/Nullkiller/CMakeLists.txt
  12. 4 0
      AI/Nullkiller/Engine/FuzzyEngines.h
  13. 5 0
      AI/Nullkiller/Engine/FuzzyHelper.h
  14. 6 1
      AI/Nullkiller/Engine/PriorityEvaluator.h
  15. 2 0
      AI/Nullkiller/Pathfinding/Actors.cpp
  16. 1 0
      AI/Nullkiller/StdInc.h
  17. 1 1
      AI/StupidAI/CMakeLists.txt
  18. 3 1
      AI/StupidAI/StdInc.h
  19. 1 1
      AI/VCAI/CMakeLists.txt
  20. 4 0
      AI/VCAI/FuzzyEngines.h
  21. 4 0
      AI/VCAI/FuzzyHelper.h
  22. 2 0
      AI/VCAI/StdInc.h
  23. 4 0
      AI/VCAI/VCAI.cpp
  24. 4 0
      AI/VCAI/VCAI.h
  25. 6 3
      AUTHORS
  26. 8 4
      CCallback.h
  27. 7 0
      CI/ios/before_install.sh
  28. 3 0
      CI/ios/post_pack.sh
  29. 4 1
      CI/msvc/before_install.sh
  30. 102 22
      CMakeLists.txt
  31. 62 5
      CMakePresets.json
  32. 27 5
      Global.h
  33. 2 0
      README.md
  34. 4 0
      Version.cpp.in
  35. 6 0
      Version.h
  36. 21 0
      client/CFocusableHelper.cpp
  37. 11 0
      client/CFocusableHelper.h
  38. 10 6
      client/CGameInfo.h
  39. 44 17
      client/CMT.cpp
  40. 85 3
      client/CMakeLists.txt
  41. 18 13
      client/CPlayerInterface.h
  42. 58 14
      client/CServerHandler.cpp
  43. 11 2
      client/CServerHandler.h
  44. 11 5
      client/Client.h
  45. 8 3
      client/Graphics.h
  46. 6 0
      client/NetPacksLobbyClient.cpp
  47. 6 1
      client/SDLRWwrapper.h
  48. 2 0
      client/StdInc.h
  49. 6 1
      client/battle/CBattleAnimations.h
  50. 16 11
      client/battle/CBattleInterface.h
  51. 13 7
      client/battle/CBattleInterfaceClasses.h
  52. 6 1
      client/gui/CAnimation.h
  53. 6 1
      client/gui/CGuiHandler.h
  54. 4 0
      client/gui/Fonts.h
  55. 31 5
      client/gui/SDL_Extensions.cpp
  56. 23 0
      client/ios/GameChatKeyboardHanlder.h
  57. 107 0
      client/ios/GameChatKeyboardHanlder.m
  58. 121 0
      client/ios/Images.xcassets/AppIcon.appiconset/Contents.json
  59. BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  60. BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  61. BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  62. BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  63. BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  64. BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  65. BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  66. BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  67. BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  68. BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  69. BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  70. BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  71. BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  72. BIN
      client/ios/Images.xcassets/AppIcon.appiconset/[email protected]
  73. 6 0
      client/ios/Images.xcassets/Contents.json
  74. 60 0
      client/ios/Info.plist
  75. 39 0
      client/ios/LaunchScreen.storyboard
  76. 31 0
      client/ios/Settings.bundle/Root.plist
  77. 3 0
      client/ios/Settings.bundle/en.lproj/Root.strings
  78. 3 0
      client/ios/Settings.bundle/ru.lproj/Root.strings
  79. 57 0
      client/ios/main.m
  80. 19 0
      client/ios/startSDL.h
  81. 162 0
      client/ios/startSDL.mm
  82. 18 0
      client/ios/utils.h
  83. 46 0
      client/ios/utils.mm
  84. BIN
      client/ios/vcmi_logo.png
  85. 7 2
      client/lobby/CBonusSelection.h
  86. 6 1
      client/lobby/CSavingScreen.h
  87. 8 3
      client/lobby/CSelectionBase.h
  88. 5 0
      client/lobby/RandomMapTab.h
  89. 7 2
      client/lobby/SelectionTab.cpp
  90. 1 0
      client/lobby/SelectionTab.h
  91. 6 1
      client/mainmenu/CCampaignScreen.h
  92. 3 0
      client/mainmenu/CMainMenu.cpp
  93. 5 0
      client/mainmenu/CMainMenu.h
  94. 6 1
      client/mapHandler.h
  95. 11 6
      client/widgets/AdventureMapClasses.h
  96. 9 5
      client/widgets/Buttons.h
  97. 6 2
      client/widgets/CArtifactHolder.h
  98. 5 0
      client/widgets/CComponent.h
  99. 8 3
      client/widgets/CGarrisonInt.h
  100. 8 3
      client/widgets/MiscWidgets.h

+ 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;

+ 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

+ 6 - 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
 {

+ 2 - 0
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
 {

+ 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 - 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:

+ 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

+ 8 - 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,18 +22,22 @@ 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:
@@ -88,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

+ 102 - 22
CMakeLists.txt

@@ -15,7 +15,6 @@ project(VCMI)
 #
 # Vckpg:
 # - Improve install code once there is better way to deploy DLLs and Qt plugins
-# - Move Vcpkg install BundleUtilities code from osx/CMakeLists.txt
 #
 # Other:
 # - Cleanup remove_directory copy_directory if performance will be a problem.
@@ -26,6 +25,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,10 +52,19 @@ 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_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")
@@ -53,7 +73,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")
@@ -133,6 +155,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)
@@ -209,7 +259,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 +286,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 +361,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 +409,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,13 +433,13 @@ 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()
+add_subdirectory(client)
+add_subdirectory(server)
+add_subdirectory_with_folder("AI" AI)
 if(ENABLE_TEST)
 	enable_testing()
 	add_subdirectory(test)
@@ -376,11 +450,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
@@ -487,9 +563,9 @@ if(WIN32)
 	set(CPACK_NSIS_EXECUTABLES_DIRECTORY ".")
 	# 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")
@@ -519,6 +595,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"
         }
     ]
-}
+}

+ 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,30 +13,34 @@
 
 #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 TerrainTypeHandler;
 
 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;

+ 85 - 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,64 @@ if(WIN32)
 		target_link_libraries(vcmiclient SDL2::SDL2main)
 	endif()
 	target_compile_definitions(vcmiclient PRIVATE WINDOWS_IGNORE_PACKING_MISMATCH)
+
+	# TODO: very hacky, find proper solution to copy AI dlls into bin dir
+	if(MSVC)
+		add_custom_command(TARGET vcmiclient POST_BUILD
+			WORKING_DIRECTORY "$<TARGET_FILE_DIR:vcmiclient>"
+			COMMAND ${CMAKE_COMMAND} -E rename AI/fuzzylite.dll fuzzylite.dll
+			COMMAND ${CMAKE_COMMAND} -E rename AI/tbb.dll tbb.dll
+		)
+	endif()
+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 +267,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
 {

+ 58 - 14
client/CServerHandler.cpp

@@ -21,10 +21,15 @@
 
 #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"
@@ -76,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);
 	}
 
@@ -132,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())
@@ -181,6 +186,14 @@ void CServerHandler::startLocalServerAndConnect()
 		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
@@ -188,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())
 	{
@@ -200,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());
 }
@@ -546,6 +580,11 @@ void CServerHandler::startCampaignScenario(std::shared_ptr<CCampaignState> cs)
 	SDL_PushEvent(&event);
 }
 
+void CServerHandler::showServerError(std::string txt)
+{
+	CInfoWindow::showInfoDialog(txt, {});
+}
+
 int CServerHandler::howManyPlayerInterfaces()
 {
 	int playerInts = 0;
@@ -698,7 +737,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()
@@ -739,9 +778,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

+ 11 - 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:
@@ -137,6 +145,7 @@ public:
 	void startGameplay();
 	void endGameplay(bool closeConnection = true, bool restart = false);
 	void startCampaignScenario(std::shared_ptr<CCampaignState> cs = {});
+	void showServerError(std::string txt);
 
 	// TODO: LobbyState must be updated within game so we should always know how many player interfaces our client handle
 	int howManyPlayerInterfaces();

+ 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 - 0
client/NetPacksLobbyClient.cpp

@@ -137,3 +137,9 @@ void LobbyUpdateState::applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler *
 	if(hostChanged)
 		lobby->toggleMode(handler->isHost());
 }
+
+void LobbyShowMessage::applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler)
+{
+	lobby->buttonStart->block(false);
+	handler->showServerError(message);
+}

+ 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;

+ 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;
 

+ 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

+ 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
 	});

+ 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
client/ios/Images.xcassets/AppIcon.appiconset/[email protected]


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


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


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


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


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


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


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


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


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


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


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


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


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";

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

@@ -0,0 +1,3 @@
+"LaunchType" = "Тип запуска приложения";
+"Launcher" = "Конфигурация (лаунчер)";
+"Game" = "Игра";

+ 57 - 0
client/ios/main.m

@@ -0,0 +1,57 @@
+/*
+ * main.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 "startSDL.h"
+
+#import <UIKit/UIKit.h>
+
+#import <objc/runtime.h>
+
+#include <stdlib.h>
+
+static void startSDLManually(int argc, char * argv[])
+{
+	id<UIApplicationDelegate> appDelegate;
+	@autoreleasepool {
+		__auto_type sdlAppDelegateClass = NSClassFromString(@"SDLUIKitDelegate");
+		NSCAssert(sdlAppDelegateClass != nil, @"SDL AppDelegate class doesn't exist");
+		NSCAssert(class_conformsToProtocol(sdlAppDelegateClass, @protocol(UIApplicationDelegate)), @"SDL AppDelegate doesn't conform to UIApplicationDelegate");
+		appDelegate = [sdlAppDelegateClass new];
+	}
+	UIApplication.sharedApplication.delegate = appDelegate;
+
+	int result = startSDL(argc, argv, YES);
+	exit(result);
+}
+
+int qt_main_wrapper(int argc, char * argv[]);
+
+int client_main(int argc, char * argv[])
+{
+	NSInteger launchType;
+	__auto_type envVar = getenv("VCMI_LAUNCH_TYPE");
+	if (envVar)
+		launchType = envVar[0] == '0' ? 0 : 1;
+	else {
+		@autoreleasepool {
+			launchType = [NSUserDefaults.standardUserDefaults integerForKey:@"LaunchType"];
+		}
+	}
+	if (launchType == 1)
+		return startSDL(argc, argv, NO);
+
+	@autoreleasepool {
+		id __block startGameObserver = [NSNotificationCenter.defaultCenter addObserverForName:@"StartGame" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
+			[NSNotificationCenter.defaultCenter removeObserver:startGameObserver];
+			startGameObserver = nil;
+			startSDLManually(argc, argv);
+		}];
+		return qt_main_wrapper(argc, argv);
+	}
+}

+ 19 - 0
client/ios/startSDL.h

@@ -0,0 +1,19 @@
+/*
+ * startSDL.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#ifdef __OBJC__
+#include <objc/objc.h>
+#endif
+
+#ifdef __cplusplus
+extern "C"
+#endif
+int startSDL(int argc, char * argv[], BOOL startManually);

+ 162 - 0
client/ios/startSDL.mm

@@ -0,0 +1,162 @@
+/*
+ * startSDL.mm, 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 "startSDL.h"
+#import "GameChatKeyboardHanlder.h"
+
+#include "../Global.h"
+#include "CMT.h"
+#include "CServerHandler.h"
+#include "CFocusableHelper.h"
+
+#include <SDL_main.h>
+#include <SDL_events.h>
+#include <SDL_render.h>
+#include <SDL_system.h>
+
+#import <UIKit/UIKit.h>
+
+@interface SDLViewObserver : NSObject <UIGestureRecognizerDelegate>
+@property (nonatomic, strong) GameChatKeyboardHanlder * gameChatHandler;
+@end
+
+@implementation SDLViewObserver
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
+	[object removeObserver:self forKeyPath:keyPath];
+
+    UIView * view = [object valueForKeyPath:keyPath];
+
+    UITextField * textField;
+    for (UIView * v in view.subviews) {
+        if ([v isKindOfClass:[UITextField class]]) {
+            textField = (UITextField *)v;
+            break;
+        }
+    }
+
+	auto r = textField.frame;
+	r.size.height = 40;
+	textField.frame = r;
+    self.gameChatHandler.textFieldSDL = textField;
+
+#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0
+	if(@available(iOS 13.0, *))
+		textField.backgroundColor = UIColor.systemBackgroundColor;
+	else
+#endif
+		textField.backgroundColor = UIColor.whiteColor;
+
+    auto longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
+    longPress.minimumPressDuration = 0.2;
+    [view addGestureRecognizer:longPress];
+
+    auto pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)];
+    [view addGestureRecognizer:pinch];
+}
+
+#pragma mark - Gestures
+
+- (void)handleLongPress:(UIGestureRecognizer *)gesture {
+    // send RMB click
+    SDL_EventType mouseButtonType;
+    switch (gesture.state)
+    {
+        case UIGestureRecognizerStateBegan:
+            mouseButtonType = SDL_MOUSEBUTTONDOWN;
+            break;
+        case UIGestureRecognizerStateEnded:
+            mouseButtonType = SDL_MOUSEBUTTONUP;
+            break;
+        default:
+            return;
+    }
+
+    auto renderer = SDL_GetRenderer(mainWindow);
+    float scaleX, scaleY;
+    SDL_Rect viewport;
+    SDL_RenderGetScale(renderer, &scaleX, &scaleY);
+    SDL_RenderGetViewport(renderer, &viewport);
+
+    auto touchedPoint = [gesture locationInView:gesture.view];
+    auto screenScale = UIScreen.mainScreen.nativeScale;
+    Sint32 x = (int)touchedPoint.x * screenScale / scaleX - viewport.x;
+    Sint32 y = (int)touchedPoint.y * screenScale / scaleY - viewport.y;
+
+    SDL_Event rmbEvent;
+    rmbEvent.button = (SDL_MouseButtonEvent){
+        .type = mouseButtonType,
+        .button = SDL_BUTTON_RIGHT,
+        .clicks = 1,
+        .x = x,
+        .y = y,
+    };
+    SDL_PushEvent(&rmbEvent);
+
+    // small hack to prevent cursor jumping
+    if (mouseButtonType == SDL_MOUSEBUTTONUP)
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.025 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+            SDL_Event motionEvent;
+            motionEvent.motion = (SDL_MouseMotionEvent){
+                .type = SDL_MOUSEMOTION,
+                .x = x,
+                .y = y,
+            };
+            SDL_PushEvent(&motionEvent);
+        });
+}
+
+- (void)handlePinch:(UIGestureRecognizer *)gesture {
+    if(gesture.state != UIGestureRecognizerStateBegan || CSH->state != EClientState::GAMEPLAY)
+        return;
+	[self.gameChatHandler triggerInput];
+}
+
+#pragma mark - UIGestureRecognizerDelegate
+
+- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
+    return [gestureRecognizer isKindOfClass:[UIPinchGestureRecognizer class]];
+}
+
+@end
+
+
+int startSDL(int argc, char * argv[], BOOL startManually)
+{
+	@autoreleasepool {
+		auto observer = [SDLViewObserver new];
+		observer.gameChatHandler = [GameChatKeyboardHanlder new];
+
+		id __block sdlWindowCreationObserver = [NSNotificationCenter.defaultCenter addObserverForName:UIWindowDidBecomeKeyNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
+			[NSNotificationCenter.defaultCenter removeObserver:sdlWindowCreationObserver];
+			sdlWindowCreationObserver = nil;
+
+			UIWindow * sdlWindow = note.object;
+			[sdlWindow.rootViewController addObserver:observer forKeyPath:NSStringFromSelector(@selector(view)) options:NSKeyValueObservingOptionNew context:NULL];
+		}];
+		id textFieldObserver = [NSNotificationCenter.defaultCenter addObserverForName:UITextFieldTextDidEndEditingNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
+			removeFocusFromActiveInput();
+		}];
+
+		int result;
+		if (startManually)
+		{
+			// copied from -[SDLUIKitDelegate postFinishLaunch]
+			SDL_SetMainReady();
+			SDL_iOSSetEventPump(SDL_TRUE);
+			result = SDL_main(argc, argv);
+			SDL_iOSSetEventPump(SDL_FALSE);
+		}
+		else
+			result = SDL_UIKitRunApp(argc, argv, SDL_main);
+
+		[NSNotificationCenter.defaultCenter removeObserver:textFieldObserver];
+		return result;
+	}
+}

+ 18 - 0
client/ios/utils.h

@@ -0,0 +1,18 @@
+/*
+ * utils.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+namespace iOS_utils
+{
+double screenScale();
+
+void showLoadingIndicator();
+void hideLoadingIndicator();
+}

+ 46 - 0
client/ios/utils.mm

@@ -0,0 +1,46 @@
+/*
+ * utils.mm, 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 "utils.h"
+
+#import <UIKit/UIKit.h>
+
+namespace
+{
+UIActivityIndicatorView *indicator;
+}
+
+namespace iOS_utils
+{
+double screenScale()
+{
+	return UIScreen.mainScreen.nativeScale;
+}
+
+void showLoadingIndicator()
+{
+	NSCAssert(!indicator, @"activity indicator must be hidden before attempting to show it again");
+	indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
+	[indicator startAnimating];
+
+	auto mainView = UIApplication.sharedApplication.keyWindow.rootViewController.view;
+	mainView.userInteractionEnabled = NO;
+	[mainView addSubview:indicator];
+	indicator.center = {CGRectGetMidX(mainView.bounds), CGRectGetMidY(mainView.bounds)};
+}
+
+void hideLoadingIndicator()
+{
+	indicator.superview.userInteractionEnabled = YES;
+	[indicator stopAnimating];
+	[indicator removeFromSuperview];
+	indicator = nil;
+}
+}

BIN
client/ios/vcmi_logo.png


+ 7 - 2
client/lobby/CBonusSelection.h

@@ -10,8 +10,13 @@
 #pragma once
 #include "../windows/CWindowObject.h"
 
-struct SDL_Surface;
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CCampaignState;
+
+VCMI_LIB_NAMESPACE_END
+
+struct SDL_Surface;
 class CButton;
 class CTextBox;
 class CToggleGroup;
@@ -90,4 +95,4 @@ public:
 	std::shared_ptr<CButton> buttonDifficultyLeft;
 	std::shared_ptr<CButton> buttonDifficultyRight;
 	std::shared_ptr<CAnimImage> iconsMapSizes;
-};
+};

+ 6 - 1
client/lobby/CSavingScreen.h

@@ -11,10 +11,15 @@
 
 #include "CSelectionBase.h"
 
-class CSelectionBase;
+VCMI_LIB_NAMESPACE_BEGIN
+
 struct StartInfo;
 class CMapInfo;
 
+VCMI_LIB_NAMESPACE_END
+
+class CSelectionBase;
+
 class CSavingScreen : public CSelectionBase
 {
 public:

+ 8 - 3
client/lobby/CSelectionBase.h

@@ -11,6 +11,14 @@
 
 #include "../mainmenu/CMainMenu.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
+class CMapInfo;
+struct StartInfo;
+struct PlayerInfo;
+
+VCMI_LIB_NAMESPACE_END
+
 class CButton;
 class CTextBox;
 class CTextInput;
@@ -21,9 +29,6 @@ class OptionsTab;
 class SelectionTab;
 class InfoCard;
 class CChatBox;
-class CMapInfo;
-struct StartInfo;
-struct PlayerInfo;
 class CLabel;
 class CFlagBox;
 class CLabelGroup;

+ 5 - 0
client/lobby/RandomMapTab.h

@@ -14,7 +14,12 @@
 #include "../../lib/FunctionList.h"
 #include "../../lib/GameConstants.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CMapGenOptions;
+
+VCMI_LIB_NAMESPACE_END
+
 class CToggleButton;
 class CLabel;
 class CLabelGroup;

+ 7 - 2
client/lobby/SelectionTab.cpp

@@ -129,7 +129,7 @@ static ESortBy getSortBySelectionScreen(ESelectionScreen Type)
 }
 
 SelectionTab::SelectionTab(ESelectionScreen Type)
-	: CIntObject(LCLICK | WHEEL | KEYBOARD | DOUBLECLICK), callOnSelect(nullptr), tabType(Type), selectionPos(0), sortModeAscending(true)
+	: CIntObject(LCLICK | WHEEL | KEYBOARD | DOUBLECLICK), callOnSelect(nullptr), tabType(Type), selectionPos(0), sortModeAscending(true), inputNameRect{32, 539, 350, 20}
 {
 	OBJ_CONSTRUCTION;
 
@@ -140,7 +140,7 @@ SelectionTab::SelectionTab(ESelectionScreen Type)
 		sortingBy = _format;
 		background = std::make_shared<CPicture>("SCSELBCK.bmp", 0, 6);
 		pos = background->pos;
-		inputName = std::make_shared<CTextInput>(Rect(32, 539, 350, 20), Point(-32, -25), "GSSTRIP.bmp", 0);
+		inputName = std::make_shared<CTextInput>(inputNameRect, Point(-32, -25), "GSSTRIP.bmp", 0);
 		inputName->filters += CTextInput::filenameFilter;
 		labelMapSizes = std::make_shared<CLabel>(87, 62, FONT_SMALL, EAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[510]);
 
@@ -273,6 +273,11 @@ void SelectionTab::clickLeft(tribool down, bool previousState)
 		{
 			select(line);
 		}
+#ifdef VCMI_IOS
+        // focus input field if clicked inside it
+        else if(inputName && inputName->active && inputNameRect.isIn(GH.current->button.x, GH.current->button.y))
+            inputName->giveFocus();
+#endif
 	}
 }
 

+ 1 - 0
client/lobby/SelectionTab.h

@@ -90,6 +90,7 @@ private:
 	std::shared_ptr<CLabel> labelTabTitle;
 	std::shared_ptr<CLabel> labelMapSizes;
 	ESelectionScreen tabType;
+	Rect inputNameRect;
 
 	void parseMaps(const std::unordered_set<ResourceID> & files);
 	void parseSaves(const std::unordered_set<ResourceID> & files);

+ 6 - 1
client/mainmenu/CCampaignScreen.h

@@ -11,11 +11,16 @@
 
 #include "../windows/CWindowObject.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
+class JsonNode;
+
+VCMI_LIB_NAMESPACE_END
+
 class CLabel;
 class CPicture;
 class CButton;
 struct SDL_Surface;
-class JsonNode;
 
 class CCampaignScreen : public CWindowObject
 {

+ 3 - 0
client/mainmenu/CMainMenu.cpp

@@ -428,7 +428,9 @@ CMultiPlayers::CMultiPlayers(const std::string & firstPlayer, ESelectionScreen S
 	statusBar = CGStatusBar::create(std::make_shared<CPicture>(Rect(7, 381, 348, 18), 0)); //226, 472
 
 	inputNames[0]->setText(firstPlayer, true);
+#ifndef VCMI_IOS
 	inputNames[0]->giveFocus();
+#endif
 }
 
 void CMultiPlayers::onChange(std::string newText)
@@ -490,6 +492,7 @@ void CSimpleJoinScreen::connectToServer()
 {
 	textTitle->setText("Connecting...");
 	buttonOk->block(true);
+	CSDL_Ext::stopTextInput();
 
 	boost::thread(&CSimpleJoinScreen::connectThread, this, inputAddress->text, boost::lexical_cast<ui16>(inputPort->text));
 }

+ 5 - 0
client/mainmenu/CMainMenu.h

@@ -12,7 +12,12 @@
 #include "../windows/CWindowObject.h"
 #include "../../lib/JsonNode.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CCampaignState;
+
+VCMI_LIB_NAMESPACE_END
+
 class CTextInput;
 class CGStatusBar;
 class CTextBox;

+ 6 - 1
client/mapHandler.h

@@ -23,17 +23,22 @@
 #undef OUT
 #endif
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CGObjectInstance;
 class CGHeroInstance;
 class CGBoat;
 class CMap;
 struct TerrainTile;
+class PlayerColor;
+
+VCMI_LIB_NAMESPACE_END
+
 struct SDL_Surface;
 struct SDL_Rect;
 class CAnimation;
 class IImage;
 class CFadeAnimation;
-class PlayerColor;
 
 enum class EWorldViewIcon
 {

+ 11 - 6
client/widgets/AdventureMapClasses.h

@@ -13,21 +13,26 @@
 #include "../../lib/FunctionList.h"
 #include "Terrain.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CArmedInstance;
-class CAnimation;
-class CAnimImage;
-class CShowableAnim;
-class CFilledTexture;
 class CGGarrison;
 class CGObjectInstance;
 class CGHeroInstance;
 class CGTownInstance;
-class CButton;
 struct Component;
-class CComponent;
 struct InfoAboutArmy;
 struct InfoAboutHero;
 struct InfoAboutTown;
+
+VCMI_LIB_NAMESPACE_END
+
+class CAnimation;
+class CAnimImage;
+class CShowableAnim;
+class CFilledTexture;
+class CButton;
+class CComponent;
 class CHeroTooltip;
 class CTownTooltip;
 class CTextBox;

+ 9 - 5
client/widgets/Buttons.h

@@ -14,17 +14,21 @@
 
 #include "../../lib/FunctionList.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
+namespace config
+{
+struct ButtonInfo;
+}
+
+VCMI_LIB_NAMESPACE_END
+
 struct SDL_Surface;
 struct Rect;
 class CAnimImage;
 class CLabel;
 class CAnimation;
 
-namespace config
-{
-	struct ButtonInfo;
-}
-
 /// Typical Heroes 3 button which can be inactive or active and can
 /// hold further information if you right-click it
 class CButton : public CKeyShortcut

+ 6 - 2
client/widgets/CArtifactHolder.h

@@ -11,12 +11,16 @@
 
 #include "MiscWidgets.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct ArtifactLocation;
+
+VCMI_LIB_NAMESPACE_END
+
 class CArtifactsOfHero;
 class CAnimImage;
 class CButton;
 
-struct ArtifactLocation;
-
 class CArtifactHolder
 {
 public:

+ 5 - 0
client/widgets/CComponent.h

@@ -11,7 +11,12 @@
 
 #include "../gui/CIntObject.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
 struct Component;
+
+VCMI_LIB_NAMESPACE_END
+
 class CAnimImage;
 class CLabel;
 

+ 8 - 3
client/widgets/CGarrisonInt.h

@@ -11,13 +11,18 @@
 
 #include "../windows/CWindowObject.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
+class CArmedInstance;
+class CCreatureSet;
+class CStackInstance;
+
+VCMI_LIB_NAMESPACE_END
+
 class CGarrisonInt;
 class CButton;
-class CArmedInstance;
 class CAnimImage;
-class CCreatureSet;
 class CGarrisonSlot;
-class CStackInstance;
 class CLabel;
 
 /// A single garrison slot which holds one creature of a specific amount

+ 8 - 3
client/widgets/MiscWidgets.h

@@ -11,13 +11,18 @@
 
 #include "../gui/CIntObject.h"
 
-class CLabel;
-class CCreatureAnim;
-class CComponent;
+VCMI_LIB_NAMESPACE_BEGIN
+
 class CGGarrison;
 struct InfoAboutArmy;
 class CArmedInstance;
 class IBonusBearer;
+
+VCMI_LIB_NAMESPACE_END
+
+class CLabel;
+class CCreatureAnim;
+class CComponent;
 class CAnimImage;
 
 /// Shows a text by moving the mouse cursor over the object

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff