Sfoglia il codice sorgente

Merge with vcmi/develop

Ivan Savenko 2 anni fa
parent
commit
17fc9d8d8f
100 ha cambiato i file con 2661 aggiunte e 1621 eliminazioni
  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. 18 6
      CMakeLists.txt
  13. 14 0
      Mods/vcmi/mod.json
  14. 2 2
      client/CGameInfo.h
  15. 47 461
      client/CMT.cpp
  16. 0 1
      client/CMT.h
  17. 5 3
      client/CMakeLists.txt
  18. 18 34
      client/CMusicHandler.cpp
  19. 0 3
      client/CMusicHandler.h
  20. 30 13
      client/CPlayerInterface.cpp
  21. 22 1
      client/Client.cpp
  22. 1 0
      client/Client.h
  23. 494 0
      client/ClientCommandManager.cpp
  24. 31 0
      client/ClientCommandManager.h
  25. 1 1
      client/NetPacksLobbyClient.cpp
  26. 1 1
      client/battle/BattleActionsController.cpp
  27. 1 1
      client/battle/BattleAnimationClasses.cpp
  28. 1 1
      client/battle/BattleFieldController.cpp
  29. 4 3
      client/battle/BattleInterface.cpp
  30. 1 1
      client/battle/BattleInterfaceClasses.cpp
  31. 4 4
      client/battle/BattleStacksController.cpp
  32. 1 1
      client/battle/BattleWindow.cpp
  33. 4 5
      client/gui/CAnimation.cpp
  34. 2 2
      client/gui/CAnimation.h
  35. 0 317
      client/gui/CCursorHandler.cpp
  36. 1 1
      client/gui/CGuiHandler.cpp
  37. 0 7
      client/gui/Canvas.cpp
  38. 0 4
      client/gui/Canvas.h
  39. 402 0
      client/gui/CursorHandler.cpp
  40. 80 30
      client/gui/CursorHandler.h
  41. 7 6
      client/lobby/RandomMapTab.cpp
  42. 1 1
      client/mainmenu/CMainMenu.cpp
  43. 18 16
      client/mapHandler.cpp
  44. 28 13
      client/widgets/AdventureMapClasses.cpp
  45. 1 2
      client/widgets/AdventureMapClasses.h
  46. 66 62
      client/widgets/CArtifactHolder.cpp
  47. 1 1
      client/widgets/CArtifactHolder.h
  48. 1 1
      client/widgets/CComponent.cpp
  49. 10 4
      client/widgets/Images.cpp
  50. 1 1
      client/widgets/Images.h
  51. 1 1
      client/widgets/MiscWidgets.cpp
  52. 0 37
      client/widgets/TextControls.cpp
  53. 0 3
      client/widgets/TextControls.h
  54. 4 3
      client/windows/CAdvmapInterface.cpp
  55. 32 23
      client/windows/CCastleInterface.cpp
  56. 9 1
      client/windows/CCastleInterface.h
  57. 2 2
      client/windows/CTradeWindow.cpp
  58. 1 1
      client/windows/CWindowObject.cpp
  59. 1 7
      client/windows/GUIClasses.cpp
  60. 0 1
      client/windows/GUIClasses.h
  61. 1 1
      client/windows/InfoWindows.cpp
  62. 6 2
      cmake_modules/VCMI_lib.cmake
  63. 1 1
      conanfile.py
  64. 3 0
      config/defaultMods.json
  65. 2 2
      config/factions/castle.json
  66. 1 0
      config/factions/neutral.json
  67. 8 0
      config/gameConfig.json
  68. 1 1
      config/randomMap.json
  69. 17 13
      config/rivers.json
  70. 13 10
      config/roads.json
  71. 1 1
      config/schemas/faction.json
  72. 54 0
      config/schemas/mod.json
  73. 37 0
      config/schemas/river.json
  74. 37 0
      config/schemas/road.json
  75. 7 2
      config/schemas/settings.json
  76. 44 13
      config/schemas/terrain.json
  77. 57 41
      config/terrains.json
  78. 2 2
      config/widgets/battleWindow.json
  79. 6 4
      docs/conan.md
  80. 35 7
      launcher/CMakeLists.txt
  81. BIN
      launcher/icons/menu-editor.png
  82. BIN
      launcher/icons/menu-game.png
  83. BIN
      launcher/icons/menu-lobby.png
  84. BIN
      launcher/icons/menu-mods.png
  85. BIN
      launcher/icons/menu-settings.png
  86. 27 4
      launcher/lobby/lobby.h
  87. 171 24
      launcher/lobby/lobby_moc.cpp
  88. 17 0
      launcher/lobby/lobby_moc.h
  89. 231 138
      launcher/lobby/lobby_moc.ui
  90. 9 0
      launcher/lobby/lobbyroomrequest_moc.cpp
  91. 1 0
      launcher/lobby/lobbyroomrequest_moc.h
  92. 11 8
      launcher/lobby/lobbyroomrequest_moc.ui
  93. 8 0
      launcher/main.cpp
  94. 1 0
      launcher/main.h
  95. 95 29
      launcher/mainwindow_moc.cpp
  96. 11 1
      launcher/mainwindow_moc.h
  97. 270 151
      launcher/mainwindow_moc.ui
  98. 21 9
      launcher/modManager/cmodlist.cpp
  99. 12 11
      launcher/modManager/cmodlistmodel_moc.cpp
  100. 32 4
      launcher/modManager/cmodlistview_moc.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

+ 18 - 6
CMakeLists.txt

@@ -90,6 +90,11 @@ if(APPLE_IOS AND COPY_CONFIG_ON_BUILD)
 	set(COPY_CONFIG_ON_BUILD OFF)
 endif()
 
+# No QT Linguist on MXE
+if((MINGW) AND (${CMAKE_CROSSCOMPILING}))
+	set(ENABLE_TRANSLATIONS OFF)
+endif()
+
 ############################################
 #        Miscellaneous options             #
 ############################################
@@ -181,6 +186,14 @@ set(CMAKE_XCODE_ATTRIBUTE_MARKETING_VERSION ${APP_SHORT_VERSION})
 set(CMAKE_XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH NO)
 set(CMAKE_XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH[variant=Debug] YES)
 
+if(ENABLE_LAUNCHER)
+	add_definitions(-DENABLE_LAUNCHER)
+endif()
+
+if(ENABLE_EDITOR)
+	add_definitions(-DENABLE_EDITOR)
+endif()
+
 if(ENABLE_SINGLE_APP_BUILD)
 	add_definitions(-DSINGLE_PROCESS_APP=1)
 endif()
@@ -232,7 +245,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)
@@ -360,12 +373,8 @@ if(ENABLE_LAUNCHER OR ENABLE_EDITOR)
 	find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets Network)
 	find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Network)
 
-	find_package(QT NAMES Qt6 Qt5 COMPONENTS LinguistTools)
-	find_package(Qt${QT_VERSION_MAJOR} COMPONENTS LinguistTools)
-	if(NOT Qt${QT_VERSION_MAJOR}LinguistTools_DIR)
-		set(ENABLE_TRANSLATIONS OFF)
-	endif()
 	if(ENABLE_TRANSLATIONS)
+		find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS LinguistTools)
 		add_definitions(-DENABLE_QT_TRANSLATIONS)
 	endif()
 endif()
@@ -563,6 +572,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()
 

+ 14 - 0
Mods/vcmi/mod.json

@@ -1,6 +1,20 @@
 {
 	"name" : "VCMI essential files",
 	"description" : "Essential files required for VCMI to run correctly",
+	
+	"german" : {
+		"name" : "VCMI - grundlegende Dateien",
+		"description" : "Grundlegende Dateien, die für die korrekte Ausführung von VCMI erforderlich sind",
+		"author" : "VCMI-Team",
+		"modType" : "Grafik",
+	},
+	
+	"ukrainian" : {
+		"name" : "VCMI - ключові файли",
+		"description" : "Ключові файли необхідні для повноцінної роботи VCMI",
+		"author" : "Команда VCMI",
+		"modType" : "Графіка",
+	},
 
 	"version" : "1.1",
 	"author" : "VCMI Team",

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

+ 47 - 461
client/CMT.cpp

@@ -13,8 +13,6 @@
 
 #include <boost/program_options.hpp>
 
-#include <vcmi/scripting/Service.h>
-
 #include "gui/SDL_Extensions.h"
 #include "CGameInfo.h"
 #include "mapHandler.h"
@@ -25,7 +23,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"
@@ -33,32 +31,26 @@
 #include "../lib/CBuildingHandler.h"
 #include "CVideoHandler.h"
 #include "../lib/CHeroHandler.h"
-#include "../lib/CCreatureHandler.h"
 #include "../lib/spells/CSpellHandler.h"
 #include "CMusicHandler.h"
 #include "../lib/CGeneralTextHandler.h"
 #include "Graphics.h"
 #include "Client.h"
-#include "../lib/CConfigHandler.h"
 #include "../lib/serializer/BinaryDeserializer.h"
 #include "../lib/serializer/BinarySerializer.h"
-#include "../lib/VCMI_Lib.h"
 #include "../lib/VCMIDirs.h"
 #include "../lib/NetPacks.h"
 #include "CMessage.h"
 #include "../lib/CModHandler.h"
-#include "../lib/ScriptHandler.h"
 #include "../lib/CTownHandler.h"
-#include "../lib/CArtHandler.h"
-#include "../lib/GameConstants.h"
 #include "gui/CGuiHandler.h"
 #include "../lib/logging/CBasicLogConfigurator.h"
-#include "../lib/StringConstants.h"
 #include "../lib/CPlayerState.h"
 #include "gui/CAnimation.h"
 #include "../lib/serializer/Connection.h"
 #include "CServerHandler.h"
 #include "gui/NotificationHandler.h"
+#include "ClientCommandManager.h"
 
 #include <boost/asio.hpp>
 
@@ -71,7 +63,7 @@
 #ifdef VCMI_ANDROID
 #include "lib/CAndroidVMHelper.h"
 #endif
-#include "../lib/UnlockGuard.h"
+
 #include "CMT.h"
 
 #if __MINGW32__
@@ -208,6 +200,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)
@@ -247,7 +241,14 @@ int main(int argc, char * argv[])
 	std::cout.flags(std::ios::unitbuf);
 #ifndef VCMI_IOS
 	console = new CConsoleHandler();
-	*console->cb = processCommand;
+
+	auto callbackFunction = [](std::string buffer, bool calledFromIngameConsole)
+	{
+		ClientCommandManager commandController;
+		commandController.processCommand(buffer, calledFromIngameConsole);
+	};
+
+	*console->cb = callbackFunction;
 	console->start();
 #endif
 
@@ -470,7 +471,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 +490,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 +539,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
@@ -548,437 +565,6 @@ int main(int argc, char * argv[])
 	return 0;
 }
 
