Bläddra i källkod

Merge with vcmi/develop

Ivan Savenko 2 år sedan
förälder
incheckning
05ac217b0f
86 ändrade filer med 1511 tillägg och 836 borttagningar
  1. 0 1
      AI/Nullkiller/Analyzers/ObjectClusterizer.cpp
  2. 12 0
      CI/conan/base/apple
  3. 5 0
      CI/conan/base/ios
  4. 4 0
      CI/conan/base/macos
  5. 2 11
      CI/conan/ios-arm64
  6. 4 10
      CI/conan/ios-armv7
  7. 2 10
      CI/conan/macos-arm
  8. 2 10
      CI/conan/macos-intel
  9. 2 2
      CI/ios/before_install.sh
  10. 2 2
      CI/mac/before_install.sh
  11. 7 7
      CI/msvc/before_install.sh
  12. 2 2
      CI/mxe/before_install.sh
  13. 4 1
      CMakeLists.txt
  14. 2 2
      client/CGameInfo.h
  15. 37 19
      client/CMT.cpp
  16. 3 3
      client/CMakeLists.txt
  17. 15 1
      client/CMusicHandler.cpp
  18. 5 2
      client/CPlayerInterface.cpp
  19. 7 1
      client/Client.cpp
  20. 1 1
      client/NetPacksLobbyClient.cpp
  21. 1 1
      client/battle/BattleActionsController.cpp
  22. 1 1
      client/battle/BattleAnimationClasses.cpp
  23. 1 1
      client/battle/BattleFieldController.cpp
  24. 1 1
      client/battle/BattleInterface.cpp
  25. 1 1
      client/battle/BattleInterfaceClasses.cpp
  26. 4 4
      client/battle/BattleStacksController.cpp
  27. 1 1
      client/battle/BattleWindow.cpp
  28. 4 5
      client/gui/CAnimation.cpp
  29. 2 2
      client/gui/CAnimation.h
  30. 0 317
      client/gui/CCursorHandler.cpp
  31. 1 1
      client/gui/CGuiHandler.cpp
  32. 0 7
      client/gui/Canvas.cpp
  33. 0 4
      client/gui/Canvas.h
  34. 402 0
      client/gui/CursorHandler.cpp
  35. 80 30
      client/gui/CursorHandler.h
  36. 1 1
      client/mainmenu/CMainMenu.cpp
  37. 66 62
      client/widgets/CArtifactHolder.cpp
  38. 1 1
      client/widgets/CArtifactHolder.h
  39. 1 1
      client/widgets/CComponent.cpp
  40. 10 4
      client/widgets/Images.cpp
  41. 1 1
      client/widgets/Images.h
  42. 1 1
      client/widgets/MiscWidgets.cpp
  43. 0 37
      client/widgets/TextControls.cpp
  44. 0 3
      client/widgets/TextControls.h
  45. 2 2
      client/windows/CAdvmapInterface.cpp
  46. 32 23
      client/windows/CCastleInterface.cpp
  47. 9 1
      client/windows/CCastleInterface.h
  48. 2 2
      client/windows/CTradeWindow.cpp
  49. 1 1
      client/windows/CWindowObject.cpp
  50. 1 7
      client/windows/GUIClasses.cpp
  51. 0 1
      client/windows/GUIClasses.h
  52. 1 1
      client/windows/InfoWindows.cpp
  53. 1 1
      conanfile.py
  54. 2 2
      config/factions/castle.json
  55. 6 2
      config/schemas/settings.json
  56. 1 1
      config/schemas/terrain.json
  57. 2 2
      config/widgets/battleWindow.json
  58. 6 4
      docs/conan.md
  59. 5 0
      launcher/CMakeLists.txt
  60. 27 4
      launcher/lobby/lobby.h
  61. 162 24
      launcher/lobby/lobby_moc.cpp
  62. 16 0
      launcher/lobby/lobby_moc.h
  63. 229 136
      launcher/lobby/lobby_moc.ui
  64. 4 0
      launcher/mainwindow_moc.cpp
  65. 1 1
      launcher/modManager/cmodlist.cpp
  66. 20 2
      launcher/modManager/cmodlistview_moc.cpp
  67. 6 0
      launcher/modManager/cmodlistview_moc.h
  68. 2 0
      launcher/settingsView/csettingsview_moc.cpp
  69. 5 0
      launcher/settingsView/csettingsview_moc.ui
  70. 0 5
      lib/CAndroidVMHelper.cpp
  71. 0 2
      lib/CAndroidVMHelper.h
  72. 40 7
      lib/CArtHandler.cpp
  73. 2 0
      lib/CArtHandler.h
  74. 83 1
      lib/CGeneralTextHandler.cpp
  75. 1 0
      lib/GameConstants.h
  76. 16 3
      lib/filesystem/CFilesystemLoader.cpp
  77. 1 1
      lib/mapObjects/CQuest.cpp
  78. 7 1
      lib/mapObjects/MiscObjects.cpp
  79. BIN
      mapeditor/icons/brush-3.png
  80. 16 0
      mapeditor/mainwindow.cpp
  81. 2 0
      mapeditor/mainwindow.h
  82. 6 2
      mapeditor/mainwindow.ui
  83. 58 0
      mapeditor/mapview.cpp
  84. 6 10
      mapeditor/resourceExtractor/ResourceConverter.cpp
  85. 28 15
      server/CGameHandler.cpp
  86. 4 0
      server/CVCMIServer.cpp

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

@@ -202,7 +202,6 @@ void ObjectClusterizer::clusterize()
 		Obj::WHIRLPOOL,
 		Obj::BUOY,
 		Obj::SIGN,
-		Obj::SIGN,
 		Obj::GARRISON,
 		Obj::MONSTER,
 		Obj::GARRISON2,

+ 12 - 0
CI/conan/base/apple

@@ -0,0 +1,12 @@
+[settings]
+compiler=apple-clang
+compiler.version=14
+compiler.libcxx=libc++
+build_type=Release
+
+# required for Boost.Locale in versions >= 1.81
+compiler.cppstd=11
+
+[conf]
+tools.apple:enable_bitcode = False
+tools.cmake.cmaketoolchain:generator = Ninja

+ 5 - 0
CI/conan/base/ios

@@ -0,0 +1,5 @@
+include(apple)
+
+[settings]
+os=iOS
+os.sdk=iphoneos

+ 4 - 0
CI/conan/base/macos

@@ -0,0 +1,4 @@
+include(apple)
+
+[settings]
+os=Macos

+ 2 - 11
CI/conan/ios-arm64

@@ -1,14 +1,5 @@
+include(base/ios)
+
 [settings]
-os=iOS
 os.version=12.0
-os.sdk=iphoneos
 arch=armv8
-compiler=apple-clang
-compiler.version=13
-compiler.libcxx=libc++
-build_type=Release
-[options]
-[build_requires]
-[env]
-[conf]
-tools.cmake.cmaketoolchain:generator = Ninja

+ 4 - 10
CI/conan/ios-armv7

@@ -1,14 +1,8 @@
+include(base/ios)
+
 [settings]
-os=iOS
 os.version=10.0
-os.sdk=iphoneos
 arch=armv7
-compiler=apple-clang
+
+# Xcode 13.x is the last version that can build for armv7
 compiler.version=13
-compiler.libcxx=libc++
-build_type=Release
-[options]
-[build_requires]
-[env]
-[conf]
-tools.cmake.cmaketoolchain:generator = Ninja

+ 2 - 10
CI/conan/macos-arm

@@ -1,13 +1,5 @@
+include(base/macos)
+
 [settings]
-os=Macos
 os.version=11.0
 arch=armv8
-compiler=apple-clang
-compiler.version=13
-compiler.libcxx=libc++
-build_type=Release
-[options]
-[build_requires]
-[env]
-[conf]
-tools.cmake.cmaketoolchain:generator = Ninja

+ 2 - 10
CI/conan/macos-intel

@@ -1,13 +1,5 @@
+include(base/macos)
+
 [settings]
-os=Macos
 os.version=10.13
 arch=x86_64
-compiler=apple-clang
-compiler.version=13
-compiler.libcxx=libc++
-build_type=Release
-[options]
-[build_requires]
-[env]
-[conf]
-tools.cmake.cmaketoolchain:generator = Ninja

+ 2 - 2
CI/ios/before_install.sh

@@ -1,7 +1,7 @@
 #!/usr/bin/env bash
 
-echo DEVELOPER_DIR=/Applications/Xcode_13.4.1.app >> $GITHUB_ENV
+echo DEVELOPER_DIR=/Applications/Xcode_14.2.app >> $GITHUB_ENV
 
 mkdir ~/.conan ; cd ~/.conan
-curl -L 'https://github.com/vcmi/vcmi-ios-deps/releases/download/1.1/ios-arm64.xz' \
+curl -L 'https://github.com/vcmi/vcmi-ios-deps/releases/download/1.2/ios-arm64.txz' \
 	| tar -xf -

+ 2 - 2
CI/mac/before_install.sh

@@ -1,9 +1,9 @@
 #!/usr/bin/env bash
 
-echo DEVELOPER_DIR=/Applications/Xcode_13.4.1.app >> $GITHUB_ENV
+echo DEVELOPER_DIR=/Applications/Xcode_14.2.app >> $GITHUB_ENV
 
 brew install ninja
 
 mkdir ~/.conan ; cd ~/.conan
-curl -L "https://github.com/vcmi/vcmi-deps-macos/releases/download/1.1/$DEPS_FILENAME.txz" \
+curl -L "https://github.com/vcmi/vcmi-deps-macos/releases/download/1.2/$DEPS_FILENAME.txz" \
 	| tar -xf -

+ 7 - 7
CI/msvc/before_install.sh

@@ -1,10 +1,10 @@
-curl -LfsS -o "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"
+curl -LfsS -o "vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z" \
+	"https://github.com/vcmi/vcmi-deps-windows/releases/download/v1.6/vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z"
+7z x "vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z"
 