-void printInfoAboutIntObject(const CIntObject *obj, int level)
-{
-	std::stringstream sbuffer;
-	sbuffer << std::string(level, '\t');
-
-	sbuffer << typeid(*obj).name() << " *** ";
-	if (obj->active)
-	{
-#define PRINT(check, text) if (obj->active & CIntObject::check) sbuffer << text
-		PRINT(LCLICK, 'L');
-		PRINT(RCLICK, 'R');
-		PRINT(HOVER, 'H');
-		PRINT(MOVE, 'M');
-		PRINT(KEYBOARD, 'K');
-		PRINT(TIME, 'T');
-		PRINT(GENERAL, 'A');
-		PRINT(WHEEL, 'W');
-		PRINT(DOUBLECLICK, 'D');
-#undef  PRINT
-	}
-	else
-		sbuffer << "inactive";
-	sbuffer << " at " << obj->pos.x <<"x"<< obj->pos.y;
-	sbuffer << " (" << obj->pos.w <<"x"<< obj->pos.h << ")";
-	logGlobal->info(sbuffer.str());
-
-	for(const CIntObject *child : obj->children)
-		printInfoAboutIntObject(child, level+1);
-}
-
-void removeGUI()
-{
-	// CClient::endGame
-	GH.curInt = nullptr;
-	if(GH.topInt())
-		GH.topInt()->deactivate();
-	adventureInt = nullptr;
-	GH.listInt.clear();
-	GH.objsToBlit.clear();
-	GH.statusbar = nullptr;
-	logGlobal->info("Removed GUI.");
-
-	LOCPLINT = nullptr;
-}
-
-#ifndef VCMI_IOS
-void processCommand(const std::string &message)
-{
-	std::istringstream readed;
-	readed.str(message);
-	std::string cn; //command name
-	readed >> cn;
-
-// Check mantis issue 2292 for details
-//	if(LOCPLINT && LOCPLINT->cingconsole)
-//		LOCPLINT->cingconsole->print(message);
-
-	if(message==std::string("die, fool"))
-	{
-		exit(EXIT_SUCCESS);
-	}
-	else if(cn==std::string("activate"))
-	{
-		int what;
-		readed >> what;
-		switch (what)
-		{
-		case 0:
-			GH.topInt()->activate();
-			break;
-		case 1:
-			adventureInt->activate();
-			break;
-		case 2:
-			LOCPLINT->castleInt->activate();
-			break;
-		}
-	}
-	else if(cn=="redraw")
-	{
-		GH.totalRedraw();
-	}
-	else if(cn=="screen")
-	{
-		std::cout << "Screenbuf points to ";
-
-		if(screenBuf == screen)
-			logGlobal->error("screen");
-		else if(screenBuf == screen2)
-			logGlobal->error("screen2");
-		else
-			logGlobal->error("?!?");
-
-		SDL_SaveBMP(screen, "Screen_c.bmp");
-		SDL_SaveBMP(screen2, "Screen2_c.bmp");
-	}
-	else if(cn=="save")
-	{
-		if(!CSH->client)
-		{
-			std::cout << "Game in not active";
-			return;
-		}
-		std::string fname;
-		readed >> fname;
-		CSH->client->save(fname);
-	}
-//	else if(cn=="load")
-//	{
-//		// TODO: this code should end the running game and manage to call startGame instead
-//		std::string fname;
-//		readed >> fname;
-//		CSH->client->loadGame(fname);
-//	}
-	else if(message=="convert txt")
-	{
-		VLC->generaltexth->dumpAllTexts();
-	}
-	else if(message=="get config")
-	{
-		std::cout << "Command accepted.\t";
-
-		const bfs::path outPath =
-			VCMIDirs::get().userExtractedPath() / "configuration";
-
-		bfs::create_directories(outPath);
-
-		const std::vector<std::string> contentNames = {"heroClasses", "artifacts", "creatures", "factions", "objects", "heroes", "spells", "skills"};
-
-		for(auto contentName : contentNames)
-		{
-			auto & content = (*VLC->modh->content)[contentName];
-
-			auto contentOutPath = outPath / contentName;
-			bfs::create_directories(contentOutPath);
-
-			for(auto & iter : content.modData)
-			{
-				const JsonNode & modData = iter.second.modData;
-
-				for(auto & nameAndObject : modData.Struct())
-				{
-					const JsonNode & object = nameAndObject.second;
-
-					std::string name = CModHandler::normalizeIdentifier(object.meta, CModHandler::scopeBuiltin(), nameAndObject.first);
-
-					boost::algorithm::replace_all(name,":","_");
-
-					const bfs::path filePath = contentOutPath / (name + ".json");
-					bfs::ofstream file(filePath);
-					file << object.toJson();
-				}
-			}
-		}
-
-		std::cout << "\rExtracting done :)\n";
-		std::cout << " Extracted files can be found in " << outPath << " directory\n";
-	}
-#if SCRIPTING_ENABLED
-	else if(message=="get scripts")
-	{
-		std::cout << "Command accepted.\t";
-
-		const bfs::path outPath =
-			VCMIDirs::get().userExtractedPath() / "scripts";
-
-		bfs::create_directories(outPath);
-
-		for(auto & kv : VLC->scriptHandler->objects)
-		{
-			std::string name = kv.first;
-			boost::algorithm::replace_all(name,":","_");
-
-			const scripting::ScriptImpl * script = kv.second.get();
-			bfs::path filePath = outPath / (name + ".lua");
-			bfs::ofstream file(filePath);
-			file << script->getSource();
-		}
-		std::cout << "\rExtracting done :)\n";
-		std::cout << " Extracted files can be found in " << outPath << " directory\n";
-	}
-#endif
-	else if(message=="get txt")
-	{
-		std::cout << "Command accepted.\t";
-
-		const bfs::path outPath =
-			VCMIDirs::get().userExtractedPath();
-
-		auto list = CResourceHandler::get()->getFilteredFiles([](const ResourceID & ident)
-		{
-			return ident.getType() == EResType::TEXT && boost::algorithm::starts_with(ident.getName(), "DATA/");
-		});
-
-		for (auto & filename : list)
-		{
-			const bfs::path filePath = outPath / (filename.getName() + ".TXT");
-
-			bfs::create_directories(filePath.parent_path());
-
-			bfs::ofstream file(filePath);
-			auto text = CResourceHandler::get()->load(filename)->readAll();
-
-			file.write((char*)text.first.get(), text.second);
-		}
-
-		std::cout << "\rExtracting done :)\n";
-		std::cout << " Extracted files can be found in " << outPath << " directory\n";
-	}
-	else if(cn=="crash")
-	{
-		int *ptr = nullptr;
-		*ptr = 666;
-		//disaster!
-	}
-	else if(cn == "mp" && adventureInt)
-	{
-		if(const CGHeroInstance *h = dynamic_cast<const CGHeroInstance *>(adventureInt->selection))
-			std::cout << h->movement << "; max: " << h->maxMovePoints(true) << "/" << h->maxMovePoints(false) << std::endl;
-	}
-	else if(cn == "bonuses")
-	{
-		bool jsonFormat = (message == "bonuses json");
-		auto format = [jsonFormat](const BonusList & b) -> std::string
-		{
-			if(jsonFormat)
-				return b.toJsonNode().toJson(true);
-			std::ostringstream ss;
-			ss << b;
-			return ss.str();
-		};
-		std::cout << "Bonuses of " << adventureInt->selection->getObjectName() << std::endl
-			<< format(adventureInt->selection->getBonusList()) << std::endl;
-
-		std::cout << "\nInherited bonuses:\n";
-		TCNodes parents;
-		adventureInt->selection->getParents(parents);
-		for(const CBonusSystemNode *parent : parents)
-		{
-			std::cout << "\nBonuses from " << typeid(*parent).name() << std::endl << format(*parent->getAllBonuses(Selector::all, Selector::all)) << std::endl;
-		}
-	}
-	else if(cn == "not dialog")
-	{
-		LOCPLINT->showingDialog->setn(false);
-	}
-	else if(cn == "gui")
-	{
-		for(auto & child : GH.listInt)
-		{
-			const auto childPtr = child.get();
-			if(const CIntObject * obj = dynamic_cast<const CIntObject *>(childPtr))
-				printInfoAboutIntObject(obj, 0);
-			else
-				std::cout << typeid(childPtr).name() << std::endl;
-		}
-	}
-	else if(cn=="tell")
-	{
-		std::string what;
-		int id1, id2;
-		readed >> what >> id1 >> id2;
-		if(what == "hs")
-		{
-			for(const CGHeroInstance *h : LOCPLINT->cb->getHeroesInfo())
-				if(h->type->ID.getNum() == id1)
-					if(const CArtifactInstance *a = h->getArt(ArtifactPosition(id2)))
-						std::cout << a->nodeName();
-		}
-	}
-	else if (cn == "set")
-	{
-		std::string what, value;
-		readed >> what;
-
-		Settings conf = settings.write["session"][what];
-
-		readed >> value;
-
-		if (value == "on")
-		{
-			conf->Bool() = true;
-			logGlobal->info("Option %s enabled!", what);
-		}
-		else if (value == "off")
-		{
-			conf->Bool() = false;
-			logGlobal->info("Option %s disabled!", what);
-		}
-	}
-	else if(cn == "unlock")
-	{
-		std::string mxname;
-		readed >> mxname;
-		if(mxname == "pim" && LOCPLINT)
-			LOCPLINT->pim->unlock();
-	}
-	else if(cn == "def2bmp")
-	{
-		std::string URI;
-		readed >> URI;
-		std::unique_ptr<CAnimation> anim = std::make_unique<CAnimation>(URI);
-		anim->preload();
-		anim->exportBitmaps(VCMIDirs::get().userExtractedPath());
-	}
-	else if(cn == "extract")
-	{
-		std::string URI;
-		readed >> URI;
-
-		if (CResourceHandler::get()->existsResource(ResourceID(URI)))
-		{
-			const bfs::path outPath = VCMIDirs::get().userExtractedPath() / URI;
-
-			auto data = CResourceHandler::get()->load(ResourceID(URI))->readAll();
-
-			bfs::create_directories(outPath.parent_path());
-			bfs::ofstream outFile(outPath, bfs::ofstream::binary);
-			outFile.write((char*)data.first.get(), data.second);
-		}
-		else
-			logGlobal->error("File not found!");
-	}
-	else if(cn == "setBattleAI")
-	{
-		std::string fname;
-		readed >> fname;
-		std::cout << "Will try loading that AI to see if it is correct name...\n";
-		try
-		{
-			if(auto ai = CDynLibHandler::getNewBattleAI(fname)) //test that given AI is indeed available... heavy but it is easy to make a typo and break the game
-			{
-				Settings neutralAI = settings.write["server"]["neutralAI"];
-				neutralAI->String() = fname;
-				std::cout << "Setting changed, from now the battle ai will be " << fname << "!\n";
-			}
-		}
-		catch(std::exception &e)
-		{
-			logGlobal->warn("Failed opening %s: %s", fname, e.what());
-			logGlobal->warn("Setting not changes, AI not found or invalid!");
-		}
-	}
-
-	auto giveTurn = [&](PlayerColor player)
-	{
-		YourTurn yt;
-		yt.player = player;
-		yt.daysWithoutCastle = CSH->client->getPlayerState(player)->daysWithoutCastle;
-		yt.applyCl(CSH->client);
-	};
-
-	Settings session = settings.write["session"];
-	if(cn == "autoskip")
-	{
-		session["autoSkip"].Bool() = !session["autoSkip"].Bool();
-	}
-	else if(cn == "gosolo")
-	{
-		boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
-		if(!CSH->client)
-		{
-			std::cout << "Game in not active";
-			return;
-		}
-		PlayerColor color;
-		if(session["aiSolo"].Bool())
-		{
-			for(auto & elem : CSH->client->gameState()->players)
-			{
-				if(elem.second.human)
-					CSH->client->installNewPlayerInterface(std::make_shared<CPlayerInterface>(elem.first), elem.first);
-			}
-		}
-		else
-		{
-			color = LOCPLINT->playerID;
-			removeGUI();
-			for(auto & elem : CSH->client->gameState()->players)
-			{
-				if(elem.second.human)
-				{
-					auto AiToGive = CSH->client->aiNameForPlayer(*CSH->client->getPlayerSettings(elem.first), false);
-					logNetwork->info("Player %s will be lead by %s", elem.first, AiToGive);
-					CSH->client->installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), elem.first);
-				}
-			}
-			GH.totalRedraw();
-			giveTurn(color);
-		}
-		session["aiSolo"].Bool() = !session["aiSolo"].Bool();
-	}
-	else if(cn == "controlai")
-	{
-		std::string colorName;
-		readed >> colorName;
-		boost::to_lower(colorName);
-
-		boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
-		if(!CSH->client)
-		{
-			std::cout << "Game in not active";
-			return;
-		}
-		PlayerColor color;
-		if(LOCPLINT)
-			color = LOCPLINT->playerID;
-		for(auto & elem : CSH->client->gameState()->players)
-		{
-			if(elem.second.human || (colorName.length() &&
-				elem.first.getNum() != vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, colorName)))
-			{
-				continue;
-			}
-
-			removeGUI();
-			CSH->client->installNewPlayerInterface(std::make_shared<CPlayerInterface>(elem.first), elem.first);
-		}
-		GH.totalRedraw();
-		if(color != PlayerColor::NEUTRAL)
-			giveTurn(color);
-	}
-	// Check mantis issue 2292 for details
-/* 	else if(client && client->serv && client->serv->connected && LOCPLINT) //send to server
-	{
-		boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
-		LOCPLINT->cb->sendMessage(message);
-	}*/
-}
-#endif
-
 //plays intro, ends when intro is over or button has been pressed (handles events)
 void playIntro()
 {

+ 0 - 1
client/CMT.h

@@ -19,5 +19,4 @@ extern SDL_Surface *screen;      // main screen surface
 extern SDL_Surface *screen2;     // and hlp surface (used to store not-active interfaces layer)
 extern SDL_Surface *screenBuf; // points to screen (if only advmapint is present) or screen2 (else) - should be used when updating controls which are not regularly redrawed
 
-void removeGUI();
 void handleQuit(bool ask = true);

+ 5 - 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
@@ -82,6 +82,7 @@ set(client_SRCS
 		NetPacksClient.cpp
 		NetPacksLobbyClient.cpp
 		SDLRWwrapper.cpp
+		ClientCommandManager.cpp
 )
 
 set(client_HEADERS
@@ -104,7 +105,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
@@ -168,6 +169,7 @@ set(client_HEADERS
 		mapHandler.h
 		resource.h
 		SDLRWwrapper.h
+		ClientCommandManager.h
 )
 
 if(APPLE_IOS)
@@ -232,7 +234,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)

+ 18 - 34
client/CMusicHandler.cpp

@@ -20,7 +20,7 @@
 #include "../lib/StringConstants.h"
 #include "../lib/CRandomGenerator.h"
 #include "../lib/VCMIDirs.h"
-#include "../lib/Terrain.h"
+#include "../lib/TerrainHandler.h"
 
 #define VCMI_SOUND_NAME(x)
 #define VCMI_SOUND_FILE(y) #y,
@@ -89,36 +89,6 @@ CSoundHandler::CSoundHandler():
 		soundBase::battle02, soundBase::battle03, soundBase::battle04,
 		soundBase::battle05, soundBase::battle06, soundBase::battle07
 	};
-	
-	//predefine terrain set
-	//TODO: support custom sounds for new terrains and load from json
-	horseSounds =
-	{
-		{Terrain::DIRT, soundBase::horseDirt},
-		{Terrain::SAND, soundBase::horseSand},
-		{Terrain::GRASS, soundBase::horseGrass},
-		{Terrain::SNOW, soundBase::horseSnow},
-		{Terrain::SWAMP, soundBase::horseSwamp},
-		{Terrain::ROUGH, soundBase::horseRough},
-		{Terrain::SUBTERRANEAN, soundBase::horseSubterranean},
-		{Terrain::LAVA, soundBase::horseLava},
-		{Terrain::WATER, soundBase::horseWater},
-		{Terrain::ROCK, soundBase::horseRock}
-	};
-}
-
-void CSoundHandler::loadHorseSounds()
-{
-	const auto & terrains = CGI->terrainTypeHandler->terrains();
-	for(const auto & terrain : terrains)
-	{
-		//since all sounds are hardcoded, let's keep it
-		if(vstd::contains(horseSounds, terrain.id))
-			continue;
-
-		//Use already existing horse sound
-		horseSounds[terrain.id] = horseSounds.at(terrains[terrain.id].horseSoundId);
-	}
 }
 
 void CSoundHandler::init()
@@ -368,9 +338,9 @@ CMusicHandler::CMusicHandler():
 
 void CMusicHandler::loadTerrainMusicThemes()
 {
-	for (const auto & terrain : CGI->terrainTypeHandler->terrains())
+	for (const auto & terrain : CGI->terrainTypeHandler->objects)
 	{
-		addEntryToSet("terrain_" + terrain.name, "Music/" + terrain.musicFilename);
+		addEntryToSet("terrain_" + terrain->getJsonKey(), "Music/" + terrain->musicFilename);
 	}
 }
 