-rm -r -f vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug
-mkdir -p vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug/bin
-cp vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/bin/* vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug/bin
+#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
+dirname "$DUMPBIN_DIR" > $GITHUB_PATH

+ 2 - 2
CI/mxe/before_install.sh

@@ -14,8 +14,8 @@ sudo add-apt-repository 'deb http://security.ubuntu.com/ubuntu bionic-security m
 sudo apt-get install -qq nsis ninja-build libssl1.0.0
 
 # MXE repository was too slow for Travis far too often
-wget -nv https://github.com/vcmi/vcmi-deps-mxe/releases/download/2022-12-26/mxe-i686-w64-mingw32.shared-2022-12-26.tar
-tar -xvf mxe-i686-w64-mingw32.shared-2022-12-26.tar
+wget -nv https://github.com/vcmi/vcmi-deps-mxe/releases/download/2021-02-20/mxe-i686-w64-mingw32.shared-2021-01-22.tar
+tar -xvf mxe-i686-w64-mingw32.shared-2021-01-22.tar
 sudo dpkg -i mxe-*.deb
 sudo apt-get install -f --yes
 

+ 4 - 1
CMakeLists.txt

@@ -232,7 +232,7 @@ if(MINGW OR MSVC)
 		#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4800") # 4800: implicit conversion from 'xxx' to bool. Possible information loss
 
 		if(ENABLE_STRICT_COMPILATION)
-			set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wx") # Treats all compiler warnings as errors
+			set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /WX") # Treats all compiler warnings as errors
 		endif()
 
 		if(ENABLE_MULTI_PROCESS_BUILDS)
@@ -563,6 +563,9 @@ if(WIN32)
 				FILES ${integration_loc}
 				DESTINATION ${BIN_DIR}/platforms
 			)
+			install(
+				FILES "$<TARGET_FILE:Qt${QT_VERSION_MAJOR}::QWindowsVistaStylePlugin>" 
+				DESTINATION ${BIN_DIR}/styles) 
 		endif()
 	endif()
 

+ 2 - 2
client/CGameInfo.h

@@ -38,7 +38,7 @@ VCMI_LIB_NAMESPACE_END
 class CMapHandler;
 class CSoundHandler;
 class CMusicHandler;
-class CCursorHandler;
+class CursorHandler;
 class IMainVideoPlayer;
 class CServerHandler;
 
@@ -49,7 +49,7 @@ public:
 	CSoundHandler * soundh;
 	CMusicHandler * musich;
 	CConsoleHandler * consoleh;
-	CCursorHandler * curh;
+	CursorHandler * curh;
 	IMainVideoPlayer * videoh;
 };
 extern CClientState * CCS;

+ 37 - 19
client/CMT.cpp

@@ -25,7 +25,7 @@
 #include "lobby/CSelectionBase.h"
 #include "windows/CCastleInterface.h"
 #include "../lib/CConsoleHandler.h"
-#include "gui/CCursorHandler.h"
+#include "gui/CursorHandler.h"
 #include "../lib/CGameState.h"
 #include "../CCallback.h"
 #include "CPlayerInterface.h"
@@ -208,6 +208,8 @@ int main(int argc, char * argv[])
 		("lobby-host", "if this client hosts session")
 		("lobby-uuid", po::value<std::string>(), "uuid to the server")
 		("lobby-connections", po::value<ui16>(), "connections of server")
+		("lobby-username", po::value<std::string>(), "player name")
+		("lobby-gamemode", po::value<ui16>(), "use 0 for new game and 1 for load game")
 		("uuid", po::value<std::string>(), "uuid for the client");
 
 	if(argc > 1)
@@ -470,7 +472,7 @@ int main(int argc, char * argv[])
 		pomtime.getDiff();
 		graphics = new Graphics(); // should be before curh
 
-		CCS->curh = new CCursorHandler();
+		CCS->curh = new CursorHandler();
 		logGlobal->info("Screen handler: %d ms", pomtime.getDiff());
 		pomtime.getDiff();
 
@@ -489,13 +491,41 @@ int main(int argc, char * argv[])
 	session["autoSkip"].Bool()  = vm.count("autoSkip");
 	session["oneGoodAI"].Bool() = vm.count("oneGoodAI");
 	session["aiSolo"].Bool() = false;
+	std::shared_ptr<CMainMenu> mmenu;
 	
+	if(vm.count("testmap"))
+	{
+		session["testmap"].String() = vm["testmap"].as<std::string>();
+		session["onlyai"].Bool() = true;
+		boost::thread(&CServerHandler::debugStartTest, CSH, session["testmap"].String(), false);
+	}
+	else if(vm.count("testsave"))
+	{
+		session["testsave"].String() = vm["testsave"].as<std::string>();
+		session["onlyai"].Bool() = true;
+		boost::thread(&CServerHandler::debugStartTest, CSH, session["testsave"].String(), true);
+	}
+	else
+	{
+		mmenu = CMainMenu::create();
+		GH.curInt = mmenu.get();
+	}
+	
+	std::vector<std::string> names;
 	session["lobby"].Bool() = false;
 	if(vm.count("lobby"))
 	{
 		session["lobby"].Bool() = true;
 		session["host"].Bool() = false;
 		session["address"].String() = vm["lobby-address"].as<std::string>();
+		if(vm.count("lobby-username"))
+			session["username"].String() = vm["lobby-username"].as<std::string>();
+		else
+			session["username"].String() = settings["launcher"]["lobbyUsername"].String();
+		if(vm.count("lobby-gamemode"))
+			session["gamemode"].Integer() = vm["lobby-gamemode"].as<ui16>();
+		else
+			session["gamemode"].Integer() = 0;
 		CSH->uuid = vm["uuid"].as<std::string>();
 		session["port"].Integer() = vm["lobby-port"].as<ui16>();
 		logGlobal->info("Remote lobby mode at %s:%d, uuid is %s", session["address"].String(), session["port"].Integer(), CSH->uuid);
@@ -510,23 +540,11 @@ int main(int argc, char * argv[])
 		//we should not reconnect to previous game in online mode
 		Settings saveSession = settings.write["server"]["reconnect"];
 		saveSession->Bool() = false;
-	}
-
-	if(vm.count("testmap"))
-	{
-		session["testmap"].String() = vm["testmap"].as<std::string>();
-		session["onlyai"].Bool() = true;
-		boost::thread(&CServerHandler::debugStartTest, CSH, session["testmap"].String(), false);
-	}
-	else if(vm.count("testsave"))
-	{
-		session["testsave"].String() = vm["testsave"].as<std::string>();
-		session["onlyai"].Bool() = true;
-		boost::thread(&CServerHandler::debugStartTest, CSH, session["testsave"].String(), true);
-	}
-	else
-	{
-		GH.curInt = CMainMenu::create().get();
+		
+		//start lobby immediately
+		names.push_back(session["username"].String());
+		ESelectionScreen sscreen = session["gamemode"].Integer() == 0 ? ESelectionScreen::newGame : ESelectionScreen::loadGame;
+		mmenu->openLobby(sscreen, session["host"].Bool(), &names, ELoadMode::MULTI);
 	}
 	
 	// Restore remote session - start game immediately

+ 3 - 3
client/CMakeLists.txt

@@ -18,7 +18,7 @@ set(client_SRCS
 
 		gui/CAnimation.cpp
 		gui/Canvas.cpp
-		gui/CCursorHandler.cpp
+		gui/CursorHandler.cpp
 		gui/CGuiHandler.cpp
 		gui/CIntObject.cpp
 		gui/ColorFilter.cpp
@@ -104,7 +104,7 @@ set(client_HEADERS
 
 		gui/CAnimation.h
 		gui/Canvas.h
-		gui/CCursorHandler.h
+		gui/CursorHandler.h
 		gui/CGuiHandler.h
 		gui/ColorFilter.h
 		gui/CIntObject.h
@@ -232,7 +232,7 @@ if(WIN32)
 		add_custom_command(TARGET vcmiclient POST_BUILD
 			WORKING_DIRECTORY "$<TARGET_FILE_DIR:vcmiclient>"
 			COMMAND ${CMAKE_COMMAND} -E copy AI/fuzzylite.dll fuzzylite.dll
-			COMMAND ${CMAKE_COMMAND} -E copy AI/tbb.dll tbb.dll
+			COMMAND ${CMAKE_COMMAND} -E copy AI/tbb12.dll tbb12.dll
 		)
 	endif()
 elseif(APPLE_IOS)

+ 15 - 1
client/CMusicHandler.cpp

@@ -512,6 +512,20 @@ MusicEntry::MusicEntry(CMusicHandler *owner, std::string setName, std::string mu
 }
 MusicEntry::~MusicEntry()
 {
+	if (playing)
+	{
+		assert(0);
+		logGlobal->error("Attempt to delete music while playing!");
+		Mix_HaltMusic();
+	}
+
+	if (loop == 0 && Mix_FadingMusic() != MIX_NO_FADING)
+	{
+		assert(0);
+		logGlobal->error("Attempt to delete music while fading out!");
+		Mix_HaltMusic();
+	}
+
 	logGlobal->trace("Del-ing music file %s", currentName);
 	if (music)
 		Mix_FreeMusic(music);
@@ -589,7 +603,7 @@ bool MusicEntry::play()
 
 bool MusicEntry::stop(int fade_ms)
 {
-	if (playing)
+	if (Mix_PlayingMusic())
 	{
 		playing = false;
 		loop = 0;

+ 5 - 2
client/CPlayerInterface.cpp

@@ -19,7 +19,7 @@
 #include "battle/BattleWindow.h"
 #include "../CCallback.h"
 #include "windows/CCastleInterface.h"
-#include "gui/CCursorHandler.h"
+#include "gui/CursorHandler.h"
 #include "windows/CKingdomInterface.h"
 #include "CGameInfo.h"
 #include "windows/CHeroWindow.h"
@@ -1624,7 +1624,7 @@ int CPlayerInterface::getLastIndex( std::string namePrefix)
 	else
 	for (directory_iterator dir(gamesDir); dir != enddir; ++dir)
 	{
-		if (is_regular(dir->status()))
+		if (is_regular_file(dir->status()))
 		{
 			std::string name = dir->path().filename().string();
 			if (starts_with(name, namePrefix) && ends_with(name, ".vcgm1"))
@@ -2487,6 +2487,9 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path)
 		// (i == 0) means hero went through all the path
 		adventureInt->updateMoveHero(h, (i != 0));
 		adventureInt->updateNextHero(h);
+
+		// ugly workaround to force instant update of adventure map
+		adventureInt->animValHitCount = 8;
 	}
 
 	setMovementStatus(false);

+ 7 - 1
client/Client.cpp

@@ -761,8 +761,14 @@ void CClient::reinitScripting()
 #endif
 }
 
-
 #ifdef VCMI_ANDROID
+extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_clientSetupJNI(JNIEnv * env, jobject cls)
+{
+	logNetwork->info("Received clientSetupJNI");
+
+	CAndroidVMHelper::cacheVM(env);
+}
+
 extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_notifyServerClosed(JNIEnv * env, jobject cls)
 {
 	logNetwork->info("Received server closed signal");

+ 1 - 1
client/NetPacksLobbyClient.cpp

@@ -63,7 +63,7 @@ void LobbyClientDisconnected::applyOnLobbyScreen(CLobbyScreen * lobby, CServerHa
 
 void LobbyChatMessage::applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler)
 {
-	if(lobby)
+	if(lobby && lobby->card)
 	{
 		lobby->card->chat->addNewMessage(playerName + ": " + message);
 		lobby->card->setChat(true);

+ 1 - 1
client/battle/BattleActionsController.cpp

@@ -19,7 +19,7 @@
 
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
-#include "../gui/CCursorHandler.h"
+#include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/CIntObject.h"
 #include "../windows/CCreatureWindow.h"

+ 1 - 1
client/battle/BattleAnimationClasses.cpp

@@ -22,7 +22,7 @@
 #include "../CGameInfo.h"
 #include "../CMusicHandler.h"
 #include "../CPlayerInterface.h"
-#include "../gui/CCursorHandler.h"
+#include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
 
 #include "../../CCallback.h"

+ 1 - 1
client/battle/BattleFieldController.cpp

@@ -26,7 +26,7 @@
 #include "../gui/CAnimation.h"
 #include "../gui/Canvas.h"
 #include "../gui/CGuiHandler.h"
-#include "../gui/CCursorHandler.h"
+#include "../gui/CursorHandler.h"
 
 #include "../../CCallback.h"
 #include "../../lib/BattleFieldHandler.h"

+ 1 - 1
client/battle/BattleInterface.cpp

@@ -28,7 +28,7 @@
 #include "../CMusicHandler.h"
 #include "../CPlayerInterface.h"
 #include "../gui/Canvas.h"
-#include "../gui/CCursorHandler.h"
+#include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../windows/CAdvmapInterface.h"
 

+ 1 - 1
client/battle/BattleInterfaceClasses.cpp

@@ -26,7 +26,7 @@
 #include "../Graphics.h"
 #include "../gui/CAnimation.h"
 #include "../gui/Canvas.h"
-#include "../gui/CCursorHandler.h"
+#include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../widgets/AdventureMapClasses.h"
 #include "../widgets/Buttons.h"

+ 4 - 4
client/battle/BattleStacksController.cpp

@@ -83,10 +83,10 @@ BattleStacksController::BattleStacksController(BattleInterface & owner):
 	amountNegative   = IImage::createFromFile("CMNUMWIN.BMP");
 	amountEffNeutral = IImage::createFromFile("CMNUMWIN.BMP");
 
-	static const auto shifterNormal   = ColorFilter::genRangeShifter( 0,0,0, 0.6, 0.2, 1.0 );
-	static const auto shifterPositive = ColorFilter::genRangeShifter( 0,0,0, 0.2, 1.0, 0.2 );
-	static const auto shifterNegative = ColorFilter::genRangeShifter( 0,0,0, 1.0, 0.2, 0.2 );
-	static const auto shifterNeutral  = ColorFilter::genRangeShifter( 0,0,0, 1.0, 1.0, 0.2 );
+	static const auto shifterNormal   = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.6f, 0.2f, 1.0f );
+	static const auto shifterPositive = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.2f, 1.0f, 0.2f );
+	static const auto shifterNegative = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 0.2f, 0.2f );
+	static const auto shifterNeutral  = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 1.0f, 0.2f );
 
 	amountNormal->adjustPalette(shifterNormal);
 	amountPositive->adjustPalette(shifterPositive);

+ 1 - 1
client/battle/BattleWindow.cpp

@@ -21,7 +21,7 @@
 #include "../CPlayerInterface.h"
 #include "../CMusicHandler.h"
 #include "../gui/Canvas.h"
-#include "../gui/CCursorHandler.h"
+#include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/CAnimation.h"
 #include "../windows/CSpellWindow.h"

+ 4 - 5
client/gui/CAnimation.cpp

@@ -94,8 +94,8 @@ public:
 	// Keep the original palette, in order to do color switching operation
 	void savePalette();
 
-	void draw(SDL_Surface * where, int posX=0, int posY=0, const Rect *src=nullptr, ui8 alpha=255) const override;
-	void draw(SDL_Surface * where, const SDL_Rect * dest, const SDL_Rect * src, ui8 alpha=255) const override;
+	void draw(SDL_Surface * where, int posX=0, int posY=0, const Rect *src=nullptr) const override;
+	void draw(SDL_Surface * where, const SDL_Rect * dest, const SDL_Rect * src) const override;
 	std::shared_ptr<IImage> scaleFast(float scale) const override;
 	void exportBitmap(const boost::filesystem::path & path) const override;
 	void playerColored(PlayerColor player) override;
@@ -642,17 +642,16 @@ SDLImage::SDLImage(std::string filename)
 	}
 }
 
-void SDLImage::draw(SDL_Surface *where, int posX, int posY, const Rect *src, ui8 alpha) const
+void SDLImage::draw(SDL_Surface *where, int posX, int posY, const Rect *src) const
 {
 	if(!surf)
 		return;
 
 	Rect destRect(posX, posY, surf->w, surf->h);
-
 	draw(where, &destRect, src);
 }
 
-void SDLImage::draw(SDL_Surface* where, const SDL_Rect* dest, const SDL_Rect* src, ui8 alpha) const
+void SDLImage::draw(SDL_Surface* where, const SDL_Rect* dest, const SDL_Rect* src) const
 {
 	if (!surf)
 		return;

+ 2 - 2
client/gui/CAnimation.h

@@ -40,8 +40,8 @@ public:
 	using SpecialPalette = std::array<SDL_Color, 7>;
 
 	//draws image on surface "where" at position
-	virtual void draw(SDL_Surface * where, int posX = 0, int posY = 0, const Rect * src = nullptr, ui8 alpha = 255) const=0;
-	virtual void draw(SDL_Surface * where, const SDL_Rect * dest, const SDL_Rect * src, ui8 alpha = 255) const = 0;
+	virtual void draw(SDL_Surface * where, int posX = 0, int posY = 0, const Rect * src = nullptr) const = 0;
+	virtual void draw(SDL_Surface * where, const SDL_Rect * dest, const SDL_Rect * src) const = 0;
 
 	virtual std::shared_ptr<IImage> scaleFast(float scale) const = 0;
 

+ 0 - 317
client/gui/CCursorHandler.cpp

@@ -1,317 +0,0 @@
-/*
- * CCursorHandler.cpp, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-#include "StdInc.h"
-#include "CCursorHandler.h"
-
-#include <SDL.h>
-
-#include "SDL_Extensions.h"
-#include "CGuiHandler.h"
-#include "../widgets/Images.h"
-
-#include "../CMT.h"
-
-void CCursorHandler::clearBuffer()
-{
-	Uint32 fillColor = SDL_MapRGBA(buffer->format, 0, 0, 0, 0);
-	CSDL_Ext::fillRect(buffer, nullptr, fillColor);
-}
-
-void CCursorHandler::updateBuffer(CIntObject * payload)
-{
-	payload->moveTo(Point(0,0));
-	payload->showAll(buffer);
-
-	needUpdate = true;
-}
-
-void CCursorHandler::replaceBuffer(CIntObject * payload)
-{
-	clearBuffer();
-	updateBuffer(payload);
-}
-
-CCursorHandler::CCursorHandler()
-	: needUpdate(true)
-	, buffer(nullptr)
-	, cursorLayer(nullptr)
-	, frameTime(0.f)
-	, showing(false)
-{
-	cursorLayer = SDL_CreateTexture(mainRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, 40, 40);
-	SDL_SetTextureBlendMode(cursorLayer, SDL_BLENDMODE_BLEND);
-
-	xpos = ypos = 0;
-	type = Cursor::Type::DEFAULT;
-	dndObject = nullptr;
-
-	cursors =
-	{
-		std::make_unique<CAnimImage>("CRADVNTR", 0),
-		std::make_unique<CAnimImage>("CRCOMBAT", 0),
-		std::make_unique<CAnimImage>("CRDEFLT",  0),
-		std::make_unique<CAnimImage>("CRSPELL",  0)
-	};
-
-	currentCursor = cursors.at(static_cast<size_t>(Cursor::Type::DEFAULT)).get();
-
-	buffer = CSDL_Ext::newSurface(40,40);
-
-	SDL_SetSurfaceBlendMode(buffer, SDL_BLENDMODE_NONE);
-	SDL_ShowCursor(SDL_DISABLE);
-
-	set(Cursor::Map::POINTER);
-}
-
-Point CCursorHandler::position() const
-{
-	return Point(xpos, ypos);
-}
-
-void CCursorHandler::changeGraphic(Cursor::Type type, size_t index)
-{
-	assert(dndObject == nullptr);
-
-	if(type != this->type)
-	{
-		this->type = type;
-		this->frame = index;
-		currentCursor = cursors.at(static_cast<size_t>(type)).get();
-		currentCursor->setFrame(index);
-	}
-	else if(index != this->frame)
-	{
-		this->frame = index;
-		currentCursor->setFrame(index);
-	}
-
-	replaceBuffer(currentCursor);
-}
-
-void CCursorHandler::set(Cursor::Default index)
-{
-	changeGraphic(Cursor::Type::DEFAULT, static_cast<size_t>(index));
-}
-
-void CCursorHandler::set(Cursor::Map index)
-{
-	changeGraphic(Cursor::Type::ADVENTURE, static_cast<size_t>(index));
-}
-
-void CCursorHandler::set(Cursor::Combat index)
-{
-	changeGraphic(Cursor::Type::COMBAT, static_cast<size_t>(index));
-}
-
-void CCursorHandler::set(Cursor::Spellcast index)
-{
-	//Note: this is animated cursor, ignore specified frame and only change type
-	changeGraphic(Cursor::Type::SPELLBOOK, frame);
-}
-
-void CCursorHandler::dragAndDropCursor(std::unique_ptr<CAnimImage> object)
-{
-	dndObject = std::move(object);
-	if(dndObject)
-		replaceBuffer(dndObject.get());
-	else
-		replaceBuffer(currentCursor);
-}
-
-void CCursorHandler::cursorMove(const int & x, const int & y)
-{
-	xpos = x;
-	ypos = y;
-}
-
-void CCursorHandler::shiftPos( int &x, int &y )
-{
-	if(( type == Cursor::Type::COMBAT && frame != static_cast<size_t>(Cursor::Combat::POINTER)) || type == Cursor::Type::SPELLBOOK)
-	{
-		x-=16;
-		y-=16;
-
-		// Properly align the melee attack cursors.
-		if (type == Cursor::Type::COMBAT)
-		{
-			switch (static_cast<Cursor::Combat>(frame))
-			{
-			case Cursor::Combat::HIT_NORTHEAST:
-				x -= 6;
-				y += 16;
-				break;
-			case Cursor::Combat::HIT_EAST:
-				x -= 16;
-				y += 10;
-				break;
-			case Cursor::Combat::HIT_SOUTHEAST:
-				x -= 6;
-				y -= 6;
-				break;
-			case Cursor::Combat::HIT_SOUTHWEST:
-				x += 16;
-				y -= 6;
-				break;
-			case Cursor::Combat::HIT_WEST:
-				x += 16;
-				y += 11;
-				break;
-			case Cursor::Combat::HIT_NORTHWEST:
-				x += 16;
-				y += 16;
-				break;
-			case Cursor::Combat::HIT_NORTH:
-				x += 9;
-				y += 16;
-				break;
-			case Cursor::Combat::HIT_SOUTH:
-				x += 9;
-				y -= 15;
-				break;
-			}
-		}
-	}
-	else if(type == Cursor::Type::ADVENTURE)
-	{
-		if (frame == 0)
-		{
-			//no-op
-		}
-		else if(frame == 2)
-		{
-			x -= 12;
-			y -= 10;
-		}
-		else if(frame == 3)
-		{
-			x -= 12;
-			y -= 12;
-		}
-		else if(frame < 27)
-		{
-			int hlpNum = (frame - 4)%6;
-			if(hlpNum == 0)
-			{
-				x -= 15;
-				y -= 13;
-			}
-			else if(hlpNum == 1)
-			{
-				x -= 13;
-				y -= 13;
-			}
-			else if(hlpNum == 2)
-			{
-				x -= 20;
-				y -= 20;
-			}
-			else if(hlpNum == 3)
-			{
-				x -= 13;
-				y -= 16;
-			}
-			else if(hlpNum == 4)
-			{
-				x -= 8;
-				y -= 9;
-			}
-			else if(hlpNum == 5)
-			{
-				x -= 14;
-				y -= 16;
-			}
-		}
-		else if(frame == 41)
-		{
-			x -= 14;
-			y -= 16;
-		}
-		else if(frame < 31 || frame == 42)
-		{
-			x -= 20;
-			y -= 20;
-		}
-	}
-}
-
-void CCursorHandler::centerCursor()
-{
-	this->xpos = static_cast<int>((screen->w / 2.) - (currentCursor->pos.w / 2.));
-	this->ypos = static_cast<int>((screen->h / 2.) - (currentCursor->pos.h / 2.));
-	SDL_EventState(SDL_MOUSEMOTION, SDL_IGNORE);
-	SDL_WarpMouse(this->xpos, this->ypos);
-	SDL_EventState(SDL_MOUSEMOTION, SDL_ENABLE);
-}
-
-void CCursorHandler::render()
-{
-	if(!showing)
-		return;
-
-	if (type == Cursor::Type::SPELLBOOK)
-	{
-		static const float frameDisplayDuration = 0.1f;
-
-		frameTime += GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
-		size_t newFrame = frame;
-
-		while (frameTime > frameDisplayDuration)
-		{
-			frameTime -= frameDisplayDuration;
-			newFrame++;
-		}
-
-		auto & animation = cursors.at(static_cast<size_t>(type));
-
-		while (newFrame > animation->size())
-			newFrame -= animation->size();
-
-		changeGraphic(Cursor::Type::SPELLBOOK, newFrame);
-	}
-
-	//the must update texture in the main (renderer) thread, but changes to cursor type may come from other threads
-	updateTexture();
-
-	int x = xpos;
-	int y = ypos;
-	shiftPos(x, y);
-
-	if(dndObject)
-	{
-		x -= dndObject->pos.w/2;
-		y -= dndObject->pos.h/2;
-	}
-
-	SDL_Rect destRect;
-	destRect.x = x;
-	destRect.y = y;
-	destRect.w = 40;
-	destRect.h = 40;
-
-	SDL_RenderCopy(mainRenderer, cursorLayer, nullptr, &destRect);
-}
-
-void CCursorHandler::updateTexture()
-{
-	if(needUpdate)
-	{
-		SDL_UpdateTexture(cursorLayer, nullptr, buffer->pixels, buffer->pitch);
-		needUpdate = false;
-	}
-}
-
-CCursorHandler::~CCursorHandler()
-{
-	if(buffer)
-		SDL_FreeSurface(buffer);
-
-	if(cursorLayer)
-		SDL_DestroyTexture(cursorLayer);
-}

+ 1 - 1
client/gui/CGuiHandler.cpp

@@ -14,7 +14,7 @@
 #include <SDL.h>
 
 #include "CIntObject.h"
-#include "CCursorHandler.h"
+#include "CursorHandler.h"
 
 #include "../CGameInfo.h"
 #include "../../lib/CThreadHelper.h"

+ 0 - 7
client/gui/Canvas.cpp

@@ -70,13 +70,6 @@ void Canvas::draw(std::shared_ptr<IImage> image, const Point & pos, const Rect &
 		image->draw(surface, renderOffset.x + pos.x, renderOffset.y + pos.y, &sourceRect);
 }
 
-void Canvas::draw(std::shared_ptr<IImage> image, const Point & pos, const Rect & sourceRect, uint8_t alpha)
-{
-	assert(image);
-	if (image)
-		image->draw(surface, renderOffset.x + pos.x, renderOffset.y + pos.y, &sourceRect, alpha);
-}
-
 void Canvas::draw(Canvas & image, const Point & pos)
 {
 	blitAt(image.surface, renderOffset.x + pos.x, renderOffset.y + pos.y, surface);

+ 0 - 4
client/gui/Canvas.h

@@ -51,10 +51,6 @@ public:
 	/// renders section of image bounded by sourceRect at specified position
 	void draw(std::shared_ptr<IImage> image, const Point & pos, const Rect & sourceRect);
 
-	/// renders section of image bounded by sourceRect at specified position at specific transparency value
-	void draw(std::shared_ptr<IImage> image, const Point & pos, const Rect & sourceRect, uint8_t alpha);
-
-
 	/// renders another canvas onto this canvas
 	void draw(Canvas & image, const Point & pos);
 

+ 402 - 0
client/gui/CursorHandler.cpp

@@ -0,0 +1,402 @@
+/*
+ * CCursorHandler.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "CursorHandler.h"
+
+#include <SDL.h>
+
+#include "SDL_Extensions.h"
+#include "CGuiHandler.h"
+#include "CAnimation.h"
+#include "../../lib/CConfigHandler.h"
+
+//#include "../CMT.h"
+
+std::unique_ptr<ICursor> CursorHandler::createCursor()
+{
+	if (settings["video"]["softwareCursor"].Bool())
+		return std::make_unique<CursorSoftware>();
+	else
+		return std::make_unique<CursorHardware>();
+}
+
+CursorHandler::CursorHandler()
+	: cursor(createCursor())
+	, frameTime(0.f)
+	, showing(false)
+	, pos(0,0)
+{
+
+	type = Cursor::Type::DEFAULT;
+	dndObject = nullptr;
+
+	cursors =
+	{
+		std::make_unique<CAnimation>("CRADVNTR"),
+		std::make_unique<CAnimation>("CRCOMBAT"),
+		std::make_unique<CAnimation>("CRDEFLT"),
+		std::make_unique<CAnimation>("CRSPELL")
+	};
+
+	for (auto & cursor : cursors)
+		cursor->preload();
+
+	set(Cursor::Map::POINTER);
+}
+
+Point CursorHandler::position() const
+{
+	return pos;
+}
+
+void CursorHandler::changeGraphic(Cursor::Type type, size_t index)
+{
+	assert(dndObject == nullptr);
+
+	if (type == this->type && index == this->frame)
+		return;
+
+	this->type = type;
+	this->frame = index;
+
+	cursor->setImage(getCurrentImage(), getPivotOffset());
+}
+
+void CursorHandler::set(Cursor::Default index)
+{
+	changeGraphic(Cursor::Type::DEFAULT, static_cast<size_t>(index));
+}
+
+void CursorHandler::set(Cursor::Map index)
+{
+	changeGraphic(Cursor::Type::ADVENTURE, static_cast<size_t>(index));
+}
+
+void CursorHandler::set(Cursor::Combat index)
+{
+	changeGraphic(Cursor::Type::COMBAT, static_cast<size_t>(index));
+}
+
+void CursorHandler::set(Cursor::Spellcast index)
+{
+	//Note: this is animated cursor, ignore specified frame and only change type
+	changeGraphic(Cursor::Type::SPELLBOOK, frame);
+}
+
+void CursorHandler::dragAndDropCursor(std::shared_ptr<IImage> image)
+{
+	dndObject = image;
+	cursor->setImage(getCurrentImage(), getPivotOffset());
+}
+
+void CursorHandler::dragAndDropCursor (std::string path, size_t index)
+{
+	CAnimation anim(path);
+	anim.load(index);
+	dragAndDropCursor(anim.getImage(index));
+}
+
+void CursorHandler::cursorMove(const int & x, const int & y)
+{
+	pos.x = x;
+	pos.y = y;
+
+	cursor->setCursorPosition(pos);
+}
+
+Point CursorHandler::getPivotOffsetDefault(size_t index)
+{
+	return {0, 0};
+}
+
+Point CursorHandler::getPivotOffsetMap(size_t index)
+{
+	static const std::array<Point, 43> offsets = {{
+		{  0,  0}, // POINTER          =  0,
+		{  0,  0}, // HOURGLASS        =  1,
+		{ 12, 10}, // HERO             =  2,
+		{ 12, 12}, // TOWN             =  3,
+
+		{ 15, 13}, // T1_MOVE          =  4,
+		{ 13, 13}, // T1_ATTACK        =  5,
+		{ 16, 32}, // T1_SAIL          =  6,
+		{ 13, 20}, // T1_DISEMBARK     =  7,
+		{  8,  9}, // T1_EXCHANGE      =  8,
+		{ 14, 16}, // T1_VISIT         =  9,
+
+		{ 15, 13}, // T2_MOVE          = 10,
+		{ 13, 13}, // T2_ATTACK        = 11,
+		{ 16, 32}, // T2_SAIL          = 12,
+		{ 13, 20}, // T2_DISEMBARK     = 13,
+		{  8,  9}, // T2_EXCHANGE      = 14,
+		{ 14, 16}, // T2_VISIT         = 15,
+
+		{ 15, 13}, // T3_MOVE          = 16,
+		{ 13, 13}, // T3_ATTACK        = 17,
+		{ 16, 32}, // T3_SAIL          = 18,
+		{ 13, 20}, // T3_DISEMBARK     = 19,
+		{  8,  9}, // T3_EXCHANGE      = 20,
+		{ 14, 16}, // T3_VISIT         = 21,
+
+		{ 15, 13}, // T4_MOVE          = 22,
+		{ 13, 13}, // T4_ATTACK        = 23,
+		{ 16, 32}, // T4_SAIL          = 24,
+		{ 13, 20}, // T4_DISEMBARK     = 25,
+		{  8,  9}, // T4_EXCHANGE      = 26,
+		{ 14, 16}, // T4_VISIT         = 27,
+
+		{ 16, 32}, // T1_SAIL_VISIT    = 28,
+		{ 16, 32}, // T2_SAIL_VISIT    = 29,
+		{ 16, 32}, // T3_SAIL_VISIT    = 30,
+		{ 16, 32}, // T4_SAIL_VISIT    = 31,
+
+		{  6,  1}, // SCROLL_NORTH     = 32,
+		{ 16,  2}, // SCROLL_NORTHEAST = 33,
+		{ 21,  6}, // SCROLL_EAST      = 34,
+		{ 16, 16}, // SCROLL_SOUTHEAST = 35,
+		{  6, 21}, // SCROLL_SOUTH     = 36,
+		{  1, 16}, // SCROLL_SOUTHWEST = 37,
+		{  1,  5}, // SCROLL_WEST      = 38,
+		{  2,  1}, // SCROLL_NORTHWEST = 39,
+
+		{  0,  0}, // POINTER_COPY     = 40,
+		{ 14, 16}, // TELEPORT         = 41,
+		{ 20, 20}, // SCUTTLE_BOAT     = 42
+	}};
+
+	assert(offsets.size() == size_t(Cursor::Map::COUNT)); //Invalid number of pivot offsets for cursor
+	assert(index < offsets.size());
+	return offsets[index];
+}
+
+Point CursorHandler::getPivotOffsetCombat(size_t index)
+{
+	static const std::array<Point, 20> offsets = {{
+		{ 12, 12 }, // BLOCKED        = 0,
+		{ 10, 14 }, // MOVE           = 1,
+		{ 14, 14 }, // FLY            = 2,
+		{ 12, 12 }, // SHOOT          = 3,
+		{ 12, 12 }, // HERO           = 4,
+		{  8, 12 }, // QUERY          = 5,
+		{  0,  0 }, // POINTER        = 6,
+		{ 21,  0 }, // HIT_NORTHEAST  = 7,
+		{ 31,  5 }, // HIT_EAST       = 8,
+		{ 21, 21 }, // HIT_SOUTHEAST  = 9,
+		{  0, 21 }, // HIT_SOUTHWEST  = 10,
+		{  0,  5 }, // HIT_WEST       = 11,
+		{  0,  0 }, // HIT_NORTHWEST  = 12,
+		{  6,  0 }, // HIT_NORTH      = 13,
+		{  6, 31 }, // HIT_SOUTH      = 14,
+		{ 14,  0 }, // SHOOT_PENALTY  = 15,
+		{ 12, 12 }, // SHOOT_CATAPULT = 16,
+		{ 12, 12 }, // HEAL           = 17,
+		{ 12, 12 }, // SACRIFICE      = 18,
+		{ 14, 20 }, // TELEPORT       = 19
+	}};
+
+	assert(offsets.size() == size_t(Cursor::Combat::COUNT)); //Invalid number of pivot offsets for cursor
+	assert(index < offsets.size());
+	return offsets[index];
+}
+
+Point CursorHandler::getPivotOffsetSpellcast()
+{
+	return { 18, 28};
+}
+
+Point CursorHandler::getPivotOffset()
+{
+	if (dndObject)
+		return dndObject->dimensions() / 2;
+
+	switch (type) {
+	case Cursor::Type::ADVENTURE: return getPivotOffsetMap(frame);
+	case Cursor::Type::COMBAT:    return getPivotOffsetCombat(frame);
+	case Cursor::Type::DEFAULT:   return getPivotOffsetDefault(frame);
+	case Cursor::Type::SPELLBOOK: return getPivotOffsetSpellcast();
+	};
+
+	assert(0);
+	return {0, 0};
+}
+
+std::shared_ptr<IImage> CursorHandler::getCurrentImage()
+{
+	if (dndObject)
+		return dndObject;
+
+	return cursors[static_cast<size_t>(type)]->getImage(frame);
+}
+
+void CursorHandler::centerCursor()
+{
+	Point screenSize {screen->w, screen->h};
+	pos = screenSize / 2 - getPivotOffset();
+
+	SDL_EventState(SDL_MOUSEMOTION, SDL_IGNORE);
+	SDL_WarpMouse(pos.x, pos.y);
+	SDL_EventState(SDL_MOUSEMOTION, SDL_ENABLE);
+
+	cursor->setCursorPosition(pos);
+}
+
+void CursorHandler::updateSpellcastCursor()
+{
+	static const float frameDisplayDuration = 0.1f;
+
+	frameTime += GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+	size_t newFrame = frame;
+
+	while (frameTime >= frameDisplayDuration)
+	{
+		frameTime -= frameDisplayDuration;
+		newFrame++;
+	}
+
+	auto & animation = cursors.at(static_cast<size_t>(type));
+
+	while (newFrame >= animation->size())
+		newFrame -= animation->size();
+
+	changeGraphic(Cursor::Type::SPELLBOOK, newFrame);
+}
+
+void CursorHandler::render()
+{
+	if(!showing)
+		return;
+
+	if (type == Cursor::Type::SPELLBOOK)
+		updateSpellcastCursor();
+
+	cursor->render();
+}
+
+void CursorSoftware::render()
+{
+	//texture must be updated in the main (renderer) thread, but changes to cursor type may come from other threads
+	if (needUpdate)
+		updateTexture();
+
+	Point renderPos = pos - pivot;
+
+	SDL_Rect destRect;
+	destRect.x = renderPos.x;
+	destRect.y = renderPos.y;
+	destRect.w = 40;
+	destRect.h = 40;
+
+	SDL_RenderCopy(mainRenderer, cursorTexture, nullptr, &destRect);
+}
+
+void CursorSoftware::createTexture(const Point & dimensions)
+{
+	if(cursorTexture)
+		SDL_DestroyTexture(cursorTexture);
+
+	if (cursorSurface)
+		SDL_FreeSurface(cursorSurface);
+
+	cursorSurface = CSDL_Ext::newSurface(dimensions.x, dimensions.y);
+	cursorTexture = SDL_CreateTexture(mainRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, dimensions.x, dimensions.y);
+
+	SDL_SetSurfaceBlendMode(cursorSurface, SDL_BLENDMODE_NONE);
+	SDL_SetTextureBlendMode(cursorTexture, SDL_BLENDMODE_BLEND);
+}
+
+void CursorSoftware::updateTexture()
+{
+	Point dimensions(-1, -1);
+
+	if (!cursorSurface ||  Point(cursorSurface->w, cursorSurface->h) != cursorImage->dimensions())
+		createTexture(cursorImage->dimensions());
+
+	Uint32 fillColor = SDL_MapRGBA(cursorSurface->format, 0, 0, 0, 0);
+	CSDL_Ext::fillRect(cursorSurface, nullptr, fillColor);
+
+	cursorImage->draw(cursorSurface);
+	SDL_UpdateTexture(cursorTexture, NULL, cursorSurface->pixels, cursorSurface->pitch);
+	needUpdate = false;
+}
+
+void CursorSoftware::setImage(std::shared_ptr<IImage> image, const Point & pivotOffset)
+{
+	assert(image != nullptr);
+	cursorImage = image;
+	pivot = pivotOffset;
+	needUpdate = true;
+}
+
+void CursorSoftware::setCursorPosition( const Point & newPos )
+{
+	pos = newPos;
+}
+
+CursorSoftware::CursorSoftware():
+	cursorTexture(nullptr),
+	cursorSurface(nullptr),
+	needUpdate(false),
+	pivot(0,0)
+{
+	SDL_ShowCursor(SDL_DISABLE);
+}
+
+CursorSoftware::~CursorSoftware()
+{
+	if(cursorTexture)
+		SDL_DestroyTexture(cursorTexture);
+
+	if (cursorSurface)
+		SDL_FreeSurface(cursorSurface);
+
+}
+
+CursorHardware::CursorHardware():
+	cursor(nullptr)
+{
+}
+
+CursorHardware::~CursorHardware()
+{
+	if(cursor)
+		SDL_FreeCursor(cursor);
+}
+
+void CursorHardware::setImage(std::shared_ptr<IImage> image, const Point & pivotOffset)
+{
+	auto cursorSurface = CSDL_Ext::newSurface(image->dimensions().x, image->dimensions().y);
+
+	Uint32 fillColor = SDL_MapRGBA(cursorSurface->format, 0, 0, 0, 0);
+	CSDL_Ext::fillRect(cursorSurface, nullptr, fillColor);
+
+	image->draw(cursorSurface);
+
+	auto oldCursor = cursor;
+	cursor = SDL_CreateColorCursor(cursorSurface, pivotOffset.x, pivotOffset.y);
+
+	if (!cursor)
+		logGlobal->error("Failed to set cursor! SDL says %s", SDL_GetError());
+
+	SDL_FreeSurface(cursorSurface);
+	SDL_SetCursor(cursor);
+
+	if (oldCursor)
+		SDL_FreeCursor(oldCursor);
+}
+
+void CursorHardware::setCursorPosition( const Point & newPos )
+{
+	//no-op
+}
+
+void CursorHardware::render()
+{
+	//no-op
+}

+ 80 - 30
client/gui/CCursorHandler.h → client/gui/CursorHandler.h

@@ -8,11 +8,14 @@
  *
  */
 #pragma once
-class CIntObject;
-class CAnimImage;
+
+class CAnimation;
+class IImage;
 struct SDL_Surface;
 struct SDL_Texture;
-struct Point;
+struct SDL_Cursor;
+
+#include "Geometries.h"
 
 namespace Cursor
 {
@@ -51,7 +54,9 @@ namespace Cursor
 		SHOOT_CATAPULT = 16,
 		HEAL           = 17,
 		SACRIFICE      = 18,
-		TELEPORT       = 19
+		TELEPORT       = 19,
+
+		COUNT
 	};
 
 	enum class Map {
@@ -97,7 +102,9 @@ namespace Cursor
 		SCROLL_NORTHWEST = 39,
 		//POINTER_COPY       = 40, // probably unused
 		TELEPORT         = 41,
-		SCUTTLE_BOAT     = 42
+		SCUTTLE_BOAT     = 42,
+
+		COUNT
 	};
 
 	enum class Spellcast {
@@ -105,49 +112,92 @@ namespace Cursor
 	};
 }
 
-/// handles mouse cursor
-class CCursorHandler final
+class ICursor
 {
-	bool needUpdate;
-	SDL_Texture * cursorLayer;
+public:
+	virtual ~ICursor() = default;
+
+	virtual void setImage(std::shared_ptr<IImage> image, const Point & pivotOffset) = 0;
+	virtual void setCursorPosition( const Point & newPos ) = 0;
+	virtual void render() = 0;
+};
 