@@ -542,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);
@@ -619,7 +603,7 @@ bool MusicEntry::play()
 
 bool MusicEntry::stop(int fade_ms)
 {
-	if (playing)
+	if (Mix_PlayingMusic())
 	{
 		playing = false;
 		loop = 0;

+ 0 - 3
client/CMusicHandler.h

@@ -11,7 +11,6 @@
 
 #include "../lib/CConfigHandler.h"
 #include "../lib/CSoundBase.h"
-#include "../lib/Terrain.h"
 
 struct _Mix_Music;
 struct SDL_RWops;
@@ -61,7 +60,6 @@ public:
 	CSoundHandler();
 
 	void init() override;
-	void loadHorseSounds();
 	void release() override;
 
 	void setVolume(ui32 percent) override;
@@ -84,7 +82,6 @@ public:
 	// Sets
 	std::vector<soundBase::soundID> pickupSounds;
 	std::vector<soundBase::soundID> battleIntroSounds;
-	std::map<TerrainId, soundBase::soundID> horseSounds;
 };
 
 // Helper //now it looks somewhat useless

+ 30 - 13
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"
@@ -61,6 +61,8 @@
 #include "windows/InfoWindows.h"
 #include "../lib/UnlockGuard.h"
 #include "../lib/CPathfinder.h"
+#include "../lib/RoadHandler.h"
+#include "../lib/TerrainHandler.h"
 #include <SDL.h>
 #include "CServerHandler.h"
 // FIXME: only needed for CGameState::mutex
@@ -156,7 +158,6 @@ void CPlayerInterface::initGameInterface(std::shared_ptr<Environment> ENV, std::
 	cb = CB;
 	env = ENV;
 
-	CCS->soundh->loadHorseSounds();
 	CCS->musich->loadTerrainMusicThemes();
 
 	initializeHeroTownList();
@@ -260,7 +261,7 @@ void CPlayerInterface::heroMoved(const TryMoveHero & details, bool verbose)
 	{
 		updateAmbientSounds();
 		//We may need to change music - select new track, music handler will change it if needed
-		CCS->musich->playMusicFromSet("terrain", LOCPLINT->cb->getTile(hero->visitablePos())->terType->name, true, false);
+		CCS->musich->playMusicFromSet("terrain", LOCPLINT->cb->getTile(hero->visitablePos())->terType->getJsonKey(), true, false);
 
 		if(details.result == TryMoveHero::TELEPORTATION)
 		{
@@ -436,7 +437,7 @@ void CPlayerInterface::heroKilled(const CGHeroInstance* hero)
 		adventureInt->select(newSelection, true);
 	else if (adventureInt->selection == hero)
 		adventureInt->selection = nullptr;
-	
+
 	if (vstd::contains(paths, hero))
 		paths.erase(hero);
 }
@@ -1623,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"))
@@ -2372,8 +2373,9 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path)
 		for (auto & elem : path.nodes)
 			elem.coord = h->convertFromVisitablePos(elem.coord);
 
-		TerrainId currentTerrain = Terrain::BORDER; // not init yet
+		TerrainId currentTerrain = ETerrainId::NONE;
 		TerrainId newTerrain;
+		bool wasOnRoad = true;
 		int sh = -1;
 
 		auto canStop = [&](CGPathNode * node) -> bool
@@ -2389,13 +2391,18 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path)
 
 		for (i=(int)path.nodes.size()-1; i>0 && (stillMoveHero.data == CONTINUE_MOVE || !canStop(&path.nodes[i])); i--)
 		{
-			int3 currentCoord = path.nodes[i].coord;
+			int3 prevCoord = path.nodes[i].coord;
 			int3 nextCoord = path.nodes[i-1].coord;
 
-			auto currentObject = getObj(currentCoord, currentCoord == h->pos);
+			auto prevRoad = cb->getTile(h->convertToVisitablePos(prevCoord))->roadType;
+			auto nextRoad = cb->getTile(h->convertToVisitablePos(nextCoord))->roadType;
+
+			bool movingOnRoad = prevRoad->getId() != Road::NO_ROAD && nextRoad->getId() != Road::NO_ROAD;
+
+			auto prevObject = getObj(prevCoord, prevCoord == h->pos);
 			auto nextObjectTop = getObj(nextCoord, false);
 			auto nextObject = getObj(nextCoord, true);
-			auto destTeleportObj = getDestTeleportObj(currentObject, nextObjectTop, nextObject);
+			auto destTeleportObj = getDestTeleportObj(prevObject, nextObjectTop, nextObject);
 			if (isTeleportAction(path.nodes[i-1].action) && destTeleportObj != nullptr)
 			{
 				CCS->soundh->stopSound(sh);
@@ -2410,7 +2417,10 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path)
 				}
 				if(i != path.nodes.size() - 1)
 				{
-					sh = CCS->soundh->playSound(CCS->soundh->horseSounds[currentTerrain], -1);
+					if (movingOnRoad)
+						sh = CCS->soundh->playSound(VLC->terrainTypeHandler->getById(currentTerrain)->horseSound, -1);
+					else
+						sh = CCS->soundh->playSound(VLC->terrainTypeHandler->getById(currentTerrain)->horseSoundPenalty, -1);
 				}
 				continue;
 			}
@@ -2428,12 +2438,16 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path)
 				sh = CCS->soundh->playSound(soundBase::horseFlying, -1);
 #endif
 			{
-				newTerrain = cb->getTile(h->convertToVisitablePos(currentCoord))->terType->id;
-				if(newTerrain != currentTerrain)
+				newTerrain = cb->getTile(h->convertToVisitablePos(prevCoord))->terType->getId();
+				if(newTerrain != currentTerrain || wasOnRoad != movingOnRoad)
 				{
 					CCS->soundh->stopSound(sh);
-					sh = CCS->soundh->playSound(CCS->soundh->horseSounds[newTerrain], -1);
+					if (movingOnRoad)
+						sh = CCS->soundh->playSound(VLC->terrainTypeHandler->getById(newTerrain)->horseSound, -1);
+					else
+						sh = CCS->soundh->playSound(VLC->terrainTypeHandler->getById(newTerrain)->horseSoundPenalty, -1);
 					currentTerrain = newTerrain;
+					wasOnRoad = movingOnRoad;
 				}
 			}
 
@@ -2473,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);

+ 22 - 1
client/Client.cpp

@@ -46,9 +46,9 @@
 #include "../lib/CThreadHelper.h"
 #include "../lib/registerTypes/RegisterTypes.h"
 #include "gui/CGuiHandler.h"
-#include "CMT.h"
 #include "CServerHandler.h"
 #include "../lib/ScriptHandler.h"
+#include "windows/CAdvmapInterface.h"
 #include <vcmi/events/EventBus.h>
 
 #ifdef VCMI_ANDROID
@@ -761,8 +761,29 @@ void CClient::reinitScripting()
 #endif
 }
 
+void CClient::removeGUI()
+{
+	// CClient::endGame
+	GH.curInt = nullptr;
+	if(GH.topInt())
+		GH.topInt()->deactivate();
+	adventureInt.reset();
+	GH.listInt.clear();
+	GH.objsToBlit.clear();
+	GH.statusbar.reset();
+	logGlobal->info("Removed GUI.");
+
+	LOCPLINT = nullptr;
+}
 
 #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 - 0
client/Client.h

@@ -240,6 +240,7 @@ public:
 
 	void showInfoDialog(InfoWindow * iw) override {};
 	void showInfoDialog(const std::string & msg, PlayerColor player) override {};
+	void removeGUI();
 
 #if SCRIPTING_ENABLED
 	scripting::Pool * getGlobalContextPool() const override;

+ 494 - 0
client/ClientCommandManager.cpp