-	SDL_Surface * buffer;
-	CAnimImage * currentCursor;
+class CursorHardware : public ICursor
+{
+	std::shared_ptr<IImage> cursorImage;
 
-	std::unique_ptr<CAnimImage> dndObject; //if set, overrides currentCursor
+	SDL_Cursor * cursor;
 
-	std::array<std::unique_ptr<CAnimImage>, 4> cursors;
+public:
+	CursorHardware();
+	~CursorHardware();
 
-	bool showing;
+	void setImage(std::shared_ptr<IImage> image, const Point & pivotOffset) override;
+	void setCursorPosition( const Point & newPos ) override;
+	void render() override;
+};
+
+class CursorSoftware : public ICursor
+{
+	std::shared_ptr<IImage> cursorImage;
 
-	void clearBuffer();
-	void updateBuffer(CIntObject * payload);
-	void replaceBuffer(CIntObject * payload);
-	void shiftPos( int &x, int &y );
+	SDL_Texture * cursorTexture;
+	SDL_Surface * cursorSurface;
 
+	Point pos;
+	Point pivot;
+	bool needUpdate;
+
+	void createTexture(const Point & dimensions);
 	void updateTexture();
+public:
+	CursorSoftware();
+	~CursorSoftware();
+
+	void setImage(std::shared_ptr<IImage> image, const Point & pivotOffset) override;
+	void setCursorPosition( const Point & newPos ) override;
+	void render() override;
+};
+
+/// handles mouse cursor
+class CursorHandler final
+{
+	std::shared_ptr<IImage> dndObject; //if set, overrides currentCursor
+
+	std::array<std::unique_ptr<CAnimation>, 4> cursors;
+
+	bool showing;
 
 	/// Current cursor
 	Cursor::Type type;
 	size_t frame;
 	float frameTime;
+	Point pos;
 
 	void changeGraphic(Cursor::Type type, size_t index);
 
-	/// position of cursor
-	int xpos, ypos;
+	Point getPivotOffsetDefault(size_t index);
+	Point getPivotOffsetMap(size_t index);
+	Point getPivotOffsetCombat(size_t index);
+	Point getPivotOffsetSpellcast();
+	Point getPivotOffset();
+
+	void updateSpellcastCursor();
+
+	std::shared_ptr<IImage> getCurrentImage();
 
+	std::unique_ptr<ICursor> cursor;
+
+	static std::unique_ptr<ICursor> createCursor();
 public:
-	CCursorHandler();
-	~CCursorHandler();
-
-	/**
-	 * Replaces the cursor with a custom image.
-	 *
-	 * @param image Image to replace cursor with or nullptr to use the normal
-	 * cursor. CursorHandler takes ownership of object
-	 */
-	void dragAndDropCursor (std::unique_ptr<CAnimImage> image);
+	CursorHandler();
+	~CursorHandler();
+
+	/// Replaces the cursor with a custom image.
+	/// @param image Image to replace cursor with or nullptr to use the normal cursor.
+	void dragAndDropCursor(std::shared_ptr<IImage> image);
+
+	void dragAndDropCursor(std::string path, size_t index);
 
 	/// Returns current position of the cursor
 	Point position() const;

+ 1 - 1
client/mainmenu/CMainMenu.cpp

@@ -21,7 +21,7 @@
 #include "../../lib/filesystem/CCompressedStream.h"
 
 #include "../gui/SDL_Extensions.h"
-#include "../gui/CCursorHandler.h"
+#include "../gui/CursorHandler.h"
 
 #include "../CGameInfo.h"
 #include "../../lib/CGeneralTextHandler.h"

+ 66 - 62
client/widgets/CArtifactHolder.cpp

@@ -11,7 +11,7 @@
 #include "CArtifactHolder.h"
 
 #include "../gui/CGuiHandler.h"
-#include "../gui/CCursorHandler.h"
+#include "../gui/CursorHandler.h"
 
 #include "Buttons.h"
 #include "CComponent.h"
@@ -257,7 +257,7 @@ void CHeroArtPlace::clickRight(tribool down, bool previousState)
 void CArtifactsOfHero::activate()
 {
 	if (commonInfo->src.AOH == this && commonInfo->src.art)
-		CCS->curh->dragAndDropCursor(std::make_unique<CAnimImage>("artifact", commonInfo->src.art->artType->getIconIndex()));
+		CCS->curh->dragAndDropCursor("artifact", commonInfo->src.art->artType->getIconIndex());
 
 	CIntObject::activate();
 }
@@ -278,19 +278,18 @@ void CHeroArtPlace::select ()
 	if (locked)
 		return;
 
-	selectSlot(true);
 	pickSlot(true);
 	if(ourArt->canBeDisassembled() && slotID < GameConstants::BACKPACK_START) //worn combined artifact -> locks have to disappear
 	{
-		for(int i = 0; i < GameConstants::BACKPACK_START; i++)
+		for(auto slot : ArtifactUtils::constituentWornSlots())
 		{
-			auto ap = ourOwner->getArtPlace(i);
+			auto ap = ourOwner->getArtPlace(slot);
 			if(ap)//getArtPlace may return null
 				ap->pickSlot(ourArt->isPart(ap->ourArt));
 		}
 	}
 
-	CCS->curh->dragAndDropCursor(std::make_unique<CAnimImage>("artifact", ourArt->artType->getIconIndex()));
+	CCS->curh->dragAndDropCursor("artifact", ourArt->artType->getIconIndex());
 	ourOwner->commonInfo->src.setTo(this, false);
 	ourOwner->markPossibleSlots(ourArt);
 
@@ -309,9 +308,9 @@ void CHeroArtPlace::deselect ()
 	pickSlot(false);
 	if(ourArt && ourArt->canBeDisassembled()) //combined art returned to its slot -> restore locks
 	{
-		for(int i = 0; i < GameConstants::BACKPACK_START; i++)
+		for(auto slot : ArtifactUtils::constituentWornSlots())
 		{
-			auto place = ourOwner->getArtPlace(i);
+			auto place = ourOwner->getArtPlace(slot);
 
 			if(nullptr != place)//getArtPlace may return null
 				place->pickSlot(false);
@@ -670,6 +669,16 @@ CArtifactsOfHero::CArtifactsOfHero(const Point & position, bool createCommonPart
 CArtifactsOfHero::~CArtifactsOfHero()
 {
 	dispose();
+	// Artifact located in artifactsTransitionPos should be returned
+	if(!curHero->artifactsTransitionPos.empty())
+	{
+		auto artPlace = getArtPlace(
+			ArtifactUtils::getArtifactDstPosition(curHero->artifactsTransitionPos.begin()->artifact, curHero, curHero->bearerType()));
+		assert(artPlace);
+		assert(artPlace->ourOwner);
+		artPlace->setMeAsDest();
+		artPlace->ourOwner->realizeCurrentTransaction();
+	}
 }
 
 void CArtifactsOfHero::updateParentWindow()
@@ -716,85 +725,76 @@ void CArtifactsOfHero::realizeCurrentTransaction()
 								ArtifactLocation(commonInfo->dst.AOH->curHero, commonInfo->dst.slotID));
 }
 
-void CArtifactsOfHero::artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst)
+void CArtifactsOfHero::artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst)
 {
 	bool isCurHeroSrc = src.isHolder(curHero),
 		isCurHeroDst = dst.isHolder(curHero);
-	if(isCurHeroSrc && src.slot >= GameConstants::BACKPACK_START)
+	if(isCurHeroSrc && ArtifactUtils::isSlotBackpack(src.slot))
 		updateSlot(src.slot);
-	if(isCurHeroDst && dst.slot >= GameConstants::BACKPACK_START)
+	if(isCurHeroDst && ArtifactUtils::isSlotBackpack(dst.slot))
 		updateSlot(dst.slot);
-	if(isCurHeroSrc  ||  isCurHeroDst) //we need to update all slots, artifact might be combined and affect more slots
+	// We need to update all slots, artifact might be combined and affect more slots
+	if(isCurHeroSrc || isCurHeroDst)
 		updateWornSlots(false);
 
-	if (!src.isHolder(curHero) && !isCurHeroDst)
+	if(!isCurHeroSrc && !isCurHeroDst)
 		return;
 
-	if(commonInfo->src == src) //artifact was taken from us
+	// When moving one artifact onto another it leads to two art movements: dst->TRANSITION_POS; src->dst
+	// however after first movement we pick the art from TRANSITION_POS and the second movement coming when
+	// we have a different artifact may look surprising... but it's valid.
+
+	// Used when doing dragAndDrop and artifact swap multiple times
+	if(src.slot == ArtifactPosition::TRANSITION_POS && 
+		commonInfo->src.slotID == ArtifactPosition::TRANSITION_POS &&
+		commonInfo->dst.slotID == ArtifactPosition::PRE_FIRST && 
+		isCurHeroDst)
+	{
+		auto art = curHero->getArt(ArtifactPosition::TRANSITION_POS);
+		assert(art);
+		CCS->curh->dragAndDropCursor("artifact", art->artType->getIconIndex());
+		markPossibleSlots(art);
+
+		commonInfo->src.art = art;
+		commonInfo->src.slotID = src.slot;
+	}
+	// Artifact was taken from us
+	else if(commonInfo->src == src)
 	{
-		assert(commonInfo->dst == dst  //expected movement from slot ot slot
-			||  dst.slot == dst.getHolderArtSet()->artifactsInBackpack.size() + GameConstants::BACKPACK_START //artifact moved back to backpack (eg. to make place for art we are moving)
+		// Expected movement from slot ot slot
+		assert(commonInfo->dst == dst
+			// Artifact moved back to backpack (eg. to make place for art we are moving)
+			||  dst.slot == dst.getHolderArtSet()->artifactsInBackpack.size() + GameConstants::BACKPACK_START
 			|| dst.getHolderArtSet()->bearerType() != ArtBearer::HERO);
 		commonInfo->reset();
 		unmarkSlots();
 	}
-	else if(commonInfo->dst == src) //the dest artifact was moved -> we are picking it
+	// The dest artifact was moved after the swap -> we are picking it
+	else if(commonInfo->dst == src)
 	{
-		assert(dst.slot >= GameConstants::BACKPACK_START);
+		assert(dst.slot == ArtifactPosition::TRANSITION_POS);
 		commonInfo->reset();
 
-		CArtifactsOfHero::ArtPlacePtr ap;
-		for(CArtifactsOfHero *aoh : commonInfo->participants)
+		for(CArtifactsOfHero * aoh : commonInfo->participants)
 		{
 			if(dst.isHolder(aoh->curHero))
 			{
 				commonInfo->src.AOH = aoh;
-				if((ap = aoh->getArtPlace(dst.slot)))//getArtPlace may return null
-					break;
+				break;
 			}
 		}
 
-		if(ap)
-		{
-			ap->select();
-		}
-		else
-		{
-			commonInfo->src.art = dst.getArt();
-			commonInfo->src.slotID = dst.slot;
-			assert(commonInfo->src.AOH);
-			CCS->curh->dragAndDropCursor(std::make_unique<CAnimImage>("artifact", dst.getArt()->artType->getIconIndex()));
-			markPossibleSlots(dst.getArt());
-		}
-	}
-	else if(src.slot >= GameConstants::BACKPACK_START &&
-	        src.slot <  commonInfo->src.slotID &&
-			    src.isHolder(commonInfo->src.AOH->curHero)) //artifact taken from before currently picked one
-	{
-		//int fixedSlot = src.hero->getArtPos(commonInfo->src.art);
-		vstd::advance(commonInfo->src.slotID, -1);
-		assert(commonInfo->src.valid());
-	}
-	else
-	{
-		//when moving one artifact onto another it leads to two art movements: dst->backapck; src->dst
-		// however after first movement we pick the art from backpack and the second movement coming when
-		// we have a different artifact may look surprising... but it's valid.
+		commonInfo->src.art = dst.getArt();
+		commonInfo->src.slotID = dst.slot;
+		assert(commonInfo->src.AOH);
+		CCS->curh->dragAndDropCursor("artifact", dst.getArt()->artType->getIconIndex());
 	}
 
 	updateParentWindow();
-	int shift = 0;
-// 	if(dst.slot >= Arts::BACKPACK_START && dst.slot - Arts::BACKPACK_START < backpackPos)
-// 		shift++;
-//
-	if(src.slot < GameConstants::BACKPACK_START  &&  dst.slot - GameConstants::BACKPACK_START < backpackPos)
-		shift++;
-	if(dst.slot < GameConstants::BACKPACK_START  &&  src.slot - GameConstants::BACKPACK_START < backpackPos)
-		shift--;
-
-	if( (isCurHeroSrc && src.slot >= GameConstants::BACKPACK_START)
-	 || (isCurHeroDst && dst.slot >= GameConstants::BACKPACK_START) )
-		scrollBackpack(shift); //update backpack slots
+	// If backpack is changed, update it
+	if((isCurHeroSrc && ArtifactUtils::isSlotBackpack(src.slot))
+	 || (isCurHeroDst && ArtifactUtils::isSlotBackpack(dst.slot)))
+		scrollBackpack(0);
 }
 
 void CArtifactsOfHero::artifactRemoved(const ArtifactLocation &al)
@@ -808,11 +808,15 @@ void CArtifactsOfHero::artifactRemoved(const ArtifactLocation &al)
 	}
 }
 
-CArtifactsOfHero::ArtPlacePtr CArtifactsOfHero::getArtPlace(int slot)
+CArtifactsOfHero::ArtPlacePtr CArtifactsOfHero::getArtPlace(ArtifactPosition slot)
 {
+	if(slot == ArtifactPosition::TRANSITION_POS)
+	{
+		return nullptr;
+	}
 	if(slot < GameConstants::BACKPACK_START)
 	{
-		if(artWorn.find(ArtifactPosition(slot)) == artWorn.end())
+		if(artWorn.find(slot) == artWorn.end())
 		{
 			logGlobal->error("CArtifactsOfHero::getArtPlace: invalid slot %d", slot);
 			return nullptr;

+ 1 - 1
client/widgets/CArtifactHolder.h

@@ -141,7 +141,7 @@ public:
 	void artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst);
 	void artifactRemoved(const ArtifactLocation &al);
 	void artifactUpdateSlots(const ArtifactLocation &al);
-	ArtPlacePtr getArtPlace(int slot);//may return null
+	ArtPlacePtr getArtPlace(ArtifactPosition slot);//may return null
 
 	void setHero(const CGHeroInstance * hero);
 	const CGHeroInstance *getHero() const;

+ 1 - 1
client/widgets/CComponent.cpp

@@ -17,7 +17,7 @@
 #include <vcmi/spells/Spell.h>
 
 #include "../gui/CGuiHandler.h"
-#include "../gui/CCursorHandler.h"
+#include "../gui/CursorHandler.h"
 
 #include "../CMessage.h"
 #include "../CGameInfo.h"

+ 10 - 4
client/widgets/Images.cpp

@@ -15,7 +15,8 @@
 #include "../gui/CAnimation.h"
 #include "../gui/SDL_Pixels.h"
 #include "../gui/CGuiHandler.h"
-#include "../gui/CCursorHandler.h"
+#include "../gui/CursorHandler.h"
+#include "../gui/ColorFilter.h"
 
 #include "../battle/BattleInterface.h"
 #include "../battle/BattleInterfaceClasses.h"
@@ -339,7 +340,7 @@ void CAnimImage::playerColored(PlayerColor currPlayer)
 			anim->getImage(0, group)->playerColored(player);
 }
 
-CShowableAnim::CShowableAnim(int x, int y, std::string name, ui8 Flags, ui32 Delay, size_t Group):
+CShowableAnim::CShowableAnim(int x, int y, std::string name, ui8 Flags, ui32 Delay, size_t Group, uint8_t alpha):
 	anim(std::make_shared<CAnimation>(name)),
 	group(Group),
 	frame(0),
@@ -349,7 +350,7 @@ CShowableAnim::CShowableAnim(int x, int y, std::string name, ui8 Flags, ui32 Del
 	flags(Flags),
 	xOffset(0),
 	yOffset(0),
-	alpha(255)
+	alpha(alpha)
 {
 	anim->loadGroup(group);
 	last = anim->size(group);
@@ -454,7 +455,12 @@ void CShowableAnim::blitImage(size_t frame, size_t group, SDL_Surface *to)
 	Rect src( xOffset, yOffset, pos.w, pos.h);
 	auto img = anim->getImage(frame, group);
 	if(img)
-		img->draw(to, pos.x, pos.y, &src, alpha);
+	{
+		const ColorFilter alphaFilter = ColorFilter::genAlphaShifter(vstd::lerp(0.0f, 1.0f, alpha/255.0f));
+		img->adjustPalette(alphaFilter);
+
+		img->draw(to, pos.x, pos.y, &src);
+	}
 }
 
 void CShowableAnim::rotate(bool on, bool vertical)

+ 1 - 1
client/widgets/Images.h

@@ -142,7 +142,7 @@ public:
 	//Set per-surface alpha, 0 = transparent, 255 = opaque
 	void setAlpha(ui32 alphaValue);
 
-	CShowableAnim(int x, int y, std::string name, ui8 flags=0, ui32 Delay=4, size_t Group=0);
+	CShowableAnim(int x, int y, std::string name, ui8 flags=0, ui32 Delay=4, size_t Group=0, uint8_t alpha = UINT8_MAX);
 	~CShowableAnim();
 
 	//set animation to group or part of group

+ 1 - 1
client/widgets/MiscWidgets.cpp

@@ -13,7 +13,7 @@
 #include "CComponent.h"
 
 #include "../gui/CGuiHandler.h"
-#include "../gui/CCursorHandler.h"
+#include "../gui/CursorHandler.h"
 
 #include "../CPlayerInterface.h"
 #include "../CMessage.h"

+ 0 - 37
client/widgets/TextControls.cpp

@@ -524,9 +524,6 @@ CKeyboardFocusListener::CKeyboardFocusListener(CTextInput * textInput)
 void CKeyboardFocusListener::focusGot()
 {
 	CSDL_Ext::startTextInput(&textInput->pos);
-#ifdef VCMI_ANDROID
-	textInput->notifyAndroidTextInputChanged(textInput->text);
-#endif
 	usageIndex++;
 }
 
@@ -588,9 +585,6 @@ void CTextInput::keyPressed(const SDL_KeyboardEvent & key)
 	{
 		redraw();
 		cb(text);
-#ifdef VCMI_ANDROID
-		notifyAndroidTextInputChanged(text);
-#endif
 	}
 }
 
@@ -604,10 +598,6 @@ void CTextInput::setText(const std::string & nText, bool callCb)
 	CLabel::setText(nText);
 	if(callCb)
 		cb(text);
-
-#ifdef VCMI_ANDROID
-	notifyAndroidTextInputChanged(text);
-#endif
 }
 
 bool CTextInput::captureThisEvent(const SDL_KeyboardEvent & key)
@@ -633,10 +623,6 @@ void CTextInput::textInputed(const SDL_TextInputEvent & event)
 		cb(text);
 	}
 	newText.clear();
-
-#ifdef VCMI_ANDROID
-	notifyAndroidTextInputChanged(text);
-#endif
 }
 
 void CTextInput::textEdited(const SDL_TextEditingEvent & event)
@@ -647,11 +633,6 @@ void CTextInput::textEdited(const SDL_TextEditingEvent & event)
 	newText = event.text;
 	redraw();
 	cb(text + newText);
-
-#ifdef VCMI_ANDROID
-	auto editedText = text + newText;
-	notifyAndroidTextInputChanged(editedText);
-#endif
 }
 
 void CTextInput::filenameFilter(std::string & text, const std::string &)
@@ -698,24 +679,6 @@ void CTextInput::numberFilter(std::string & text, const std::string & oldText, i
 	}
 }
 
-#ifdef VCMI_ANDROID
-void CTextInput::notifyAndroidTextInputChanged(std::string & text)
-{
-	if(!focus)
-		return;
-
-	auto fun = [&text](JNIEnv * env, jclass cls, jmethodID method)
-	{
-		auto jtext = env->NewStringUTF(text.c_str());
-		env->CallStaticVoidMethod(cls, method, jtext);
-		env->DeleteLocalRef(jtext);
-	};
-	CAndroidVMHelper vmHelper;
-	vmHelper.callCustomMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "notifyTextInputChanged",
-		"(Ljava/lang/String;)V", fun, true);
-}
-#endif //VCMI_ANDROID
-
 CFocusable::CFocusable()
 	:CFocusable(std::make_shared<IFocusListener>())
 {

+ 0 - 3
client/widgets/TextControls.h

@@ -212,9 +212,6 @@ class CTextInput : public CLabel, public CFocusable
 protected:
 	std::string visibleText() override;
 
-#ifdef VCMI_ANDROID
-	void notifyAndroidTextInputChanged(std::string & text);
-#endif
 public:
 	CFunctionList<void(const std::string &)> cb;
 	CFunctionList<void(std::string &, const std::string &)> filters;

+ 2 - 2
client/windows/CAdvmapInterface.cpp

@@ -32,7 +32,7 @@
 #include "../mapHandler.h"
 
 #include "../gui/CAnimation.h"
-#include "../gui/CCursorHandler.h"
+#include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/SDL_Extensions.h"
 #include "../widgets/MiscWidgets.h"
@@ -1039,7 +1039,7 @@ void CAdvMapInt::show(SDL_Surface * to)
 	{
 		++heroAnim;
 	}
-	if(animValHitCount == 8)
+	if(animValHitCount >= 8)
 	{
 		CGI->mh->updateWater();
 		animValHitCount = 0;

+ 32 - 23
client/windows/CCastleInterface.cpp

@@ -44,15 +44,29 @@
 
 CBuildingRect::CBuildingRect(CCastleBuildings * Par, const CGTownInstance * Town, const CStructure * Str)
 	: CShowableAnim(0, 0, Str->defName, CShowableAnim::BASE),
-	parent(Par),
-	town(Town),
-	str(Str),
-	stateCounter(80)
+	  parent(Par),
+	  town(Town),
+	  str(Str),
+	  stateTimeCounter(BUILD_ANIMATION_FINISHED_TIMEPOINT)
 {
 	addUsedEvents(LCLICK | RCLICK | HOVER);
 	pos.x += str->pos.x;
 	pos.y += str->pos.y;
 
+	// special animation frame manipulation for castle shipyard with and without ship
+	// done due to .def used in special way, not to animate building - first image is for shipyard without citadel moat, 2nd image is for including moat
+	if(Town->town->faction->getId() == FactionID::CASTLE && Str->building &&
+		(Str->building->bid == BuildingID::SHIPYARD || Str->building->bid == BuildingID::SHIP))
+	{
+		if(Town->hasBuilt(BuildingID::CITADEL))
+		{
+			this->first = 1;
+			this->frame = 1;
+		}
+		else
+			this->last = 0;
+	}
+
 	if(!str->borderName.empty())
 		border = BitmapHandler::loadBitmap(str->borderName);
 	else
@@ -154,16 +168,11 @@ SDL_Color multiplyColors(const SDL_Color & b, const SDL_Color & a, double f)
 
 void CBuildingRect::show(SDL_Surface * to)
 {
-	const ui32 stageDelay = 16;
-
-	const ui32 S1_TRANSP  = 16; //0.5 sec building appear 0->100 transparency
-	const ui32 S2_WHITE_B = 32; //0.5 sec border glows from white to yellow
-	const ui32 S3_YELLOW_B= 48; //0.5 sec border glows from yellow to normal
-	const ui32 BUILDED    = 80; //  1 sec delay, nothing happens
+	uint32_t stageDelay = BUILDING_APPEAR_TIMEPOINT;
 
-	if(stateCounter < S1_TRANSP)
+	if(stateTimeCounter < BUILDING_APPEAR_TIMEPOINT)
 	{
-		setAlpha(255*stateCounter/stageDelay);
+		setAlpha(255 * stateTimeCounter / stageDelay);
 		CShowableAnim::show(to);
 	}
 	else
@@ -172,9 +181,9 @@ void CBuildingRect::show(SDL_Surface * to)
 		CShowableAnim::show(to);
 	}
 
-	if(border && stateCounter > S1_TRANSP)
+	if(border && stateTimeCounter > BUILDING_APPEAR_TIMEPOINT)
 	{
-		if(stateCounter == BUILDED)
+		if(stateTimeCounter >= BUILD_ANIMATION_FINISHED_TIMEPOINT)
 		{
 			if(parent->selectedBuilding == this)
 				blitAtLoc(border,0,0,to);
@@ -191,11 +200,11 @@ void CBuildingRect::show(SDL_Surface * to)
 			SDL_Color oldColor = border->format->palette->colors[colorID];
 			SDL_Color newColor;
 
-			if (stateCounter < S2_WHITE_B)
-				newColor = multiplyColors(c1, c2, static_cast<double>(stateCounter % stageDelay) / stageDelay);
+			if (stateTimeCounter < BUILDING_WHITE_BORDER_TIMEPOINT)
+				newColor = multiplyColors(c1, c2, static_cast<double>(stateTimeCounter % stageDelay) / stageDelay);
 			else
-			if (stateCounter < S3_YELLOW_B)
-				newColor = multiplyColors(c2, c3, static_cast<double>(stateCounter % stageDelay) / stageDelay);
+			if (stateTimeCounter < BUILDING_YELLOW_BORDER_TIMEPOINT)
+				newColor = multiplyColors(c2, c3, static_cast<double>(stateTimeCounter % stageDelay) / stageDelay);
 			else
 				newColor = oldColor;
 
@@ -204,13 +213,13 @@ void CBuildingRect::show(SDL_Surface * to)
 			SDL_SetColors(border, &oldColor, colorID, 1);
 		}
 	}
-	if(stateCounter < BUILDED)
-		stateCounter++;
+	if(stateTimeCounter < BUILD_ANIMATION_FINISHED_TIMEPOINT)
+		stateTimeCounter += GH.mainFPSmng->getElapsedMilliseconds();
 }
 
 void CBuildingRect::showAll(SDL_Surface * to)
 {
-	if (stateCounter == 0)
+	if (stateTimeCounter == 0)
 		return;
 
 	CShowableAnim::showAll(to);
@@ -632,9 +641,9 @@ void CCastleBuildings::addBuilding(BuildingID building)
 		{
 			//reset animation
 			if(structures.size() == 1)
-				buildingRect->stateCounter = 0; // transparency -> fully visible stage
+				buildingRect->stateTimeCounter = 0; // transparency -> fully visible stage
 			else
-				buildingRect->stateCounter = 16; // already in fully visible stage
+				buildingRect->stateTimeCounter = CBuildingRect::BUILDING_APPEAR_TIMEPOINT; // already in fully visible stage
 			break;
 		}
 	}

+ 9 - 1
client/windows/CCastleInterface.h

@@ -42,6 +42,14 @@ class CBuildingRect : public CShowableAnim
 {
 	std::string getSubtitle();
 public:
+	enum EBuildingCreationAnimationPhases : uint32_t
+	{
+		BUILDING_APPEAR_TIMEPOINT = 500, //500 msec building appears: 0->100% transparency
+		BUILDING_WHITE_BORDER_TIMEPOINT = 1000, //500 msec border glows from white to yellow
+		BUILDING_YELLOW_BORDER_TIMEPOINT = 1500, //500 msec border glows from yellow to normal
+		BUILD_ANIMATION_FINISHED_TIMEPOINT = 2500 //1000 msec delay, nothing happens
+	};
+
 	/// returns building associated with this structure
 	const CBuilding * getBuilding();
 
@@ -51,7 +59,7 @@ public:
 	SDL_Surface* border;
 	SDL_Surface* area;
 
-	ui32 stateCounter;//For building construction - current stage in animation
+	ui32 stateTimeCounter;//For building construction - current stage in animation
 
 	CBuildingRect(CCastleBuildings * Par, const CGTownInstance *Town, const CStructure *Str);
 	~CBuildingRect();

+ 2 - 2
client/windows/CTradeWindow.cpp

@@ -13,7 +13,7 @@
 #include "CAdvmapInterface.h"
 
 #include "../gui/CGuiHandler.h"
-#include "../gui/CCursorHandler.h"
+#include "../gui/CursorHandler.h"
 #include "../widgets/Images.h"
 
 #include "../CGameInfo.h"
@@ -188,7 +188,7 @@ void CTradeWindow::CTradeableItem::clickLeft(tribool down, bool previousState)
 				aw->arts->markPossibleSlots(art);
 
 				//aw->arts->commonInfo->dst.AOH = aw->arts;
-				CCS->curh->dragAndDropCursor(std::make_unique<CAnimImage>("artifact", art->artType->iconIndex));
+				CCS->curh->dragAndDropCursor("artifact", art->artType->iconIndex);
 
 				aw->arts->artifactsOnAltar.erase(art);
 				setID(-1);

+ 1 - 1
client/windows/CWindowObject.cpp

@@ -18,7 +18,7 @@
 #include "../gui/SDL_Pixels.h"
 #include "../gui/SDL_Extensions.h"
 #include "../gui/CGuiHandler.h"
-#include "../gui/CCursorHandler.h"
+#include "../gui/CursorHandler.h"
 
 #include "../battle/BattleInterface.h"
 #include "../battle/BattleInterfaceClasses.h"

+ 1 - 7
client/windows/GUIClasses.cpp

@@ -32,7 +32,7 @@
 #include "../gui/CAnimation.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/SDL_Extensions.h"
-#include "../gui/CCursorHandler.h"
+#include "../gui/CursorHandler.h"
 
 #include "../widgets/CComponent.h"
 #include "../widgets/MiscWidgets.h"
@@ -1247,12 +1247,6 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2,
 	updateWidgets();
 }
 
-CExchangeWindow::~CExchangeWindow()
-{
-	artifs[0]->commonInfo = nullptr;
-	artifs[1]->commonInfo = nullptr;
-}
-
 const CGarrisonSlot * CExchangeWindow::getSelectedSlotID() const
 {
 	return garr->getSelection();

+ 0 - 1
client/windows/GUIClasses.h

@@ -379,7 +379,6 @@ public:
 	const CGarrisonSlot * getSelectedSlotID() const;
 
 	CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID);
-	~CExchangeWindow();
 };
 
 /// Here you can buy ships

+ 1 - 1
client/windows/InfoWindows.cpp

@@ -24,7 +24,7 @@
 #include "../gui/SDL_Pixels.h"
 #include "../gui/SDL_Extensions.h"
 #include "../gui/CGuiHandler.h"
-#include "../gui/CCursorHandler.h"
+#include "../gui/CursorHandler.h"
 
 #include "../battle/BattleInterface.h"
 #include "../battle/BattleInterfaceClasses.h"

+ 1 - 1
conanfile.py

@@ -13,7 +13,7 @@ class VCMI(ConanFile):
         "minizip/[~1.2.12]",
         "onetbb/[^2021.3]", # Nullkiller AI
         "qt/[~5.15.2]", # launcher
-        "sdl/[~2.24.0]",
+        "sdl/[~2.26.1 || >=2.0.20 <=2.22.0]", # versions in between have broken sound
         "sdl_image/[~2.0.5]",
         "sdl_mixer/[~2.0.4]",
         "sdl_ttf/[~2.0.18]",

+ 2 - 2
config/factions/castle.json

@@ -84,7 +84,7 @@
 				"mageGuild3":     { "animation" : "TBCSMAG3.def", "x" : 704, "y" : 107, "z" : 1, "border" : "TOCSM301.bmp", "area" : "TZCSM301.bmp" },
 				"mageGuild4":     { "animation" : "TBCSMAG4.def", "x" : 704, "y" : 76,  "z" : 1, "border" : "TOCSM401.bmp", "area" : "TZCSM401.bmp" },
 				"tavern":         { "animation" : "TBCSTVRN.def", "x" : 0,   "y" : 230, "z" : 1, "border" : "TOCSTAV1.bmp", "area" : "TZCSTAV1.bmp" },
-				"shipyard":       { "animation" : "TBCSDOCK.def", "x" : 478, "y" : 134, "border" : "TOCSDKMS.bmp", "area" : "TZCSDKMS.bmp" },
+				"shipyard":       { "animation" : "TBCSDOCK.def", "x" : 478, "y" : 134, "z" : 1, "border" : "TOCSDKMS.bmp", "area" : "TZCSDKMS.bmp" },
 				"fort":           { "animation" : "TBCSCSTL.def", "x" : 595, "y" : 66,  "border" : "TOCSCAS1.bmp", "area" : "TZCSCAS1.bmp" },
 				"citadel":        { "animation" : "TBCSCAS2.def", "x" : 478, "y" : 66,  "border" : "TOCSCAS2.bmp", "area" : "TZCSCAS2.bmp" },
 				"castle":         { "animation" : "TBCSCAS3.def", "x" : 478, "y" : 37,  "border" : "TOCSCAS3.bmp", "area" : "TZCSCAS3.bmp" },
@@ -98,7 +98,7 @@
 				"special1":       { "animation" : "TBCSSPEC.def", "x" : 533, "y" : 71,  "border" : "TOCSLT01.bmp", "area" : "TZCSLT01.bmp" },
 				"horde1":         { "animation" : "TBCSHRD1.def", "x" : 76,  "y" : 53,  "z" : -1, "border" : "TOCSGR1H.bmp", "area" : "TZCSGR1H.bmp", "hidden" : true },
 				"horde1Upgr":     { "animation" : "TBCSHRD2.def", "x" : 76,  "y" : 35,  "z" : -1, "border" : "TOCSGR2H.bmp", "area" : "TZCSGR2H.bmp", "hidden" : true, "builds" : "horde1" },
-				"ship":           { "animation" : "TBCSBOAT.def", "x" : 478, "y" : 134, "border" : "TOCSDKMN.bmp", "area" : "TZCSDKMN.bmp", "hidden" : true },
+				"ship":           { "animation" : "TBCSBOAT.def", "x" : 478, "y" : 134, "z" : 1, "border" : "TOCSDKMN.bmp", "area" : "TZCSDKMN.bmp", "hidden" : true },
 				"special2":       { "animation" : "TBCSEXT0.def", "x" : 384, "y" : 193, "z" : -2, "border" : "TOCSCAVM.bmp", "area" : "TZCSCAVM.bmp" },
 				"special3":       { "animation" : "TBCSEXT1.def", "x" : 0,   "y" : 198, "z" :  1, "border" : "TOCSTAV2.bmp", "area" : "TZCSTAV2.bmp" },
 				"grail":          { "animation" : "TBCSHOLY.def", "x" : 456, "y" : 109, "z" : -1, "border" : "TOCSHOLY.bmp", "area" : "TZCSHOLY.bmp" },

+ 6 - 2
config/schemas/settings.json

@@ -37,7 +37,7 @@
 				},
 				"encoding" : {
 					"type" : "string",
-					"default" : "CP1252"
+					"default" : "auto"
 				},
 				"swipe" : {
 					"type" : "boolean",
@@ -81,7 +81,7 @@
 			"type" : "object",
 			"additionalProperties" : false,
 			"default": {},
-			"required" : [ "screenRes", "bitsPerPixel", "fullscreen", "realFullscreen", "spellbookAnimation","driver", "showIntro", "displayIndex" ],
+			"required" : [ "screenRes", "bitsPerPixel", "fullscreen", "realFullscreen", "softwareCursor", "spellbookAnimation", "driver", "showIntro", "displayIndex" ],
 			"properties" : {
 				"screenRes" : {
 					"type" : "object",
@@ -105,6 +105,10 @@
 					"type" : "boolean",
 					"default" : false
 				},
+				"softwareCursor" :  {
+					"type" : "boolean",
+					"default" : false
+				},
 				"showIntro" : {
 					"type" : "boolean",
 					"default" : true

+ 1 - 1
config/schemas/terrain.json

@@ -68,7 +68,7 @@
 		"rockTerrain":
 		{
 			"type": "string",
-			"description": "The name of tock type terrain which will be used as borders in the underground"
+			"description": "The name of rock type terrain which will be used as borders in the underground"
 		},
 		"river":
 		{

+ 2 - 2
config/widgets/battleWindow.json

@@ -115,7 +115,7 @@
 		{
 			"type": "button",
 			"name": "tacticNext",
-			"position": {"x": 213, "y": 4},
+			"position": {"x": 213, "y": 560},
 			"image": "icm011",
 			"callback": "tacticNext",
 			"hotkey": "space"
@@ -124,7 +124,7 @@
 		{
 			"type": "button",
 			"name": "tacticEnd",
-			"position": {"x": 419, "y": 4},
+			"position": {"x": 419, "y": 560},
 			"image": "icm012",
 			"callback": "tacticEnd",
 			"hotkey": "enter"

+ 6 - 4
docs/conan.md

@@ -20,8 +20,8 @@ The following platforms are supported and known to work, others might require ch
 
 1. Check if your build environment can use the prebuilt binaries: basically, that your compiler version (or Xcode major version) matches the information below. If you're unsure, simply advance to the next step.
 
-    - macOS: libraries are built with Apple clang 13 (Xcode 13.4.1), should be consumable by Xcode and Xcode CLT 13.x
-    - iOS: libraries are built with Apple clang 13 (Xcode 13.4.1), should be consumable by Xcode 13.x
+    - macOS: libraries are built with Apple clang 14 (Xcode 14.2), should be consumable by Xcode and Xcode CLT 14.x (older library versions are also available for Xcode 13, see Releases in the respective repo)
+    - iOS: libraries are built with Apple clang 14 (Xcode 14.2), should be consumable by Xcode 14.x (older library versions are also available for Xcode 13, see Releases in the respective repo)
 
 2. Download the binaries archive and unpack it to `~/.conan` directory:
 
@@ -85,7 +85,8 @@ conan install . \
   --no-imports \
   --build=never \
   --profile:build=default \
-  --profile:host=CI/conan/macos-intel
+  --profile:host=CI/conan/macos-intel \
+  -o with_apple_system_libs=True
 
 cmake -S . -B build -G Xcode \
   --toolchain conan-generated/conan_toolchain.cmake
@@ -116,7 +117,8 @@ conan install . \
   --no-imports \
   --build=never \
   --profile:build=default \
-  --profile:host=CI/conan/ios-arm64
+  --profile:host=CI/conan/ios-arm64 \
+  -o with_apple_system_libs=True
 
 cmake --preset ios-conan
 ```

+ 5 - 0
launcher/CMakeLists.txt

@@ -128,6 +128,11 @@ enable_pch(vcmilauncher)
 if(APPLE_IOS)
 	set(ICONS_DESTINATION ${DATA_DIR})
 
+	# TODO: remove after fixing Conan's Qt recipe
+	if(XCODE_VERSION VERSION_GREATER_EQUAL 14.0)
+		target_link_libraries(vcmilauncher "-framework IOKit")
+	endif()
+
 	# workaround https://github.com/conan-io/conan-center-index/issues/13332
 	if(USING_CONAN)
 		file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/QIOSIntegrationPlugin.h

+ 27 - 4
launcher/lobby/lobby.h

@@ -12,7 +12,7 @@
 #include <QTcpSocket>
 #include <QAbstractSocket>
 
-const unsigned int ProtocolVersion = 3;
+const unsigned int ProtocolVersion = 4;
 const std::string ProtocolEncoding = "utf8";
 
 class ProtocolError: public std::runtime_error
@@ -24,10 +24,10 @@ public:
 enum ProtocolConsts
 {
 	//client consts
-	GREETING, USERNAME, MESSAGE, VERSION, CREATE, JOIN, LEAVE, KICK, READY, FORCESTART,
+	GREETING, USERNAME, MESSAGE, VERSION, CREATE, JOIN, LEAVE, KICK, READY, FORCESTART, HERE, ALIVE, HOSTMODE,
 
 	//server consts
-	SESSIONS, CREATED, JOINED, KICKED, SRVERROR, CHAT, START, STATUS, HOST, MODS, CLIENTMODS
+	SESSIONS, CREATED, JOINED, KICKED, SRVERROR, CHAT, START, STATUS, HOST, MODS, CLIENTMODS, USERS, HEALTH, GAMEMODE
 };
 
 const QMap<ProtocolConsts, QString> ProtocolStrings
@@ -78,6 +78,16 @@ const QMap<ProtocolConsts, QString> ProtocolStrings
 	//[unsupported] start session immediately
 	//%1: room name
 	{FORCESTART, "<FORCESTART>%1"},
+	
+	//request user list
+	{HERE, "<HERE>"},
+	
+	//used as reponse to healcheck
+	{ALIVE, "<ALIVE>"},
+	
+	//host sets game mode (new game or load game)
+	//%1: game mode - 0 for new game, 1 for load game
+	{HOSTMODE, "<HOSTMODE>%1"},
 
 	//=== server commands ===
 	//server commands are started from :>>, arguments are enumerated by : symbol
@@ -140,7 +150,20 @@ const QMap<ProtocolConsts, QString> ProtocolStrings
 	//received chat message
 	//arg[0]: sender username
 	//arg[1]: message text
-	{CHAT, "MSG"}
+	{CHAT, "MSG"},
+	
+	//list of users currently in lobby
+	//arg[0]: amount of players, following arguments depend on it
+	//arg[x]: username
+	//arg[x+1]: room (empty if not in the room)
+	{USERS, "USERS"},
+	
+	//healthcheck from server
+	{HEALTH, "HEALTH"},
+	
+	//game mode (new game or load game) set by host
+	//arg[0]: game mode
+	{GAMEMODE, "GAMEMODE"},
 };
 
 class ServerCommand

+ 162 - 24
launcher/lobby/lobby_moc.cpp

@@ -16,6 +16,18 @@
 #include "../modManager/cmodlist.h"
 #include "../../lib/CConfigHandler.h"
 
+enum GameMode
+{
+	NEW_GAME = 0, LOAD_GAME = 1
+};
+
+enum ModResolutionRoles
+{
+	ModNameRole = Qt::UserRole + 1,
+	ModEnableRole,
+	ModResolvableRole
+};
+
 Lobby::Lobby(QWidget *parent) :
 	QWidget(parent),
 	ui(new Ui::Lobby)
@@ -134,10 +146,14 @@ void Lobby::serverCommand(const ServerCommand & command) try
 
 		if(args[1] == username)
 		{
+			hostModsMap.clear();
 			ui->buttonReady->setText("Ready");
-			ui->chat->clear(); //cleanup the chat
+			ui->optNewGame->setChecked(true);
 			sysMessage(joinStr.arg("you", args[0]));
 			session = args[0];
+			bool isHost = command.command == JOINED && hostSession == session;
+			ui->optNewGame->setEnabled(isHost);
+			ui->optLoadGame->setEnabled(isHost);
 			ui->stackedWidget->setCurrentWidget(command.command == JOINED ? ui->roomPage : ui->sessionsPage);
 		}
 		else
@@ -152,33 +168,15 @@ void Lobby::serverCommand(const ServerCommand & command) try
 		protocolAssert(amount * 2 == (args.size() - 1));
 
 		tagPoint = 1;
-		ui->modsList->clear();
-		auto enabledMods = buildModsMap();
 		for(int i = 0; i < amount; ++i, tagPoint += 2)
-		{
-			if(enabledMods.contains(args[tagPoint]))
-			{
-				if(enabledMods[args[tagPoint]] == args[tagPoint + 1])
-					enabledMods.remove(args[tagPoint]);
-				else
-					ui->modsList->addItem(new QListWidgetItem(QIcon("icons:mod-update.png"), QString("%1 (v%2)").arg(args[tagPoint], args[tagPoint + 1])));
-			}
-			else if(isModAvailable(args[tagPoint], args[tagPoint + 1]))
-				ui->modsList->addItem(new QListWidgetItem(QIcon("icons:mod-enabled.png"), QString("%1 (v%2)").arg(args[tagPoint], args[tagPoint + 1])));
-			else
-				ui->modsList->addItem(new QListWidgetItem(QIcon("icons:mod-delete.png"), QString("%1 (v%2)").arg(args[tagPoint], args[tagPoint + 1])));
-		}
-		for(auto & remainMod : enabledMods.keys())
-		{
-			ui->modsList->addItem(new QListWidgetItem(QIcon("icons:mod-disabled.png"), QString("%1 (v%2)").arg(remainMod, enabledMods[remainMod])));
-		}
-		if(!ui->modsList->count())
-			ui->modsList->addItem("No issues detected");
+			hostModsMap[args[tagPoint]] = args[tagPoint + 1];
+		
+		updateMods();
 		break;
 		}
 			
 	case CLIENTMODS: {
-		protocolAssert(args.size() > 1);
+		protocolAssert(args.size() >= 1);
 		amount = args[1].toInt();
 		protocolAssert(amount * 2 == (args.size() - 2));
 
@@ -217,6 +215,8 @@ void Lobby::serverCommand(const ServerCommand & command) try
 		gameArgs << "--lobby";
 		gameArgs << "--lobby-address" << serverUrl;
 		gameArgs << "--lobby-port" << QString::number(serverPort);
+		gameArgs << "--lobby-username" << username;
+		gameArgs << "--lobby-gamemode" << QString::number(isLoadGameMode);
 		gameArgs << "--uuid" << args[0];
 		startGame(gameArgs);		
 		break;
@@ -238,6 +238,34 @@ void Lobby::serverCommand(const ServerCommand & command) try
 		chatMessage(args[0], msg);
 		break;
 		}
+			
+	case HEALTH: {
+		socketLobby.send(ProtocolStrings[ALIVE]);
+		break;
+	}
+			
+	case USERS: {
+		protocolAssert(args.size() > 0);
+		amount = args[0].toInt();
+		
+		protocolAssert(amount == (args.size() - 1));
+		ui->listUsers->clear();
+		for(int i = 0; i < amount; ++i)
+		{
+			ui->listUsers->addItem(new QListWidgetItem(args[i + 1]));
+		}
+		break;
+	}
+			
+	case GAMEMODE: {
+		protocolAssert(args.size() == 1);
+		isLoadGameMode = args[0].toInt();
+		if(isLoadGameMode)
+			ui->optLoadGame->setChecked(true);
+		else
+			ui->optNewGame->setChecked(true);
+		break;
+	}
 
 	default:
 		sysMessage("Unknown server command");
@@ -291,7 +319,7 @@ void Lobby::onDisconnected()
 	ui->userEdit->setEnabled(true);
 	ui->newButton->setEnabled(false);
 	ui->joinButton->setEnabled(false);
-	ui->sessionsTable->clear();
+	ui->sessionsTable->setRowCount(0);
 }
 
 void Lobby::chatMessage(QString title, QString body, bool isSystem)
@@ -329,6 +357,7 @@ void Lobby::on_connectButton_toggled(bool checked)
 {
 	if(checked)
 	{
+		ui->connectButton->setText(tr("Disconnect"));
 		authentificationStatus = AuthStatus::AUTH_NONE;
 		username = ui->userEdit->text();
 		const int connectionTimeout = settings["launcher"]["connectionTimeout"].Integer();
@@ -360,12 +389,74 @@ void Lobby::on_connectButton_toggled(bool checked)
 	}
 	else
 	{
+		ui->connectButton->setText(tr("Connect"));
 		ui->serverEdit->setEnabled(true);
 		ui->userEdit->setEnabled(true);
+		ui->listUsers->clear();
+		hostModsMap.clear();
+		updateMods();
 		socketLobby.disconnectServer();
 	}
 }
 
+void Lobby::updateMods()
+{
+	ui->modsList->clear();
+	if(hostModsMap.empty())
+		return;
+	
+	auto createModListWidget = [](const QIcon & icon, const QString & label, const QString & name, bool enableFlag, bool resolveFlag)
+	{
+		auto * lw = new QListWidgetItem(icon, label);
+		lw->setData(ModResolutionRoles::ModNameRole, name);
+		lw->setData(ModResolutionRoles::ModEnableRole, enableFlag);
+		lw->setData(ModResolutionRoles::ModResolvableRole, resolveFlag);
+		return lw;
+	};
+	
+	auto enabledMods = buildModsMap();
+	for(const auto & mod : hostModsMap.keys())
+	{
+		auto & modValue = hostModsMap[mod];
+		auto modName = QString("%1 (v%2)").arg(mod, modValue);
+		if(enabledMods.contains(mod))
+		{
+			if(enabledMods[mod] == modValue)
+				enabledMods.remove(mod); //mod fully matches, remove from list
+			else
+			{
+				//mod version mismatch
+				ui->modsList->addItem(createModListWidget(QIcon("icons:mod-update.png"), modName, mod, true, false));
+			}
+		}
+		else if(isModAvailable(mod, modValue))
+		{
+			//mod is available and needs to be enabled
+			ui->modsList->addItem(createModListWidget(QIcon("icons:mod-enabled.png"), modName, mod, true, true));
+		}
+		else
+		{
+			//mod is not available and needs to be installed
+			ui->modsList->addItem(createModListWidget(QIcon("icons:mod-delete.png"), modName, mod, true, false));
+		}
+	}
+	for(const auto & remainMod : enabledMods.keys())
+	{
+		auto modName = QString("%1 (v%2)").arg(remainMod, enabledMods[remainMod]);
+		//mod needs to be disabled
+		ui->modsList->addItem(createModListWidget(QIcon("icons:mod-disabled.png"), modName, remainMod, false, true));
+	}
+	if(!ui->modsList->count())
+	{
+		ui->buttonResolve->setEnabled(false);
+		ui->modsList->addItem(tr("No issues detected"));
+	}
+	else
+	{
+		ui->buttonResolve->setEnabled(true);
+	}
+}
+
 void Lobby::on_newButton_clicked()
 {
 	new LobbyRoomRequest(socketLobby, "", buildModsMap(), this);
@@ -417,3 +508,50 @@ void Lobby::on_kickButton_clicked()
 		socketLobby.send(ProtocolStrings[KICK].arg(ui->playersList->currentItem()->text()));
 }
 
+
+void Lobby::on_buttonResolve_clicked()
+{
+	QStringList toEnableList, toDisableList;
+	for(auto * item : ui->modsList->selectedItems())
+	{
+		auto modName = item->data(ModResolutionRoles::ModNameRole);
+		if(modName.isNull())
+			continue;
+		
+		bool modToEnable = item->data(ModResolutionRoles::ModEnableRole).toBool();
+		bool modToResolve = item->data(ModResolutionRoles::ModResolvableRole).toBool();
+		
+		if(!modToResolve)
+			continue;
+		
+		if(modToEnable)
+			toEnableList << modName.toString();
+		else
+			toDisableList << modName.toString();
+	}
+	
+	//disabling first, then enabling
+	for(auto & mod : toDisableList)
+		emit disableMod(mod);
+	for(auto & mod : toEnableList)
+		emit enableMod(mod);
+}
+
+void Lobby::on_optNewGame_toggled(bool checked)
+{
+	if(checked)
+	{
+		if(isLoadGameMode)
+			socketLobby.send(ProtocolStrings[HOSTMODE].arg(GameMode::NEW_GAME));
+	}
+}
+
+void Lobby::on_optLoadGame_toggled(bool checked)
+{
+	if(checked)
+	{
+		if(!isLoadGameMode)
+			socketLobby.send(ProtocolStrings[HOSTMODE].arg(GameMode::LOAD_GAME));
+	}
+}
+

+ 16 - 0
launcher/lobby/lobby_moc.h

@@ -22,6 +22,14 @@ class Lobby : public QWidget
 public:
 	explicit Lobby(QWidget *parent = nullptr);
 	~Lobby();
+	
+signals:
+	
+	void enableMod(QString mod);
+	void disableMod(QString mod);
+	
+public slots:
+	void updateMods();
 
 private slots:
 	void on_messageEdit_returnPressed();
@@ -49,9 +57,16 @@ private slots:
 
 	void on_kickButton_clicked();
 
+	void on_buttonResolve_clicked();
+
+	void on_optNewGame_toggled(bool checked);
+
+	void on_optLoadGame_toggled(bool checked);
+
 private:
 	QString serverUrl;
 	int serverPort;
+	bool isLoadGameMode = false;
 	
 	Ui::Lobby *ui;
 	SocketLobby socketLobby;
@@ -59,6 +74,7 @@ private:
 	QString session;
 	QString username;
 	QStringList gameArgs;
+	QMap<QString, QString> hostModsMap;
 
 	enum AuthStatus
 	{

+ 229 - 136
launcher/lobby/lobby_moc.ui

@@ -7,48 +7,42 @@
     <x>0</x>
     <y>0</y>
     <width>652</width>
-    <height>329</height>
+    <height>383</height>
    </rect>
   </property>
   <property name="windowTitle">
    <string>Form</string>
   </property>
   <layout class="QGridLayout" name="gridLayout">
-   <item row="0" column="5">
-    <widget class="QPushButton" name="connectButton">
+   <item row="0" column="3">
+    <widget class="QLabel" name="label_4">
      <property name="sizePolicy">
-      <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+      <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
        <horstretch>0</horstretch>
        <verstretch>0</verstretch>
       </sizepolicy>
      </property>
      <property name="text">
-      <string>Connect</string>
-     </property>
-     <property name="checkable">
-      <bool>true</bool>
+      <string>Username</string>
      </property>
     </widget>
    </item>
-   <item row="3" column="0" colspan="3">
-    <widget class="QLineEdit" name="messageEdit"/>
-   </item>
-   <item row="0" column="3">
-    <widget class="QLabel" name="label_4">
+   <item row="0" column="5">
+    <widget class="QPushButton" name="connectButton">
      <property name="sizePolicy">
-      <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+      <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
        <horstretch>0</horstretch>
        <verstretch>0</verstretch>
       </sizepolicy>
      </property>
      <property name="text">
-      <string>Username</string>
+      <string>Connect</string>
+     </property>
+     <property name="checkable">
+      <bool>true</bool>
      </property>
     </widget>
    </item>
-   <item row="0" column="4">
-    <widget class="QLineEdit" name="userEdit"/>
-   </item>
    <item row="0" column="1">
     <widget class="QLineEdit" name="serverEdit">
      <property name="text">
@@ -56,13 +50,6 @@
      </property>
     </widget>
    </item>
-   <item row="2" column="0" colspan="3">
-    <widget class="QPlainTextEdit" name="chat">
-     <property name="readOnly">
-      <bool>true</bool>
-     </property>
-    </widget>
-   </item>
    <item row="0" column="0">
     <widget class="QLabel" name="label">
      <property name="text">
@@ -70,144 +57,250 @@
      </property>
     </widget>
    </item>
-   <item row="2" column="3" rowspan="2" colspan="3">
-    <widget class="QStackedWidget" name="stackedWidget">
-     <property name="currentIndex">
+   <item row="0" column="4">
+    <widget class="QLineEdit" name="userEdit"/>
+   </item>
+   <item row="1" column="0" colspan="6">
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <property name="bottomMargin">
       <number>0</number>
      </property>
-     <widget class="QWidget" name="sessionsPage">
-      <layout class="QGridLayout" name="gridLayout_2">
-       <item row="0" column="0" colspan="2">
-        <widget class="QTableWidget" name="sessionsTable">
-         <property name="editTriggers">
-          <set>QAbstractItemView::NoEditTriggers</set>
-         </property>
-         <property name="selectionMode">
-          <enum>QAbstractItemView::SingleSelection</enum>
-         </property>
-         <property name="selectionBehavior">
-          <enum>QAbstractItemView::SelectRows</enum>
-         </property>
-         <attribute name="horizontalHeaderCascadingSectionResizes">
-          <bool>false</bool>
-         </attribute>
-         <attribute name="horizontalHeaderDefaultSectionSize">
-          <number>80</number>
-         </attribute>
-         <attribute name="horizontalHeaderShowSortIndicator" stdset="0">
-          <bool>false</bool>
-         </attribute>
-         <attribute name="horizontalHeaderStretchLastSection">
-          <bool>true</bool>
-         </attribute>
-         <attribute name="verticalHeaderMinimumSectionSize">
-          <number>20</number>
-         </attribute>
-         <attribute name="verticalHeaderDefaultSectionSize">
-          <number>20</number>
-         </attribute>
-         <column>
-          <property name="text">
-           <string>Session</string>
-          </property>
-         </column>
-         <column>
-          <property name="text">
-           <string>Players</string>
-          </property>
-         </column>
-         <column>
-          <property name="text">
-           <string/>
-          </property>
-         </column>
-        </widget>
-       </item>
-       <item row="1" column="0">
-        <widget class="QPushButton" name="newButton">
-         <property name="enabled">
-          <bool>false</bool>
-         </property>
+     <item>
+      <layout class="QVBoxLayout" name="verticalLayout">
+       <property name="bottomMargin">
+        <number>0</number>
+       </property>
+       <item>
+        <widget class="QLabel" name="label_2">
          <property name="text">
-          <string>New room</string>
+          <string>People in lobby</string>
          </property>
         </widget>
        </item>
-       <item row="1" column="1">
-        <widget class="QPushButton" name="joinButton">
-         <property name="enabled">
-          <bool>false</bool>
+       <item>
+        <widget class="QListWidget" name="listUsers">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Expanding" vsizetype="Maximum">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
          </property>
-         <property name="text">
-          <string>Join room</string>
-         </property>
-        </widget>
-       </item>
-      </layout>
-     </widget>
-     <widget class="QWidget" name="roomPage">
-      <layout class="QGridLayout" name="gridLayout_3">
-       <item row="5" column="1">
-        <widget class="QPushButton" name="buttonReady">
-         <property name="text">
-          <string>Ready</string>
-         </property>
-        </widget>
-       </item>
-       <item row="3" column="0">
-        <widget class="QLabel" name="label_5">
-         <property name="text">
-          <string>Mods mismatch</string>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>96</height>
+          </size>
          </property>
-        </widget>
-       </item>
-       <item row="5" column="0">
-        <widget class="QPushButton" name="buttonLeave">
-         <property name="text">
-          <string>Leave</string>
+         <property name="midLineWidth">
+          <number>0</number>
          </property>
-        </widget>
-       </item>
-       <item row="4" column="0" colspan="2">
-        <widget class="QListWidget" name="modsList">
          <property name="editTriggers">
           <set>QAbstractItemView::NoEditTriggers</set>
          </property>
          <property name="selectionMode">
           <enum>QAbstractItemView::NoSelection</enum>
          </property>
-        </widget>
-       </item>
-       <item row="2" column="0" colspan="2">
-        <widget class="QListWidget" name="playersList">
-         <property name="editTriggers">
-          <set>QAbstractItemView::NoEditTriggers</set>
-         </property>
-         <property name="selectionMode">
-          <enum>QAbstractItemView::SingleSelection</enum>
+         <property name="isWrapping" stdset="0">
+          <bool>true</bool>
          </property>
-         <property name="selectionBehavior">
-          <enum>QAbstractItemView::SelectRows</enum>
+         <property name="layoutMode">
+          <enum>QListView::SinglePass</enum>
          </property>
         </widget>
        </item>
-       <item row="1" column="1">
-        <widget class="QPushButton" name="kickButton">
+       <item>
+        <widget class="QLabel" name="label_6">
          <property name="text">
-          <string>Kick player</string>
+          <string>Lobby chat</string>
          </property>
         </widget>
        </item>
-       <item row="1" column="0">
-        <widget class="QLabel" name="label_3">
-         <property name="text">
-          <string>Players in the room</string>
+       <item>
+        <widget class="QPlainTextEdit" name="chat">
+         <property name="readOnly">
+          <bool>true</bool>
          </property>
         </widget>
        </item>
+       <item>
+        <widget class="QLineEdit" name="messageEdit"/>
+       </item>
       </layout>
-     </widget>
-    </widget>
+     </item>
+     <item>
+      <widget class="QStackedWidget" name="stackedWidget">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="currentIndex">
+        <number>0</number>
+       </property>
+       <widget class="QWidget" name="sessionsPage">
+        <layout class="QGridLayout" name="gridLayout_2">
+         <item row="1" column="0">
+          <widget class="QPushButton" name="newButton">
+           <property name="enabled">
+            <bool>false</bool>
+           </property>
+           <property name="text">
+            <string>New room</string>
+           </property>
+          </widget>
+         </item>
+         <item row="1" column="1">
+          <widget class="QPushButton" name="joinButton">
+           <property name="enabled">
+            <bool>false</bool>
+           </property>
+           <property name="text">
+            <string>Join room</string>
+           </property>
+          </widget>
+         </item>
+         <item row="0" column="0" colspan="2">
+          <widget class="QTableWidget" name="sessionsTable">
+           <property name="editTriggers">
+            <set>QAbstractItemView::NoEditTriggers</set>
+           </property>
+           <property name="selectionMode">
+            <enum>QAbstractItemView::SingleSelection</enum>
+           </property>
+           <property name="selectionBehavior">
+            <enum>QAbstractItemView::SelectRows</enum>
+           </property>
+           <attribute name="horizontalHeaderCascadingSectionResizes">
+            <bool>false</bool>
+           </attribute>
+           <attribute name="horizontalHeaderDefaultSectionSize">
+            <number>80</number>
+           </attribute>
+           <attribute name="horizontalHeaderShowSortIndicator" stdset="0">
+            <bool>false</bool>
+           </attribute>
+           <attribute name="horizontalHeaderStretchLastSection">
+            <bool>true</bool>
+           </attribute>
+           <attribute name="verticalHeaderMinimumSectionSize">
+            <number>20</number>
+           </attribute>
+           <attribute name="verticalHeaderDefaultSectionSize">
+            <number>20</number>
+           </attribute>
+           <column>
+            <property name="text">
+             <string>Session</string>
+            </property>
+           </column>
+           <column>
+            <property name="text">
+             <string>Players</string>
+            </property>
+           </column>
+           <column>
+            <property name="text">
+             <string/>
+            </property>
+           </column>
+          </widget>
+         </item>
+        </layout>
+       </widget>
+       <widget class="QWidget" name="roomPage">
+        <layout class="QGridLayout" name="gridLayout_3">
+         <item row="1" column="1">
+          <widget class="QPushButton" name="kickButton">
+           <property name="text">
+            <string>Kick player</string>
+           </property>
+          </widget>
+         </item>
+         <item row="2" column="0" colspan="2">
+          <widget class="QListWidget" name="playersList">
+           <property name="editTriggers">
+            <set>QAbstractItemView::NoEditTriggers</set>
+           </property>
+           <property name="selectionMode">
+            <enum>QAbstractItemView::SingleSelection</enum>
+           </property>
+           <property name="selectionBehavior">
+            <enum>QAbstractItemView::SelectRows</enum>
+           </property>
+          </widget>
+         </item>
+         <item row="1" column="0">
+          <widget class="QLabel" name="label_3">
+           <property name="text">
+            <string>Players in the room</string>
+           </property>
+          </widget>
+         </item>
+         <item row="6" column="0">
+          <widget class="QPushButton" name="buttonLeave">
+           <property name="text">
+            <string>Leave</string>
+           </property>
+          </widget>
+         </item>
+         <item row="3" column="0">
+          <widget class="QLabel" name="label_5">
+           <property name="text">
+            <string>Mods mismatch</string>
+           </property>
+          </widget>
+         </item>
+         <item row="4" column="0" colspan="2">
+          <widget class="QListWidget" name="modsList">
+           <property name="editTriggers">
+            <set>QAbstractItemView::NoEditTriggers</set>
+           </property>
+           <property name="selectionMode">
+            <enum>QAbstractItemView::MultiSelection</enum>
+           </property>
+          </widget>
+         </item>
+         <item row="6" column="1">
+          <widget class="QPushButton" name="buttonReady">
+           <property name="text">
+            <string>Ready</string>
+           </property>
+          </widget>
+         </item>
+         <item row="3" column="1">
+          <widget class="QPushButton" name="buttonResolve">
+           <property name="text">
+            <string>Resolve</string>
+           </property>
+          </widget>
+         </item>
+         <item row="5" column="0" colspan="2">
+          <layout class="QHBoxLayout" name="horizontalLayout_3">
+           <property name="bottomMargin">
+            <number>0</number>
+           </property>
+           <item>
+            <widget class="QRadioButton" name="optNewGame">
+             <property name="text">
+              <string>New game</string>
+             </property>
+            </widget>
+           </item>
+           <item>
+            <widget class="QRadioButton" name="optLoadGame">
+             <property name="text">
+              <string>Load game</string>
+             </property>
+            </widget>
+           </item>
+          </layout>
+         </item>
+        </layout>
+       </widget>
+      </widget>
+     </item>
+    </layout>
    </item>
   </layout>
  </widget>

+ 4 - 0
launcher/mainwindow_moc.cpp

@@ -53,6 +53,10 @@ MainWindow::MainWindow(QWidget * parent)
 	load(); // load FS before UI
 
 	ui->setupUi(this);
+	
+	connect(ui->lobbyView, &Lobby::enableMod, ui->modlistView, &CModListView::enableModByName);
+	connect(ui->lobbyView, &Lobby::disableMod, ui->modlistView, &CModListView::disableModByName);
+	connect(ui->modlistView, &CModListView::modsChanged, ui->lobbyView, &Lobby::updateMods);
 
 	//load window settings
 	QSettings s(Ui::teamName, Ui::appName);

+ 1 - 1
launcher/modManager/cmodlist.cpp

@@ -125,7 +125,7 @@ bool CModEntry::isCompatible() const
 
 bool CModEntry::isEssential() const
 {
-	return getValue("storedLocaly").toBool();
+	return getName() == "vcmi";
 }
 
 bool CModEntry::isInstalled() const

+ 20 - 2
launcher/modManager/cmodlistview_moc.cpp

@@ -496,7 +496,14 @@ QStringList CModListView::findDependentMods(QString mod, bool excludeDisabled)
 void CModListView::on_enableButton_clicked()
 {
 	QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString();
+	
+	enableModByName(modName);
+	
+	checkManagerErrors();
+}
 
+void CModListView::enableModByName(QString modName)
+{
 	assert(findBlockingMods(modName).empty());
 	assert(findInvalidDependencies(modName).empty());
 
@@ -505,17 +512,24 @@ void CModListView::on_enableButton_clicked()
 		if(modModel->getMod(name).isDisabled())
 			manager->enableMod(name);
 	}
-	checkManagerErrors();
+	emit modsChanged();
 }
 
 void CModListView::on_disableButton_clicked()
 {
 	QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString();
 
+	disableModByName(modName);
+	
+	checkManagerErrors();
+}
+
+void CModListView::disableModByName(QString modName)
+{
 	if(modModel->hasMod(modName) && modModel->getMod(modName).isEnabled())
 		manager->disableMod(modName);
 
-	checkManagerErrors();
+	emit modsChanged();
 }
 
 void CModListView::on_updateButton_clicked()
@@ -544,6 +558,8 @@ void CModListView::on_uninstallButton_clicked()
 			manager->disableMod(modName);
 		manager->uninstallMod(modName);
 	}
+	
+	emit modsChanged();
 	checkManagerErrors();
 }
 
@@ -631,6 +647,8 @@ void CModListView::downloadFinished(QStringList savedFiles, QStringList failedFi
 
 	if(doInstallFiles)
 		installFiles(savedFiles);
+	
+	emit modsChanged();
 }
 
 void CModListView::hideProgressBar()

+ 6 - 0
launcher/modManager/cmodlistview_moc.h

@@ -66,6 +66,8 @@ class CModListView : public QWidget
 
 signals:
 	void extraResolutionsEnabledChanged(bool enabled);
+	
+	void modsChanged();
 
 public:
 	explicit CModListView(QWidget * parent = 0);
@@ -82,6 +84,10 @@ public:
 	bool isExtraResolutionsModEnabled() const;
 
 	const CModList & getModList() const;
+	
+public slots:
+	void enableModByName(QString modName);
+	void disableModByName(QString modName);
 
 private slots:
 	void dataChanged(const QModelIndex & topleft, const QModelIndex & bottomRight);

+ 2 - 0
launcher/settingsView/csettingsview_moc.cpp

@@ -33,6 +33,8 @@ QString resolutionToString(const QSize & resolution)
 /// Note that it is possible to specify enconding manually in settings.json
 static const std::string knownEncodingsList[] = //TODO: remove hardcode
 {
+	// Asks vcmi to automatically detect encoding
+	"auto",
 	// European Windows-125X encodings
 	"CP1250", // West European, covers mostly Slavic languages that use latin script
 	"CP1251", // Covers languages that use cyrillic scrypt

+ 5 - 0
launcher/settingsView/csettingsview_moc.ui

@@ -526,6 +526,11 @@
    </item>
    <item row="10" column="7" colspan="3">
     <widget class="QComboBox" name="comboBoxEncoding">
+     <item>
+      <property name="text">
+       <string>Automatic detection</string>
+      </property>
+     </item>
      <item>
       <property name="text">
        <string>Central European (Windows 1250)</string>

+ 0 - 5
lib/CAndroidVMHelper.cpp

@@ -21,11 +21,6 @@ void CAndroidVMHelper::cacheVM(JNIEnv * env)
 	env->GetJavaVM(&vmCache);
 }
 
-void CAndroidVMHelper::cacheVM(JavaVM * vm)
-{
-	vmCache = vm;
-}
-
 CAndroidVMHelper::CAndroidVMHelper()
 {
 	auto res = vmCache->GetEnv((void **) &envPtr, JNI_VERSION_1_1);

+ 0 - 2
lib/CAndroidVMHelper.h

@@ -42,8 +42,6 @@ public:
 
 	static void cacheVM(JNIEnv * env);
 
-	static void cacheVM(JavaVM * vm);
-
 	static constexpr const char * NATIVE_METHODS_DEFAULT_CLASS = "eu/vcmi/vcmi/NativeMethods";
 };
 

+ 40 - 7
lib/CArtHandler.cpp

@@ -824,7 +824,12 @@ bool CArtifactInstance::canBePutAt(const ArtifactLocation & al, bool assumeDestR
 
 bool CArtifactInstance::canBePutAt(const CArtifactSet *artSet, ArtifactPosition slot, bool assumeDestRemoved) const
 {
-	if(slot >= GameConstants::BACKPACK_START)
+	if(slot == ArtifactPosition::TRANSITION_POS)
+	{
+		return true;
+	}
+
+	if(ArtifactUtils::isSlotBackpack(slot))
 	{
 		if(artType->isBig())
 			return false;
@@ -851,7 +856,7 @@ void CArtifactInstance::putAt(ArtifactLocation al)
 	assert(canBePutAt(al));
 
 	al.getHolderArtSet()->setNewArtSlot(al.slot, this, false);
-	if(!ArtifactUtils::isSlotBackpack(al.slot))
+	if(ArtifactUtils::isSlotEquipment(al.slot))
 		al.getHolderNode()->attachTo(*this);
 }
 
@@ -859,7 +864,7 @@ void CArtifactInstance::removeFrom(ArtifactLocation al)
 {
 	assert(al.getHolderArtSet()->getArt(al.slot) == this);
 	al.getHolderArtSet()->eraseArtSlot(al.slot);
-	if(!ArtifactUtils::isSlotBackpack(al.slot))
+	if(ArtifactUtils::isSlotEquipment(al.slot))
 		al.getHolderNode()->detachFrom(*this);
 }
 
@@ -998,6 +1003,8 @@ bool CArtifactInstance::isPart(const CArtifactInstance *supposedPart) const
 
 bool CCombinedArtifactInstance::canBePutAt(const CArtifactSet *artSet, ArtifactPosition slot, bool assumeDestRemoved) const
 {
+	if(slot == ArtifactPosition::TRANSITION_POS)
+		return true;
 	bool canMainArtifactBePlaced = CArtifactInstance::canBePutAt(artSet, slot, assumeDestRemoved);
 	if(!canMainArtifactBePlaced)
 		return false; //no is no...
@@ -1070,7 +1077,11 @@ void CCombinedArtifactInstance::addAsConstituent(CArtifactInstance *art, Artifac
 
 void CCombinedArtifactInstance::putAt(ArtifactLocation al)
 {
-	if(ArtifactUtils::isSlotBackpack(al.slot))
+	if(al.slot == ArtifactPosition::TRANSITION_POS)
+	{
+		CArtifactInstance::putAt(al);
+	}
+	else if(ArtifactUtils::isSlotBackpack(al.slot))
 	{
 		CArtifactInstance::putAt(al);
 		for(ConstituentInfo &ci : constituentsInfo)
@@ -1108,7 +1119,7 @@ void CCombinedArtifactInstance::putAt(ArtifactLocation al)
 
 void CCombinedArtifactInstance::removeFrom(ArtifactLocation al)
 {
-	if(ArtifactUtils::isSlotBackpack(al.slot))
+	if(ArtifactUtils::isSlotBackpack(al.slot) || al.slot == ArtifactPosition::TRANSITION_POS)
 	{
 		CArtifactInstance::removeFrom(al);
 	}
@@ -1329,6 +1340,12 @@ const CCombinedArtifactInstance *CArtifactSet::getAssemblyByConstituent(Artifact
 
 const ArtSlotInfo * CArtifactSet::getSlot(ArtifactPosition pos) const
 {
+	if(pos == ArtifactPosition::TRANSITION_POS)
+	{
+		// Always add to the end. Always take from the beginning.
+		assert(!artifactsTransitionPos.empty());
+		return &(*artifactsTransitionPos.begin());
+	}
 	if(vstd::contains(artifactsWorn, pos))
 		return &artifactsWorn.at(pos);
 	if(pos >= ArtifactPosition::AFTER_LAST )
@@ -1355,7 +1372,13 @@ ArtSlotInfo & CArtifactSet::retrieveNewArtSlot(ArtifactPosition slot)
 {
 	assert(!vstd::contains(artifactsWorn, slot));
 
-	if (!ArtifactUtils::isSlotBackpack(slot))
+	if(slot == ArtifactPosition::TRANSITION_POS)
+	{
+		// Always add to the end. Always take from the beginning.
+		artifactsTransitionPos.push_back(ArtSlotInfo());
+		return artifactsTransitionPos.back();
+	}
+	if(!ArtifactUtils::isSlotBackpack(slot))
 		return artifactsWorn[slot];
 
 	ArtSlotInfo newSlot;
@@ -1375,7 +1398,12 @@ void CArtifactSet::setNewArtSlot(ArtifactPosition slot, CArtifactInstance *art,
 
 void CArtifactSet::eraseArtSlot(ArtifactPosition slot)
 {
-	if(ArtifactUtils::isSlotBackpack(slot))
+	if(slot == ArtifactPosition::TRANSITION_POS)
+	{
+		assert(!artifactsTransitionPos.empty());
+		artifactsTransitionPos.erase(artifactsTransitionPos.begin());
+	}
+	else if(ArtifactUtils::isSlotBackpack(slot))
 	{
 		auto backpackSlot = ArtifactPosition(slot - GameConstants::BACKPACK_START);
 
@@ -1612,4 +1640,9 @@ DLL_LINKAGE bool ArtifactUtils::isSlotBackpack(ArtifactPosition slot)
 	return slot >= GameConstants::BACKPACK_START;
 }
 
+DLL_LINKAGE bool ArtifactUtils::isSlotEquipment(ArtifactPosition slot)
+{
+	return slot < GameConstants::BACKPACK_START && slot >= 0;
+}
+
 VCMI_LIB_NAMESPACE_END

+ 2 - 0
lib/CArtHandler.h

@@ -317,6 +317,7 @@ class DLL_LINKAGE CArtifactSet
 public:
 	std::vector<ArtSlotInfo> artifactsInBackpack; //hero's artifacts from bag
 	std::map<ArtifactPosition, ArtSlotInfo> artifactsWorn; //map<position,artifact_id>; positions: 0 - head; 1 - shoulders; 2 - neck; 3 - right hand; 4 - left hand; 5 - torso; 6 - right ring; 7 - left ring; 8 - feet; 9 - misc1; 10 - misc2; 11 - misc3; 12 - misc4; 13 - mach1; 14 - mach2; 15 - mach3; 16 - mach4; 17 - spellbook; 18 - misc5
+	std::vector<ArtSlotInfo> artifactsTransitionPos; // Used as transition position for dragAndDrop artifact exchange
 
 	ArtSlotInfo & retrieveNewArtSlot(ArtifactPosition slot);
 	void setNewArtSlot(ArtifactPosition slot, CArtifactInstance *art, bool locked);
@@ -392,6 +393,7 @@ namespace ArtifactUtils
 	DLL_LINKAGE bool isArtRemovable(const std::pair<ArtifactPosition, ArtSlotInfo> & slot);
 	DLL_LINKAGE bool checkSpellbookIsNeeded(const CGHeroInstance * heroPtr, ArtifactID artID, ArtifactPosition slot);
 	DLL_LINKAGE bool isSlotBackpack(ArtifactPosition slot);
+	DLL_LINKAGE bool isSlotEquipment(ArtifactPosition slot);
 }
 
 VCMI_LIB_NAMESPACE_END

+ 83 - 1
lib/CGeneralTextHandler.cpp

@@ -105,7 +105,84 @@ bool Unicode::isValidString(const char * data, size_t size)
 
 static std::string getSelectedEncoding()
 {
-	return settings["general"]["encoding"].String();
+	auto explicitSetting = settings["general"]["encoding"].String();
+	if (explicitSetting != "auto")
+		return explicitSetting;
+	return settings["session"]["encoding"].String();
+}
+
+/// Detects encoding of H3 text files based on matching against pregenerated footprints of H3 file
+/// Can also detect language of H3 install, however right now this is not necessary
+static void detectEncoding()
+{
+	static const size_t knownCount = 6;
+
+	// "footprints" of data collected from known versions of H3
+	static const std::array<std::array<double, 16>, knownCount> knownFootprints =
+	{ {
+		{ { 0.0559, 0.0000, 0.1983, 0.0051, 0.0222, 0.0183, 0.4596, 0.2405, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000 } },
+		{ { 0.0493, 0.0000, 0.1926, 0.0047, 0.0230, 0.0121, 0.4133, 0.2780, 0.0002, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0259, 0.0008 } },
+		{ { 0.0534, 0.0000, 0.1705, 0.0047, 0.0418, 0.0208, 0.4775, 0.2191, 0.0001, 0.0000, 0.0000, 0.0000, 0.0000, 0.0005, 0.0036, 0.0080 } },
+		{ { 0.0534, 0.0000, 0.1701, 0.0067, 0.0157, 0.0133, 0.4328, 0.2540, 0.0001, 0.0043, 0.0000, 0.0244, 0.0000, 0.0000, 0.0181, 0.0071 } },
+		{ { 0.0548, 0.0000, 0.1744, 0.0061, 0.0031, 0.0009, 0.0046, 0.0136, 0.0000, 0.0004, 0.0000, 0.0000, 0.0227, 0.0061, 0.4882, 0.2252 } },
+		{ { 0.0559, 0.0000, 0.1807, 0.0059, 0.0036, 0.0013, 0.0046, 0.0134, 0.0000, 0.0004, 0.0000, 0.0487, 0.0209, 0.0060, 0.4615, 0.1972 } }
+	} };
+
+	// languages of known footprints
+	static const std::array<std::string, knownCount> knownLanguages =
+	{ {
+		  "English", "French", "German", "Polish", "Russian", "Ukrainian"
+	} };
+
+	// encoding that should be used for known footprints
+	static const std::array<std::string, knownCount> knownEncodings =
+	{ {
+		  "CP1252", "CP1252", "CP1252", "CP1250", "CP1251", "CP1251"
+	} };
+
+	// load file that will be used for footprint generation
+	// this is one of the most text-heavy files in game and consists solely from translated texts
+	auto resource = CResourceHandler::get()->load(ResourceID("DATA/GENRLTXT.TXT", EResType::TEXT));
+
+	std::array<size_t, 256> charCount;
+	std::array<double, 16> footprint;
+	std::array<double, knownCount> deviations;
+
+	boost::range::fill(charCount, 0);
+	boost::range::fill(footprint, 0.0);
+	boost::range::fill(deviations, 0.0);
+
+	auto data = resource->readAll();
+
+	// compute how often each character occurs in input file
+	for (si64 i = 0; i < data.second; ++i)
+		charCount[data.first[i]] += 1;
+
+	// and convert computed data into weights
+	// to reduce amount of data, group footprint data into 16-char blocks.
+	// While this will reduce precision, it should not affect output
+	// since we expect only tiny differences compared to reference footprints
+	for (size_t i = 0; i < 256; ++i)
+		footprint[i/16] += double(charCount[i]) / data.second;
+
+	logGlobal->debug("Language footprint: %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f",
+			footprint[0], footprint[1], footprint[2],  footprint[3],  footprint[4],  footprint[5],  footprint[6],  footprint[7],
+			footprint[8], footprint[9], footprint[10], footprint[11], footprint[12], footprint[13], footprint[14], footprint[15]
+		);
+
+	for (size_t i = 0; i < deviations.size(); ++i)
+	{
+		for (size_t j = 0; j < footprint.size(); ++j)
+			deviations[i] += std::abs((footprint[j] - knownFootprints[i][j]));
+	}
+
+	size_t bestIndex = boost::range::min_element(deviations) - deviations.begin();
+
+	for (size_t i = 0; i < deviations.size(); ++i)
+		logGlobal->debug("Comparing to %s: %f", knownLanguages[i], deviations[i]);
+
+	Settings s = settings.write["session"]["encoding"];
+	s->String() = knownEncodings[bestIndex];
 }
 
 std::string Unicode::toUnicode(const std::string &text)
@@ -362,6 +439,9 @@ CGeneralTextHandler::CGeneralTextHandler():
 	znpc00           (*this, "vcmi.znpc00"  ), // technically - wog
 	qeModCommands    (*this, "vcmi.quickExchange" )
 {
+	if (getSelectedEncoding().empty())
+		detectEncoding();
+
 	readToVector("core.vcdesc",   "DATA/VCDESC.TXT"   );
 	readToVector("core.lcdesc",   "DATA/LCDESC.TXT"   );
 	readToVector("core.tcommand", "DATA/TCOMMAND.TXT" );
@@ -371,6 +451,8 @@ CGeneralTextHandler::CGeneralTextHandler():
 	readToVector("core.xtrainfo", "DATA/XTRAINFO.TXT" );
 	readToVector("core.restypes", "DATA/RESTYPES.TXT" );
 	readToVector("core.randsign", "DATA/RANDSIGN.TXT" );
+	readToVector("core.crgen1",   "DATA/CRGEN1.TXT"   );
+	readToVector("core.crgen4",   "DATA/CRGEN4.TXT"   );
 	readToVector("core.overview", "DATA/OVERVIEW.TXT" );
 	readToVector("core.arraytxt", "DATA/ARRAYTXT.TXT" );
 	readToVector("core.priskill", "DATA/PRISKILL.TXT" );

+ 1 - 0
lib/GameConstants.h

@@ -1020,6 +1020,7 @@ class ArtifactPosition
 public:
 	enum EArtifactPosition
 	{
+		TRANSITION_POS = -3,
 		FIRST_AVAILABLE = -2,
 		PRE_FIRST = -1, //sometimes used as error, sometimes as first free in backpack
 		HEAD, SHOULDERS, NECK, RIGHT_HAND, LEFT_HAND, TORSO, //5

+ 16 - 3
lib/filesystem/CFilesystemLoader.cpp

@@ -112,18 +112,31 @@ std::unordered_map<ResourceID, bfs::path> CFilesystemLoader::listFiles(const std
 	std::vector<bfs::path> path; //vector holding relative path to our file
 
 	bfs::recursive_directory_iterator enddir;
+#if BOOST_VERSION >= 107200 // 1.72
+	bfs::recursive_directory_iterator it(baseDirectory, bfs::directory_options::follow_directory_symlink);
+#else
 	bfs::recursive_directory_iterator it(baseDirectory, bfs::symlink_option::recurse);
+#endif
 
 	for(; it != enddir; ++it)
 	{
 		EResType::Type type;
+#if BOOST_VERSION >= 107200
+		const auto currentDepth = it.depth();
+#else
+		const auto currentDepth = it.level();
+#endif
 
 		if (bfs::is_directory(it->status()))
 		{
-			path.resize(it.level() + 1);
+			path.resize(currentDepth + 1);
 			path.back() = it->path().filename();
 			// don't iterate into directory if depth limit reached
-			it.no_push(depth <= it.level());
+#if BOOST_VERSION >= 107200
+			it.disable_recursion_pending(depth <= currentDepth);
+#else
+			it.no_push(depth <= currentDepth);
+#endif
 
 			type = EResType::DIRECTORY;
 		}
@@ -134,7 +147,7 @@ std::unordered_map<ResourceID, bfs::path> CFilesystemLoader::listFiles(const std
 		{
 			//reconstruct relative filename (not possible via boost AFAIK)
 			bfs::path filename;
-			const size_t iterations = std::min((size_t)it.level(), path.size());
+			const size_t iterations = std::min(static_cast<size_t>(currentDepth), path.size());
 			if (iterations)
 			{
 				filename = path.front();

+ 1 - 1
lib/mapObjects/CQuest.cpp

@@ -588,7 +588,7 @@ void CGSeerHut::initObj(CRandomGenerator & rand)
 	quest->progress = CQuest::NOT_ACTIVE;
 	if(quest->missionType)
 	{
-		std::string questName  = quest->missionName(CQuest::Emission(quest->missionType-1));
+		std::string questName  = quest->missionName(quest->missionType);
 
 		if(!quest->isCustomFirst)
 			quest->firstVisitText = VLC->generaltexth->translate("core.seerhut.quest." + questName + "." + quest->missionState(0), quest->textOption);

+ 7 - 1
lib/mapObjects/MiscObjects.cpp

@@ -1963,7 +1963,13 @@ void CGSirens::onHeroVisit( const CGHeroInstance * h ) const
 
 		for (auto i = h->Slots().begin(); i != h->Slots().end(); i++)
 		{
-			TQuantity drown = static_cast<TQuantity>(i->second->count * 0.3);
+			// 1-sized stacks are not affected by sirens
+			if (i->second->count == 1)
+				continue;
+
+			// tested H3 behavior: 30% (rounded up) of stack drowns
+			TQuantity drown = std::ceil(i->second->count * 0.3);
+
 			if(drown)
 			{
 				cb->changeStackCount(StackLocation(h, i->first), -drown);

BIN
mapeditor/icons/brush-3.png


+ 16 - 0
mapeditor/mainwindow.cpp

@@ -869,6 +869,22 @@ void MainWindow::on_toolArea_clicked(bool checked)
 	ui->tabWidget->setCurrentIndex(0);
 }
 
+void MainWindow::on_toolLasso_clicked(bool checked)
+{
+	ui->toolBrush->setChecked(false);
+	ui->toolBrush2->setChecked(false);
+	ui->toolBrush4->setChecked(false);
+	ui->toolArea->setChecked(false);
+	//ui->toolLasso->setChecked(false);
+	
+	if(checked)
+		ui->mapView->selectionTool = MapView::SelectionTool::Lasso;
+	else
+		ui->mapView->selectionTool = MapView::SelectionTool::None;
+	
+	ui->tabWidget->setCurrentIndex(0);
+}
+
 void MainWindow::on_actionErase_triggered()
 {
 	on_toolErase_clicked();

+ 2 - 0
mapeditor/mainwindow.h

@@ -92,6 +92,8 @@ private slots:
 	void on_toolBrush2_clicked(bool checked);
 
 	void on_toolBrush4_clicked(bool checked);
+	
+	void on_toolLasso_clicked(bool checked);
 
 	void on_inspectorWidget_itemChanged(QTableWidgetItem *item);
 

+ 6 - 2
mapeditor/mainwindow.ui

@@ -642,7 +642,7 @@
         <item row="2" column="0">
          <widget class="QPushButton" name="toolLasso">
           <property name="enabled">
-           <bool>false</bool>
+           <bool>true</bool>
           </property>
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
@@ -663,7 +663,11 @@
            </size>
           </property>
           <property name="text">
-           <string notr="true">O</string>
+           <string/>
+          </property>
+          <property name="icon">
+           <iconset>
+            <normaloff>icons:brush-3.png</normaloff>icons:brush-3.png</iconset>
           </property>
           <property name="checkable">
            <bool>true</bool>

+ 58 - 0
mapeditor/mapview.cpp

@@ -155,6 +155,14 @@ void MapView::mouseMoveEvent(QMouseEvent *mouseEvent)
 		}
 		sc->selectionTerrainView.draw();
 		break;
+			
+	case MapView::SelectionTool::Lasso:
+		if(mouseEvent->buttons() == Qt::LeftButton)
+		{
+			sc->selectionTerrainView.select(tile);
+			sc->selectionTerrainView.draw();
+		}
+		break;
 
 	case MapView::SelectionTool::None:
 		if(mouseEvent->buttons() & Qt::RightButton)
@@ -247,6 +255,7 @@ void MapView::mousePressEvent(QMouseEvent *event)
 		break;
 
 	case MapView::SelectionTool::Area:
+	case MapView::SelectionTool::Lasso:
 		if(event->button() == Qt::RightButton)
 			break;
 
@@ -329,6 +338,55 @@ void MapView::mouseReleaseEvent(QMouseEvent *event)
 
 	switch(selectionTool)
 	{
+	case MapView::SelectionTool::Lasso: {
+		if(event->button() == Qt::RightButton)
+			break;
+				
+		//key: y position of tile
+		//value.first: x position of left tile
+		//value.second: x postiion of right tile
+		std::map<int, std::pair<int, int>> selectionRangeMapX, selectionRangeMapY;
+		for(auto & t : sc->selectionTerrainView.selection())
+		{
+			auto pairIter = selectionRangeMapX.find(t.y);
+			if(pairIter == selectionRangeMapX.end())
+				selectionRangeMapX[t.y] = std::make_pair(t.x, t.x);
+			else
+			{
+				pairIter->second.first = std::min(pairIter->second.first, t.x);
+				pairIter->second.second = std::max(pairIter->second.second, t.x);
+			}
+			
+			pairIter = selectionRangeMapY.find(t.x);
+			if(pairIter == selectionRangeMapY.end())
+				selectionRangeMapY[t.x] = std::make_pair(t.y, t.y);
+			else
+			{
+				pairIter->second.first = std::min(pairIter->second.first, t.y);
+				pairIter->second.second = std::max(pairIter->second.second, t.y);
+			}
+		}
+		
+		std::set<int3> selectionByX, selectionByY;
+		std::vector<int3> finalSelection;
+		for(auto & selectionRange : selectionRangeMapX)
+		{
+			for(int i = selectionRange.second.first; i < selectionRange.second.second; ++i)
+				selectionByX.insert(int3(i, selectionRange.first, sc->level));
+		}
+		for(auto & selectionRange : selectionRangeMapY)
+		{
+			for(int i = selectionRange.second.first; i < selectionRange.second.second; ++i)
+				selectionByY.insert(int3(selectionRange.first, i, sc->level));
+		}
+		std::set_intersection(selectionByX.begin(), selectionByX.end(), selectionByY.begin(), selectionByY.end(), std::back_inserter(finalSelection));
+		for(auto & lassoTile : finalSelection)
+			sc->selectionTerrainView.select(lassoTile);
+		
+		sc->selectionTerrainView.draw();
+		break;
+	}
+			
 	case MapView::SelectionTool::None:
 		if(event->button() == Qt::RightButton)
 			break;

+ 6 - 10
mapeditor/resourceExtractor/ResourceConverter.cpp

@@ -33,36 +33,32 @@ void ResourceConverter::convertExtractedResourceFiles(ConversionOptions conversi
 
 void ResourceConverter::doConvertPcxToPng(bool deleteOriginals)
 {
-	std::string filename;
-
 	bfs::path imagesPath = VCMIDirs::get().userExtractedPath() / "IMAGES";
 	bfs::directory_iterator end_iter;
 
 	for(bfs::directory_iterator dir_itr(imagesPath); dir_itr != end_iter; ++dir_itr)
 	{
+		const auto filename = dir_itr->path().filename();
 		try
 		{
 			if (!bfs::is_regular_file(dir_itr->status()))
 				return;
 
-			std::string filePath = dir_itr->path().string();
-			std::string fileStem = dir_itr->path().stem().string();
-			filename = dir_itr->path().filename().string();
-			std::string filenameLowerCase = boost::algorithm::to_lower_copy(filename);
+			std::string filenameLowerCase = boost::algorithm::to_lower_copy(filename.string());
 
-			if(bfs::extension(filenameLowerCase) == ".pcx")
+			if(boost::algorithm::to_lower_copy(filename.extension().string()) == ".pcx")
 			{
 				auto img = BitmapHandler::loadBitmap(filenameLowerCase);
-				bfs::path pngFilePath = imagesPath / (fileStem + ".png");
+				bfs::path pngFilePath = imagesPath / (dir_itr->path().stem().string() + ".png");
 				img.save(pathToQString(pngFilePath), "PNG");
 
 				if(deleteOriginals)
-					bfs::remove(filePath);
+					bfs::remove(dir_itr->path());
 			}
 		}
 		catch(const std::exception & ex)
 		{
-			logGlobal->info(filename + " " + ex.what() + "\n");
+			logGlobal->info(filename.string() + " " + ex.what() + "\n");
 		}
 	}
 }

+ 28 - 15
server/CGameHandler.cpp

@@ -3893,7 +3893,7 @@ bool CGameHandler::moveArtifact(const ArtifactLocation &al1, const ArtifactLocat
 
 	// Check if src/dest slots are appropriate for the artifacts exchanged.
 	// Moving to the backpack is always allowed.
-	if ((!srcArtifact || dst.slot < GameConstants::BACKPACK_START)
+	if ((!srcArtifact || !ArtifactUtils::isSlotBackpack(dst.slot))
 		&& srcArtifact && !srcArtifact->canBePutAt(dst, true))
 		COMPLAIN_RET("Cannot move artifact!");
 
@@ -3908,24 +3908,37 @@ bool CGameHandler::moveArtifact(const ArtifactLocation &al1, const ArtifactLocat
 	if (src.slot == ArtifactPosition::MACH4 || dst.slot == ArtifactPosition::MACH4)
 		COMPLAIN_RET("Cannot move catapult!");
 
-	if (dst.slot >= GameConstants::BACKPACK_START)
+	if(ArtifactUtils::isSlotBackpack(dst.slot))
 		vstd::amin(dst.slot, ArtifactPosition(GameConstants::BACKPACK_START + (si32)dst.getHolderArtSet()->artifactsInBackpack.size()));
 
-	if (src.slot == dst.slot  &&  src.artHolder == dst.artHolder)
-		COMPLAIN_RET("Won't move artifact: Dest same as source!");
-
-	if (dst.slot < GameConstants::BACKPACK_START  &&  destArtifact) //moving art to another slot
+	if(!(src.slot == ArtifactPosition::TRANSITION_POS && dst.slot == ArtifactPosition::TRANSITION_POS))
 	{
-		//old artifact must be removed first
-		moveArtifact(dst, ArtifactLocation(dst.artHolder, ArtifactPosition(
-			(si32)dst.getHolderArtSet()->artifactsInBackpack.size() + GameConstants::BACKPACK_START)));
-	}
-	auto hero = boost::get<ConstTransitivePtr<CGHeroInstance>>(dst.artHolder);
-	if(ArtifactUtils::checkSpellbookIsNeeded(hero, srcArtifact->artType->id, dst.slot))
-		giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK);
+		if(src.slot == dst.slot && src.artHolder == dst.artHolder)
+			COMPLAIN_RET("Won't move artifact: Dest same as source!");
 
-	MoveArtifact ma(&src, &dst);
-	sendAndApply(&ma);
+		// Check if dst slot is occupied
+		if(!ArtifactUtils::isSlotBackpack(dst.slot) && destArtifact)
+		{
+			// Previous artifact must be removed first
+			moveArtifact(dst, ArtifactLocation(dst.artHolder, ArtifactPosition::TRANSITION_POS));
+		}
+
+		try
+		{
+			auto hero = boost::get<ConstTransitivePtr<CGHeroInstance>>(dst.artHolder);
+			if(ArtifactUtils::checkSpellbookIsNeeded(hero, srcArtifact->artType->id, dst.slot))
+				giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK);
+		}
+		catch (boost::bad_get const &)
+		{
+			// object other than hero received an art - ignore
+		}
+
+		MoveArtifact ma(&src, &dst);
+		if(dst.slot == ArtifactPosition::TRANSITION_POS)
+			ma.askAssemble = false;
+		sendAndApply(&ma);
+	}
 	return true;
 }
 

+ 4 - 0
server/CVCMIServer.cpp

@@ -201,6 +201,10 @@ void CVCMIServer::run()
 
 void CVCMIServer::establishRemoteConnections()
 {
+	//wait for host connection
+	while(connections.empty())
+		boost::this_thread::sleep(boost::posix_time::milliseconds(50));
+	
 	uuid = cmdLineOptions["lobby-uuid"].as<std::string>();
     int numOfConnections = cmdLineOptions["connections"].as<ui16>();
 	auto address = cmdLineOptions["lobby"].as<std::string>();