@@ -0,0 +1,494 @@
+/*
+ * ClientCommandManager.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 "ClientCommandManager.h"
+
+#include "Client.h"
+#include "CPlayerInterface.h"
+#include "CServerHandler.h"
+#include "gui/CGuiHandler.h"
+#include "../lib/NetPacks.h"
+#include "../lib/CConfigHandler.h"
+#include "../lib/CGameState.h"
+#include "../lib/CPlayerState.h"
+#include "../lib/StringConstants.h"
+#include "gui/CAnimation.h"
+#include "windows/CAdvmapInterface.h"
+#include "windows/CCastleInterface.h"
+#include "../CCallback.h"
+#include "../lib/CGeneralTextHandler.h"
+#include "../lib/CHeroHandler.h"
+#include "../lib/CModHandler.h"
+#include "../lib/VCMIDirs.h"
+
+#ifdef SCRIPTING_ENABLED
+#include "../lib/ScriptHandler.h"
+#endif
+
+void ClientCommandManager::handleGoSolo()
+{
+	Settings session = settings.write["session"];
+
+	boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
+	if(!CSH->client)
+	{
+		printCommandMessage("Game is not in playing state");
+		return;
+	}
+	PlayerColor color;
+	if(session["aiSolo"].Bool())
+	{
+		for(auto & elem : CSH->client->gameState()->players)
+		{
+			if(elem.second.human)
+				CSH->client->installNewPlayerInterface(std::make_shared<CPlayerInterface>(elem.first), elem.first);
+		}
+	}
+	else
+	{
+		color = LOCPLINT->playerID;
+		CSH->client->removeGUI();
+		for(auto & elem : CSH->client->gameState()->players)
+		{
+			if(elem.second.human)
+			{
+				auto AiToGive = CSH->client->aiNameForPlayer(*CSH->client->getPlayerSettings(elem.first), false);
+				printCommandMessage("Player " + elem.first.getStr() + " will be lead by " + AiToGive, ELogLevel::INFO);
+				CSH->client->installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), elem.first);
+			}
+		}
+		GH.totalRedraw();
+		giveTurn(color);
+	}
+	session["aiSolo"].Bool() = !session["aiSolo"].Bool();
+}
+
+void ClientCommandManager::handleControlAi(const std::string &colorName)
+{
+	boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
+	if(!CSH->client)
+	{
+		printCommandMessage("Game is not in playing state");
+		return;
+	}
+	PlayerColor color;
+	if(LOCPLINT)
+		color = LOCPLINT->playerID;
+	for(auto & elem : CSH->client->gameState()->players)
+	{
+		if(elem.second.human || (colorName.length() &&
+								 elem.first.getNum() != vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, colorName)))
+		{
+			continue;
+		}
+
+		CSH->client->removeGUI();
+		CSH->client->installNewPlayerInterface(std::make_shared<CPlayerInterface>(elem.first), elem.first);
+	}
+	GH.totalRedraw();
+	if(color != PlayerColor::NEUTRAL)
+		giveTurn(color);
+}
+
+void ClientCommandManager::processCommand(const std::string &message, bool calledFromIngameConsole)
+{
+	std::istringstream singleWordBuffer;
+	singleWordBuffer.str(message);
+	std::string commandName;
+	singleWordBuffer >> commandName;
+	currentCallFromIngameConsole = calledFromIngameConsole;
+
+	if(message==std::string("die, fool"))
+	{
+		exit(EXIT_SUCCESS);
+	}
+	else if(commandName == std::string("activate"))
+	{
+		int what;
+		singleWordBuffer >> what;
+		switch (what)
+		{
+			case 0:
+				GH.topInt()->activate();
+				break;
+			case 1:
+				adventureInt->activate();
+				break;
+			case 2:
+				LOCPLINT->castleInt->activate();
+				break;
+			default:
+				printCommandMessage("Wrong argument specified!", ELogLevel::ERROR);
+		}
+	}
+	else if(commandName == "redraw")
+	{
+		GH.totalRedraw();
+	}
+	else if(commandName == "screen")
+	{
+		printCommandMessage("Screenbuf points to ");
+
+		if(screenBuf == screen)
+			printCommandMessage("screen", ELogLevel::ERROR);
+		else if(screenBuf == screen2)
+			printCommandMessage("screen2", ELogLevel::ERROR);
+		else
+			printCommandMessage("?!?", ELogLevel::ERROR);
+
+		SDL_SaveBMP(screen, "Screen_c.bmp");
+		SDL_SaveBMP(screen2, "Screen2_c.bmp");
+	}
+	else if(commandName == "save")
+	{
+		if(!CSH->client)
+		{
+			printCommandMessage("Game is not in playing state");
+			return;
+		}
+		std::string fname;
+		singleWordBuffer >> fname;
+		CSH->client->save(fname);
+	}
+//	else if(commandName=="load")
+//	{
+//		// TODO: this code should end the running game and manage to call startGame instead
+//		std::string fname;
+//		singleWordBuffer >> fname;
+//		CSH->client->loadGame(fname);
+//	}
+	else if(message=="convert txt")
+	{
+		VLC->generaltexth->dumpAllTexts();
+	}
+	else if(message=="get config")
+	{
+		printCommandMessage("Command accepted.\t");
+
+		const boost::filesystem::path outPath =
+				VCMIDirs::get().userExtractedPath() / "configuration";
+
+		boost::filesystem::create_directories(outPath);
+
+		const std::vector<std::string> contentNames = {"heroClasses", "artifacts", "creatures", "factions", "objects", "heroes", "spells", "skills"};
+
+		for(auto contentName : contentNames)
+		{
+			auto & content = (*VLC->modh->content)[contentName];
+
+			auto contentOutPath = outPath / contentName;
+			boost::filesystem::create_directories(contentOutPath);
+
+			for(auto & iter : content.modData)
+			{
+				const JsonNode & modData = iter.second.modData;
+
+				for(auto & nameAndObject : modData.Struct())
+				{
+					const JsonNode & object = nameAndObject.second;
+
+					std::string name = CModHandler::normalizeIdentifier(object.meta, CModHandler::scopeBuiltin(), nameAndObject.first);
+
+					boost::algorithm::replace_all(name,":","_");
+
+					const boost::filesystem::path filePath = contentOutPath / (name + ".json");
+					boost::filesystem::ofstream file(filePath);
+					file << object.toJson();
+				}
+			}
+		}
+
+		printCommandMessage("\rExtracting done :)\n");
+		printCommandMessage("Extracted files can be found in " + outPath.string() + " directory\n");
+	}
+#if SCRIPTING_ENABLED
+		else if(message=="get scripts")
+	{
+		printCommandMessage("Command accepted.\t");
+
+		const boost::filesystem::path outPath =
+			VCMIDirs::get().userExtractedPath() / "scripts";
+
+		boost::filesystem::create_directories(outPath);
+
+		for(auto & kv : VLC->scriptHandler->objects)
+		{
+			std::string name = kv.first;
+			boost::algorithm::replace_all(name,":","_");
+
+			const scripting::ScriptImpl * script = kv.second.get();
+			boost::filesystem::path filePath = outPath / (name + ".lua");
+			boost::filesystem::ofstream file(filePath);
+			file << script->getSource();
+		}
+		printCommandMessage("\rExtracting done :)\n");
+		printCommandMessage("Extracted files can be found in " + outPath.string() + " directory\n");
+	}
+#endif
+	else if(message=="get txt")
+	{
+		printCommandMessage("Command accepted.\t");
+
+		const boost::filesystem::path outPath =
+				VCMIDirs::get().userExtractedPath();
+
+		auto list =
+				CResourceHandler::get()->getFilteredFiles([](const ResourceID & ident)
+				{
+					return ident.getType() == EResType::TEXT && boost::algorithm::starts_with(ident.getName(), "DATA/");
+				});
+
+		for (auto & filename : list)
+		{
+			const boost::filesystem::path filePath = outPath / (filename.getName() + ".TXT");
+
+			boost::filesystem::create_directories(filePath.parent_path());
+
+			boost::filesystem::ofstream file(filePath);
+			auto text = CResourceHandler::get()->load(filename)->readAll();
+
+			file.write((char*)text.first.get(), text.second);
+		}
+
+		printCommandMessage("\rExtracting done :)\n");
+		printCommandMessage("Extracted files can be found in " + outPath.string() + " directory\n");
+	}
+	else if(commandName == "crash")
+	{
+		int *ptr = nullptr;
+		*ptr = 666;
+		//disaster!
+	}
+	else if(commandName == "mp" && adventureInt)
+	{
+		if(const CGHeroInstance *h = dynamic_cast<const CGHeroInstance *>(adventureInt->selection))
+			printCommandMessage(std::to_string(h->movement) + "; max: " + std::to_string(h->maxMovePoints(true)) + "/" + std::to_string(h->maxMovePoints(false)) + "\n");
+	}
+	else if(commandName == "bonuses")
+	{
+		bool jsonFormat = (message == "bonuses json");
+		auto format = [jsonFormat](const BonusList & b) -> std::string
+		{
+			if(jsonFormat)
+				return b.toJsonNode().toJson(true);
+			std::ostringstream ss;
+			ss << b;
+			return ss.str();
+		};
+		printCommandMessage("Bonuses of " + adventureInt->selection->getObjectName() + "\n");
+		printCommandMessage(format(adventureInt->selection->getBonusList()) + "\n");
+
+		printCommandMessage("\nInherited bonuses:\n");
+		TCNodes parents;
+		adventureInt->selection->getParents(parents);
+		for(const CBonusSystemNode *parent : parents)
+		{
+			printCommandMessage(std::string("\nBonuses from ") + typeid(*parent).name() + "\n" + format(*parent->getAllBonuses(Selector::all, Selector::all)) + "\n");
+		}
+	}
+	else if(commandName == "not dialog")
+	{
+		LOCPLINT->showingDialog->setn(false);
+	}
+	else if(commandName == "gui")
+	{
+		for(auto & child : GH.listInt)
+		{
+			const auto childPtr = child.get();
+			if(const CIntObject * obj = dynamic_cast<const CIntObject *>(childPtr))
+				printInfoAboutInterfaceObject(obj, 0);
+			else
+				printCommandMessage(std::string(typeid(childPtr).name()) + "\n");
+		}
+	}
+	else if(commandName == "tell")
+	{
+		std::string what;
+		int id1, id2;
+		singleWordBuffer >> what >> id1 >> id2;
+		if(what == "hs")
+		{
+			for(const CGHeroInstance *h : LOCPLINT->cb->getHeroesInfo())
+				if(h->type->ID.getNum() == id1)
+					if(const CArtifactInstance *a = h->getArt(ArtifactPosition(id2)))
+						printCommandMessage(a->nodeName());
+		}
+	}
+	else if (commandName == "set")
+	{
+		std::string what, value;
+		singleWordBuffer >> what;
+
+		Settings config = settings.write["session"][what];
+
+		singleWordBuffer >> value;
+
+		if (value == "on")
+		{
+			config->Bool() = true;
+			printCommandMessage("Option " + what + " enabled!", ELogLevel::INFO);
+		}
+		else if (value == "off")
+		{
+			config->Bool() = false;
+			printCommandMessage("Option " + what + " disabled!", ELogLevel::INFO);
+		}
+	}
+	else if(commandName == "unlock")
+	{
+		std::string mxname;
+		singleWordBuffer >> mxname;
+		if(mxname == "pim" && LOCPLINT)
+			LOCPLINT->pim->unlock();
+	}
+	else if(commandName == "def2bmp")
+	{
+		std::string URI;
+		singleWordBuffer >> URI;
+		std::unique_ptr<CAnimation> anim = std::make_unique<CAnimation>(URI);
+		anim->preload();
+		anim->exportBitmaps(VCMIDirs::get().userExtractedPath());
+	}
+	else if(commandName == "extract")
+	{
+		std::string URI;
+		singleWordBuffer >> URI;
+
+		if (CResourceHandler::get()->existsResource(ResourceID(URI)))
+		{
+			const boost::filesystem::path outPath = VCMIDirs::get().userExtractedPath() / URI;
+
+			auto data = CResourceHandler::get()->load(ResourceID(URI))->readAll();
+
+			boost::filesystem::create_directories(outPath.parent_path());
+			boost::filesystem::ofstream outFile(outPath, boost::filesystem::ofstream::binary);
+			outFile.write((char*)data.first.get(), data.second);
+		}
+		else
+			printCommandMessage("File not found!", ELogLevel::ERROR);
+	}
+	else if(commandName == "setBattleAI")
+	{
+		std::string fname;
+		singleWordBuffer >> fname;
+		printCommandMessage("Will try loading that AI to see if it is correct name...\n");
+		try
+		{
+			if(auto ai = CDynLibHandler::getNewBattleAI(fname)) //test that given AI is indeed available... heavy but it is easy to make a typo and break the game
+			{
+				Settings neutralAI = settings.write["server"]["neutralAI"];
+				neutralAI->String() = fname;
+				printCommandMessage("Setting changed, from now the battle ai will be " + fname + "!\n");
+			}
+		}
+		catch(std::exception &e)
+		{
+			printCommandMessage("Failed opening " + fname + ": " + e.what(), ELogLevel::WARN);
+			printCommandMessage("Setting not changed, AI not found or invalid!", ELogLevel::WARN);
+		}
+	}
+	else if(commandName == "autoskip")
+	{
+		Settings session = settings.write["session"];
+		session["autoSkip"].Bool() = !session["autoSkip"].Bool();
+	}
+	else if(commandName == "gosolo")
+	{
+		ClientCommandManager::handleGoSolo();
+	}
+	else if(commandName == "controlai")
+	{
+		std::string colorName;
+		singleWordBuffer >> colorName;
+		boost::to_lower(colorName);
+
+		ClientCommandManager::handleControlAi(colorName);
+	}
+	else
+	{
+		printCommandMessage("Command not found :(", ELogLevel::ERROR);
+	}
+}
+
+void ClientCommandManager::giveTurn(const PlayerColor &colorIdentifier)
+{
+	YourTurn yt;
+	yt.player = colorIdentifier;
+	yt.daysWithoutCastle = CSH->client->getPlayerState(colorIdentifier)->daysWithoutCastle;
+	yt.applyCl(CSH->client);
+}
+
+void ClientCommandManager::printInfoAboutInterfaceObject(const CIntObject *obj, int level)
+{
+	std::stringstream sbuffer;
+	sbuffer << std::string(level, '\t');
+
+	sbuffer << typeid(*obj).name() << " *** ";
+	if (obj->active)
+	{
+#define PRINT(check, text) if (obj->active & CIntObject::check) sbuffer << text
+		PRINT(LCLICK, 'L');
+		PRINT(RCLICK, 'R');
+		PRINT(HOVER, 'H');
+		PRINT(MOVE, 'M');
+		PRINT(KEYBOARD, 'K');
+		PRINT(TIME, 'T');
+		PRINT(GENERAL, 'A');
+		PRINT(WHEEL, 'W');
+		PRINT(DOUBLECLICK, 'D');
+#undef  PRINT
+	}
+	else
+		sbuffer << "inactive";
+	sbuffer << " at " << obj->pos.x <<"x"<< obj->pos.y;
+	sbuffer << " (" << obj->pos.w <<"x"<< obj->pos.h << ")";
+	printCommandMessage(sbuffer.str(), ELogLevel::INFO);
+
+	for(const CIntObject *child : obj->children)
+		printInfoAboutInterfaceObject(child, level+1);
+}
+
+void ClientCommandManager::printCommandMessage(const std::string &commandMessage, ELogLevel::ELogLevel messageType)
+{
+	switch(messageType)
+	{
+		case ELogLevel::NOT_SET:
+			std::cout << commandMessage;
+			break;
+		case ELogLevel::TRACE:
+			logGlobal->trace(commandMessage);
+			break;
+		case ELogLevel::DEBUG:
+			logGlobal->debug(commandMessage);
+			break;
+		case ELogLevel::INFO:
+			logGlobal->info(commandMessage);
+			break;
+		case ELogLevel::WARN:
+			logGlobal->warn(commandMessage);
+			break;
+		case ELogLevel::ERROR:
+			logGlobal->error(commandMessage);
+			break;
+		default:
+			std::cout << commandMessage;
+			break;
+	}
+
+	if(currentCallFromIngameConsole)
+	{
+		boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
+		if(LOCPLINT && LOCPLINT->cingconsole)
+		{
+			LOCPLINT->cingconsole->print(commandMessage);
+		}
+	}
+}

+ 31 - 0
client/ClientCommandManager.h

@@ -0,0 +1,31 @@
+/*
+ * ClientCommandManager.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#pragma once
+
+VCMI_LIB_NAMESPACE_BEGIN
+class PlayerColor;
+VCMI_LIB_NAMESPACE_END
+class CIntObject;
+
+class ClientCommandManager //take mantis #2292 issue about account if thinking about handling cheats from command-line
+{
+	bool currentCallFromIngameConsole;
+
+	void giveTurn(const PlayerColor &color);
+	void printInfoAboutInterfaceObject(const CIntObject *obj, int level);
+	void printCommandMessage(const std::string &commandMessage, ELogLevel::ELogLevel messageType = ELogLevel::NOT_SET);
+	void handleGoSolo();
+	void handleControlAi(const std::string &colorName);
+
+public:
+	ClientCommandManager() = default;
+	void processCommand(const std::string &message, bool calledFromIngameConsole);
+};

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

+ 4 - 3
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"
 
@@ -41,6 +41,7 @@
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/NetPacks.h"
 #include "../../lib/UnlockGuard.h"
+#include "../../lib/TerrainHandler.h"
 
 CondSh<BattleAction *> BattleInterface::givenCommand(nullptr);
 
@@ -136,8 +137,8 @@ BattleInterface::~BattleInterface()
 	if (adventureInt && adventureInt->selection)
 	{
 		//FIXME: this should be moved to adventureInt which should restore correct track based on selection/active player
-		const auto & terrain = *(LOCPLINT->cb->getTile(adventureInt->selection->visitablePos())->terType);
-		CCS->musich->playMusicFromSet("terrain", terrain.name, true, false);
+		const auto * terrain = LOCPLINT->cb->getTile(adventureInt->selection->visitablePos())->terType;
+		CCS->musich->playMusicFromSet("terrain", terrain->getJsonKey(), true, false);
 	}
 
 	// may happen if user decided to close game while in battle

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

+ 7 - 6
client/lobby/RandomMapTab.cpp

@@ -29,6 +29,7 @@
 #include "../../lib/rmg/CMapGenOptions.h"
 #include "../../lib/CModHandler.h"
 #include "../../lib/rmg/CRmgTemplateStorage.h"
+#include "../../lib/RoadHandler.h"
 
 RandomMapTab::RandomMapTab():
 	InterfaceObjectConfigurable()
@@ -108,12 +109,12 @@ RandomMapTab::RandomMapTab():
 		GH.pushIntT<TeamAlignmentsWidget>(*this);
 	});
 	
-	for(auto road : VLC->terrainTypeHandler->roads())
+	for(auto road : VLC->roadTypeHandler->objects)
 	{
-		std::string cbRoadType = "selectRoad_" + road.name;
+		std::string cbRoadType = "selectRoad_" + road->getJsonKey();
 		addCallback(cbRoadType, [&, road](bool on)
 		{
-			mapGenOptions->setRoadEnabled(road.name, on);
+			mapGenOptions->setRoadEnabled(road->getJsonKey(), on);
 			updateMapInfoByHost();
 		});
 	}
@@ -283,11 +284,11 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr<CMapGenOptions> opts)
 		else
 			w->addTextOverlay(readText(variables["defaultTemplate"]), EFonts::FONT_SMALL);
 	}
-	for(auto r : VLC->terrainTypeHandler->roads())
+	for(auto r : VLC->roadTypeHandler->objects)
 	{
-		if(auto w = widget<CToggleButton>(r.name))
+		if(auto w = widget<CToggleButton>(r->getJsonKey()))
 		{
-			w->setSelected(opts->isRoadEnabled(r.name));
+			w->setSelected(opts->isRoadEnabled(r->getJsonKey()));
 		}
 	}
 }

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

+ 18 - 16
client/mapHandler.cpp

@@ -30,7 +30,9 @@
 #include "CMT.h"
 #include "CMusicHandler.h"
 #include "../lib/CRandomGenerator.h"
-#include "../lib/Terrain.h"
+#include "../lib/RoadHandler.h"
+#include "../lib/RiverHandler.h"
+#include "../lib/TerrainHandler.h"
 #include "../lib/filesystem/ResourceID.h"
 #include "../lib/JsonDetail.h"
 
@@ -175,17 +177,17 @@ void CMapHandler::initTerrainGraphics()
 	std::map<std::string, std::string> terrainFiles;
 	std::map<std::string, std::string> riverFiles;
 	std::map<std::string, std::string> roadFiles;
-	for(const auto & terrain : VLC->terrainTypeHandler->terrains())
+	for(const auto & terrain : VLC->terrainTypeHandler->objects)
 	{
-		terrainFiles[terrain.name] = terrain.tilesFilename;
+		terrainFiles[terrain->getJsonKey()] = terrain->tilesFilename;
 	}
-	for(const auto & river : VLC->terrainTypeHandler->rivers())
+	for(const auto & river : VLC->riverTypeHandler->objects)
 	{
-		riverFiles[river.fileName] = river.fileName;
+		riverFiles[river->getJsonKey()] = river->tilesFilename;
 	}
-	for(const auto & road : VLC->terrainTypeHandler->roads())
+	for(const auto & road : VLC->roadTypeHandler->objects)
 	{
-		roadFiles[road.fileName] = road.fileName;
+		roadFiles[road->getJsonKey()] = road->tilesFilename;
 	}
 	
 	loadFlipped(terrainAnimations, terrainImages, terrainFiles);
@@ -606,7 +608,7 @@ void CMapHandler::CMapBlitter::drawTileTerrain(SDL_Surface * targetSurf, const T
 	ui8 rotation = tinfo.extTileFlags % 4;
 	
 	//TODO: use ui8 instead of string key
-	auto terrainName = tinfo.terType->name;
+	auto terrainName = tinfo.terType->getJsonKey();
 
 	if(parent->terrainImages[terrainName].size()<=tinfo.terView)
 		return;
@@ -786,21 +788,21 @@ void CMapHandler::CMapBlitter::drawObjects(SDL_Surface * targetSurf, const Terra
 
 void CMapHandler::CMapBlitter::drawRoad(SDL_Surface * targetSurf, const TerrainTile & tinfo, const TerrainTile * tinfoUpper) const
 {
-	if (tinfoUpper && tinfoUpper->roadType->id != Road::NO_ROAD)
+	if (tinfoUpper && tinfoUpper->roadType->getId() != Road::NO_ROAD)
 	{
 		ui8 rotation = (tinfoUpper->extTileFlags >> 4) % 4;
 		Rect source(0, tileSize / 2, tileSize, tileSize / 2);
 		Rect dest(realPos.x, realPos.y, tileSize, tileSize / 2);
-		drawElement(EMapCacheType::ROADS, parent->roadImages[tinfoUpper->roadType->fileName][tinfoUpper->roadDir][rotation],
+		drawElement(EMapCacheType::ROADS, parent->roadImages[tinfoUpper->roadType->getJsonKey()][tinfoUpper->roadDir][rotation],
 				&source, targetSurf, &dest);
 	}
 
-	if(tinfo.roadType->id != Road::NO_ROAD) //print road from this tile
+	if(tinfo.roadType->getId() != Road::NO_ROAD) //print road from this tile
 	{
 		ui8 rotation = (tinfo.extTileFlags >> 4) % 4;
 		Rect source(0, 0, tileSize, halfTileSizeCeil);
 		Rect dest(realPos.x, realPos.y + tileSize / 2, tileSize, tileSize / 2);
-		drawElement(EMapCacheType::ROADS, parent->roadImages[tinfo.roadType->fileName][tinfo.roadDir][rotation],
+		drawElement(EMapCacheType::ROADS, parent->roadImages[tinfo.roadType->getJsonKey()][tinfo.roadDir][rotation],
 				&source, targetSurf, &dest);
 	}
 }
@@ -809,7 +811,7 @@ void CMapHandler::CMapBlitter::drawRiver(SDL_Surface * targetSurf, const Terrain
 {
 	Rect destRect(realTileRect);
 	ui8 rotation = (tinfo.extTileFlags >> 2) % 4;
-	drawElement(EMapCacheType::RIVERS, parent->riverImages[tinfo.riverType->fileName][tinfo.riverDir][rotation], nullptr, targetSurf, &destRect);
+	drawElement(EMapCacheType::RIVERS, parent->riverImages[tinfo.riverType->getJsonKey()][tinfo.riverDir][rotation], nullptr, targetSurf, &destRect);
 }
 
 void CMapHandler::CMapBlitter::drawFow(SDL_Surface * targetSurf) const
@@ -860,7 +862,7 @@ void CMapHandler::CMapBlitter::blit(SDL_Surface * targetSurf, const MapDrawingIn
 			if(isVisible || info->showAllTerrain)
 			{
 				drawTileTerrain(targetSurf, tinfo, tile);
-				if(tinfo.riverType->id != River::NO_RIVER)
+				if(tinfo.riverType->getId() != River::NO_RIVER)
 					drawRiver(targetSurf, tinfo);
 				drawRoad(targetSurf, tinfo, tinfoUpper);
 			}
@@ -1388,8 +1390,9 @@ void CMapHandler::getTerrainDescr(const int3 & pos, std::string & out, bool isRM
 			break;
 		}
 	}
+
 	if(!isTile2Terrain || out.empty())
-		out = CGI->generaltexth->terrainNames[t.terType->id];
+		out = t.terType->getNameTranslated();
 
 	if(t.getDiggingStatus(false) == EDiggingStatus::CAN_DIG)
 	{
@@ -1485,4 +1488,3 @@ TerrainTileObject::TerrainTileObject(const CGObjectInstance * obj_, SDL_Rect rec
 TerrainTileObject::~TerrainTileObject()
 {
 }
-

+ 28 - 13
client/widgets/AdventureMapClasses.cpp

@@ -41,13 +41,14 @@
 #include "../../lib/CHeroHandler.h"
 #include "../../lib/CModHandler.h"
 #include "../../lib/CTownHandler.h"
-#include "../../lib/Terrain.h"
+#include "../../lib/TerrainHandler.h"
 #include "../../lib/filesystem/Filesystem.h"
 #include "../../lib/JsonNode.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/mapping/CMap.h"
 #include "../../lib/NetPacksBase.h"
 #include "../../lib/StringConstants.h"
+#include "ClientCommandManager.h"
 
 CList::CListItem::CListItem(CList * Parent)
 	: CIntObject(LCLICK | RCLICK | HOVER),
@@ -390,7 +391,7 @@ const SDL_Color & CMinimapInstance::getTileColor(const int3 & pos)
 	}
 
 	// else - use terrain color (blocked version or normal)
-	const auto & colorPair = parent->colors.find(tile->terType->id)->second;
+	const auto & colorPair = parent->colors.find(tile->terType->getId())->second;
 	if (tile->blocked && (!tile->visitable))
 		return colorPair.second;
 	else
@@ -499,25 +500,25 @@ std::map<TerrainId, std::pair<SDL_Color, SDL_Color> > CMinimap::loadColors()
 {
 	std::map<TerrainId, std::pair<SDL_Color, SDL_Color> > ret;
 
-	for(const auto & terrain : CGI->terrainTypeHandler->terrains())
+	for(const auto & terrain : CGI->terrainTypeHandler->objects)
 	{
 		SDL_Color normal =
 		{
-			ui8(terrain.minimapUnblocked[0]),
-			ui8(terrain.minimapUnblocked[1]),
-			ui8(terrain.minimapUnblocked[2]),
+			ui8(terrain->minimapUnblocked[0]),
+			ui8(terrain->minimapUnblocked[1]),
+			ui8(terrain->minimapUnblocked[2]),
 			ui8(255)
 		};
 
 		SDL_Color blocked =
 		{
-			ui8(terrain.minimapBlocked[0]),
-			ui8(terrain.minimapBlocked[1]),
-			ui8(terrain.minimapBlocked[2]),
+			ui8(terrain->minimapBlocked[0]),
+			ui8(terrain->minimapBlocked[1]),
+			ui8(terrain->minimapBlocked[2]),
 			ui8(255)
 		};
 
-		ret[terrain.id] = std::make_pair(normal, blocked);
+		ret[terrain->getId()] = std::make_pair(normal, blocked);
 	}
 	return ret;
 }
@@ -1141,15 +1142,29 @@ void CInGameConsole::startEnteringText()
 	GH.statusbar->setEnteredText(enteredText);
 }
 
-void CInGameConsole::endEnteringText(bool printEnteredText)
+void CInGameConsole::endEnteringText(bool processEnteredText)
 {
 	captureAllKeys = false;
 	prevEntDisp = -1;
-	if(printEnteredText)
+	if(processEnteredText)
 	{
 		std::string txt = enteredText.substr(0, enteredText.size()-1);
-		LOCPLINT->cb->sendMessage(txt, LOCPLINT->getSelection());
 		previouslyEntered.push_back(txt);
+
+		if(txt.at(0) == '/')
+		{
+			//some commands like gosolo don't work when executed from GUI thread
+			auto threadFunction = [=]()
+			{
+				ClientCommandManager commandController;
+				commandController.processCommand(txt.substr(1), true);
+			};
+
+			boost::thread clientCommandThread(threadFunction);
+			clientCommandThread.detach();
+		}
+		else
+			LOCPLINT->cb->sendMessage(txt, LOCPLINT->getSelection());
 	}
 	enteredText.clear();
 

+ 1 - 2
client/widgets/AdventureMapClasses.h

@@ -11,7 +11,6 @@
 
 #include "ObjectLists.h"
 #include "../../lib/FunctionList.h"
-#include "Terrain.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -424,7 +423,7 @@ public:
 	void textEdited(const SDL_TextEditingEvent & event) override;
 
 	void startEnteringText();
-	void endEnteringText(bool printEnteredText);
+	void endEnteringText(bool processEnteredText);
 	void refreshEnteredText();
 
 	CInGameConsole();

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

+ 4 - 3
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"
@@ -53,6 +53,7 @@
 #include "../../lib/VCMI_Lib.h"
 #include "../../lib/StartInfo.h"
 #include "../../lib/mapping/CMapInfo.h"
+#include "../../lib/TerrainHandler.h"
 
 #define ADVOPT (conf.go()->ac)
 using namespace CSDL_Ext;
@@ -1038,7 +1039,7 @@ void CAdvMapInt::show(SDL_Surface * to)
 	{
 		++heroAnim;
 	}
-	if(animValHitCount == 8)
+	if(animValHitCount >= 8)
 	{
 		CGI->mh->updateWater();
 		animValHitCount = 0;
@@ -1414,7 +1415,7 @@ void CAdvMapInt::select(const CArmedInstance *sel, bool centerView)
 		auto pos = sel->visitablePos();
 		auto tile = LOCPLINT->cb->getTile(pos);
 		if(tile)
-			CCS->musich->playMusicFromSet("terrain", tile->terType->name, true, false);
+			CCS->musich->playMusicFromSet("terrain", tile->terType->getJsonKey(), true, false);
 	}
 	if(centerView)
 		centerOn(sel);

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

+ 6 - 2
cmake_modules/VCMI_lib.cmake

@@ -191,8 +191,10 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 		${MAIN_LIB_DIR}/ObstacleHandler.cpp
 		${MAIN_LIB_DIR}/StartInfo.cpp
 		${MAIN_LIB_DIR}/ResourceSet.cpp
+		${MAIN_LIB_DIR}/RiverHandler.cpp
+		${MAIN_LIB_DIR}/RoadHandler.cpp
 		${MAIN_LIB_DIR}/ScriptHandler.cpp
-		${MAIN_LIB_DIR}/Terrain.cpp
+		${MAIN_LIB_DIR}/TerrainHandler.cpp
 		${MAIN_LIB_DIR}/VCMIDirs.cpp
 		${MAIN_LIB_DIR}/VCMI_Lib.cpp
 
@@ -439,11 +441,13 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 		${MAIN_LIB_DIR}/ObstacleHandler.h
 		${MAIN_LIB_DIR}/PathfinderUtil.h
 		${MAIN_LIB_DIR}/ResourceSet.h
+		${MAIN_LIB_DIR}/RiverHandler.h
+		${MAIN_LIB_DIR}/RoadHandler.h
 		${MAIN_LIB_DIR}/ScriptHandler.h
 		${MAIN_LIB_DIR}/ScopeGuard.h
 		${MAIN_LIB_DIR}/StartInfo.h
 		${MAIN_LIB_DIR}/StringConstants.h
-		${MAIN_LIB_DIR}/Terrain.h
+		${MAIN_LIB_DIR}/TerrainHandler.h
 		${MAIN_LIB_DIR}/UnlockGuard.h
 		${MAIN_LIB_DIR}/VCMIDirs.h
 		${MAIN_LIB_DIR}/vcmi_endian.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]",

+ 3 - 0
config/defaultMods.json

@@ -10,6 +10,9 @@
 		"hero"       : 156,
 		"spell"      : 81,
 		"object"     : 256,
+		"terrain"    : 10,
+		"river"      : 5,
+		"road"       : 4,
 		"mapVersion" : 28 // max supported version, SoD
 	},
 

+ 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" },

+ 1 - 0
config/factions/neutral.json

@@ -3,6 +3,7 @@
 	{
 		"name" : "Neutral",
 		"index" : 9,
+		"nativeTerrain" : "none",
 		"alignment" : "neutral",
 		"creatureBackground" :
 		{

+ 8 - 0
config/gameConfig.json

@@ -85,6 +85,14 @@
 	[
 		"config/terrains.json"
 	],
+	"roads":
+	[
+		"config/roads.json"
+	],
+	"rivers":
+	[
+		"config/rivers.json"
+	],
 	"battlefields":
 	[
 		"config/battlefields.json"

+ 1 - 1
config/randomMap.json

@@ -42,4 +42,4 @@
     "value" : [2000, 5333, 8666, 12000],
     "rewardValue" : [5000, 10000, 15000, 20000]
   }
-}
+}

+ 17 - 13
config/rivers.json

@@ -1,30 +1,34 @@
 {
     "waterRiver":
     {
-        "originalRiverId": 1,
-        "code": "rw", //must be 2 characters
-        "animation": "clrrvr",
+        "index": 1,
+        "text" : "Water river",
+        "shortIdentifier": "rw", //must be 2 characters
+        "tilesFilename": "clrrvr",
         "delta": "clrdelt"
     },
     "iceRiver":
     {
-        "originalRiverId": 2,
-        "code": "ri",
-        "animation": "icyrvr",
+        "index": 2,
+        "text" : "Ice river",
+        "shortIdentifier": "ri",
+        "tilesFilename": "icyrvr",
         "delta": "icedelt"
     },
     "mudRiver":
     {
-        "originalRiverId": 3,
-        "code": "rm",
-        "animation": "mudrvr",
+        "index": 3,
+        "text" : "Mud river",
+        "shortIdentifier": "rm",
+        "tilesFilename": "mudrvr",
         "delta": "muddelt"
     },
     "lavaRiver":
     {
-        "originalRiverId": 4,
-        "code": "rl",
-        "animation": "lavrvr",
+        "index": 4,
+        "text" : "Lava river",
+        "shortIdentifier": "rl",
+        "tilesFilename": "lavrvr",
         "delta": "lavdelt"
     }
-}
+}

+ 13 - 10
config/roads.json

@@ -1,23 +1,26 @@
 {
     "dirtRoad":
     {
-        "originalRoadId": 1,
-        "code": "pd", //must be 2 characters
-        "animation": "dirtrd",
+        "index": 1,
+        "text" : "Dirt road",
+        "shortIdentifier": "pd", //must be 2 characters
+        "tilesFilename": "dirtrd",
         "moveCost": 75
     },
     "gravelRoad":
     {
-        "originalRoadId": 2,
-        "code": "pg",
-        "animation": "gravrd",
+        "index": 2,
+        "text" : "Gravel road",
+        "shortIdentifier": "pg",
+        "tilesFilename": "gravrd",
         "moveCost": 65
     },
     "cobblestoneRoad":
     {
-        "originalRoadId": 3,
-        "code": "pc",
-        "animation": "cobbrd",
+        "index": 3,
+        "text" : "Cobblestone road",
+        "shortIdentifier": "pc",
+        "tilesFilename": "cobbrd",
         "moveCost": 50
     }
-}
+}

+ 1 - 1
config/schemas/faction.json

@@ -31,7 +31,7 @@
 	"$schema": "http://json-schema.org/draft-04/schema",
 	"title" : "VCMI faction format",
 	"description": "Json format for defining new faction (aka towns) in VCMI",
-	"required" : [ "name", "alignment", "creatureBackground" ],
+	"required" : [ "name", "alignment", "creatureBackground", "nativeTerrain" ],
 	"dependencies" : {
 		"town" : [ "puzzleMap" ]
 	},

+ 54 - 0
config/schemas/mod.json

@@ -4,7 +4,41 @@
 	"title" : "VCMI mod file format",
 	"description" : "Format used to define main mod file (mod.json) in VCMI",
 	"required" : [ "name", "description", "version", "author", "contact", "modType" ],
+	"definitions" : {
+		"localizable" : {
+			"type":"object",
+			"additionalProperties" : false,
+			"required" : [ "name", "description", "author", "modType" ],
+			"properties":{
+				"name": {
+					"type":"string",
+					"description": "Short name of your mod. No more than 2-3 words"
+				},
+				"description": {
+					"type":"string",
+					"description": "More lengthy description of mod. No hard limit"
+				},
 
+				"modType" : {
+					"type":"string",
+					"description": "Type of mod, e.g. Town, Artifacts, Graphical."
+				},
+				"author" : {
+					"type":"string",
+					"description": "Author of the mod. Can be nickname, real name or name of team"
+				},
+				"changelog" : {
+					"type":"object",
+					"description": "List of changes/new features in each version",
+					"additionalProperties" : {
+						"type" : "array",
+						"items" : { "type":"string" }
+					}
+				}
+			}
+		}
+	},
+	
 	"additionalProperties" : false,
 	"properties":{
 		"name": {
@@ -78,6 +112,26 @@
 			"type":"boolean",
 			"description": "If set to true, mod will not be enabled automatically on install"
 		},
+		
+		"english" : {
+			"$ref" : "#/definitions/localizable"
+		},
+
+		"german" : {
+			"$ref" : "#/definitions/localizable"
+		},
+
+		"polish" : {
+			"$ref" : "#/definitions/localizable"
+		},
+
+		"russian" : {
+			"$ref" : "#/definitions/localizable"
+		},
+
+		"ukrainian" : {
+			"$ref" : "#/definitions/localizable"
+		},
 
 		"artifacts": {
 			"type":"array",

+ 37 - 0
config/schemas/river.json

@@ -0,0 +1,37 @@
+{
+	"type":"object",
+	"$schema": "http://json-schema.org/draft-04/schema",
+	"title" : "VCMI river format",
+	"description" : "Format used to define new rivers in VCMI",
+	"required" : [ "text", "shortIdentifier", "tilesFilename", "delta" ],
+
+	"additionalProperties" : false,
+	"properties":{
+		"index" : 
+		{
+			"type": "number",
+			"description": "Internal, do not use"
+		},
+		"text":
+		{
+			"type": "string",
+			"description": "Human-readable name of the river"
+		},
+		"shortIdentifier":
+		{
+			"type": "string",
+			"description": "Two-letters unique indentifier for this road. Used in map format"
+		},
+		"tilesFilename":
+		{
+			"type": "string",
+			"description": "Name of file with river graphics",
+			"format": "defFile"
+		},
+		"delta":
+		{
+			"type": "string",
+			"description": "Name of file with river delta graphics"
+		}
+	}
+}

+ 37 - 0
config/schemas/road.json

@@ -0,0 +1,37 @@
+{
+	"type":"object",
+	"$schema": "http://json-schema.org/draft-04/schema",
+	"title" : "VCMI road format",
+	"description" : "Format used to define new roads in VCMI",
+	"required" : [ "text", "shortIdentifier", "tilesFilename", "moveCost" ],
+
+	"additionalProperties" : false,
+	"properties":{
+		"index" : 
+		{
+			"type": "number",
+			"description": "Internal, do not use"
+		},
+		"text":
+		{
+			"type": "string",
+			"description": "Human-readable name of the road"
+		},
+		"shortIdentifier":
+		{
+			"type": "string",
+			"description": "Two-letters unique indentifier for this road. Used in map format"
+		},
+		"tilesFilename":
+		{
+			"type": "string",
+			"description": "Name of file with road graphics",
+			"format": "defFile"
+		},
+		"moveCost":
+		{
+			"type": "number",
+			"description": "How many movement points needed to move hero"
+		}
+	}
+}

+ 7 - 2
config/schemas/settings.json

@@ -37,7 +37,7 @@
 				},
 				"encoding" : {
 					"type" : "string",
-					"default" : "CP1252"
+					"default" : "auto"
 				},
 				"swipe" : {
 					"type" : "boolean",
@@ -53,6 +53,7 @@
 				},
 				"language" : {
 					"type":"string",
+					"enum" : [ "english", "german", "polish", "russian", "ukrainian" ],
 					"default" : "english"
 				},
 				"lastSave" : {
@@ -81,7 +82,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 +106,10 @@
 					"type" : "boolean",
 					"default" : false
 				},
+				"softwareCursor" :  {
+					"type" : "boolean",
+					"default" : false
+				},
 				"showIntro" : {
 					"type" : "boolean",
 					"default" : true

+ 44 - 13
config/schemas/terrain.json

@@ -3,10 +3,20 @@
 	"$schema": "http://json-schema.org/draft-04/schema",
 	"title" : "VCMI terrain format",
 	"description" : "Format used to define new terrains in VCMI",
-	"required" : [ "tiles", "code", "moveCost" ],
+	"required" : [ "text", "moveCost", "minimapUnblocked", "minimapBlocked", "music", "tiles", "type", "horseSound", "horseSoundPenalty", "shortIdentifier", "battleFields" ],
 
 	"additionalProperties" : false,
 	"properties":{
+		"index" :
+		{
+			"type": "number",
+			"description": "Internal, do not use"
+		},
+		"text":
+		{
+			"type": "string",
+			"description": "Human-readable name of this terrain"
+		},
 		"moveCost":
 		{
 			"type": "number",
@@ -47,35 +57,38 @@
 		},
 		"type":
 		{
-			"type": "string",
+			"type": "array",
 			"description": "Type of this terrain. Can be land, water, subterranean or rock",
-			"enum": ["LAND", "WATER", "SUB", "ROCK"]
+			"items":
+			{
+				"enum": ["LAND", "WATER", "SUB", "ROCK", "SURFACE"],
+				"type": "string"
+			}
 		},
 		"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":
 		{
 			"type": "string",
-			"description": "River type which should be used for that terrain",
-			"enum": ["", "rw", "ri", "rm", "rl"]
+			"description": "River type which should be used for that terrain"
 		},
-		"horseSoundId":
+		"horseSound":
 		{
-			"type": "number",
-			"description": "Id of horse sound to be played when hero is moving across terrain"
+			"type": "string",
+			"description": "Hero movement sound for this terrain, version for moving on tiles with road"
 		},
-		"text":
+		"horseSoundPenalty":
 		{
 			"type": "string",
-			"description": "Text to be shown when mouse if over terrain"
+			"description": "Hero movement sound for this terrain, version for moving on tiles without road"
 		},
-		"code":
+		"shortIdentifier":
 		{
 			"type": "string",
-			"description": "Two-letters unique indentifier for this terrain. Used for terrain serializaion"
+			"description": "Two-letters unique indentifier for this terrain. Used for map format"
 		},
 		"battleFields":
 		{
@@ -86,6 +99,24 @@
 				"type": "string"
 			}
 		},
+		"sounds":
+		{
+			"type": "object",
+			"description": "list of sounds for this terrain",
+			"additionalProperties" : false,
+			"properties":
+			{
+				"ambient" : 
+				{
+					"type": "array",
+					"description": "list of ambient sounds for this terrain",
+					"items":
+					{
+						"type": "string"
+					}
+				}
+			}
+		},
 		"prohibitTransitions":
 		{
 			"type": "array",

+ 57 - 41
config/terrains.json

@@ -1,146 +1,162 @@
 {
 	"dirt" :
 	{
-		"originalTerrainId": 0,
+		"index": 0,
 		"moveCost" : 100,
 		"minimapUnblocked" : [ 82, 56, 8 ],
 		"minimapBlocked"   : [ 57, 40, 8 ],
 		"music" : "Dirt.mp3",
 		"tiles" : "DIRTTL",
-		"code" : "dt",
-		"river" : "rm",
+		"type" : ["SURFACE"],
+		"shortIdentifier" : "dt",
+		"river" : "mudRiver",
 		"battleFields" : ["dirt_birches", "dirt_hills", "dirt_pines"],
 		"terrainViewPatterns" : "dirt",
-		"horseSoundId" : 0
+		"horseSound" : "horse00",
+		"horseSoundPenalty" : "horse20"
 	},
 	"sand" :
 	{
-		"originalTerrainId": 1,
+		"index": 1,
 		"moveCost" : 150,
 		"minimapUnblocked" : [ 222, 207, 140 ],
 		"minimapBlocked"   : [ 165, 158, 107 ],
 		"music" : "Sand.mp3",
 		"tiles" : "SANDTL",
-		"code" : "sa",
-		"river" : "rm",
+		"type" : ["SURFACE"],
+		"shortIdentifier" : "sa",
+		"river" : "mudRiver",
 		"battleFields" : ["sand_mesas"],
 		"transitionRequired" : true,
 		"terrainViewPatterns" : "sand",
-		"horseSoundId" : 1
+		"horseSound" : "horse01",
+		"horseSoundPenalty" : "horse21"
 	},
 	"grass" :
 	{
-		"originalTerrainId": 2,
+		"index": 2,
 		"moveCost" : 100,
 		"minimapUnblocked" : [ 0, 65, 0 ],
 		"minimapBlocked"   : [ 0, 48, 0 ],
 		"music" : "Grass.mp3",
 		"tiles" : "GRASTL",
-		"code" : "gr",
-		"river" : "rw",
+		"type" : ["SURFACE"],
+		"shortIdentifier" : "gr",
+		"river" : "waterRiver",
 		"battleFields" : ["grass_hills", "grass_pines"],
-		"horseSoundId" : 2
+		"horseSound" : "horse02",
+		"horseSoundPenalty" : "horse22"
 	},
 	"snow" :
 	{
-		"originalTerrainId": 3,
+		"index": 3,
 		"moveCost" : 150,
 		"minimapUnblocked" : [ 181, 199, 198 ],
 		"minimapBlocked"   : [ 140, 158, 156 ],
 		"music" : "Snow.mp3",
 		"tiles" : "SNOWTL",
-		"code" : "sn",
-		"river" : "ri",
+		"type" : ["SURFACE"],
+		"shortIdentifier" : "sn",
+		"river" : "iceRiver",
 		"battleFields" : ["snow_mountains", "snow_trees"],
-		"horseSoundId" : 3
+		"horseSound" : "horse03",
+		"horseSoundPenalty" : "horse23"
 	},
 	"swamp" :
 	{
-		"originalTerrainId": 4,
+		"index": 4,
 		"moveCost" : 175,
 		"minimapUnblocked" : [ 74, 134, 107 ],
 		"minimapBlocked"   : [ 33,  89,  66 ],
 		"music" : "Swamp.mp3",
 		"tiles" : "SWMPTL",
-		"code" : "sw",
-		"river" : "rw",
+		"type" : ["SURFACE"],
+		"shortIdentifier" : "sw",
+		"river" : "waterRiver",
 		"battleFields" : ["swamp_trees"],
-		"horseSoundId" : 4
+		"horseSound" : "horse04",
+		"horseSoundPenalty" : "horse24"
 	},
 	"rough" :
 	{
-		"originalTerrainId": 5,
+		"index": 5,
 		"moveCost" : 125,
 		"minimapUnblocked" : [ 132, 113, 49 ],
 		"minimapBlocked"   : [  99,  81, 33 ],
 		"music" : "Rough.mp3",
 		"tiles" : "ROUGTL",
-		"code" : "rg",
-		"river" : "rm",
+		"type" : ["SURFACE"],
+		"shortIdentifier" : "rg",
+		"river" : "mudRiver",
 		"battleFields" : ["rough"],
-		"horseSoundId" : 5
+		"horseSound" : "horse05",
+		"horseSoundPenalty" : "horse25"
 	},
 	"subterra" :
 	{
-		"originalTerrainId": 6,
+		"index": 6,
 		"moveCost" : 100,
 		"minimapUnblocked" : [ 132, 48, 0 ],
 		"minimapBlocked"   : [  90,  8, 0 ],
 		"music" : "Underground.mp3",
 		"tiles" : "SUBBTL",
-		"type" : "SUB",
-		"code" : "sb",
-		"river" : "rw",
+		"type" : [ "SUB" ],
+		"shortIdentifier" : "sb",
+		"river" : "waterRiver",
 		"battleFields" : ["subterranean"],
 		"rockTerrain" : "rock",
-		"horseSoundId" : 6
+		"horseSound" : "horse06",
+		"horseSoundPenalty" : "horse26"
 	},
 	"lava" :
 	{
-		"originalTerrainId": 7,
+		"index": 7,
 		"moveCost" : 100,
 		"minimapUnblocked" : [ 74, 73, 74 ],
 		"minimapBlocked"   : [ 41, 40, 41 ],
 		"music" : "Lava.mp3",
 		"tiles" : "LAVATL",
 		"type" : ["SUB", "SURFACE"],
-		"code" : "lv",
-		"river" : "rl",
+		"shortIdentifier" : "lv",
+		"river" : "lavaRiver",
 		"battleFields" : ["lava"],
 		"rockTerrain" : "rock",
-		"horseSoundId" : 7
+		"horseSound" : "horse07",
+		"horseSoundPenalty" : "horse27"
 	},
 	"water" :
 	{
-		"originalTerrainId": 8,
+		"index": 8,
 		"moveCost" : 100,
 		"minimapUnblocked" : [ 8, 81, 148 ],
 		"minimapBlocked"   : [ 8, 81, 148 ],
 		"music" : "Water.mp3",
 		"tiles" : "WATRTL",
-		"type" : "WATER",
-		"code" : "wt",
+		"type" : [ "WATER" ],
+		"shortIdentifier" : "wt",
 		"battleFields" : ["ship"],
 		"transitionRequired" : true,
 		"terrainViewPatterns" : "water",
-		"horseSoundId" : 8,
+		"horseSound" : "horse08",
+		"horseSoundPenalty" : "horse28",
 		"sounds": {
 			"ambient": ["LOOPOCEA"]
 		}
 	},
 	"rock" :
 	{
-		"originalTerrainId": 9,
+		"index": 9,
 		"moveCost" : -1,
 		"minimapUnblocked" : [ 0, 0, 0 ],
 		"minimapBlocked"   : [ 0, 0, 0 ],
 		"music" : "Underground.mp3", // Impossible in H3
 		"tiles" : "ROCKTL",
-		"type" : "ROCK",
-		"code" : "rc",
+		"type" : [ "ROCK" ],
+		"shortIdentifier" : "rc",
 		"battleFields" : ["rocklands"],
 		"transitionRequired" : true,
 		"terrainViewPatterns" : "rock",
-		"horseSoundId" : 9
+		"horseSound" : "horse09",
+		"horseSoundPenalty" : "horse29"
 	}
 }

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

+ 35 - 7
launcher/CMakeLists.txt

@@ -62,6 +62,13 @@ set(launcher_FORMS
 		lobby/lobbyroomrequest_moc.ui
 )
 
+set(launcher_TS
+	translation/english.ts
+	translation/german.ts
+	translation/polish.ts
+	translation/russian.ts
+	translation/ukrainian.ts)
+
 if(APPLE_IOS)
 	list(APPEND launcher_SRCS
 		ios/main.m
@@ -83,18 +90,28 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON)
 
 if(TARGET Qt6::Core)
 	qt_wrap_ui(launcher_UI_HEADERS ${launcher_FORMS})
+	if(ENABLE_TRANSLATIONS)
+		set_source_files_properties(${launcher_TS} PROPERTIES OUTPUT_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/translation)
+		# TODO: consider using qt_add_translations: https://doc.qt.io/qt-6/qtlinguist-cmake-qt-add-translations.html
+		qt_add_translation( launcher_QM ${launcher_TS} )
+	endif()
 else()
 	qt5_wrap_ui(launcher_UI_HEADERS ${launcher_FORMS})
+	if(ENABLE_TRANSLATIONS)
+		set_source_files_properties(${launcher_TS} PROPERTIES OUTPUT_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/translation)
+		qt5_add_translation( launcher_QM ${launcher_TS} )
+	endif()
 endif()
 
+
 if(WIN32)
 	set(launcher_ICON VCMI_launcher.rc)
 endif()
 
 if(ENABLE_SINGLE_APP_BUILD)
-	add_library(vcmilauncher STATIC ${launcher_SRCS} ${launcher_HEADERS} ${launcher_UI_HEADERS})
+	add_library(vcmilauncher STATIC ${launcher_QM} ${launcher_SRCS} ${launcher_HEADERS} ${launcher_UI_HEADERS})
 else()
-	add_executable(vcmilauncher WIN32 ${launcher_SRCS} ${launcher_HEADERS} ${launcher_UI_HEADERS} ${launcher_ICON})
+	add_executable(vcmilauncher WIN32 ${launcher_QM} ${launcher_SRCS} ${launcher_HEADERS} ${launcher_UI_HEADERS} ${launcher_ICON})
 endif()
 
 if(WIN32)
@@ -126,7 +143,12 @@ vcmi_set_output_dir(vcmilauncher "")
 enable_pch(vcmilauncher)
 
 if(APPLE_IOS)
-	set(ICONS_DESTINATION ${DATA_DIR})
+	set(RESOURCES_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)
@@ -140,12 +162,14 @@ if(APPLE_IOS)
 		)
 	endif()
 else()
-	set(ICONS_DESTINATION ${DATA_DIR}/launcher)
+	set(RESOURCES_DESTINATION ${DATA_DIR}/launcher)
 
 	# Copy to build directory for easier debugging
 	add_custom_command(TARGET vcmilauncher POST_BUILD
-		COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/icons
-		COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/launcher/icons ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/icons
+		COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher
+		COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_SOURCE_DIR}/launcher/icons ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/icons
+		COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_BINARY_DIR}/translation ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/translation
+
 	)
 
 	install(TARGETS vcmilauncher DESTINATION ${BIN_DIR})
@@ -156,4 +180,8 @@ else()
 		install(FILES "eu.vcmi.VCMI.metainfo.xml" DESTINATION share/metainfo)
 	endif()
 endif()
-install(DIRECTORY icons DESTINATION ${ICONS_DESTINATION})
+
+install(DIRECTORY icons DESTINATION ${RESOURCES_DESTINATION})
+if(ENABLE_TRANSLATIONS)
+	install(FILES ${launcher_QM} DESTINATION ${RESOURCES_DESTINATION}/translation)
+endif()

BIN
launcher/icons/menu-editor.png


BIN
launcher/icons/menu-game.png


BIN
launcher/icons/menu-lobby.png


BIN
launcher/icons/menu-mods.png


BIN
launcher/icons/menu-settings.png


+ 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

+ 171 - 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)
@@ -35,6 +47,15 @@ Lobby::Lobby(QWidget *parent) :
 	ui->kickButton->setVisible(false);
 }
 
+void Lobby::changeEvent(QEvent *event)
+{
+	if(event->type() == QEvent::LanguageChange)
+	{
+		ui->retranslateUi(this);
+	}
+	QWidget::changeEvent(event);
+}
+
 Lobby::~Lobby()
 {
 	delete ui;
@@ -134,10 +155,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 +177,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 +224,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 +247,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 +328,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 +366,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 +398,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 +517,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));
+	}
+}
+

+ 17 - 0
launcher/lobby/lobby_moc.h

@@ -19,9 +19,18 @@ class Lobby : public QWidget
 {
 	Q_OBJECT
 
+	void changeEvent(QEvent *event) override;
 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 +58,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 +75,7 @@ private:
 	QString session;
 	QString username;
 	QStringList gameArgs;
+	QMap<QString, QString> hostModsMap;
 
 	enum AuthStatus
 	{

+ 231 - 138
launcher/lobby/lobby_moc.ui

@@ -7,59 +7,46 @@
     <x>0</x>
     <y>0</y>
     <width>652</width>
-    <height>329</height>
+    <height>383</height>
    </rect>
   </property>
   <property name="windowTitle">
-   <string>Form</string>
+   <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">
-      <string>127.0.0.1:5002</string>
-     </property>
-    </widget>
-   </item>
-   <item row="2" column="0" colspan="3">
-    <widget class="QPlainTextEdit" name="chat">
-     <property name="readOnly">
-      <bool>true</bool>
+      <string notr="true">127.0.0.1:5002</string>
      </property>
     </widget>
    </item>
@@ -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>

+ 9 - 0
launcher/lobby/lobbyroomrequest_moc.cpp

@@ -27,6 +27,15 @@ LobbyRoomRequest::LobbyRoomRequest(SocketLobby & socket, const QString & room, c
 	show();
 }
 
+void LobbyRoomRequest::changeEvent(QEvent *event)
+{
+	if(event->type() == QEvent::LanguageChange)
+	{
+		ui->retranslateUi(this);
+	}
+	QDialog::changeEvent(event);
+}
+
 LobbyRoomRequest::~LobbyRoomRequest()
 {
 	delete ui;

+ 1 - 0
launcher/lobby/lobbyroomrequest_moc.h

@@ -21,6 +21,7 @@ class LobbyRoomRequest : public QDialog
 {
 	Q_OBJECT
 
+	void changeEvent(QEvent *event) override;
 public:
 	explicit LobbyRoomRequest(SocketLobby & socket, const QString & room, const QMap<QString, QString> & mods, QWidget *parent = nullptr);
 	~LobbyRoomRequest();

+ 11 - 8
launcher/lobby/lobbyroomrequest_moc.ui

@@ -9,7 +9,7 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>193</width>
+    <width>227</width>
     <height>188</height>
    </rect>
   </property>
@@ -51,39 +51,42 @@
        <verstretch>0</verstretch>
       </sizepolicy>
      </property>
+     <property name="currentText">
+      <string notr="true">2</string>
+     </property>
      <item>
       <property name="text">
-       <string>2</string>
+       <string notr="true">2</string>
       </property>
      </item>
      <item>
       <property name="text">
-       <string>3</string>
+       <string notr="true">3</string>
       </property>
      </item>
      <item>
       <property name="text">
-       <string>4</string>
+       <string notr="true">4</string>
       </property>
      </item>
      <item>
       <property name="text">
-       <string>5</string>
+       <string notr="true">5</string>
       </property>
      </item>
      <item>
       <property name="text">
-       <string>6</string>
+       <string notr="true">6</string>
       </property>
      </item>
      <item>
       <property name="text">
-       <string>7</string>
+       <string notr="true">7</string>
       </property>
      </item>
      <item>
       <property name="text">
-       <string>8</string>
+       <string notr="true">8</string>
       </property>
      </item>
     </widget>

+ 8 - 0
launcher/main.cpp

@@ -32,6 +32,7 @@ int main(int argc, char * argv[])
 	{
 #endif
 	QApplication vcmilauncher(argc, argv);
+
 	MainWindow mainWindow;
 	mainWindow.show();
 	result = vcmilauncher.exec();
@@ -65,6 +66,13 @@ void startGame(const QStringList & args)
 #endif
 }
 
+void startEditor(const QStringList & args)
+{
+#ifdef ENABLE_EDITOR
+	startExecutable(pathToQString(VCMIDirs::get().mapEditorPath()), args);
+#endif
+}
+
 #ifndef Q_OS_IOS
 void startExecutable(QString name, const QStringList & args)
 {

+ 1 - 0
launcher/main.h

@@ -10,6 +10,7 @@
 #pragma once
 
 void startGame(const QStringList & args);
+void startEditor(const QStringList & args);
 
 #ifdef VCMI_IOS
 extern "C" void launchGame(int argc, char * argv[]);

+ 95 - 29
launcher/mainwindow_moc.cpp

@@ -47,12 +47,40 @@ void MainWindow::load()
 	settings.init();
 }
 
+void MainWindow::computeSidePanelSizes()
+{
+	QVector<QToolButton*> widgets = {
+		ui->modslistButton,
+		ui->settingsButton,
+		ui->lobbyButton,
+		ui->startEditorButton,
+		ui->startGameButton
+	};
+
+	for(auto & widget : widgets)
+	{
+		QFontMetrics metrics(widget->font());
+		QSize iconSize = widget->iconSize();
+
+		// this is minimal space that is needed for our button to avoid text clipping
+		int buttonHeight = iconSize.height() + metrics.height() + 4;
+
+		widget->setMinimumHeight(buttonHeight);
+		widget->setMaximumHeight(buttonHeight * 1.2);
+	}
+}
+
 MainWindow::MainWindow(QWidget * parent)
 	: QMainWindow(parent), ui(new Ui::MainWindow)
 {
 	load(); // load FS before UI
+	updateTranslation(); // load translation
 
 	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);
@@ -68,45 +96,32 @@ MainWindow::MainWindow(QWidget * parent)
 		move(position);
 	}
 
-	//set default margins
+#ifndef ENABLE_EDITOR
+	ui->startEditorButton->hide();
+#endif
+
+	computeSidePanelSizes();
 
-	auto width = ui->startGameTitle->fontMetrics().boundingRect(ui->startGameTitle->text()).width();
-	if(ui->startGameButton->iconSize().width() < width)
-	{
-		ui->startGameButton->setIconSize(QSize(width, width));
-	}
-	auto tab_icon_size = ui->tabSelectList->iconSize();
-	if(tab_icon_size.width() < width)
-	{
-		ui->tabSelectList->setIconSize(QSize(width, width + tab_icon_size.height() - tab_icon_size.width()));
-		ui->tabSelectList->setGridSize(QSize(width, width));
-		// 4 is a dirty hack to make it look right
-		ui->tabSelectList->setMaximumWidth(width + 4);
-	}
 	ui->tabListWidget->setCurrentIndex(0);
 
 	ui->settingsView->isExtraResolutionsModEnabled = ui->modlistView->isExtraResolutionsModEnabled();
 	ui->settingsView->setDisplayList();
 	connect(ui->modlistView, &CModListView::extraResolutionsEnabledChanged,
 		ui->settingsView, &CSettingsView::fillValidResolutions);
-
-	connect(ui->tabSelectList, &QListWidget::currentRowChanged, [this](int i) {
-#ifdef Q_OS_IOS
-		if(auto widget = qApp->focusWidget())
-			widget->clearFocus();
-#endif
-		ui->tabListWidget->setCurrentIndex(i);
-	});
 	
-#ifdef Q_OS_IOS
-	QScroller::grabGesture(ui->tabSelectList, QScroller::LeftMouseButtonGesture);
-	ui->tabSelectList->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
-#endif
-
 	if(settings["launcher"]["updateOnStartup"].Bool())
 		UpdateDialog::showUpdateDialog(false);
 }
 
+void MainWindow::changeEvent(QEvent *event)
+{
+	if(event->type() == QEvent::LanguageChange)
+	{
+		ui->retranslateUi(this);
+	}
+	QMainWindow::changeEvent(event);
+}
+
 MainWindow::~MainWindow()
 {
 	//save window settings
@@ -122,12 +137,63 @@ void MainWindow::on_startGameButton_clicked()
 	startGame({});
 }
 
-void MainWindow::on_tabSelectList_currentRowChanged(int currentRow)
+void MainWindow::on_startEditorButton_clicked()
 {
-	ui->startGameButton->setEnabled(currentRow != TabRows::LOBBY);
+	startEditor({});
 }
 
 const CModList & MainWindow::getModList() const
 {
 	return ui->modlistView->getModList();
 }
+
+void MainWindow::on_modslistButton_clicked()
+{
+	ui->startGameButton->setEnabled(true);
+	ui->tabListWidget->setCurrentIndex(TabRows::MODS);
+}
+
+void MainWindow::on_settingsButton_clicked()
+{
+	ui->startGameButton->setEnabled(true);
+	ui->tabListWidget->setCurrentIndex(TabRows::SETTINGS);
+}
+
+void MainWindow::on_lobbyButton_clicked()
+{
+	ui->startGameButton->setEnabled(false);
+	ui->tabListWidget->setCurrentIndex(TabRows::LOBBY);
+}
+
+void MainWindow::updateTranslation()
+{
+#ifdef ENABLE_QT_TRANSLATIONS
+	std::string translationFile = settings["general"]["language"].String() + ".qm";
+	logGlobal->info("Loading translation '%s'", translationFile);
+
+	QVector<QString> searchPaths;
+
+#ifdef Q_OS_IOS
+	searchPaths.push_back(pathToQString(VCMIDirs::get().binaryPath() / "translation" / translationFile));
+#else
+	for(auto const & string : VCMIDirs::get().dataPaths())
+		searchPaths.push_back(pathToQString(string / "launcher" / "translation" / translationFile));
+	searchPaths.push_back(pathToQString(VCMIDirs::get().userDataPath() / "launcher" / "translation" / translationFile));
+#endif
+
+	for(auto const & string : boost::adaptors::reverse(searchPaths))
+	{
+		logGlobal->info("Searching for translation at '%s'", string.toStdString());
+		if (translator.load(string))
+		{
+			logGlobal->info("Translation found");
+			if (!qApp->installTranslator(&translator))
+				logGlobal->error("Failed to install translator");
+			return;
+		}
+	}
+
+	logGlobal->error("Failed to find translation");
+
+#endif
+}

+ 11 - 1
launcher/mainwindow_moc.h

@@ -10,6 +10,7 @@
 #pragma once
 #include <QMainWindow>
 #include <QStringList>
+#include <QTranslator>
 
 namespace Ui
 {
@@ -26,6 +27,9 @@ class MainWindow : public QMainWindow
 {
 	Q_OBJECT
 
+#ifdef ENABLE_QT_TRANSLATIONS
+	QTranslator translator;
+#endif
 private:
 	Ui::MainWindow * ui;
 	void load();
@@ -35,16 +39,22 @@ private:
 		MODS = 0, SETTINGS = 1, LOBBY = 2
 	};
 
+	void changeEvent(QEvent *event) override;
 public:
 	explicit MainWindow(QWidget * parent = 0);
 	~MainWindow();
 
 	const CModList & getModList() const;
 
+	void updateTranslation();
+	void computeSidePanelSizes();
 	
 public slots:
 	void on_startGameButton_clicked();
 	
 private slots:
-	void on_tabSelectList_currentRowChanged(int currentRow);
+	void on_modslistButton_clicked();
+	void on_settingsButton_clicked();
+	void on_lobbyButton_clicked();
+	void on_startEditorButton_clicked();
 };

+ 270 - 151
launcher/mainwindow_moc.ui

@@ -7,7 +7,7 @@
     <x>0</x>
     <y>0</y>
     <width>800</width>
-    <height>480</height>
+    <height>410</height>
    </rect>
   </property>
   <property name="sizePolicy">
@@ -30,127 +30,288 @@
    </size>
   </property>
   <widget class="QWidget" name="centralWidget">
-   <layout class="QGridLayout" name="gridLayout">
-    <item row="0" column="0">
-     <widget class="QListWidget" name="tabSelectList">
-      <property name="sizePolicy">
-       <sizepolicy hsizetype="Fixed" vsizetype="Expanding">
-        <horstretch>89</horstretch>
-        <verstretch>0</verstretch>
-       </sizepolicy>
-      </property>
-      <property name="minimumSize">
-       <size>
-        <width>89</width>
-        <height>89</height>
-       </size>
-      </property>
-      <property name="verticalScrollBarPolicy">
-       <enum>Qt::ScrollBarAlwaysOff</enum>
-      </property>
-      <property name="horizontalScrollBarPolicy">
-       <enum>Qt::ScrollBarAlwaysOff</enum>
-      </property>
-      <property name="sizeAdjustPolicy">
-       <enum>QAbstractScrollArea::AdjustToContents</enum>
-      </property>
-      <property name="editTriggers">
-       <set>QAbstractItemView::NoEditTriggers</set>
-      </property>
-      <property name="showDropIndicator" stdset="0">
-       <bool>false</bool>
-      </property>
-      <property name="dragDropMode">
-       <enum>QAbstractItemView::NoDragDrop</enum>
-      </property>
-      <property name="selectionBehavior">
-       <enum>QAbstractItemView::SelectItems</enum>
-      </property>
-      <property name="iconSize">
-       <size>
-        <width>89</width>
-        <height>89</height>
-       </size>
-      </property>
-      <property name="movement">
-       <enum>QListView::Static</enum>
-      </property>
-      <property name="flow">
-       <enum>QListView::TopToBottom</enum>
-      </property>
-      <property name="isWrapping" stdset="0">
-       <bool>false</bool>
-      </property>
-      <property name="resizeMode">
-       <enum>QListView::Adjust</enum>
-      </property>
-      <property name="gridSize">
-       <size>
-        <width>100</width>
-        <height>100</height>
-       </size>
-      </property>
-      <property name="viewMode">
-       <enum>QListView::IconMode</enum>
-      </property>
-      <property name="uniformItemSizes">
-       <bool>true</bool>
-      </property>
-      <property name="wordWrap">
-       <bool>false</bool>
-      </property>
+   <layout class="QHBoxLayout" name="horizontalLayout">
+    <item>
+     <layout class="QVBoxLayout" name="verticalLayout">
       <item>
-       <property name="text">
-        <string>Mods</string>
-       </property>
-       <property name="icon">
-        <iconset>
-         <normaloff>icons:menu-mods.png</normaloff>icons:menu-mods.png</iconset>
-       </property>
+       <widget class="QToolButton" name="modslistButton">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+          <horstretch>1</horstretch>
+          <verstretch>10</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="minimumSize">
+         <size>
+          <width>0</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>16777215</width>
+          <height>16777215</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Mods</string>
+        </property>
+        <property name="icon">
+         <iconset>
+          <normaloff>icons:menu-mods.png</normaloff>icons:menu-mods.png</iconset>
+        </property>
+        <property name="iconSize">
+         <size>
+          <width>60</width>
+          <height>60</height>
+         </size>
+        </property>
+        <property name="checkable">
+         <bool>true</bool>
+        </property>
+        <property name="checked">
+         <bool>true</bool>
+        </property>
+        <property name="autoExclusive">
+         <bool>true</bool>
+        </property>
+        <property name="toolButtonStyle">
+         <enum>Qt::ToolButtonTextUnderIcon</enum>
+        </property>
+        <property name="autoRaise">
+         <bool>true</bool>
+        </property>
+       </widget>
       </item>
       <item>
-       <property name="text">
-        <string>Settings</string>
-       </property>
-       <property name="icon">
-        <iconset>
-         <normaloff>icons:menu-settings.png</normaloff>icons:menu-settings.png</iconset>
-       </property>
+       <widget class="QToolButton" name="settingsButton">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+          <horstretch>1</horstretch>
+          <verstretch>10</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="minimumSize">
+         <size>
+          <width>0</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>16777215</width>
+          <height>16777215</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Settings</string>
+        </property>
+        <property name="icon">
+         <iconset>
+          <normaloff>icons:menu-settings.png</normaloff>icons:menu-settings.png</iconset>
+        </property>
+        <property name="iconSize">
+         <size>
+          <width>60</width>
+          <height>60</height>
+         </size>
+        </property>
+        <property name="checkable">
+         <bool>true</bool>
+        </property>
+        <property name="checked">
+         <bool>false</bool>
+        </property>
+        <property name="autoExclusive">
+         <bool>true</bool>
+        </property>
+        <property name="toolButtonStyle">
+         <enum>Qt::ToolButtonTextUnderIcon</enum>
+        </property>
+        <property name="autoRaise">
+         <bool>true</bool>
+        </property>
+       </widget>
       </item>
       <item>
-       <property name="text">
-        <string>Lobby</string>
-       </property>
-       <property name="icon">
-        <iconset>
-         <normaloff>icons:menu-lobby.png</normaloff>icons:menu-lobby.png</iconset>
-       </property>
+       <widget class="QToolButton" name="lobbyButton">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+          <horstretch>1</horstretch>
+          <verstretch>10</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="minimumSize">
+         <size>
+          <width>0</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>16777215</width>
+          <height>16777215</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Lobby</string>
+        </property>
+        <property name="icon">
+         <iconset>
+          <normaloff>icons:menu-lobby.png</normaloff>icons:menu-lobby.png</iconset>
+        </property>
+        <property name="iconSize">
+         <size>
+          <width>60</width>
+          <height>60</height>
+         </size>
+        </property>
+        <property name="checkable">
+         <bool>true</bool>
+        </property>
+        <property name="checked">
+         <bool>false</bool>
+        </property>
+        <property name="autoExclusive">
+         <bool>true</bool>
+        </property>
+        <property name="toolButtonStyle">
+         <enum>Qt::ToolButtonTextUnderIcon</enum>
+        </property>
+        <property name="autoRaise">
+         <bool>true</bool>
+        </property>
+       </widget>
       </item>
-     </widget>
-    </item>
-    <item row="2" column="0">
-     <widget class="QLabel" name="startGameTitle">
-      <property name="font">
-       <font>
-        <bold>true</bold>
-       </font>
-      </property>
-      <property name="text">
-       <string>Start game</string>
-      </property>
-      <property name="alignment">
-       <set>Qt::AlignCenter</set>
-      </property>
-     </widget>
+      <item>
+       <spacer name="verticalSpacer">
+        <property name="orientation">
+         <enum>Qt::Vertical</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>100</width>
+          <height>0</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item>
+       <widget class="QToolButton" name="startEditorButton">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+          <horstretch>1</horstretch>
+          <verstretch>5</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="minimumSize">
+         <size>
+          <width>0</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>16777215</width>
+          <height>16777215</height>
+         </size>
+        </property>
+        <property name="font">
+         <font>
+          <weight>75</weight>
+          <bold>true</bold>
+         </font>
+        </property>
+        <property name="text">
+         <string>Map Editor</string>
+        </property>
+        <property name="icon">
+         <iconset>
+          <normaloff>icons:menu-editor.png</normaloff>icons:menu-editor.png</iconset>
+        </property>
+        <property name="iconSize">
+         <size>
+          <width>30</width>
+          <height>30</height>
+         </size>
+        </property>
+        <property name="checkable">
+         <bool>false</bool>
+        </property>
+        <property name="checked">
+         <bool>false</bool>
+        </property>
+        <property name="toolButtonStyle">
+         <enum>Qt::ToolButtonTextUnderIcon</enum>
+        </property>
+        <property name="autoRaise">
+         <bool>false</bool>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QToolButton" name="startGameButton">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+          <horstretch>1</horstretch>
+          <verstretch>10</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="minimumSize">
+         <size>
+          <width>0</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>16777215</width>
+          <height>16777215</height>
+         </size>
+        </property>
+        <property name="font">
+         <font>
+          <weight>75</weight>
+          <bold>true</bold>
+         </font>
+        </property>
+        <property name="text">
+         <string>Start game</string>
+        </property>
+        <property name="icon">
+         <iconset>
+          <normaloff>icons:menu-game.png</normaloff>icons:menu-game.png</iconset>
+        </property>
+        <property name="iconSize">
+         <size>
+          <width>60</width>
+          <height>60</height>
+         </size>
+        </property>
+        <property name="checkable">
+         <bool>false</bool>
+        </property>
+        <property name="checked">
+         <bool>false</bool>
+        </property>
+        <property name="toolButtonStyle">
+         <enum>Qt::ToolButtonTextUnderIcon</enum>
+        </property>
+        <property name="autoRaise">
+         <bool>false</bool>
+        </property>
+       </widget>
+      </item>
+     </layout>
     </item>
-    <item row="0" column="1" rowspan="3">
+    <item>
      <widget class="QStackedWidget" name="tabListWidget">
       <property name="enabled">
        <bool>true</bool>
       </property>
       <property name="sizePolicy">
        <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
-        <horstretch>0</horstretch>
+        <horstretch>10</horstretch>
         <verstretch>0</verstretch>
        </sizepolicy>
       </property>
@@ -162,44 +323,6 @@
       <widget class="Lobby" name="lobbyView"/>
      </widget>
     </item>
-    <item row="1" column="0">
-     <widget class="QToolButton" name="startGameButton">
-      <property name="sizePolicy">
-       <sizepolicy hsizetype="Fixed" vsizetype="Maximum">
-        <horstretch>89</horstretch>
-        <verstretch>0</verstretch>
-       </sizepolicy>
-      </property>
-      <property name="minimumSize">
-       <size>
-        <width>89</width>
-        <height>89</height>
-       </size>
-      </property>
-      <property name="text">
-       <string>Play</string>
-      </property>
-      <property name="icon">
-       <iconset>
-        <normaloff>icons:menu-game.png</normaloff>icons:menu-game.png</iconset>
-      </property>
-      <property name="iconSize">
-       <size>
-        <width>60</width>
-        <height>60</height>
-       </size>
-      </property>
-      <property name="checkable">
-       <bool>false</bool>
-      </property>
-      <property name="checked">
-       <bool>false</bool>
-      </property>
-      <property name="toolButtonStyle">
-       <enum>Qt::ToolButtonIconOnly</enum>
-      </property>
-     </widget>
-    </item>
    </layout>
   </widget>
  </widget>
@@ -224,10 +347,6 @@
    <container>1</container>
   </customwidget>
  </customwidgets>
- <tabstops>
-  <tabstop>tabSelectList</tabstop>
-  <tabstop>startGameButton</tabstop>
- </tabstops>
  <resources/>
  <connections/>
 </ui>

+ 21 - 9
launcher/modManager/cmodlist.cpp

@@ -10,6 +10,7 @@
 #include "StdInc.h"
 #include "cmodlist.h"
 
+#include "../lib/CConfigHandler.h"
 #include "../../lib/JsonNode.h"
 #include "../../lib/filesystem/CFileInputStream.h"
 #include "../../lib/GameConstants.h"
@@ -125,7 +126,7 @@ bool CModEntry::isCompatible() const
 
 bool CModEntry::isEssential() const
 {
-	return getValue("storedLocaly").toBool();
+	return getName() == "vcmi";
 }
 
 bool CModEntry::isInstalled() const
@@ -158,23 +159,34 @@ QString CModEntry::getName() const
 
 QVariant CModEntry::getValue(QString value) const
 {
+	QString langValue = QString::fromStdString(settings["general"]["language"].String());
+
+	// Priorities
+	// 1) data from newest version
+	// 2) data from preferred language
+
+	bool useRepositoryData = repository.contains(value);
+
 	if(repository.contains(value) && localData.contains(value))
 	{
 		// value is present in both repo and locally installed. Select one from latest version
 		QString installedVer = localData["installedVersion"].toString();
 		QString availableVer = repository["latestVersion"].toString();
 
-		if(compareVersions(installedVer, availableVer))
-			return repository[value];
-		else
-			return localData[value];
+		useRepositoryData = compareVersions(installedVer, availableVer);
 	}
 
-	if(repository.contains(value))
-		return repository[value];
+	auto & storage = useRepositoryData ? repository : localData;
+
+	if(storage.contains(langValue))
+	{
+		auto langStorage = storage[langValue].toMap();
+		if (langStorage.contains(value))
+			return langStorage[value];
+	}
 
-	if(localData.contains(value))
-		return localData[value];
+	if(storage.contains(value))
+		return storage[value];
 
 	return QVariant();
 }

+ 12 - 11
launcher/modManager/cmodlistmodel_moc.cpp

@@ -25,16 +25,6 @@ static const QString names[ModFields::COUNT] =
 	"author"
 };
 
-static const QString header[ModFields::COUNT] =
-{
-	"Name",
-	"", // status icon
-	"", // status icon
-	"Type",
-	"Version",
-	"Size",
-	"Author"
-};
 }
 
 namespace ModStatus
@@ -155,8 +145,19 @@ Qt::ItemFlags CModListModel::flags(const QModelIndex &) const
 
 QVariant CModListModel::headerData(int section, Qt::Orientation orientation, int role) const
 {
+	static const QString header[ModFields::COUNT] =
+	{
+		QT_TR_NOOP("Name"),
+		QT_TR_NOOP(""), // status icon
+		QT_TR_NOOP(""), // status icon
+		QT_TR_NOOP("Type"),
+		QT_TR_NOOP("Version"),
+		QT_TR_NOOP("Size"),
+		QT_TR_NOOP("Author")
+	};
+
 	if(role == Qt::DisplayRole && orientation == Qt::Horizontal)
-		return ModFields::header[section];
+		return QCoreApplication::translate("ModFields", header[section].toStdString().c_str());
 	return QVariant();
 }
 

+ 32 - 4
launcher/modManager/cmodlistview_moc.cpp

@@ -34,6 +34,16 @@ void CModListView::setupModModel()
 		this, &CModListView::extraResolutionsEnabledChanged);
 }
 
+void CModListView::changeEvent(QEvent *event)
+{
+	if(event->type() == QEvent::LanguageChange)
+	{
+		ui->retranslateUi(this);
+		modModel->reloadRepositories();
+	}
+	QWidget::changeEvent(event);
+}
+
 void CModListView::setupFilterModel()
 {
 	filterModel = new CModFilterModel(modModel, this);
@@ -227,8 +237,8 @@ QString CModListView::genModInfoText(CModEntry & mod)
 	QString textTemplate = prefix + "</p><p align=\"justify\">%2</p>";
 	QString listTemplate = "<p align=\"justify\">%1: %2</p>";
 	QString noteTemplate = "<p align=\"justify\">%1</p>";
-	QString compatibleString = prefix + "Mod is compatible</p>";
-	QString incompatibleString = redPrefix + "Mod is incompatible</p>";
+	QString compatibleString = prefix + tr("Mod is compatible") + "</p>";
+	QString incompatibleString = redPrefix + tr("Mod is incompatible") + "</p>";
 	QString supportedVersions = redPrefix + "%2 %3 %4</p>";
 
 	QString result;
@@ -496,7 +506,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 +522,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 +568,8 @@ void CModListView::on_uninstallButton_clicked()
 			manager->disableMod(modName);
 		manager->uninstallMod(modName);
 	}
+	
+	emit modsChanged();
 	checkManagerErrors();
 }
 
@@ -631,6 +657,8 @@ void CModListView::downloadFinished(QStringList savedFiles, QStringList failedFi
 
 	if(doInstallFiles)
 		installFiles(savedFiles);
+	
+	emit modsChanged();
 }
 
 void CModListView::hideProgressBar()

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