浏览代码

Merge remote-tracking branch 'vcmi/develop' into battleint_refactor

Ivan Savenko 2 年之前
父节点
当前提交
495e0b3657
共有 100 个文件被更改,包括 2037 次插入288 次删除
  1. 1 1
      AI/Nullkiller/AIGateway.cpp
  2. 1 1
      AI/VCAI/VCAI.cpp
  3. 二进制
      Mods/vcmi/Sprites/vcmi/creatureIcons/towerLarge.png
  4. 二进制
      Mods/vcmi/Sprites/vcmi/creatureIcons/towerSmall.png
  5. 1 1
      Mods/vcmi/mod.json
  6. 36 7
      client/CMT.cpp
  7. 50 11
      client/CServerHandler.cpp
  8. 3 0
      client/CServerHandler.h
  9. 4 2
      client/Graphics.cpp
  10. 1 1
      client/Graphics.h
  11. 1 1
      client/battle/CBattleControlPanel.cpp
  12. 2 1
      client/battle/CBattleInterface.cpp
  13. 3 1
      client/battle/CBattleInterface.h
  14. 17 2
      client/battle/CBattleInterfaceClasses.cpp
  15. 2 0
      client/battle/CBattleInterfaceClasses.h
  16. 2 0
      client/ios/Info.plist
  17. 7 1
      client/ios/main.m
  18. 2 2
      client/mainmenu/CMainMenu.cpp
  19. 4 3
      client/widgets/AdventureMapClasses.cpp
  20. 3 2
      client/widgets/CComponent.cpp
  21. 14 0
      client/widgets/TextControls.cpp
  22. 6 0
      client/widgets/TextControls.h
  23. 6 0
      client/windows/CAdvmapInterface.cpp
  24. 1 1
      client/windows/CCastleInterface.cpp
  25. 2 0
      config/creatures/special.json
  26. 2 0
      config/factions/castle.json
  27. 2 0
      config/factions/conflux.json
  28. 2 0
      config/factions/dungeon.json
  29. 2 0
      config/factions/fortress.json
  30. 2 0
      config/factions/inferno.json
  31. 2 0
      config/factions/necropolis.json
  32. 2 0
      config/factions/rampart.json
  33. 2 0
      config/factions/stronghold.json
  34. 2 0
      config/factions/tower.json
  35. 22 2
      config/schemas/settings.json
  36. 11 0
      config/schemas/townSiege.json
  37. 1 1
      include/vcmi/Entity.h
  38. 8 0
      launcher/CMakeLists.txt
  39. 二进制
      launcher/icons/menu-lobby.png
  40. 二进制
      launcher/icons/room-private.png
  41. 6 2
      launcher/ios/main.m
  42. 123 0
      launcher/lobby/lobby.cpp
  43. 188 0
      launcher/lobby/lobby.h
  44. 417 0
      launcher/lobby/lobby_moc.cpp
  45. 76 0
      launcher/lobby/lobby_moc.h
  46. 216 0
      launcher/lobby/lobby_moc.ui
  47. 50 0
      launcher/lobby/lobbyroomrequest_moc.cpp
  48. 37 0
      launcher/lobby/lobbyroomrequest_moc.h
  49. 148 0
      launcher/lobby/lobbyroomrequest_moc.ui
  50. 86 38
      launcher/main.cpp
  51. 18 14
      launcher/main.h
  52. 15 26
      launcher/mainwindow_moc.cpp
  53. 14 4
      launcher/mainwindow_moc.h
  54. 24 3
      launcher/mainwindow_moc.ui
  55. 17 0
      launcher/modManager/cmodlistview_moc.cpp
  56. 3 0
      launcher/modManager/cmodlistview_moc.h
  57. 57 30
      lib/CArtHandler.cpp
  58. 15 13
      lib/CArtHandler.h
  59. 2 2
      lib/CCreatureHandler.cpp
  60. 3 3
      lib/CGameState.cpp
  61. 4 4
      lib/CHeroHandler.cpp
  62. 3 3
      lib/CSkillHandler.cpp
  63. 16 9
      lib/CTownHandler.cpp
  64. 4 0
      lib/CTownHandler.h
  65. 8 0
      lib/GameConstants.h
  66. 38 35
      lib/VCMIDirs.cpp
  67. 3 0
      lib/VCMIDirs.h
  68. 4 4
      lib/VCMI_Lib.cpp
  69. 3 3
      lib/VCMI_Lib.h
  70. 64 2
      lib/filesystem/CArchiveLoader.cpp
  71. 15 2
      lib/filesystem/CArchiveLoader.h
  72. 8 7
      lib/filesystem/Filesystem.cpp
  73. 7 3
      lib/filesystem/Filesystem.h
  74. 2 2
      lib/mapObjects/CQuest.cpp
  75. 3 3
      lib/mapObjects/CQuest.h
  76. 7 7
      lib/mapping/MapFormatH3M.cpp
  77. 2 2
      lib/mapping/MapFormatJson.cpp
  78. 4 4
      lib/spells/CSpellHandler.cpp
  79. 35 2
      mapeditor/Animation.cpp
  80. 2 0
      mapeditor/Animation.h
  81. 3 1
      mapeditor/CMakeLists.txt
  82. 4 2
      mapeditor/graphics.cpp
  83. 1 1
      mapeditor/graphics.h
  84. 二进制
      mapeditor/icons/brush-1.png
  85. 二进制
      mapeditor/icons/brush-2.png
  86. 二进制
      mapeditor/icons/brush-4.png
  87. 二进制
      mapeditor/icons/brush-5.png
  88. 二进制
      mapeditor/icons/document-new.png
  89. 二进制
      mapeditor/icons/document-open.png
  90. 二进制
      mapeditor/icons/document-save.png
  91. 二进制
      mapeditor/icons/edit-clear.png
  92. 二进制
      mapeditor/icons/edit-copy.png
  93. 二进制
      mapeditor/icons/edit-cut.png
  94. 二进制
      mapeditor/icons/edit-paste.png
  95. 二进制
      mapeditor/icons/fill-obstacles.png
  96. 二进制
      mapeditor/icons/toggle-grid.png
  97. 二进制
      mapeditor/icons/toggle-pass.png
  98. 二进制
      mapeditor/icons/toggle-underground.png
  99. 1 1
      mapeditor/inspector/questwidget.cpp
  100. 52 15
      mapeditor/mainwindow.cpp

+ 1 - 1
AI/Nullkiller/AIGateway.cpp

@@ -1140,7 +1140,7 @@ HeroPtr AIGateway::getHeroWithGrail() const
 {
 	for(const CGHeroInstance * h : cb->getHeroesInfo())
 	{
-		if(h->hasArt(2)) //grail
+		if(h->hasArt(ArtifactID::GRAIL))
 			return h;
 	}
 	return nullptr;

+ 1 - 1
AI/VCAI/VCAI.cpp

@@ -1755,7 +1755,7 @@ HeroPtr VCAI::getHeroWithGrail() const
 {
 	for(const CGHeroInstance * h : cb->getHeroesInfo())
 	{
-		if(h->hasArt(2)) //grail
+		if(h->hasArt(ArtifactID::GRAIL))
 			return h;
 	}
 	return nullptr;

二进制
Mods/vcmi/Sprites/vcmi/creatureIcons/towerLarge.png


二进制
Mods/vcmi/Sprites/vcmi/creatureIcons/towerSmall.png


+ 1 - 1
Mods/vcmi/mod.json

@@ -2,7 +2,7 @@
 	"name" : "VCMI essential files",
 	"description" : "Essential files required for VCMI to run correctly",
 
-	"version" : "0.99",
+	"version" : "1.0",
 	"author" : "VCMI Team",
 	"contact" : "http://forum.vcmi.eu/index.php",
 	"modType" : "Graphical",

+ 36 - 7
client/CMT.cpp

@@ -201,7 +201,14 @@ int main(int argc, char * argv[])
 		("donotstartserver,d","do not attempt to start server and just connect to it instead server")
 		("serverport", po::value<si64>(), "override port specified in config file")
 		("saveprefix", po::value<std::string>(), "prefix for auto save files")
-		("savefrequency", po::value<si64>(), "limit auto save creation to each N days");
+		("savefrequency", po::value<si64>(), "limit auto save creation to each N days")
+		("lobby", "parameters address, port, uuid to connect ro remote lobby session")
+		("lobby-address", po::value<std::string>(), "address to remote lobby")
+		("lobby-port", po::value<ui16>(), "port to remote lobby")
+		("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")
+		("uuid", po::value<std::string>(), "uuid for the client");
 
 	if(argc > 1)
 	{
@@ -483,6 +490,28 @@ int main(int argc, char * argv[])
 	session["autoSkip"].Bool()  = vm.count("autoSkip");
 	session["oneGoodAI"].Bool() = vm.count("oneGoodAI");
 	session["aiSolo"].Bool() = false;
+	
+	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>();
+		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);
+		if(vm.count("lobby-host"))
+		{
+			session["host"].Bool() = true;
+			session["hostConnections"].String() = std::to_string(vm["lobby-connections"].as<ui16>());
+			session["hostUuid"].String() = vm["lobby-uuid"].as<std::string>();
+			logGlobal->info("This client will host session, server uuid is %s", session["hostUuid"].String());
+		}
+		
+		//we should not reconnect to previous game in online mode
+		Settings saveSession = settings.write["server"]["reconnect"];
+		saveSession->Bool() = false;
+	}
 
 	if(vm.count("testmap"))
 	{
@@ -638,7 +667,7 @@ void processCommand(const std::string &message)
 		std::cout << "Command accepted.\t";
 
 		const bfs::path outPath =
-			VCMIDirs::get().userCachePath() / "extracted";
+			VCMIDirs::get().userExtractedPath();
 
 		bfs::create_directories(outPath);
 
@@ -671,7 +700,7 @@ void processCommand(const std::string &message)
 		std::cout << "Command accepted.\t";
 
 		const bfs::path outPath =
-			VCMIDirs::get().userCachePath() / "extracted" / "configuration";
+			VCMIDirs::get().userExtractedPath() / "configuration";
 
 		bfs::create_directories(outPath);
 
@@ -712,7 +741,7 @@ void processCommand(const std::string &message)
 		std::cout << "Command accepted.\t";
 
 		const bfs::path outPath =
-			VCMIDirs::get().userCachePath() / "extracted" / "scripts";
+			VCMIDirs::get().userExtractedPath() / "scripts";
 
 		bfs::create_directories(outPath);
 
@@ -735,7 +764,7 @@ void processCommand(const std::string &message)
 		std::cout << "Command accepted.\t";
 
 		const bfs::path outPath =
-			VCMIDirs::get().userCachePath() / "extracted";
+			VCMIDirs::get().userExtractedPath();
 
 		auto list = CResourceHandler::get()->getFilteredFiles([](const ResourceID & ident)
 		{
@@ -851,7 +880,7 @@ void processCommand(const std::string &message)
 		readed >> URI;
 		std::unique_ptr<CAnimation> anim = make_unique<CAnimation>(URI);
 		anim->preload();
-		anim->exportBitmaps(VCMIDirs::get().userCachePath() / "extracted");
+		anim->exportBitmaps(VCMIDirs::get().userExtractedPath());
 	}
 	else if(cn == "extract")
 	{
@@ -860,7 +889,7 @@ void processCommand(const std::string &message)
 
 		if (CResourceHandler::get()->existsResource(ResourceID(URI)))
 		{
-			const bfs::path outPath = VCMIDirs::get().userCachePath() / "extracted" / URI;
+			const bfs::path outPath = VCMIDirs::get().userExtractedPath() / URI;
 
 			auto data = CResourceHandler::get()->load(ResourceID(URI))->readAll();
 

+ 50 - 11
client/CServerHandler.cpp

@@ -118,6 +118,9 @@ CServerHandler::CServerHandler()
 	: state(EClientState::NONE), mx(std::make_shared<boost::recursive_mutex>()), client(nullptr), loadMode(0), campaignStateToSend(nullptr), campaignServerRestartLock(false)
 {
 	uuid = boost::uuids::to_string(boost::uuids::random_generator()());
+	//read from file to restore last session
+	if(!settings["server"]["uuid"].isNull() && !settings["server"]["uuid"].String().empty())
+		uuid = settings["server"]["uuid"].String();
 	applier = std::make_shared<CApplier<CBaseForLobbyApply>>();
 	registerTypesLobbyPacks(*applier);
 }
@@ -190,9 +193,17 @@ void CServerHandler::startLocalServerAndConnect()
 	}
 #elif defined(SINGLE_PROCESS_APP)
 	boost::condition_variable cond;
-	threadRunLocalServer = std::make_shared<boost::thread>([&cond, this] {
+	std::vector<std::string> args{"--uuid=" + uuid, "--port=" + boost::lexical_cast<std::string>(getHostPort())};
+	if(settings["session"]["lobby"].Bool() && settings["session"]["host"].Bool())
+	{
+		args.push_back("--lobby=" + settings["session"]["address"].String());
+		args.push_back("--connections=" + settings["session"]["hostConnections"].String());
+		args.push_back("--lobby-port=" + boost::lexical_cast<std::string>(settings["session"]["port"].Integer()));
+		args.push_back("--lobby-uuid=" + settings["session"]["hostUuid"].String());
+	}
+	threadRunLocalServer = std::make_shared<boost::thread>([&cond, args, this] {
 		setThreadName("CVCMIServer");
-		CVCMIServer::create(&cond, uuid);
+		CVCMIServer::create(&cond, args);
 		onServerFinished();
 	});
 	threadRunLocalServer->detach();
@@ -259,8 +270,8 @@ void CServerHandler::justConnectToServer(const std::string & addr, const ui16 po
 		{
 			logNetwork->info("Establishing connection...");
 			c = std::make_shared<CConnection>(
-					addr.size() ? addr : settings["server"]["server"].String(),
-					port ? port : getDefaultPort(),
+					addr.size() ? addr : getHostAddress(),
+					port ? port : getHostPort(),
 					NAME, uuid);
 		}
 		catch(...)
@@ -278,12 +289,12 @@ void CServerHandler::justConnectToServer(const std::string & addr, const ui16 po
 
 	c->handler = std::make_shared<boost::thread>(&CServerHandler::threadHandleConnection, this);
 
-	if(!addr.empty() && addr != localhostAddress)
+	if(!addr.empty() && addr != getHostAddress())
 	{
 		Settings serverAddress = settings.write["server"]["server"];
 		serverAddress->String() = addr;
 	}
-	if(port && port != getDefaultPort())
+	if(port && port != getHostPort())
 	{
 		Settings serverPort = settings.write["server"]["port"];
 		serverPort->Integer() = port;
@@ -358,10 +369,7 @@ bool CServerHandler::isGuest() const
 
 ui16 CServerHandler::getDefaultPort()
 {
-	if(settings["session"]["serverport"].Integer())
-		return static_cast<ui16>(settings["session"]["serverport"].Integer());
-	else
-		return static_cast<ui16>(settings["server"]["port"].Integer());
+	return static_cast<ui16>(settings["server"]["port"].Integer());
 }
 
 std::string CServerHandler::getDefaultPortStr()
@@ -369,6 +377,28 @@ std::string CServerHandler::getDefaultPortStr()
 	return boost::lexical_cast<std::string>(getDefaultPort());
 }
 
+std::string CServerHandler::getHostAddress() const
+{
+	if(settings["session"]["lobby"].isNull() || !settings["session"]["lobby"].Bool())
+		return settings["server"]["server"].String();
+	
+	if(settings["session"]["host"].Bool())
+		return localhostAddress;
+	
+	return settings["session"]["address"].String();
+}
+
+ui16 CServerHandler::getHostPort() const
+{
+	if(settings["session"]["lobby"].isNull() || !settings["session"]["lobby"].Bool())
+		return getDefaultPort();
+	
+	if(settings["session"]["host"].Bool())
+		return getDefaultPort();
+	
+	return settings["session"]["port"].Integer();
+}
+
 void CServerHandler::sendClientConnecting() const
 {
 	LobbyClientConnected lcc;
@@ -813,9 +843,17 @@ void CServerHandler::threadRunServer()
 	setThreadName("CServerHandler::threadRunServer");
 	const std::string logName = (VCMIDirs::get().userLogsPath() / "server_log.txt").string();
 	std::string comm = VCMIDirs::get().serverPath().string()
-		+ " --port=" + getDefaultPortStr()
+		+ " --port=" + boost::lexical_cast<std::string>(getHostPort())
 		+ " --run-by-client"
 		+ " --uuid=" + uuid;
+	if(settings["session"]["lobby"].Bool() && settings["session"]["host"].Bool())
+	{
+		comm += " --lobby=" + settings["session"]["address"].String();
+		comm += " --connections=" + settings["session"]["hostConnections"].String();
+		comm += " --lobby-port=" + boost::lexical_cast<std::string>(settings["session"]["port"].Integer());
+		comm += " --lobby-uuid=" + settings["session"]["hostUuid"].String();
+	}
+		
 	if(shm)
 	{
 		comm += " --enable-shm";
@@ -823,6 +861,7 @@ void CServerHandler::threadRunServer()
 			comm += " --enable-shm-uuid";
 	}
 	comm += " > \"" + logName + '\"';
+    logGlobal->info("Server command line: %s", comm);
 
 #ifdef VCMI_WINDOWS
 	int result = -1;

+ 3 - 0
client/CServerHandler.h

@@ -111,6 +111,9 @@ public:
 	static const std::string localhostAddress;
 
 	CServerHandler();
+	
+	std::string getHostAddress() const;
+	ui16 getHostPort() const;
 
 	void resetStateForLobby(const StartInfo::EMode mode, const std::vector<std::string> * names = nullptr);
 	void startLocalServerAndConnect();

+ 4 - 2
client/Graphics.cpp

@@ -428,11 +428,13 @@ void Graphics::loadErmuToPicture()
 	assert (etp_idx == 44);
 }
 
-void Graphics::addImageListEntry(size_t index, const std::string & listName, const std::string & imageName)
+void Graphics::addImageListEntry(size_t index, size_t group, const std::string & listName, const std::string & imageName)
 {
 	if (!imageName.empty())
 	{
 		JsonNode entry;
+		if (group != 0)
+			entry["group"].Integer() = group;
 		entry["frame"].Integer() = index;
 		entry["file"].String() = imageName;
 
@@ -442,7 +444,7 @@ void Graphics::addImageListEntry(size_t index, const std::string & listName, con
 
 void Graphics::addImageListEntries(const EntityService * service)
 {
-	auto cb = std::bind(&Graphics::addImageListEntry, this, _1, _2, _3);
+	auto cb = std::bind(&Graphics::addImageListEntry, this, _1, _2, _3, _4);
 
 	auto loopCb = [&](const Entity * entity, bool & stop)
 	{

+ 1 - 1
client/Graphics.h

@@ -38,7 +38,7 @@ enum EFonts : int
 /// Handles fonts, hero images, town images, various graphics
 class Graphics
 {
-	void addImageListEntry(size_t index, const std::string & listName, const std::string & imageName);
+	void addImageListEntry(size_t index, size_t group, const std::string & listName, const std::string & imageName);
 
 	void addImageListEntries(const EntityService * service);
 

+ 1 - 1
client/battle/CBattleControlPanel.cpp

@@ -231,7 +231,7 @@ void CBattleControlPanel::bSpellf()
 
 		if (blockingBonus->source == Bonus::ARTIFACT)
 		{
-			const int32_t artID = blockingBonus->sid;
+			const auto artID = ArtifactID(blockingBonus->sid);
 			//If we have artifact, put name of our hero. Otherwise assume it's the enemy.
 			//TODO check who *really* is source of bonus
 			std::string heroName = myHero->hasArt(artID) ? myHero->name : owner->enemyHero().name;

+ 2 - 1
client/battle/CBattleInterface.cpp

@@ -97,7 +97,6 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet
 
 		queue->moveTo(Point(pos.x, pos.y - queue->pos.h));
 	}
-	queue->update();
 
 	//preparing siege info
 	const CGTownInstance *town = curInt->cb->battleGetDefendedTown();
@@ -188,6 +187,8 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet
 
 	addUsedEvents(RCLICK | MOVE | KEYBOARD);
 	controlPanel->blockUI(true);
+	queue->update();
+	blockUI(true);
 }
 
 CBattleInterface::~CBattleInterface()

+ 3 - 1
client/battle/CBattleInterface.h

@@ -40,6 +40,8 @@ class CPlayerInterface;
 class CClickableHex;
 class CAnimation;
 struct BattleEffect;
+class IImage;
+class CStackQueue;
 
 class CBattleProjectileController;
 class CBattleSiegeController;
@@ -182,7 +184,7 @@ public:
 
 	friend class CPlayerInterface;
 	friend class CInGameConsole;
-
+	friend class CStackQueue;
 	friend class CBattleResultWindow;
 	friend class CBattleHero;
 	friend class CBattleStackAnimation;

+ 17 - 2
client/battle/CBattleInterfaceClasses.cpp

@@ -694,7 +694,13 @@ void CStackQueue::update()
 		stackBoxes[boxIndex]->setUnit(nullptr);
 }
 
-CStackQueue::StackBox::StackBox(CStackQueue * owner)
+int32_t CStackQueue::getSiegeShooterIconID()
+{
+	return owner->siegeH->town->town->faction->index;
+}
+
+CStackQueue::StackBox::StackBox(CStackQueue * owner):
+	owner(owner)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 	background = std::make_shared<CPicture>(owner->embedded ? "StackQueueSmall" : "StackQueueLarge");
@@ -726,7 +732,16 @@ void CStackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn)
 	{
 		background->colorize(unit->unitOwner());
 		icon->visible = true;
-		icon->setFrame(unit->creatureIconIndex());
+
+		// temporary code for mod compatibility:
+		// first, set icon that should definitely exist (arrow tower icon in base vcmi mod)
+		// second, try to switch to icon that should be provided by mod
+		// if mod is not up to date and does have arrow tower icon yet - second setFrame call will fail and retain previously set image
+		// for 1.2 release & later next line should be moved into 'else' block
+		icon->setFrame(unit->creatureIconIndex(), 0);
+		if (unit->unitType()->idNumber == CreatureID::ARROW_TOWERS)
+			icon->setFrame(owner->getSiegeShooterIconID(), 1);
+
 		amount->setText(makeNumberShort(unit->getCount()));
 
 		if(stateIcon)

+ 2 - 0
client/battle/CBattleInterfaceClasses.h

@@ -158,6 +158,7 @@ class CStackQueue : public CIntObject
 {
 	class StackBox : public CIntObject
 	{
+		CStackQueue * owner;
 	public:
 		std::shared_ptr<CPicture> background;
 		std::shared_ptr<CAnimImage> icon;
@@ -176,6 +177,7 @@ class CStackQueue : public CIntObject
 	std::shared_ptr<CAnimation> icons;
 	std::shared_ptr<CAnimation> stateIcons;
 
+	int32_t getSiegeShooterIconID();
 public:
 	const bool embedded;
 

+ 2 - 0
client/ios/Info.plist

@@ -31,6 +31,8 @@
 	<string>1</string>
 	<key>LSRequiresIPhoneOS</key>
 	<true/>
+	<key>LSSupportsOpeningDocumentsInPlace</key>
+	<true/>
 	<key>NSAppTransportSecurity</key>
 	<dict>
 		<key>NSAllowsArbitraryLoads</key>

+ 7 - 1
client/ios/main.m

@@ -50,7 +50,13 @@ int client_main(int argc, char * argv[])
 		id __block startGameObserver = [NSNotificationCenter.defaultCenter addObserverForName:@"StartGame" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
 			[NSNotificationCenter.defaultCenter removeObserver:startGameObserver];
 			startGameObserver = nil;
-			startSDLManually(argc, argv);
+
+			NSArray<NSString *> * args = note.userInfo[@"args"];
+			const char * newArgv[args.count];
+			NSUInteger i = 0;
+			for (NSString * obj in args)
+				newArgv[i++] = obj.UTF8String;
+			startSDLManually(args.count, (char **)(newArgv));
 		}];
 		return qt_main_wrapper(argc, argv);
 	}

+ 2 - 2
client/mainmenu/CMainMenu.cpp

@@ -480,8 +480,8 @@ CSimpleJoinScreen::CSimpleJoinScreen(bool host)
 
 		inputAddress->giveFocus();
 	}
-	inputAddress->setText(host ? CServerHandler::localhostAddress : settings["server"]["server"].String(), true);
-	inputPort->setText(CServerHandler::getDefaultPortStr(), true);
+	inputAddress->setText(host ? CServerHandler::localhostAddress : CSH->getHostAddress(), true);
+	inputPort->setText(boost::lexical_cast<std::string>(CSH->getHostPort()), true);
 
 	buttonCancel = std::make_shared<CButton>(Point(142, 142), "MUBCANC.DEF", CGI->generaltexth->zelp[561], std::bind(&CSimpleJoinScreen::leaveScreen, this), SDLK_ESCAPE);
 	statusBar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(7, 186, 218, 18), 7, 186));

+ 4 - 3
client/widgets/AdventureMapClasses.cpp

@@ -1038,12 +1038,10 @@ void CInGameConsole::keyPressed (const SDL_KeyboardEvent & key)
 		{
 			if(captureAllKeys)
 			{
-				captureAllKeys = false;
 				endEnteringText(false);
 			}
 			else if(SDLK_TAB == key.keysym.sym)
 			{
-				captureAllKeys = true;
 				startEnteringText();
 			}
 			break;
@@ -1052,7 +1050,6 @@ void CInGameConsole::keyPressed (const SDL_KeyboardEvent & key)
 		{
 			if(enteredText.size() > 0  &&  captureAllKeys)
 			{
-				captureAllKeys = false;
 				endEnteringText(true);
 				CCS->soundh->playSound("CHAT");
 			}
@@ -1133,6 +1130,8 @@ void CInGameConsole::startEnteringText()
 
 	if (statusBar)
 	{
+		captureAllKeys = true;
+
 		CSDL_Ext::startTextInput(&statusBar->pos);
 
 		enteredText = "_";
@@ -1145,6 +1144,8 @@ void CInGameConsole::startEnteringText()
 
 void CInGameConsole::endEnteringText(bool printEnteredText)
 {
+	captureAllKeys = false;
+
 	CSDL_Ext::stopTextInput();
 
 	prevEntDisp = -1;

+ 3 - 2
client/widgets/CComponent.cpp

@@ -155,10 +155,11 @@ std::string CComponent::getDescription()
 	case creature:   return "";
 	case artifact:
 	{
+		auto artID = ArtifactID(subtype);
 		std::unique_ptr<CArtifactInstance> art;
-		if (subtype != ArtifactID::SPELL_SCROLL)
+		if (artID != ArtifactID::SPELL_SCROLL)
 		{
-			art.reset(CArtifactInstance::createNewArtifactInstance(subtype));
+			art.reset(CArtifactInstance::createNewArtifactInstance(artID));
 		}
 		else
 		{

+ 14 - 0
client/widgets/TextControls.cpp

@@ -394,6 +394,20 @@ void CGStatusBar::init()
 	GH.statusbar = shared_from_this();
 }
 
+void CGStatusBar::clickLeft(tribool down, bool previousState)
+{
+	if(!down && onClick)
+	{
+		onClick();
+	}
+}
+
+void CGStatusBar::setOnClick(std::function<void()> handler)
+{
+	onClick = handler;
+	addUsedEvents(LCLICK);
+}
+
 Point CGStatusBar::getBorderSize()
 {
 	//Width of borders where text should not be printed

+ 6 - 0
client/widgets/TextControls.h

@@ -124,6 +124,11 @@ class CGStatusBar : public CLabel, public std::enable_shared_from_this<CGStatusB
 protected:
 	Point getBorderSize() override;
 
+	void clickLeft(tribool down, bool previousState) override;
+
+private:
+	std::function<void()> onClick;
+
 public:
 	template<typename ...Args>
 	static std::shared_ptr<CGStatusBar> create(Args... args)
@@ -140,6 +145,7 @@ public:
 	void show(SDL_Surface * to) override; //shows statusbar (with current text)
 
 	void lock(bool shouldLock) override; //If true, current text cannot be changed until lock(false) is called
+	void setOnClick(std::function<void()> handler);
 };
 
 class CFocusable;

+ 6 - 0
client/windows/CAdvmapInterface.cpp

@@ -716,6 +716,11 @@ CAdvMapInt::CAdvMapInt():
 	worldViewUnderground->block(!CGI->mh->map->twoLevel);
 
 	addUsedEvents(MOVE);
+
+	statusbar->setOnClick([&]
+		{
+			if(LOCPLINT) LOCPLINT->cingconsole->startEnteringText();
+		});
 }
 
 CAdvMapInt::~CAdvMapInt()
@@ -949,6 +954,7 @@ void CAdvMapInt::activate()
 		}
 		minimap.activate();
 		terrain.activate();
+		statusbar->activate();
 
 		GH.fakeMouseMove(); //to restore the cursor
 	}

+ 1 - 1
client/windows/CCastleInterface.cpp

@@ -935,7 +935,7 @@ void CCastleBuildings::enterMagesGuild()
 
 void CCastleBuildings::enterTownHall()
 {
-	if(town->visitingHero && town->visitingHero->hasArt(2) &&
+	if(town->visitingHero && town->visitingHero->hasArt(ArtifactID::GRAIL) &&
 		!vstd::contains(town->builtBuildings, BuildingID::GRAIL)) //hero has grail, but town does not have it
 	{
 		if(!vstd::contains(town->forbiddenBuildings, BuildingID::GRAIL))

+ 2 - 0
config/creatures/special.json

@@ -123,6 +123,8 @@
 		},
 		"graphics" :
 		{
+			"iconSmall" : "vcmi/creatureIcons/towerSmall",
+			"iconLarge" : "vcmi/creatureIcons/towerLarge",
 			"animation": "CLCBOW.DEF" // needed to pass validation, never used
 		},
 		"sound": {}

+ 2 - 0
config/factions/castle.json

@@ -203,6 +203,8 @@
 			"siege" :
 			{
 				"shooter" : "archer",
+				"towerIconSmall" : "vcmi/creatureIcons/towerSmall",
+				"towerIconLarge" : "vcmi/creatureIcons/towerLarge",
 				"imagePrefix" : "SGCS",
 				"gate" :
 				{

+ 2 - 0
config/factions/conflux.json

@@ -210,6 +210,8 @@
 			"siege" :
 			{
 				"shooter" : "stormElemental",
+				"towerIconSmall" : "vcmi/creatureIcons/towerSmall",
+				"towerIconLarge" : "vcmi/creatureIcons/towerLarge",
 				"imagePrefix" : "SGEL",
 				"gate" :
 				{

+ 2 - 0
config/factions/dungeon.json

@@ -204,6 +204,8 @@
 			"siege" :
 			{
 				"shooter" : "medusa",
+				"towerIconSmall" : "vcmi/creatureIcons/towerSmall",
+				"towerIconLarge" : "vcmi/creatureIcons/towerLarge",
 				"imagePrefix" : "SGDN",
 				"gate" :
 				{

+ 2 - 0
config/factions/fortress.json

@@ -209,6 +209,8 @@
 			"siege" :
 			{
 				"shooter" : "lizardman",
+				"towerIconSmall" : "vcmi/creatureIcons/towerSmall",
+				"towerIconLarge" : "vcmi/creatureIcons/towerLarge",
 				"imagePrefix" : "SGFR",
 				"gate" :
 				{

+ 2 - 0
config/factions/inferno.json

@@ -204,6 +204,8 @@
 			"siege" :
 			{
 				"shooter" : "gog",
+				"towerIconSmall" : "vcmi/creatureIcons/towerSmall",
+				"towerIconLarge" : "vcmi/creatureIcons/towerLarge",
 				"imagePrefix" : "SGIN",
 				"gate" :
 				{

+ 2 - 0
config/factions/necropolis.json

@@ -214,6 +214,8 @@
 			"siege" :
 			{
 				"shooter" : "lich",
+				"towerIconSmall" : "vcmi/creatureIcons/towerSmall",
+				"towerIconLarge" : "vcmi/creatureIcons/towerLarge",
 				"imagePrefix" : "SGNC",
 				"gate" :
 				{

+ 2 - 0
config/factions/rampart.json

@@ -211,6 +211,8 @@
 			"siege" :
 			{
 				"shooter" : "woodElf",
+				"towerIconSmall" : "vcmi/creatureIcons/towerSmall",
+				"towerIconLarge" : "vcmi/creatureIcons/towerLarge",
 				"imagePrefix" : "SGRM",
 				"gate" :
 				{

+ 2 - 0
config/factions/stronghold.json

@@ -203,6 +203,8 @@
 			{
 				"shooter" : "orc",
 				"imagePrefix" : "SGST",
+				"towerIconSmall" : "vcmi/creatureIcons/towerSmall",
+				"towerIconLarge" : "vcmi/creatureIcons/towerLarge",
 				"gate" :
 				{
 					"arch" : { "x" : 478, "y" : 235 },

+ 2 - 0
config/factions/tower.json

@@ -202,6 +202,8 @@
 			"siege" :
 			{
 				"shooter" : "mage",
+				"towerIconSmall" : "vcmi/creatureIcons/towerSmall",
+				"towerIconLarge" : "vcmi/creatureIcons/towerLarge",
 				"imagePrefix" : "SGTW",
 				"gate" :
 				{

+ 22 - 2
config/schemas/settings.json

@@ -293,7 +293,7 @@
 				},
 				"names" : {
 					"type" : "array",
-					"default" : {},
+					"default" : [],
 					"items":
 					{
 						"type" : "string",
@@ -380,7 +380,7 @@
 			"type" : "object",
 			"default": {},
 			"additionalProperties" : false,
-			"required" : [ "repositoryURL", "enableInstalledMods", "extraResolutionsModPath", "autoCheckRepositories", "updateOnStartup", "updateConfigUrl" ],
+			"required" : [ "repositoryURL", "enableInstalledMods", "extraResolutionsModPath", "autoCheckRepositories", "updateOnStartup", "updateConfigUrl", "lobbyUrl", "lobbyPort", "lobbyUsername", "connectionTimeout" ],
 			"properties" : {
 				"repositoryURL" : {
 					"type" : "array",
@@ -410,6 +410,26 @@
 				"updateConfigUrl" : {
 					"type" : "string",
 					"default" : "https://raw.githubusercontent.com/vcmi/vcmi-updates/master/vcmi-updates.json"
+				},
+				"lobbyUrl" : {
+					"type" : "string",
+					"description" : "ip address or web link to remote proxy server",
+					"default" : "beholder.vcmi.eu"
+				},
+				"lobbyPort" : {
+					"type" : "number",
+					"description" : "connection port for remote proxy server",
+					"default" : 5002
+				},
+				"lobbyUsername" : {
+					"type" : "string",
+					"description" : "username for the client on the remote proxy server",
+					"default" : ""
+				},
+				"connectionTimeout" : {
+					"type" : "number",
+					"description" : "maximum time in ms, should be enough to establish socket connection to remote proxy server.",
+					"default" : 2000
 				}
 			}
 		}

+ 11 - 0
config/schemas/townSiege.json

@@ -6,6 +6,7 @@
 	"description" : "Format used to define town siege screen in VCMI",
 	"required" : [
 		"gate", "imagePrefix", "moat", "shooter",
+		"towerIconLarge", "towerIconSmall",
 		"static", "towers", "walls"
 	 ],
 	
@@ -80,6 +81,16 @@
 			"type":"string",
 			"description" : "Identifier of creature that will be used as tower shooter"
 		},
+		"towerIconSmall": {
+			"type":"string",
+			"description": "Small icon for tower, used in battle queue",
+			"format" : "imageFile"
+		},
+		"towerIconLarge": {
+			"type":"string",
+			"description": "Large icon for tower, used in battle queue",
+			"format" : "imageFile"
+		},
 		"static": {
 			"type":"object",
 			"additionalProperties" : false,

+ 1 - 1
include/vcmi/Entity.h

@@ -17,7 +17,7 @@ class IBonusBearer;
 class DLL_LINKAGE Entity
 {
 public:
-	using IconRegistar = std::function<void(int32_t index, const std::string & listName, const std::string & imageName)>;
+	using IconRegistar = std::function<void(int32_t index, int32_t group, const std::string & listName, const std::string & imageName)>;
 
 	virtual ~Entity() = default;
 

+ 8 - 0
launcher/CMakeLists.txt

@@ -33,6 +33,9 @@ set(launcher_SRCS
 		launcherdirs.cpp
 		jsonutils.cpp
 		updatedialog_moc.cpp
+		lobby/lobby.cpp
+		lobby/lobby_moc.cpp
+		lobby/lobbyroomrequest_moc.cpp
 )
 
 set(launcher_HEADERS
@@ -43,6 +46,9 @@ set(launcher_HEADERS
 		launcherdirs.h
 		jsonutils.h
 		updatedialog_moc.h
+		lobby/lobby.h
+		lobby/lobby_moc.h
+		lobby/lobbyroomrequest_moc.h
 		main.h
 )
 
@@ -52,6 +58,8 @@ set(launcher_FORMS
 		settingsView/csettingsview_moc.ui
 		mainwindow_moc.ui
 		updatedialog_moc.ui
+		lobby/lobby_moc.ui
+		lobby/lobbyroomrequest_moc.ui
 )
 
 if(APPLE_IOS)

二进制
launcher/icons/menu-lobby.png


二进制
launcher/icons/room-private.png


+ 6 - 2
launcher/ios/main.m

@@ -26,5 +26,9 @@ void launchGame(int argc, char * argv[]) {
 			qtNativeWindow.windowScene = nil;
 #endif
 	}
-	[NSNotificationCenter.defaultCenter postNotificationName:@"StartGame" object:nil];
-}
+
+	__auto_type args = [NSMutableArray arrayWithCapacity:argc];
+	for (int i = 0; i < argc; ++i)
+		[args addObject:@(argv[i])];
+	[NSNotificationCenter.defaultCenter postNotificationName:@"StartGame" object:nil userInfo:@{@"args": args}];
+}

+ 123 - 0
launcher/lobby/lobby.cpp

@@ -0,0 +1,123 @@
+/*
+ * lobby.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 "lobby.h"
+#include "../lib/GameConstants.h"
+
+SocketLobby::SocketLobby(QObject *parent) :
+	QObject(parent)
+{
+	socket = new QTcpSocket(this);
+	connect(socket, SIGNAL(connected()), this, SLOT(connected()));
+	connect(socket, SIGNAL(disconnected()), this, SLOT(disconnected()));
+	connect(socket, SIGNAL(readyRead()), this, SLOT(readyRead()));
+	connect(socket, SIGNAL(bytesWritten(qint64)), this, SLOT(bytesWritten(qint64)));
+}
+
+void SocketLobby::connectServer(const QString & host, int port, const QString & usr, int timeout)
+{
+	username = usr;
+
+	socket->connectToHost(host, port);
+
+	if(!socket->waitForDisconnected(timeout) && !isConnected)
+	{
+		emit text("Error: " + socket->errorString());
+		emit disconnect();
+	}
+}
+
+void SocketLobby::disconnectServer()
+{
+	socket->disconnectFromHost();
+}
+
+void SocketLobby::requestNewSession(const QString & session, int totalPlayers, const QString & pswd, const QMap<QString, QString> & mods)
+{
+	const QString sessionMessage = ProtocolStrings[CREATE].arg(session, pswd, QString::number(totalPlayers), prepareModsClientString(mods));
+	send(sessionMessage);
+}
+
+void SocketLobby::requestJoinSession(const QString & session, const QString & pswd, const QMap<QString, QString> & mods)
+{
+	const QString sessionMessage = ProtocolStrings[JOIN].arg(session, pswd, prepareModsClientString(mods));
+	send(sessionMessage);
+}
+
+void SocketLobby::requestLeaveSession(const QString & session)
+{
+	const QString sessionMessage = ProtocolStrings[LEAVE].arg(session);
+	send(sessionMessage);
+}
+
+void SocketLobby::requestReadySession(const QString & session)
+{
+	const QString sessionMessage = ProtocolStrings[READY].arg(session);
+	send(sessionMessage);
+}
+
+void SocketLobby::send(const QString & msg)
+{
+	QByteArray str = msg.toUtf8();
+	int sz = str.size();
+	QByteArray pack((const char *)&sz, sizeof(sz));
+	pack.append(str);
+	socket->write(pack);
+}
+
+void SocketLobby::connected()
+{
+	isConnected = true;
+	emit text("Connected!");
+
+	QByteArray greetingBytes;
+	greetingBytes.append(ProtocolVersion);
+	greetingBytes.append(ProtocolEncoding.size());
+	const QString greetingConst = QString(greetingBytes)
+								+ ProtocolStrings[GREETING].arg(QString::fromStdString(ProtocolEncoding),
+																username,
+																QString::fromStdString(GameConstants::VCMI_VERSION));
+	send(greetingConst);
+}
+
+void SocketLobby::disconnected()
+{
+	isConnected = false;
+	emit disconnect();
+	emit text("Disconnected!");
+}
+
+void SocketLobby::bytesWritten(qint64 bytes)
+{
+	qDebug() << "We wrote: " << bytes;
+}
+
+void SocketLobby::readyRead()
+{
+	qDebug() << "Reading...";
+	emit receive(socket->readAll());
+}
+
+
+ServerCommand::ServerCommand(ProtocolConsts cmd, const QStringList & args):
+	command(cmd),
+	arguments(args)
+{
+}
+
+QString prepareModsClientString(const QMap<QString, QString> & mods)
+{
+	QStringList result;
+	for(auto & mod : mods.keys())
+	{
+		result << mod + "&" + mods[mod];
+	}
+	return result.join(";");
+}

+ 188 - 0
launcher/lobby/lobby.h

@@ -0,0 +1,188 @@
+/*
+ * lobby.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
+
+#include <QTcpSocket>
+#include <QAbstractSocket>
+
+const unsigned int ProtocolVersion = 3;
+const std::string ProtocolEncoding = "utf8";
+
+class ProtocolError: public std::runtime_error
+{
+public:
+	ProtocolError(const char * w): std::runtime_error(w) {}
+};
+
+enum ProtocolConsts
+{
+	//client consts
+	GREETING, USERNAME, MESSAGE, VERSION, CREATE, JOIN, LEAVE, KICK, READY, FORCESTART,
+
+	//server consts
+	SESSIONS, CREATED, JOINED, KICKED, SRVERROR, CHAT, START, STATUS, HOST, MODS, CLIENTMODS
+};
+
+const QMap<ProtocolConsts, QString> ProtocolStrings
+{
+	//=== client commands ===
+	
+	//handshaking with server
+	//%1: first byte is protocol_version, then size of encoding string in bytes, then encoding string
+	//%2: client name
+	//%3: VCMI version
+	{GREETING, "%1<GREETINGS>%2<VER>%3"},
+	
+	//[unsupported] autorization with username
+	//%1: username
+	{USERNAME, "<USER>%1"},
+	
+	//sending message to the chat
+	//%1: message text
+	{MESSAGE, "<MSG>%1"},
+	
+	//create new room
+	//%1: room name
+	//%2: password for the room
+	//%3: max number of players
+	//%4: mods used by host
+	// each mod has a format modname&modversion, mods should be separated by ; symbol
+	{CREATE, "<NEW>%1<PSWD>%2<COUNT>%3<MODS>%4"},
+	
+	//join to the room
+	//%1: room name
+	//%2: password for the room
+	//%3: list of mods used by player
+	// each mod has a format modname&modversion, mods should be separated by ; symbol
+	{JOIN, "<JOIN>%1<PSWD>%2<MODS>%3"},
+	
+	//leave the room
+	//%1: room name
+	{LEAVE, "<LEAVE>%1"},
+	
+	//kick user from the current room
+	//%1: player username
+	{KICK, "<KICK>%1"},
+	
+	//signal that player is ready for game
+	//%1: room name
+	{READY, "<READY>%1"},
+	
+	//[unsupported] start session immediately
+	//%1: room name
+	{FORCESTART, "<FORCESTART>%1"},
+
+	//=== server commands ===
+	//server commands are started from :>>, arguments are enumerated by : symbol
+	
+	//new session was created
+	//arg[0]: room name
+	{CREATED, "CREATED"},
+	
+	//list of existing sessions
+	//arg[0]: amount of sessions, following arguments depending on it
+	//arg[x]: session name
+	//arg[x+1]: amount of players in the session
+	//arg[x+2]: total amount of players allowed
+	//arg[x+3]: True if session is protected by password
+	{SESSIONS, "SESSIONS"},
+	
+	//user has joined to the session
+	//arg[0]: session name
+	//arg[1]: username (who was joined)
+	{JOINED, "JOIN"},
+	
+	//user has left the session
+	//arg[0]: session name
+	//arg[1]: username (who has left)
+	{KICKED, "KICK"},
+	
+	//session has been started
+	//arg[0]: session name
+	//arg[1]: uuid to be used for connection
+	{START, "START"},
+	
+	//host ownership for the game session
+	//arg[0]: uuid to be used by vcmiserver
+	//arg[1]: amount of players (clients) to be connected
+	{HOST, "HOST"},
+	
+	//room status
+	//arg[0]: amount of players, following arguments depending on it
+	//arg[x]: player username
+	//arg[x+1]: True if player is ready
+	{STATUS, "STATUS"}, //joined_players:player_name:is_ready
+	
+	//server error
+	//arg[0]: error message
+	{SRVERROR, "ERROR"},
+	
+	//mods used in the session by host player
+	//arg[0]: amount of mods, following arguments depending on it
+	//arg[x]: mod name
+	//arg[x+1]: mod version
+	{MODS, "MODS"},
+	
+	//mods used by user
+	//arg[0]: username
+	//arg[1]: amount of mods, following arguments depending on it
+	//arg[x]: mod name
+	//arg[x+1]: mod version
+	{CLIENTMODS, "MODSOTHER"},
+	
+	//received chat message
+	//arg[0]: sender username
+	//arg[1]: message text
+	{CHAT, "MSG"}
+};
+
+class ServerCommand
+{
+public:
+	ServerCommand(ProtocolConsts, const QStringList & arguments);
+
+	const ProtocolConsts command;
+	const QStringList arguments;
+};
+
+class SocketLobby : public QObject
+{
+	Q_OBJECT
+public:
+	explicit SocketLobby(QObject *parent = 0);
+	void connectServer(const QString & host, int port, const QString & username, int timeout);
+	void disconnectServer();
+	void requestNewSession(const QString & session, int totalPlayers, const QString & pswd, const QMap<QString, QString> & mods);
+	void requestJoinSession(const QString & session, const QString & pswd, const QMap<QString, QString> & mods);
+	void requestLeaveSession(const QString & session);
+	void requestReadySession(const QString & session);
+
+	void send(const QString &);
+
+signals:
+
+	void text(QString);
+	void receive(QString);
+	void disconnect();
+
+public slots:
+
+	void connected();
+	void disconnected();
+	void bytesWritten(qint64 bytes);
+	void readyRead();
+
+private:
+	QTcpSocket *socket;
+	bool isConnected = false;
+	QString username;
+};
+
+QString prepareModsClientString(const QMap<QString, QString> & mods);

+ 417 - 0
launcher/lobby/lobby_moc.cpp

@@ -0,0 +1,417 @@
+/*
+ * lobby_moc.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 "main.h"
+#include "lobby_moc.h"
+#include "ui_lobby_moc.h"
+#include "lobbyroomrequest_moc.h"
+#include "../mainwindow_moc.h"
+#include "../modManager/cmodlist.h"
+#include "../../lib/CConfigHandler.h"
+
+Lobby::Lobby(QWidget *parent) :
+	QWidget(parent),
+	ui(new Ui::Lobby)
+{
+	ui->setupUi(this);
+
+	connect(&socketLobby, SIGNAL(text(QString)), this, SLOT(sysMessage(QString)));
+	connect(&socketLobby, SIGNAL(receive(QString)), this, SLOT(dispatchMessage(QString)));
+	connect(&socketLobby, SIGNAL(disconnect()), this, SLOT(onDisconnected()));
+	
+	QString hostString("%1:%2");
+	hostString = hostString.arg(QString::fromStdString(settings["launcher"]["lobbyUrl"].String()));
+	hostString = hostString.arg(settings["launcher"]["lobbyPort"].Integer());
+	
+	ui->serverEdit->setText(hostString);
+	ui->userEdit->setText(QString::fromStdString(settings["launcher"]["lobbyUsername"].String()));
+	ui->kickButton->setVisible(false);
+}
+
+Lobby::~Lobby()
+{
+	delete ui;
+}
+
+QMap<QString, QString> Lobby::buildModsMap() const
+{
+	QMap<QString, QString> result;
+	QObject * mainWindow = qApp->activeWindow();
+	if(!mainWindow)
+		mainWindow = parent();
+	if(!mainWindow)
+		return result; //probably something is really wrong here
+	
+	while(mainWindow->parent())
+		mainWindow = mainWindow->parent();
+	const auto & modlist = qobject_cast<MainWindow*>(mainWindow)->getModList();
+	
+	for(auto & modname : modlist.getModList())
+	{
+		auto mod = modlist.getMod(modname);
+		if(mod.isEnabled())
+		{
+			result[modname] = mod.getValue("version").toString();
+		}
+	}
+	return result;
+}
+
+bool Lobby::isModAvailable(const QString & modName, const QString & modVersion) const
+{
+	QObject * mainWindow = qApp->activeWindow();
+	while(mainWindow->parent())
+		mainWindow = mainWindow->parent();
+	const auto & modlist = qobject_cast<MainWindow*>(mainWindow)->getModList();
+	
+	if(!modlist.hasMod(modName))
+		return false;
+
+	auto mod = modlist.getMod(modName);
+	return (mod.isInstalled () || mod.isAvailable()) && (mod.getValue("version") == modVersion);
+}
+
+void Lobby::serverCommand(const ServerCommand & command) try
+{
+	//initialize variables outside of switch block
+	const QString statusPlaceholder("%1 %2\n");
+	const auto & args = command.arguments;
+	int amount, tagPoint;
+	QString joinStr;
+	switch(command.command)
+	{
+	case SRVERROR:
+		protocolAssert(args.size());
+		chatMessage("System error", args[0], true);
+		if(authentificationStatus == AuthStatus::AUTH_NONE)
+			authentificationStatus = AuthStatus::AUTH_ERROR;
+		break;
+
+	case CREATED:
+		protocolAssert(args.size());
+		hostSession = args[0];
+		session = args[0];
+		sysMessage("new session started");
+		break;
+
+	case SESSIONS:
+		protocolAssert(args.size());
+		amount = args[0].toInt();
+		protocolAssert(amount * 4 == (args.size() - 1));
+		ui->sessionsTable->setRowCount(amount);
+
+		tagPoint = 1;
+		for(int i = 0; i < amount; ++i)
+		{
+			QTableWidgetItem * sessionNameItem = new QTableWidgetItem(args[tagPoint++]);
+			ui->sessionsTable->setItem(i, 0, sessionNameItem);
+
+			int playersJoined = args[tagPoint++].toInt();
+			int playersTotal = args[tagPoint++].toInt();
+			QTableWidgetItem * sessionPlayerItem = new QTableWidgetItem(QString("%1/%2").arg(playersJoined).arg(playersTotal));
+			ui->sessionsTable->setItem(i, 1, sessionPlayerItem);
+
+			QTableWidgetItem * sessionProtectedItem = new QTableWidgetItem();
+			bool isPrivate = (args[tagPoint++] == "True");
+			sessionProtectedItem->setData(Qt::UserRole, isPrivate);
+			if(isPrivate)
+				sessionProtectedItem->setIcon(QIcon("icons:room-private.png"));
+			ui->sessionsTable->setItem(i, 2, sessionProtectedItem);
+		}
+		break;
+
+	case JOINED:
+	case KICKED:
+		protocolAssert(args.size() == 2);
+		joinStr = (command.command == JOINED ? "%1 joined to the session %2" : "%1 left session %2");
+
+		if(args[1] == username)
+		{
+			ui->buttonReady->setText("Ready");
+			ui->chat->clear(); //cleanup the chat
+			sysMessage(joinStr.arg("you", args[0]));
+			session = args[0];
+			ui->stackedWidget->setCurrentWidget(command.command == JOINED ? ui->roomPage : ui->sessionsPage);
+		}
+		else
+		{
+			sysMessage(joinStr.arg(args[1], args[0]));
+		}
+		break;
+
+	case MODS: {
+		protocolAssert(args.size() > 0);
+		amount = args[0].toInt();
+		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");
+		break;
+		}
+			
+	case CLIENTMODS: {
+		protocolAssert(args.size() > 1);
+		amount = args[1].toInt();
+		protocolAssert(amount * 2 == (args.size() - 2));
+
+		tagPoint = 2;
+		break;
+		}
+
+
+	case STATUS:
+		protocolAssert(args.size() > 0);
+		amount = args[0].toInt();
+		protocolAssert(amount * 2 == (args.size() - 1));
+
+		tagPoint = 1;
+		ui->playersList->clear();
+		for(int i = 0; i < amount; ++i, tagPoint += 2)
+		{
+			if(args[tagPoint + 1] == "True")
+				ui->playersList->addItem(new QListWidgetItem(QIcon("icons:mod-enabled.png"), args[tagPoint]));
+			else
+				ui->playersList->addItem(new QListWidgetItem(QIcon("icons:mod-disabled.png"), args[tagPoint]));
+			
+			if(args[tagPoint] == username)
+				if(args[tagPoint + 1] == "True")
+					ui->buttonReady->setText("Not ready");
+				else
+					ui->buttonReady->setText("Ready");
+		}
+		break;
+
+	case START: {
+		protocolAssert(args.size() == 1);
+		//actually start game
+		gameArgs << "--lobby";
+		gameArgs << "--lobby-address" << serverUrl;
+		gameArgs << "--lobby-port" << QString::number(serverPort);
+		gameArgs << "--uuid" << args[0];
+		startGame(gameArgs);		
+		break;
+		}
+
+	case HOST: {
+		protocolAssert(args.size() == 2);
+		gameArgs << "--lobby-host";
+		gameArgs << "--lobby-uuid" << args[0];
+		gameArgs << "--lobby-connections" << args[1];
+		break;
+		}
+
+	case CHAT: {
+		protocolAssert(args.size() > 1);
+		QString msg;
+		for(int i = 1; i < args.size(); ++i)
+			msg += args[i];
+		chatMessage(args[0], msg);
+		break;
+		}
+
+	default:
+		sysMessage("Unknown server command");
+	}
+
+	if(authentificationStatus == AuthStatus::AUTH_ERROR)
+	{
+		socketLobby.disconnectServer();
+	}
+	else
+	{
+		authentificationStatus = AuthStatus::AUTH_OK;
+		ui->newButton->setEnabled(true);
+	}
+}
+catch(const ProtocolError & e)
+{
+	chatMessage("System error", e.what(), true);
+}
+
+void Lobby::dispatchMessage(QString txt) try
+{
+	if(txt.isEmpty())
+		return;
+
+	QStringList parseTags = txt.split(":>>");
+	protocolAssert(parseTags.size() > 1 && parseTags[0].isEmpty() && !parseTags[1].isEmpty());
+
+	for(int c = 1; c < parseTags.size(); ++c)
+	{
+		QStringList parseArgs = parseTags[c].split(":");
+		protocolAssert(parseArgs.size() > 1);
+
+		auto ctype = ProtocolStrings.key(parseArgs[0]);
+		parseArgs.pop_front();
+		ServerCommand cmd(ctype, parseArgs);
+		serverCommand(cmd);
+	}
+}
+catch(const ProtocolError & e)
+{
+	chatMessage("System error", e.what(), true);
+}
+
+void Lobby::onDisconnected()
+{
+	authentificationStatus = AuthStatus::AUTH_NONE;
+	ui->stackedWidget->setCurrentWidget(ui->sessionsPage);
+	ui->connectButton->setChecked(false);
+	ui->serverEdit->setEnabled(true);
+	ui->userEdit->setEnabled(true);
+	ui->newButton->setEnabled(false);
+	ui->joinButton->setEnabled(false);
+	ui->sessionsTable->clear();
+}
+
+void Lobby::chatMessage(QString title, QString body, bool isSystem)
+{
+	QTextCharFormat fmtBody, fmtTitle;
+	fmtTitle.setFontWeight(QFont::Bold);
+	if(isSystem)
+		fmtBody.setFontWeight(QFont::DemiBold);
+	
+	QTextCursor curs(ui->chat->document());
+	curs.movePosition(QTextCursor::End);
+	curs.insertText(title + ": ", fmtTitle);
+	curs.insertText(body + "\n", fmtBody);
+	ui->chat->ensureCursorVisible();
+}
+
+void Lobby::sysMessage(QString body)
+{
+	chatMessage("System", body, true);
+}
+
+void Lobby::protocolAssert(bool expr)
+{
+	if(!expr)
+		throw ProtocolError("Protocol error");
+}
+
+void Lobby::on_messageEdit_returnPressed()
+{
+	socketLobby.send(ProtocolStrings[MESSAGE].arg(ui->messageEdit->text()));
+	ui->messageEdit->clear();
+}
+
+void Lobby::on_connectButton_toggled(bool checked)
+{
+	if(checked)
+	{
+		authentificationStatus = AuthStatus::AUTH_NONE;
+		username = ui->userEdit->text();
+		const int connectionTimeout = settings["launcher"]["connectionTimeout"].Integer();
+		
+		auto serverStrings = ui->serverEdit->text().split(":");
+		if(serverStrings.size() != 2)
+		{
+			QMessageBox::critical(this, "Connection error", "Server address must have the format URL:port");
+			return;
+		}
+		
+		serverUrl = serverStrings[0];
+		serverPort = serverStrings[1].toInt();
+
+		Settings node = settings.write["launcher"];
+		node["lobbyUrl"].String() = serverUrl.toStdString();
+		node["lobbyPort"].Integer() = serverPort;
+		node["lobbyUsername"].String() = username.toStdString();
+		
+		ui->serverEdit->setEnabled(false);
+		ui->userEdit->setEnabled(false);
+
+		sysMessage("Connecting to " + serverUrl + ":" + QString::number(serverPort));
+		//show text immediately
+		ui->chat->repaint();
+		qApp->processEvents();
+		
+		socketLobby.connectServer(serverUrl, serverPort, username, connectionTimeout);
+	}
+	else
+	{
+		ui->serverEdit->setEnabled(true);
+		ui->userEdit->setEnabled(true);
+		socketLobby.disconnectServer();
+	}
+}
+
+void Lobby::on_newButton_clicked()
+{
+	new LobbyRoomRequest(socketLobby, "", buildModsMap(), this);
+}
+
+void Lobby::on_joinButton_clicked()
+{
+	auto * item = ui->sessionsTable->item(ui->sessionsTable->currentRow(), 0);
+	if(item)
+	{
+		auto isPrivate = ui->sessionsTable->item(ui->sessionsTable->currentRow(), 2)->data(Qt::UserRole).toBool();
+		if(isPrivate)
+			new LobbyRoomRequest(socketLobby, item->text(), buildModsMap(), this);
+		else
+			socketLobby.requestJoinSession(item->text(), "", buildModsMap());
+	}
+}
+
+void Lobby::on_buttonLeave_clicked()
+{
+	socketLobby.requestLeaveSession(session);
+}
+
+void Lobby::on_buttonReady_clicked()
+{
+	if(ui->buttonReady->text() == "Ready")
+		ui->buttonReady->setText("Not ready");
+	else
+		ui->buttonReady->setText("Ready");
+	socketLobby.requestReadySession(session);
+}
+
+void Lobby::on_sessionsTable_itemSelectionChanged()
+{
+	auto selection = ui->sessionsTable->selectedItems();
+	ui->joinButton->setEnabled(!selection.empty());
+}
+
+void Lobby::on_playersList_currentRowChanged(int currentRow)
+{
+	ui->kickButton->setVisible(ui->playersList->currentItem()
+							   && currentRow > 0
+							   && ui->playersList->currentItem()->text() != username);
+}
+
+void Lobby::on_kickButton_clicked()
+{
+	if(ui->playersList->currentItem() && ui->playersList->currentItem()->text() != username)
+		socketLobby.send(ProtocolStrings[KICK].arg(ui->playersList->currentItem()->text()));
+}
+

+ 76 - 0
launcher/lobby/lobby_moc.h

@@ -0,0 +1,76 @@
+/*
+ * lobby_moc.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
+#include <QWidget>
+#include "lobby.h"
+
+namespace Ui {
+class Lobby;
+}
+
+class Lobby : public QWidget
+{
+	Q_OBJECT
+
+public:
+	explicit Lobby(QWidget *parent = nullptr);
+	~Lobby();
+
+private slots:
+	void on_messageEdit_returnPressed();
+
+	void chatMessage(QString title, QString body, bool isSystem = false);
+	void sysMessage(QString body);
+	void dispatchMessage(QString);
+	void serverCommand(const ServerCommand &);
+
+	void on_connectButton_toggled(bool checked);
+
+	void on_newButton_clicked();
+
+	void on_joinButton_clicked();
+
+	void on_buttonLeave_clicked();
+
+	void on_buttonReady_clicked();
+
+	void onDisconnected();
+
+	void on_sessionsTable_itemSelectionChanged();
+
+	void on_playersList_currentRowChanged(int currentRow);
+
+	void on_kickButton_clicked();
+
+private:
+	QString serverUrl;
+	int serverPort;
+	
+	Ui::Lobby *ui;
+	SocketLobby socketLobby;
+	QString hostSession;
+	QString session;
+	QString username;
+	QStringList gameArgs;
+
+	enum AuthStatus
+	{
+		AUTH_NONE, AUTH_OK, AUTH_ERROR
+	};
+
+	AuthStatus authentificationStatus = AUTH_NONE;
+
+private:
+	QMap<QString, QString> buildModsMap() const;
+	bool isModAvailable(const QString & modName, const QString & modVersion) const;
+
+
+	void protocolAssert(bool);
+};

+ 216 - 0
launcher/lobby/lobby_moc.ui

@@ -0,0 +1,216 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Lobby</class>
+ <widget class="QWidget" name="Lobby">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>652</width>
+    <height>329</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="0" column="5">
+    <widget class="QPushButton" name="connectButton">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="text">
+      <string>Connect</string>
+     </property>
+     <property name="checkable">
+      <bool>true</bool>
+     </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">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="text">
+      <string>Username</string>
+     </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>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="0">
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>Server</string>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="3" rowspan="2" colspan="3">
+    <widget class="QStackedWidget" name="stackedWidget">
+     <property name="currentIndex">
+      <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>
+         <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>
+      </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>
+        </widget>
+       </item>
+       <item row="5" column="0">
+        <widget class="QPushButton" name="buttonLeave">
+         <property name="text">
+          <string>Leave</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::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>
+         <property name="selectionBehavior">
+          <enum>QAbstractItemView::SelectRows</enum>
+         </property>
+        </widget>
+       </item>
+       <item row="1" column="1">
+        <widget class="QPushButton" name="kickButton">
+         <property name="text">
+          <string>Kick player</string>
+         </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>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 50 - 0
launcher/lobby/lobbyroomrequest_moc.cpp

@@ -0,0 +1,50 @@
+/*
+ * lobbyroomrequest_moc.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 "lobbyroomrequest_moc.h"
+#include "ui_lobbyroomrequest_moc.h"
+
+LobbyRoomRequest::LobbyRoomRequest(SocketLobby & socket, const QString & room, const QMap<QString, QString> & mods, QWidget *parent) :
+	QDialog(parent),
+	ui(new Ui::LobbyRoomRequest),
+	socketLobby(socket),
+	mods(mods)
+{
+	ui->setupUi(this);
+	ui->nameEdit->setText(room);
+	if(!room.isEmpty())
+	{
+		ui->nameEdit->setReadOnly(true);
+		ui->totalPlayers->setEnabled(false);
+	}
+
+	show();
+}
+
+LobbyRoomRequest::~LobbyRoomRequest()
+{
+	delete ui;
+}
+
+void LobbyRoomRequest::on_buttonBox_accepted()
+{
+	if(ui->nameEdit->isReadOnly())
+	{
+		socketLobby.requestJoinSession(ui->nameEdit->text(), ui->passwordEdit->text(), mods);
+	}
+	else
+	{
+		if(!ui->nameEdit->text().isEmpty())
+		{
+			int totalPlayers = ui->totalPlayers->currentIndex() + 2; //where 2 is a minimum amount of players
+			socketLobby.requestNewSession(ui->nameEdit->text(), totalPlayers, ui->passwordEdit->text(), mods);
+		}
+	}
+}
+

+ 37 - 0
launcher/lobby/lobbyroomrequest_moc.h

@@ -0,0 +1,37 @@
+/*
+ * lobbyroomrequest_moc.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
+ *
+ */
+#ifndef LOBBYROOMREQUEST_MOC_H
+#define LOBBYROOMREQUEST_MOC_H
+
+#include <QDialog>
+#include "lobby.h"
+
+namespace Ui {
+class LobbyRoomRequest;
+}
+
+class LobbyRoomRequest : public QDialog
+{
+	Q_OBJECT
+
+public:
+	explicit LobbyRoomRequest(SocketLobby & socket, const QString & room, const QMap<QString, QString> & mods, QWidget *parent = nullptr);
+	~LobbyRoomRequest();
+
+private slots:
+	void on_buttonBox_accepted();
+
+private:
+	Ui::LobbyRoomRequest *ui;
+	SocketLobby & socketLobby;
+	QMap<QString, QString> mods;
+};
+
+#endif // LOBBYROOMREQUEST_MOC_H

+ 148 - 0
launcher/lobby/lobbyroomrequest_moc.ui

@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>LobbyRoomRequest</class>
+ <widget class="QDialog" name="LobbyRoomRequest">
+  <property name="windowModality">
+   <enum>Qt::WindowModal</enum>
+  </property>
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>193</width>
+    <height>188</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Room settings</string>
+  </property>
+  <property name="locale">
+   <locale language="English" country="UnitedStates"/>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>false</bool>
+  </property>
+  <property name="modal">
+   <bool>true</bool>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="0" column="0" colspan="2">
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>Room name</string>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="0" colspan="2">
+    <widget class="QLineEdit" name="nameEdit"/>
+   </item>
+   <item row="2" column="0">
+    <widget class="QLabel" name="label_2">
+     <property name="text">
+      <string>Maximum players</string>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="1">
+    <widget class="QComboBox" name="totalPlayers">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <item>
+      <property name="text">
+       <string>2</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>3</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>4</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>5</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>6</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>7</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>8</string>
+      </property>
+     </item>
+    </widget>
+   </item>
+   <item row="3" column="0" colspan="2">
+    <widget class="QLabel" name="label_3">
+     <property name="text">
+      <string>Password (optional)</string>
+     </property>
+    </widget>
+   </item>
+   <item row="4" column="0" colspan="2">
+    <widget class="QLineEdit" name="passwordEdit"/>
+   </item>
+   <item row="5" column="0" colspan="2">
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>LobbyRoomRequest</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>LobbyRoomRequest</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>

+ 86 - 38
launcher/main.cpp

@@ -1,38 +1,86 @@
-/*
- * main.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 "main.h"
-#include "mainwindow_moc.h"
-
-#include <QApplication>
-
-// Conan workaround https://github.com/conan-io/conan-center-index/issues/13332
-#ifdef VCMI_IOS
-#if __has_include("QIOSIntegrationPlugin.h")
-#include "QIOSIntegrationPlugin.h"
-#endif
-#endif
-
-int main(int argc, char * argv[])
-{
-	int result;
-#ifdef VCMI_IOS
-	{
-#endif
-	QApplication vcmilauncher(argc, argv);
-	MainWindow mainWindow;
-	mainWindow.show();
-	result = vcmilauncher.exec();
-#ifdef VCMI_IOS
-	}
-	if (result == 0)
-		launchGame(argc, argv);
-#endif
-	return result;
-}
+/*
+ * main.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 "main.h"
+#include "mainwindow_moc.h"
+
+#include <QApplication>
+#include <QProcess>
+#include <QMessageBox>
+#include "../lib/VCMIDirs.h"
+
+// Conan workaround https://github.com/conan-io/conan-center-index/issues/13332
+#ifdef VCMI_IOS
+#if __has_include("QIOSIntegrationPlugin.h")
+#include "QIOSIntegrationPlugin.h"
+#endif
+int argcForClient;
+char ** argvForClient;
+#endif
+
+int main(int argc, char * argv[])
+{
+	int result;
+#ifdef VCMI_IOS
+	{
+#endif
+	QApplication vcmilauncher(argc, argv);
+	MainWindow mainWindow;
+	mainWindow.show();
+	result = vcmilauncher.exec();
+#ifdef VCMI_IOS
+	}
+	if (result == 0)
+		launchGame(argcForClient, argvForClient);
+#endif
+	return result;
+}
+
+void startGame(const QStringList & args)
+{
+	logGlobal->warn("Starting game with the arguments: %s", args.join(" ").toStdString());
+
+#ifdef Q_OS_IOS
+	argcForClient = args.size() + 1; //first argument is omitted
+    argvForClient = new char*[argcForClient];
+	argvForClient[0] = "vcmiclient";
+	for(int i = 1; i < argcForClient; ++i)
+	{
+        std::string s = args.at(i - 1).toStdString();
+        argvForClient[i] = new char[s.size() + 1];
+        strcpy(argvForClient[i], s.c_str());
+	}
+	qApp->quit();
+#else
+	startExecutable(pathToQString(VCMIDirs::get().clientPath()), args);
+#endif
+}
+
+#ifndef Q_OS_IOS
+void startExecutable(QString name, const QStringList & args)
+{
+	QProcess process;
+	
+	// Start the executable
+	if(process.startDetached(name, args))
+	{
+		qApp->quit();
+	}
+	else
+	{
+		QMessageBox::critical(qApp->activeWindow(),
+							  "Error starting executable",
+							  "Failed to start " + name + "\n"
+							  "Reason: " + process.errorString(),
+							  QMessageBox::Ok,
+							  QMessageBox::Ok);
+	}
+}
+#endif

+ 18 - 14
launcher/main.h

@@ -1,14 +1,18 @@
-/*
- * main.h, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-#pragma once
-
-#ifdef VCMI_IOS
-extern "C" void launchGame(int argc, char * argv[]);
-#endif
+/*
+ * main.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
+
+void startGame(const QStringList & args);
+
+#ifdef VCMI_IOS
+extern "C" void launchGame(int argc, char * argv[]);
+#else
+void startExecutable(QString name, const QStringList & args);
+#endif

+ 15 - 26
launcher/mainwindow_moc.cpp

@@ -11,7 +11,6 @@
 #include "mainwindow_moc.h"
 #include "ui_mainwindow_moc.h"
 
-#include <QProcess>
 #include <QDir>
 
 #include "../lib/CConfigHandler.h"
@@ -20,6 +19,7 @@
 #include "../lib/logging/CBasicLogConfigurator.h"
 
 #include "updatedialog_moc.h"
+#include "main.h"
 
 void MainWindow::load()
 {
@@ -85,9 +85,9 @@ MainWindow::MainWindow(QWidget * parent)
 	}
 	ui->tabListWidget->setCurrentIndex(0);
 
-	ui->settingsView->isExtraResolutionsModEnabled = ui->stackedWidgetPage2->isExtraResolutionsModEnabled();
+	ui->settingsView->isExtraResolutionsModEnabled = ui->modlistView->isExtraResolutionsModEnabled();
 	ui->settingsView->setDisplayList();
-	connect(ui->stackedWidgetPage2, &CModListView::extraResolutionsEnabledChanged,
+	connect(ui->modlistView, &CModListView::extraResolutionsEnabledChanged,
 		ui->settingsView, &CSettingsView::fillValidResolutions);
 
 	connect(ui->tabSelectList, &QListWidget::currentRowChanged, [this](int i) {
@@ -97,6 +97,11 @@ MainWindow::MainWindow(QWidget * parent)
 #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);
@@ -114,31 +119,15 @@ MainWindow::~MainWindow()
 
 void MainWindow::on_startGameButton_clicked()
 {
-#ifdef Q_OS_IOS
-	qApp->quit();
-#else
-	startExecutable(pathToQString(VCMIDirs::get().clientPath()));
-#endif
+	startGame({});
 }
 
-#ifndef Q_OS_IOS
-void MainWindow::startExecutable(QString name)
+void MainWindow::on_tabSelectList_currentRowChanged(int currentRow)
 {
-	QProcess process;
+	ui->startGameButton->setEnabled(currentRow != TabRows::LOBBY);
+}
 
-	// Start the executable
-	if(process.startDetached(name, {}))
-	{
-		close(); // exit launcher
-	}
-	else
-	{
-		QMessageBox::critical(this,
-		                      "Error starting executable",
-		                      "Failed to start " + name + "\n"
-		                      "Reason: " + process.errorString(),
-		                      QMessageBox::Ok,
-		                      QMessageBox::Ok);
-	}
+const CModList & MainWindow::getModList() const
+{
+	return ui->modlistView->getModList();
 }
-#endif

+ 14 - 4
launcher/mainwindow_moc.h

@@ -19,6 +19,8 @@ const QString appName = "VCMI Launcher";
 }
 
 class QTableWidgetItem;
+class CModList;
+
 
 class MainWindow : public QMainWindow
 {
@@ -27,14 +29,22 @@ class MainWindow : public QMainWindow
 private:
 	Ui::MainWindow * ui;
 	void load();
-#ifndef Q_OS_IOS
-	void startExecutable(QString name);
-#endif
+	
+	enum TabRows
+	{
+		MODS = 0, SETTINGS = 1, LOBBY = 2
+	};
 
 public:
 	explicit MainWindow(QWidget * parent = 0);
 	~MainWindow();
 
-private slots:
+	const CModList & getModList() const;
+
+	
+public slots:
 	void on_startGameButton_clicked();
+	
+private slots:
+	void on_tabSelectList_currentRowChanged(int currentRow);
 };

+ 24 - 3
launcher/mainwindow_moc.ui

@@ -51,6 +51,9 @@
       <property name="horizontalScrollBarPolicy">
        <enum>Qt::ScrollBarAlwaysOff</enum>
       </property>
+      <property name="sizeAdjustPolicy">
+       <enum>QAbstractScrollArea::AdjustToContents</enum>
+      </property>
       <property name="editTriggers">
        <set>QAbstractItemView::NoEditTriggers</set>
       </property>
@@ -75,6 +78,9 @@
       <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>
@@ -91,7 +97,7 @@
        <bool>true</bool>
       </property>
       <property name="wordWrap">
-       <bool>true</bool>
+       <bool>false</bool>
       </property>
       <item>
        <property name="text">
@@ -111,13 +117,21 @@
          <normaloff>icons:menu-settings.png</normaloff>icons:menu-settings.png</iconset>
        </property>
       </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>
+      </item>
      </widget>
     </item>
     <item row="2" column="0">
      <widget class="QLabel" name="startGameTitle">
       <property name="font">
        <font>
-        <weight>75</weight>
         <bold>true</bold>
        </font>
       </property>
@@ -143,8 +157,9 @@
       <property name="currentIndex">
        <number>0</number>
       </property>
-      <widget class="CModListView" name="stackedWidgetPage2"/>
+      <widget class="CModListView" name="modlistView"/>
       <widget class="CSettingsView" name="settingsView"/>
+      <widget class="Lobby" name="lobbyView"/>
      </widget>
     </item>
     <item row="1" column="0">
@@ -202,6 +217,12 @@
    <header>settingsView/csettingsview_moc.h</header>
    <container>1</container>
   </customwidget>
+  <customwidget>
+   <class>Lobby</class>
+   <extends>QWidget</extends>
+   <header>lobby/lobby_moc.h</header>
+   <container>1</container>
+  </customwidget>
  </customwidgets>
  <tabstops>
   <tabstop>tabSelectList</tabstop>

+ 17 - 0
launcher/modManager/cmodlistview_moc.cpp

@@ -102,6 +102,17 @@ CModListView::CModListView(QWidget * parent)
 	{
 		manager->resetRepositories();
 	}
+	
+#ifdef Q_OS_IOS
+	for(auto * scrollWidget : {
+		(QAbstractItemView*)ui->allModsView,
+		(QAbstractItemView*)ui->screenshotsList})
+	{
+		QScroller::grabGesture(scrollWidget, QScroller::LeftMouseButtonGesture);
+		scrollWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
+		scrollWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+	}
+#endif
 }
 
 void CModListView::loadRepositories()
@@ -825,3 +836,9 @@ void CModListView::on_showInfoButton_clicked()
 {
 	showModInfo();
 }
+
+const CModList & CModListView::getModList() const
+{
+	assert(modModel);
+	return *modModel;
+}

+ 3 - 0
launcher/modManager/cmodlistview_moc.h

@@ -18,6 +18,7 @@ class CModListView;
 }
 
 class CModManager;
+class CModList;
 class CModListModel;
 class CModFilterModel;
 class CDownloadManager;
@@ -80,6 +81,8 @@ public:
 	void selectMod(const QModelIndex & index);
 	bool isExtraResolutionsModEnabled() const;
 
+	const CModList & getModList() const;
+
 private slots:
 	void dataChanged(const QModelIndex & topleft, const QModelIndex & bottomRight);
 	void modSelected(const QModelIndex & current, const QModelIndex & previous);

+ 57 - 30
lib/CArtHandler.cpp

@@ -72,8 +72,8 @@ const std::string & CArtifact::getJsonKey() const
 
 void CArtifact::registerIcons(const IconRegistar & cb) const
 {
-	cb(getIconIndex(), "ARTIFACT", image);
-	cb(getIconIndex(), "ARTIFACTLARGE", large);
+	cb(getIconIndex(), 0, "ARTIFACT", image);
+	cb(getIconIndex(), 0, "ARTIFACTLARGE", large);
 }
 
 ArtifactID CArtifact::getId() const
@@ -851,7 +851,7 @@ void CArtifactInstance::putAt(ArtifactLocation al)
 	assert(canBePutAt(al));
 
 	al.getHolderArtSet()->setNewArtSlot(al.slot, this, false);
-	if(al.slot < GameConstants::BACKPACK_START)
+	if(!ArtifactUtils::isSlotBackpack(al.slot))
 		al.getHolderNode()->attachTo(*this);
 }
 
@@ -859,7 +859,7 @@ void CArtifactInstance::removeFrom(ArtifactLocation al)
 {
 	assert(al.getHolderArtSet()->getArt(al.slot) == this);
 	al.getHolderArtSet()->eraseArtSlot(al.slot);
-	if(al.slot < GameConstants::BACKPACK_START)
+	if(!ArtifactUtils::isSlotBackpack(al.slot))
 		al.getHolderNode()->detachFrom(*this);
 
 	//TODO delete me?
@@ -938,12 +938,12 @@ CArtifactInstance * CArtifactInstance::createNewArtifactInstance(CArtifact *Art)
 	}
 }
 
-CArtifactInstance * CArtifactInstance::createNewArtifactInstance(int aid)
+CArtifactInstance * CArtifactInstance::createNewArtifactInstance(ArtifactID aid)
 {
 	return createNewArtifactInstance(VLC->arth->objects[aid]);
 }
 
-CArtifactInstance * CArtifactInstance::createArtifact(CMap * map, int aid, int spellID)
+CArtifactInstance * CArtifactInstance::createArtifact(CMap * map, ArtifactID aid, int spellID)
 {
 	CArtifactInstance * a = nullptr;
 	if(aid >= 0)
@@ -1003,7 +1003,7 @@ bool CCombinedArtifactInstance::canBePutAt(const CArtifactSet *artSet, ArtifactP
 	bool canMainArtifactBePlaced = CArtifactInstance::canBePutAt(artSet, slot, assumeDestRemoved);
 	if(!canMainArtifactBePlaced)
 		return false; //no is no...
-	if(slot >= GameConstants::BACKPACK_START)
+	if(ArtifactUtils::isSlotBackpack(slot))
 		return true; //we can always remove combined art to the backapck
 
 
@@ -1019,11 +1019,12 @@ bool CCombinedArtifactInstance::canBePutAt(const CArtifactSet *artSet, ArtifactP
 	}
 
 	//we iterate over all active slots and check if constituents fits them
-	for (int i = 0; i < GameConstants::BACKPACK_START; i++)
+	for(const auto pos : ArtifactUtils::constituentWornSlots())
 	{
 		for(auto art = constituentsToBePlaced.begin(); art != constituentsToBePlaced.end(); art++)
 		{
-			if(art->art->canBePutAt(artSet, ArtifactPosition(i), i == slot)) // i == al.slot because we can remove already worn artifact only from that slot  that is our main destination
+			// pos == slot because we can remove already worn artifact only from that slot. That is our main destination
+			if(art->art->canBePutAt(artSet, pos, pos == slot))
 			{
 				constituentsToBePlaced.erase(art);
 				break;
@@ -1069,7 +1070,7 @@ void CCombinedArtifactInstance::addAsConstituent(CArtifactInstance *art, Artifac
 
 void CCombinedArtifactInstance::putAt(ArtifactLocation al)
 {
-	if(al.slot >= GameConstants::BACKPACK_START)
+	if(ArtifactUtils::isSlotBackpack(al.slot))
 	{
 		CArtifactInstance::putAt(al);
 		for(ConstituentInfo &ci : constituentsInfo)
@@ -1094,7 +1095,7 @@ void CCombinedArtifactInstance::putAt(ArtifactLocation al)
 				else
 					ci.slot = pos = ci.art->firstAvailableSlot(al.getHolderArtSet());
 
-				assert(pos < GameConstants::BACKPACK_START);
+				assert(!ArtifactUtils::isSlotBackpack(pos));
 				al.getHolderArtSet()->setNewArtSlot(pos, ci.art, true); //sets as lock
 			}
 			else
@@ -1107,7 +1108,7 @@ void CCombinedArtifactInstance::putAt(ArtifactLocation al)
 
 void CCombinedArtifactInstance::removeFrom(ArtifactLocation al)
 {
-	if(al.slot >= GameConstants::BACKPACK_START)
+	if(ArtifactUtils::isSlotBackpack(al.slot))
 	{
 		CArtifactInstance::removeFrom(al);
 	}
@@ -1199,19 +1200,19 @@ CArtifactInstance* CArtifactSet::getArt(ArtifactPosition pos, bool excludeLocked
 	return const_cast<CArtifactInstance*>((const_cast<const CArtifactSet*>(this))->getArt(pos, excludeLocked));
 }
 
-ArtifactPosition CArtifactSet::getArtPos(int aid, bool onlyWorn, bool allowLocked) const
+ArtifactPosition CArtifactSet::getArtPos(ArtifactID aid, bool onlyWorn, bool allowLocked) const
 {
 	const auto result = getAllArtPositions(aid, onlyWorn, allowLocked, false);
 	return result.empty() ? ArtifactPosition{ArtifactPosition::PRE_FIRST} : result[0];
 }
 
-ArtifactPosition CArtifactSet::getArtBackpackPos(int aid) const
+ArtifactPosition CArtifactSet::getArtBackpackPos(ArtifactID aid) const
 {
 	const auto result = getBackpackArtPositions(aid);
 	return result.empty() ? ArtifactPosition{ArtifactPosition::PRE_FIRST} : result[0];
 }
 
-std::vector<ArtifactPosition> CArtifactSet::getAllArtPositions(int aid, bool onlyWorn, bool allowLocked, bool getAll) const
+std::vector<ArtifactPosition> CArtifactSet::getAllArtPositions(ArtifactID aid, bool onlyWorn, bool allowLocked, bool getAll) const
 {
 	std::vector<ArtifactPosition> result;
 	for(auto & slotInfo : artifactsWorn)
@@ -1228,7 +1229,7 @@ std::vector<ArtifactPosition> CArtifactSet::getAllArtPositions(int aid, bool onl
 	return result;
 }
 
-std::vector<ArtifactPosition> CArtifactSet::getBackpackArtPositions(int aid) const
+std::vector<ArtifactPosition> CArtifactSet::getBackpackArtPositions(ArtifactID aid) const
 {
 	std::vector<ArtifactPosition> result;
 
@@ -1270,7 +1271,7 @@ const CArtifactInstance * CArtifactSet::getArtByInstanceId( ArtifactInstanceID a
 }
 
 bool CArtifactSet::hasArt(
-	ui32 aid,
+	ArtifactID aid,
 	bool onlyWorn,
     bool searchBackpackAssemblies,
 	bool allowLocked) const
@@ -1278,12 +1279,12 @@ bool CArtifactSet::hasArt(
 	return getArtPosCount(aid, onlyWorn, searchBackpackAssemblies, allowLocked) > 0;
 }
 
-bool CArtifactSet::hasArtBackpack(ui32 aid) const
+bool CArtifactSet::hasArtBackpack(ArtifactID aid) const
 {
 	return getBackpackArtPositions(aid).size() > 0;
 }
 
-unsigned CArtifactSet::getArtPosCount(int aid, bool onlyWorn, bool searchBackpackAssemblies, bool allowLocked) const
+unsigned CArtifactSet::getArtPosCount(ArtifactID aid, bool onlyWorn, bool searchBackpackAssemblies, bool allowLocked) const
 {
 	const auto allPositions = getAllArtPositions(aid, onlyWorn, allowLocked, true);
 	if(!allPositions.empty())
@@ -1296,7 +1297,7 @@ unsigned CArtifactSet::getArtPosCount(int aid, bool onlyWorn, bool searchBackpac
 }
 
 std::pair<const CCombinedArtifactInstance *, const CArtifactInstance *>
-CArtifactSet::searchForConstituent(int aid) const
+CArtifactSet::searchForConstituent(ArtifactID aid) const
 {
 	for(auto & slot : artifactsInBackpack)
 	{
@@ -1316,12 +1317,12 @@ CArtifactSet::searchForConstituent(int aid) const
 	return {nullptr, nullptr};
 }
 
-const CArtifactInstance *CArtifactSet::getHiddenArt(int aid) const
+const CArtifactInstance *CArtifactSet::getHiddenArt(ArtifactID aid) const
 {
 	return searchForConstituent(aid).second;
 }
 
-const CCombinedArtifactInstance *CArtifactSet::getAssemblyByConstituent(int aid) const
+const CCombinedArtifactInstance *CArtifactSet::getAssemblyByConstituent(ArtifactID aid) const
 {
 	return searchForConstituent(aid).first;
 }
@@ -1353,7 +1354,7 @@ bool CArtifactSet::isPositionFree(ArtifactPosition pos, bool onlyLockCheck) cons
 ArtSlotInfo & CArtifactSet::retrieveNewArtSlot(ArtifactPosition slot)
 {
 	assert(!vstd::contains(artifactsWorn, slot));
-	ArtSlotInfo &ret = slot < GameConstants::BACKPACK_START
+	ArtSlotInfo &ret = !ArtifactUtils::isSlotBackpack(slot)
 		? artifactsWorn[slot]
 		: *artifactsInBackpack.insert(artifactsInBackpack.begin() + (slot - GameConstants::BACKPACK_START), ArtSlotInfo());
 
@@ -1369,14 +1370,15 @@ void CArtifactSet::setNewArtSlot(ArtifactPosition slot, CArtifactInstance *art,
 
 void CArtifactSet::eraseArtSlot(ArtifactPosition slot)
 {
-	if(slot < GameConstants::BACKPACK_START)
+	if(ArtifactUtils::isSlotBackpack(slot))
 	{
-		artifactsWorn.erase(slot);
+		assert(artifactsInBackpack.begin() + slot < artifactsInBackpack.end());
+		slot = ArtifactPosition(slot - GameConstants::BACKPACK_START);
+		artifactsInBackpack.erase(artifactsInBackpack.begin() + slot);
 	}
 	else
 	{
-		slot = ArtifactPosition(slot - GameConstants::BACKPACK_START);
-		artifactsInBackpack.erase(artifactsInBackpack.begin() + slot);
+		artifactsWorn.erase(slot);
 	}
 }
 
@@ -1543,16 +1545,41 @@ DLL_LINKAGE ArtifactPosition ArtifactUtils::getArtifactDstPosition(	const CArtif
 	return ArtifactPosition(GameConstants::BACKPACK_START);
 }
 
-DLL_LINKAGE std::vector<ArtifactPosition> ArtifactUtils::unmovablePositions()
+DLL_LINKAGE const std::vector<ArtifactPosition::EArtifactPosition> & ArtifactUtils::unmovableSlots()
 {
-	return { ArtifactPosition::SPELLBOOK, ArtifactPosition::MACH4 };
+	return
+	{
+		ArtifactPosition::SPELLBOOK,
+		ArtifactPosition::MACH4
+	};
+}
+
+DLL_LINKAGE const std::vector<ArtifactPosition::EArtifactPosition> & ArtifactUtils::constituentWornSlots()
+{
+	return
+	{
+		ArtifactPosition::HEAD,
+		ArtifactPosition::SHOULDERS,
+		ArtifactPosition::NECK,
+		ArtifactPosition::RIGHT_HAND,
+		ArtifactPosition::LEFT_HAND,
+		ArtifactPosition::TORSO,
+		ArtifactPosition::RIGHT_RING,
+		ArtifactPosition::LEFT_RING,
+		ArtifactPosition::FEET,
+		ArtifactPosition::MISC1,
+		ArtifactPosition::MISC2,
+		ArtifactPosition::MISC3,
+		ArtifactPosition::MISC4,
+		ArtifactPosition::MISC5,
+	};
 }
 
 DLL_LINKAGE bool ArtifactUtils::isArtRemovable(const std::pair<ArtifactPosition, ArtSlotInfo> & slot)
 {
 	return slot.second.artifact
 		&& !slot.second.locked
-		&& !vstd::contains(unmovablePositions(), slot.first);
+		&& !vstd::contains(unmovableSlots(), slot.first);
 }
 
 DLL_LINKAGE bool ArtifactUtils::checkSpellbookIsNeeded(const CGHeroInstance * heroPtr, ArtifactID artID, ArtifactPosition slot)

+ 15 - 13
lib/CArtHandler.h

@@ -176,7 +176,7 @@ public:
 
 	static CArtifactInstance *createScroll(SpellID sid);
 	static CArtifactInstance *createNewArtifactInstance(CArtifact *Art);
-	static CArtifactInstance *createNewArtifactInstance(int aid);
+	static CArtifactInstance *createNewArtifactInstance(ArtifactID aid);
 
 	/**
 	 * Creates an artifact instance.
@@ -185,7 +185,7 @@ public:
 	 * @param spellID optional. the id of a spell if a spell scroll object should be created
 	 * @return the created artifact instance
 	 */
-	static CArtifactInstance * createArtifact(CMap * map, int aid, int spellID = -1);
+	static CArtifactInstance * createArtifact(CMap * map, ArtifactID aid, int spellID = -1);
 };
 
 class DLL_LINKAGE CCombinedArtifactInstance : public CArtifactInstance
@@ -327,20 +327,20 @@ public:
 	CArtifactInstance* getArt(ArtifactPosition pos, bool excludeLocked = true); //nullptr - no artifact
 	/// Looks for equipped artifact with given ID and returns its slot ID or -1 if none
 	/// (if more than one such artifact lower ID is returned)
-	ArtifactPosition getArtPos(int aid, bool onlyWorn = true, bool allowLocked = true) const;
+	ArtifactPosition getArtPos(ArtifactID aid, bool onlyWorn = true, bool allowLocked = true) const;
 	ArtifactPosition getArtPos(const CArtifactInstance *art) const;
-	ArtifactPosition getArtBackpackPos(int aid) const;
-	std::vector<ArtifactPosition> getAllArtPositions(int aid, bool onlyWorn, bool allowLocked, bool getAll) const;
-	std::vector<ArtifactPosition> getBackpackArtPositions(int aid) const;
+	ArtifactPosition getArtBackpackPos(ArtifactID aid) const;
+	std::vector<ArtifactPosition> getAllArtPositions(ArtifactID aid, bool onlyWorn, bool allowLocked, bool getAll) const;
+	std::vector<ArtifactPosition> getBackpackArtPositions(ArtifactID aid) const;
 	const CArtifactInstance *getArtByInstanceId(ArtifactInstanceID artInstId) const;
 	/// Search for constituents of assemblies in backpack which do not have an ArtifactPosition
-	const CArtifactInstance *getHiddenArt(int aid) const;
-	const CCombinedArtifactInstance *getAssemblyByConstituent(int aid) const;
+	const CArtifactInstance *getHiddenArt(ArtifactID aid) const;
+	const CCombinedArtifactInstance *getAssemblyByConstituent(ArtifactID aid) const;
 	/// Checks if hero possess artifact of given id (either in backack or worn)
-	bool hasArt(ui32 aid, bool onlyWorn = false, bool searchBackpackAssemblies = false, bool allowLocked = true) const;
-	bool hasArtBackpack(ui32 aid) const;
+	bool hasArt(ArtifactID aid, bool onlyWorn = false, bool searchBackpackAssemblies = false, bool allowLocked = true) const;
+	bool hasArtBackpack(ArtifactID aid) const;
 	bool isPositionFree(ArtifactPosition pos, bool onlyLockCheck = false) const;
-	unsigned getArtPosCount(int aid, bool onlyWorn = true, bool searchBackpackAssemblies = true, bool allowLocked = true) const;
+	unsigned getArtPosCount(ArtifactID aid, bool onlyWorn = true, bool searchBackpackAssemblies = true, bool allowLocked = true) const;
 
 	virtual ArtBearer::ArtBearer bearerType() const = 0;
 	virtual void putArtifact(ArtifactPosition pos, CArtifactInstance * art) = 0;
@@ -358,7 +358,7 @@ public:
 protected:
 
 
-	std::pair<const CCombinedArtifactInstance *, const CArtifactInstance *> searchForConstituent(int aid) const;
+	std::pair<const CCombinedArtifactInstance *, const CArtifactInstance *> searchForConstituent(ArtifactID aid) const;
 private:
 	void serializeJsonHero(JsonSerializeFormat & handler, CMap * map);
 	void serializeJsonCreature(JsonSerializeFormat & handler, CMap * map);
@@ -386,7 +386,9 @@ namespace ArtifactUtils
 	DLL_LINKAGE ArtifactPosition getArtifactDstPosition(	const CArtifactInstance * artifact, 
 								const CArtifactSet * target,
 								ArtBearer::ArtBearer bearer);
-	DLL_LINKAGE std::vector<ArtifactPosition> unmovablePositions(); // TODO: Make this constexpr when the toolset is upgraded
+	// TODO: Make this constexpr when the toolset is upgraded
+	DLL_LINKAGE const std::vector<ArtifactPosition::EArtifactPosition> & unmovableSlots();
+	DLL_LINKAGE const std::vector<ArtifactPosition::EArtifactPosition> & constituentWornSlots();
 	DLL_LINKAGE bool isArtRemovable(const std::pair<ArtifactPosition, ArtSlotInfo> & slot);
 	DLL_LINKAGE bool checkSpellbookIsNeeded(const CGHeroInstance * heroPtr, ArtifactID artID, ArtifactPosition slot);
 	DLL_LINKAGE bool isSlotBackpack(ArtifactPosition slot);

+ 2 - 2
lib/CCreatureHandler.cpp

@@ -46,8 +46,8 @@ const std::string & CCreature::getJsonKey() const
 
 void CCreature::registerIcons(const IconRegistar & cb) const
 {
-	cb(getIconIndex(), "CPRSMALL", smallIconName);
-	cb(getIconIndex(), "TWCRPORT", largeIconName);
+	cb(getIconIndex(), 0, "CPRSMALL", smallIconName);
+	cb(getIconIndex(), 0, "TWCRPORT", largeIconName);
 }
 
 CreatureID CCreature::getId() const

+ 3 - 3
lib/CGameState.cpp

@@ -2256,7 +2256,7 @@ bool CGameState::checkForVictory(PlayerColor player, const EventCondition & cond
 		case EventCondition::HAVE_ARTIFACT: //check if any hero has winning artifact
 		{
 			for(auto & elem : p->heroes)
-				if(elem->hasArt(condition.objectType))
+				if(elem->hasArt(ArtifactID(condition.objectType)))
 					return true;
 			return false;
 		}
@@ -2342,8 +2342,8 @@ bool CGameState::checkForVictory(PlayerColor player, const EventCondition & cond
 		case EventCondition::TRANSPORT:
 		{
 			const CGTownInstance *t = static_cast<const CGTownInstance *>(condition.object);
-			if((t->visitingHero && t->visitingHero->hasArt(condition.objectType))
-				|| (t->garrisonHero && t->garrisonHero->hasArt(condition.objectType)))
+			if((t->visitingHero && t->visitingHero->hasArt(ArtifactID(condition.objectType)))
+				|| (t->garrisonHero && t->garrisonHero->hasArt(ArtifactID(condition.objectType))))
 			{
 				return true;
 			}

+ 4 - 4
lib/CHeroHandler.cpp

@@ -59,10 +59,10 @@ HeroTypeID CHero::getId() const
 
 void CHero::registerIcons(const IconRegistar & cb) const
 {
-	cb(getIconIndex(), "UN32", iconSpecSmall);
-	cb(getIconIndex(), "UN44", iconSpecLarge);
-	cb(getIconIndex(), "PORTRAITSLARGE", portraitLarge);
-	cb(getIconIndex(), "PORTRAITSSMALL", portraitSmall);
+	cb(getIconIndex(), 0, "UN32", iconSpecSmall);
+	cb(getIconIndex(), 0, "UN44", iconSpecLarge);
+	cb(getIconIndex(), 0, "PORTRAITSLARGE", portraitLarge);
+	cb(getIconIndex(), 0, "PORTRAITSSMALL", portraitSmall);
 }
 
 void CHero::updateFrom(const JsonNode & data)

+ 3 - 3
lib/CSkillHandler.cpp

@@ -70,9 +70,9 @@ void CSkill::registerIcons(const IconRegistar & cb) const
 	{
 		int frame = 2 + level + 3 * id;
 		const LevelInfo & skillAtLevel = at(level);
-		cb(frame, "SECSK32", skillAtLevel.iconSmall);
-		cb(frame, "SECSKILL", skillAtLevel.iconMedium);
-		cb(frame, "SECSK82", skillAtLevel.iconLarge);
+		cb(frame, 0, "SECSK32", skillAtLevel.iconSmall);
+		cb(frame, 0, "SECSKILL", skillAtLevel.iconMedium);
+		cb(frame, 0, "SECSK82", skillAtLevel.iconLarge);
 	}
 }
 

+ 16 - 9
lib/CTownHandler.cpp

@@ -126,15 +126,19 @@ void CFaction::registerIcons(const IconRegistar & cb) const
 	if(town)
 	{
 		auto & info = town->clientInfo;
-		cb(info.icons[0][0], "ITPT", info.iconLarge[0][0]);
-		cb(info.icons[0][1], "ITPT", info.iconLarge[0][1]);
-		cb(info.icons[1][0], "ITPT", info.iconLarge[1][0]);
-		cb(info.icons[1][1], "ITPT", info.iconLarge[1][1]);
-
-		cb(info.icons[0][0] + 2, "ITPA", info.iconSmall[0][0]);
-		cb(info.icons[0][1] + 2, "ITPA", info.iconSmall[0][1]);
-		cb(info.icons[1][0] + 2, "ITPA", info.iconSmall[1][0]);
-		cb(info.icons[1][1] + 2, "ITPA", info.iconSmall[1][1]);
+		cb(info.icons[0][0], 0, "ITPT", info.iconLarge[0][0]);
+		cb(info.icons[0][1], 0, "ITPT", info.iconLarge[0][1]);
+		cb(info.icons[1][0], 0, "ITPT", info.iconLarge[1][0]);
+		cb(info.icons[1][1], 0, "ITPT", info.iconLarge[1][1]);
+
+		cb(info.icons[0][0] + 2, 0, "ITPA", info.iconSmall[0][0]);
+		cb(info.icons[0][1] + 2, 0, "ITPA", info.iconSmall[0][1]);
+		cb(info.icons[1][0] + 2, 0, "ITPA", info.iconSmall[1][0]);
+		cb(info.icons[1][1] + 2, 0, "ITPA", info.iconSmall[1][1]);
+
+		cb(index, 1, "CPRSMALL", info.towerIconSmall);
+		cb(index, 1, "TWCRPORT", info.towerIconLarge);
+
 	}
 }
 
@@ -755,6 +759,9 @@ CTown::ClientInfo::Point JsonToPoint(const JsonNode & node)
 void CTownHandler::loadSiegeScreen(CTown &town, const JsonNode & source)
 {
 	town.clientInfo.siegePrefix = source["imagePrefix"].String();
+	town.clientInfo.towerIconSmall = source["towerIconSmall"].String();
+	town.clientInfo.towerIconLarge = source["towerIconLarge"].String();
+
 	VLC->modh->identifiers.requestIdentifier("creature", source["shooter"], [&town](si32 creature)
 	{
 		auto crId = CreatureID(creature);

+ 4 - 0
lib/CTownHandler.h

@@ -303,6 +303,8 @@ public:
 		std::string siegePrefix;
 		std::vector<Point> siegePositions;
 		CreatureID siegeShooter; // shooter creature ID
+		std::string towerIconSmall;
+		std::string towerIconLarge;
 
 		template <typename Handler> void serialize(Handler &h, const int version)
 		{
@@ -321,6 +323,8 @@ public:
 			h & siegePrefix;
 			h & siegePositions;
 			h & siegeShooter;
+			h & towerIconSmall;
+			h & towerIconLarge;
 		}
 	} clientInfo;
 

+ 8 - 0
lib/GameConstants.h

@@ -1054,6 +1054,14 @@ public:
 	ID_LIKE_CLASS_COMMON(ArtifactID, EArtifactID)
 
 	EArtifactID num;
+
+	struct hash
+	{
+		size_t operator()(const ArtifactID & aid) const
+		{
+			return std::hash<int>()(aid.num);
+		}
+	};
 };
 
 ID_LIKE_OPERATORS(ArtifactID, ArtifactID::EArtifactID)

+ 38 - 35
lib/VCMIDirs.cpp

@@ -23,6 +23,8 @@ bfs::path IVCMIDirs::userLogsPath() const { return userCachePath(); }
 
 bfs::path IVCMIDirs::userSavePath() const { return userDataPath() / "Saves"; }
 
+bfs::path IVCMIDirs::userExtractedPath() const { return userCachePath() / "extracted"; }
+
 bfs::path IVCMIDirs::fullLibraryPath(const std::string &desiredFolder, const std::string &baseLibName) const
 {
 	return libraryPath() / desiredFolder / libraryName(baseLibName);
@@ -36,15 +38,16 @@ std::string IVCMIDirs::genHelpString() const
 	const auto gdStringA = boost::algorithm::join(tempVec, ":");
 
 	return
-		"  game data:   " + gdStringA + "\n"
-		"  libraries:   " + libraryPath().string() + "\n"
-		"  server:      " + serverPath().string() + "\n"
+		"  game data:		" + gdStringA + "\n"
+		"  libraries:		" + libraryPath().string() + "\n"
+		"  server:			" + serverPath().string() + "\n"
 		"\n"
-		"  user data:   " + userDataPath().string() + "\n"
-		"  user cache:  " + userCachePath().string() + "\n"
-		"  user config: " + userConfigPath().string() + "\n"
-		"  user logs:   " + userLogsPath().string() + "\n"
-		"  user saves:  " + userSavePath().string() + "\n"; // Should end without new-line?
+		"  user data:		" + userDataPath().string() + "\n"
+		"  user cache:		" + userCachePath().string() + "\n"
+		"  user config:		" + userConfigPath().string() + "\n"
+		"  user logs:		" + userLogsPath().string() + "\n"
+		"  user saves:		" + userSavePath().string() + "\n";
+		"  user extracted:	" + userExtractedPath().string() + "\n"; // Should end without new-line?
 }
 
 void IVCMIDirs::init()
@@ -148,24 +151,24 @@ bool StartBatchCopyDataProgram(
 class VCMIDirsWIN32 final : public IVCMIDirs
 {
 	public:
-		boost::filesystem::path userDataPath() const override;
-		boost::filesystem::path userCachePath() const override;
-		boost::filesystem::path userConfigPath() const override;
+		bfs::path userDataPath() const override;
+		bfs::path userCachePath() const override;
+		bfs::path userConfigPath() const override;
 
-		std::vector<boost::filesystem::path> dataPaths() const override;
+		std::vector<bfs::path> dataPaths() const override;
 
-		boost::filesystem::path clientPath() const override;
-		boost::filesystem::path serverPath() const override;
+		bfs::path clientPath() const override;
+		bfs::path serverPath() const override;
 
-		boost::filesystem::path libraryPath() const override;
-		boost::filesystem::path binaryPath() const override;
+		bfs::path libraryPath() const override;
+		bfs::path binaryPath() const override;
 
 		std::string libraryName(const std::string& basename) const override;
 
 		void init() override;
 	protected:
-		boost::filesystem::path oldUserDataPath() const;
-		boost::filesystem::path oldUserSavePath() const;
+		bfs::path oldUserDataPath() const;
+		bfs::path oldUserSavePath() const;
 };
 
 void VCMIDirsWIN32::init()
@@ -355,8 +358,8 @@ std::string VCMIDirsWIN32::libraryName(const std::string& basename) const { retu
 class IVCMIDirsUNIX : public IVCMIDirs
 {
 	public:
-		boost::filesystem::path clientPath() const override;
-		boost::filesystem::path serverPath() const override;
+		bfs::path clientPath() const override;
+		bfs::path serverPath() const override;
 
 		virtual bool developmentMode() const;
 };
@@ -427,14 +430,14 @@ bfs::path VCMIDirsIOS::binaryPath() const { return {iOS_utils::bundlePath()}; }
 class VCMIDirsOSX final : public VCMIDirsApple
 {
 public:
-	boost::filesystem::path userDataPath() const override;
-	boost::filesystem::path userCachePath() const override;
-	boost::filesystem::path userLogsPath() const override;
+	bfs::path userDataPath() const override;
+	bfs::path userCachePath() const override;
+	bfs::path userLogsPath() const override;
 
-	std::vector<boost::filesystem::path> dataPaths() const override;
+	std::vector<bfs::path> dataPaths() const override;
 
-	boost::filesystem::path libraryPath() const override;
-	boost::filesystem::path binaryPath() const override;
+	bfs::path libraryPath() const override;
+	bfs::path binaryPath() const override;
 
 	void init() override;
 };
@@ -464,12 +467,12 @@ void VCMIDirsOSX::init()
 
 		for (bfs::directory_iterator file(from); file != bfs::directory_iterator(); ++file)
 		{
-			const boost::filesystem::path& srcFilePath = file->path();
-			const boost::filesystem::path  dstFilePath = to / srcFilePath.filename();
+			const bfs::path& srcFilePath = file->path();
+			const bfs::path  dstFilePath = to / srcFilePath.filename();
 
 			// TODO: Aplication should ask user what to do when file exists:
 			// replace/ignore/stop process/replace all/ignore all
-			if (!boost::filesystem::exists(dstFilePath))
+			if (!bfs::exists(dstFilePath))
 				bfs::rename(srcFilePath, dstFilePath);
 		}
 
@@ -526,14 +529,14 @@ bfs::path VCMIDirsOSX::binaryPath() const { return "."; }
 class VCMIDirsXDG : public IVCMIDirsUNIX
 {
 public:
-	boost::filesystem::path userDataPath() const override;
-	boost::filesystem::path userCachePath() const override;
-	boost::filesystem::path userConfigPath() const override;
+	bfs::path userDataPath() const override;
+	bfs::path userCachePath() const override;
+	bfs::path userConfigPath() const override;
 
-	std::vector<boost::filesystem::path> dataPaths() const override;
+	std::vector<bfs::path> dataPaths() const override;
 
-	boost::filesystem::path libraryPath() const override;
-	boost::filesystem::path binaryPath() const override;
+	bfs::path libraryPath() const override;
+	bfs::path binaryPath() const override;
 
 	std::string libraryName(const std::string& basename) const override;
 };

+ 3 - 0
lib/VCMIDirs.h

@@ -29,6 +29,9 @@ public:
 	// Path to saved games
 	virtual boost::filesystem::path userSavePath() const;
 
+	// Path to "extracted" directory, used to temporarily hold extracted Original H3 files
+	virtual boost::filesystem::path userExtractedPath() const;
+
 	// Paths to global system-wide data directories. First items have higher priority
 	virtual std::vector<boost::filesystem::path> dataPaths() const = 0;
 

+ 4 - 4
lib/VCMI_Lib.cpp

@@ -39,13 +39,13 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 LibClasses * VLC = nullptr;
 
-DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool onlyEssential)
+DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool onlyEssential, bool extractArchives)
 {
 	console = Console;
 	VLC = new LibClasses();
 	try
 	{
-		VLC->loadFilesystem(onlyEssential);
+		VLC->loadFilesystem(onlyEssential, extractArchives);
 	}
 	catch(...)
 	{
@@ -157,7 +157,7 @@ void LibClasses::updateEntity(Metatype metatype, int32_t index, const JsonNode &
 	}
 }
 
-void LibClasses::loadFilesystem(bool onlyEssential)
+void LibClasses::loadFilesystem(bool onlyEssential, bool extractArchives)
 {
 	CStopWatch totalTime;
 	CStopWatch loadTime;
@@ -165,7 +165,7 @@ void LibClasses::loadFilesystem(bool onlyEssential)
 	CResourceHandler::initialize();
 	logGlobal->info("\tInitialization: %d ms", loadTime.getDiff());
 
-	CResourceHandler::load("config/filesystem.json");
+	CResourceHandler::load("config/filesystem.json", extractArchives);
 	logGlobal->info("\tData loading: %d ms", loadTime.getDiff());
 
 	modh = new CModHandler();

+ 3 - 3
lib/VCMI_Lib.h

@@ -100,8 +100,8 @@ public:
 	void init(bool onlyEssential); //uses standard config file
 	void clear(); //deletes all handlers and its data
 
-
-	void loadFilesystem(bool onlyEssential);// basic initialization. should be called before init()
+	// basic initialization. should be called before init(). Can also extract original H3 archives
+	void loadFilesystem(bool onlyEssential, bool extractArchives = false);
 
 #if SCRIPTING_ENABLED
 	void scriptsLoaded();
@@ -151,7 +151,7 @@ public:
 
 extern DLL_LINKAGE LibClasses * VLC;
 
-DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool onlyEssential = false);
+DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool onlyEssential = false, bool extractArchives = false);
 DLL_LINKAGE void loadDLLClasses(bool onlyEssential = false);
 
 

+ 64 - 2
lib/filesystem/CArchiveLoader.cpp

@@ -10,6 +10,7 @@
 #include "StdInc.h"
 #include "CArchiveLoader.h"
 
+#include "VCMIDirs.h"
 #include "CFileInputStream.h"
 #include "CCompressedStream.h"
 
@@ -23,9 +24,10 @@ ArchiveEntry::ArchiveEntry()
 
 }
 
-CArchiveLoader::CArchiveLoader(std::string _mountPoint, boost::filesystem::path _archive) :
+CArchiveLoader::CArchiveLoader(std::string _mountPoint, bfs::path _archive, bool extractArchives) :
     archive(std::move(_archive)),
-    mountPoint(std::move(_mountPoint))
+    mountPoint(std::move(_mountPoint)),
+	extractArchives(extractArchives)
 {
 	// Open archive file(.snd, .vid, .lod)
 	CFileInputStream fileStream(archive);
@@ -77,6 +79,25 @@ void CArchiveLoader::initLODArchive(const std::string &mountPoint, CFileInputStr
 
 		// Add lod entry to local entries map
 		entries[ResourceID(mountPoint + entry.name)] = entry;
+
+		if(extractArchives)
+		{
+			si64 currentPosition = fileStream.tell(); // save filestream position
+
+			std::string fName = filename;
+			boost::to_upper(fName);
+
+			if(fName.find(".PCX") != std::string::npos)
+				extractToFolder("IMAGES", mountPoint, entry);
+			else if ((fName.find(".DEF") != std::string::npos ) || (fName.find(".MSK") != std::string::npos) || (fName.find(".FNT") != std::string::npos) || (fName.find(".PAL") != std::string::npos))
+				extractToFolder("SPRITES", mountPoint, entry);
+			else if ((fName.find(".h3c") != std::string::npos))
+				extractToFolder("SPRITES", mountPoint, entry);
+			else
+				extractToFolder("MISC", mountPoint, entry);
+
+			fileStream.seek(currentPosition); // restore filestream position
+		}
 	}
 }
 
@@ -112,6 +133,9 @@ void CArchiveLoader::initVIDArchive(const std::string &mountPoint, CFileInputStr
 		auto it = offsets.find(entry.second.offset);
 		it++;
 		entry.second.fullSize = *it - entry.second.offset;
+
+		if(extractArchives)
+			extractToFolder("VIDEO", fileStream, entry.second);
 	}
 }
 
@@ -139,6 +163,9 @@ void CArchiveLoader::initSNDArchive(const std::string &mountPoint, CFileInputStr
 		entry.fullSize = reader.readInt32();
 		entry.compressedSize = 0;
 		entries[ResourceID(mountPoint + entry.name)] = entry;
+
+		if(extractArchives)
+			extractToFolder("SOUND", fileStream, entry);
 	}
 }
 
@@ -182,4 +209,39 @@ std::unordered_set<ResourceID> CArchiveLoader::getFilteredFiles(std::function<bo
 	return foundID;
 }
 
+void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, CInputStream & fileStream, ArchiveEntry entry)
+{
+	si64 currentPosition = fileStream.tell(); // save filestream position
+
+	std::vector<ui8> data(entry.fullSize);
+	fileStream.seek(entry.offset);
+	fileStream.read(data.data(), entry.fullSize);
+
+	bfs::path extractedFilePath = createExtractedFilePath(outputSubFolder, entry.name);
+
+	// writeToOutputFile
+	std::ofstream out(extractedFilePath.string(), std::ofstream::binary);
+	out.exceptions(std::ifstream::failbit | std::ifstream::badbit);
+	out.write((char*)data.data(), entry.fullSize);
+
+	fileStream.seek(currentPosition); // restore filestream position
+}
+
+void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, const std::string & mountPoint, ArchiveEntry entry)
+{
+	std::unique_ptr<CInputStream> inputStream = load(ResourceID(mountPoint + entry.name));
+
+	extractToFolder(outputSubFolder, *inputStream, entry);
+}
+
+bfs::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName)
+{
+	bfs::path extractionFolderPath = VCMIDirs::get().userExtractedPath() / outputSubFolder;
+	bfs::path extractedFilePath = extractionFolderPath / entryName;
+
+	bfs::create_directories(extractionFolderPath);
+
+	return extractedFilePath;
+}
+
 VCMI_LIB_NAMESPACE_END

+ 15 - 2
lib/filesystem/CArchiveLoader.h

@@ -12,6 +12,8 @@
 #include "ISimpleResourceLoader.h"
 #include "ResourceID.h"
 
+namespace bfs = boost::filesystem;
+
 VCMI_LIB_NAMESPACE_BEGIN
 
 class CFileInputStream;
@@ -52,10 +54,11 @@ public:
 	 * These are valid extensions: .LOD, .SND, .VID
 	 *
 	 * @param archive Specifies the file path to the archive which should be indexed and loaded.
+	 * @param extractArchives Specifies if the original H3 archives should be extracted to a separate folder.
 	 *
 	 * @throws std::runtime_error if the archive wasn't found or if the archive isn't supported
 	 */
-	CArchiveLoader(std::string mountPoint, boost::filesystem::path archive);
+	CArchiveLoader(std::string mountPoint, bfs::path archive, bool extractArchives = false);
 
 	/// Interface implementation
 	/// @see ISimpleResourceLoader
@@ -64,6 +67,10 @@ public:
 	std::string getMountPoint() const override;
 	void updateFilteredFiles(std::function<bool(const std::string &)> filter) const override {}
 	std::unordered_set<ResourceID> getFilteredFiles(std::function<bool(const ResourceID &)> filter) const override;
+	/** Extracts one archive entry to the specified subfolder. Used for Video and Sound */
+	void extractToFolder(const std::string & outputSubFolder, CInputStream & fileStream, ArchiveEntry entry);
+	/** Extracts one archive entry to the specified subfolder. Used for Images, Sprites, etc */
+	void extractToFolder(const std::string & outputSubFolder, const std::string & mountPoint, ArchiveEntry entry);
 
 private:
 	/**
@@ -88,12 +95,18 @@ private:
 	void initSNDArchive(const std::string &mountPoint, CFileInputStream & fileStream);
 
 	/** The file path to the archive which is scanned and indexed. */
-	boost::filesystem::path archive;
+	bfs::path archive;
 
 	std::string mountPoint;
 
 	/** Holds all entries of the archive file. An entry can be accessed via the entry name. **/
 	std::unordered_map<ResourceID, ArchiveEntry> entries;
+
+	/** Specifies if Original H3 archives should be extracted to a separate folder **/
+	bool extractArchives;
 };
 
+/** Constructs the file path for the extracted file. Creates the subfolder hierarchy aswell **/
+bfs::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName);
+
 VCMI_LIB_NAMESPACE_END

+ 8 - 7
lib/filesystem/Filesystem.cpp

@@ -26,9 +26,10 @@ VCMI_LIB_NAMESPACE_BEGIN
 std::map<std::string, ISimpleResourceLoader*> CResourceHandler::knownLoaders = std::map<std::string, ISimpleResourceLoader*>();
 CResourceHandler CResourceHandler::globalResourceHandler;
 
-CFilesystemGenerator::CFilesystemGenerator(std::string prefix):
+CFilesystemGenerator::CFilesystemGenerator(std::string prefix, bool extractArchives):
 	filesystem(new CFilesystemList()),
-	prefix(prefix)
+	prefix(prefix),
+	extractArchives(extractArchives)
 {
 }
 
@@ -105,7 +106,7 @@ void CFilesystemGenerator::loadArchive(const std::string &mountPoint, const Json
 	std::string URI = prefix + config["path"].String();
 	auto filename = CResourceHandler::get("initial")->getResourceName(ResourceID(URI, archiveType));
 	if (filename)
-		filesystem->addLoader(new CArchiveLoader(mountPoint, *filename), false);
+		filesystem->addLoader(new CArchiveLoader(mountPoint, *filename, extractArchives), false);
 }
 
 void CFilesystemGenerator::loadJsonMap(const std::string &mountPoint, const JsonNode & config)
@@ -200,13 +201,13 @@ ISimpleResourceLoader * CResourceHandler::get(std::string identifier)
 	return knownLoaders.at(identifier);
 }
 
-void CResourceHandler::load(const std::string &fsConfigURI)
+void CResourceHandler::load(const std::string &fsConfigURI, bool extractArchives)
 {
 	auto fsConfigData = get("initial")->load(ResourceID(fsConfigURI, EResType::TEXT))->readAll();
 
 	const JsonNode fsConfig((char*)fsConfigData.first.get(), fsConfigData.second);
 
-	addFilesystem("data", "core", createFileSystem("", fsConfig["filesystem"]));
+	addFilesystem("data", "core", createFileSystem("", fsConfig["filesystem"], extractArchives));
 }
 
 void CResourceHandler::addFilesystem(const std::string & parent, const std::string & identifier, ISimpleResourceLoader * loader)
@@ -246,9 +247,9 @@ bool CResourceHandler::removeFilesystem(const std::string & parent, const std::s
 	return true;
 }
 
-ISimpleResourceLoader * CResourceHandler::createFileSystem(const std::string & prefix, const JsonNode &fsConfig)
+ISimpleResourceLoader * CResourceHandler::createFileSystem(const std::string & prefix, const JsonNode &fsConfig, bool extractArchives)
 {
-	CFilesystemGenerator generator(prefix);
+	CFilesystemGenerator generator(prefix, extractArchives);
 	generator.loadConfig(fsConfig);
 	return generator.getFilesystem();
 }

+ 7 - 3
lib/filesystem/Filesystem.h

@@ -36,7 +36,8 @@ class DLL_LINKAGE CFilesystemGenerator
 	TLoadFunctorMap genFunctorMap();
 public:
 	/// prefix = prefix that will be given to file entries in all nodes of this filesystem
-	CFilesystemGenerator(std::string prefix);
+	/// extractArchives = Specifies if Original H3 archives should be extracted to a separate folder
+	CFilesystemGenerator(std::string prefix, bool extractArchives = false);
 
 	/// loads configuration from json
 	/// config - configuration to load, using format of "filesystem" entry in config/filesystem.json
@@ -44,6 +45,9 @@ public:
 
 	/// returns generated filesystem
 	CFilesystemList * getFilesystem();
+
+	/** Specifies if Original H3 archives should be extracted to a separate folder **/
+	bool extractArchives;
 };
 
 /**
@@ -81,7 +85,7 @@ public:
 	 * Will load all filesystem data from Json data at this path (normally - config/filesystem.json)
 	 * @param fsConfigURI - URI from which data will be loaded
 	 */
-	static void load(const std::string & fsConfigURI);
+	static void load(const std::string & fsConfigURI, bool extractArchives = false);
 
 	/**
 	 * @brief addFilesystem adds filesystem into global resource loader
@@ -104,7 +108,7 @@ public:
 	 * @param fsConfig - configuration to load
 	 * @return generated filesystem that contains all config entries
 	 */
-	static ISimpleResourceLoader * createFileSystem(const std::string &prefix, const JsonNode & fsConfig);
+	static ISimpleResourceLoader * createFileSystem(const std::string &prefix, const JsonNode & fsConfig, bool extractArchives = false);
 
 	~CResourceHandler() = default;
 private:

+ 2 - 2
lib/mapObjects/CQuest.cpp

@@ -422,7 +422,7 @@ void CQuest::getCompletionText(MetaString &iwText, std::vector<Component> &compo
 	}
 }
 
-void CQuest::addArtifactID(ui16 id)
+void CQuest::addArtifactID(ArtifactID id)
 {
 	m5arts.push_back(id);
 	++artifactsRequirements[id];
@@ -474,7 +474,7 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi
 		break;
 	case MISSION_ART:
 		//todo: ban artifacts
-		handler.serializeIdArray<ui16, ArtifactID>("artifacts", m5arts);
+		handler.serializeIdArray<ArtifactID>("artifacts", m5arts);
 		break;
 	case MISSION_ARMY:
         {

+ 3 - 3
lib/mapObjects/CQuest.h

@@ -21,7 +21,7 @@ class CGCreature;
 
 class DLL_LINKAGE CQuest final
 {
-	mutable std::unordered_map<ui16, unsigned> artifactsRequirements; // artifact ID -> required count
+	mutable std::unordered_map<ArtifactID, unsigned, ArtifactID::hash> artifactsRequirements; // artifact ID -> required count
 
 public:
 	enum Emission {MISSION_NONE = 0, MISSION_LEVEL = 1, MISSION_PRIMARY_STAT = 2, MISSION_KILL_HERO = 3, MISSION_KILL_CREATURE = 4,
@@ -36,7 +36,7 @@ public:
 
 	ui32 m13489val;
 	std::vector<ui32> m2stats;
-	std::vector<ui16> m5arts; // artifact IDs. Add IDs through addArtifactID(), not directly to the field.
+	std::vector<ArtifactID> m5arts; // artifact IDs. Add IDs through addArtifactID(), not directly to the field.
 	std::vector<CStackBasicDescriptor> m6creatures; //pair[cre id, cre count], CreatureSet info irrelevant
 	std::vector<ui32> m7resources; //TODO: use resourceset?
 
@@ -62,7 +62,7 @@ public:
 	virtual void getRolloverText (MetaString &text, bool onHover) const; //hover or quest log entry
 	virtual void completeQuest (const CGHeroInstance * h) const {};
 	virtual void addReplacements(MetaString &out, const std::string &base) const;
-	void addArtifactID(ui16 id);
+	void addArtifactID(ArtifactID id);
 
 	bool operator== (const CQuest & quest) const
 	{

+ 7 - 7
lib/mapping/MapFormatH3M.cpp

@@ -870,15 +870,15 @@ void CMapLoaderH3M::loadArtifactsOfHero(CGHeroInstance * hero)
 bool CMapLoaderH3M::loadArtifactToSlot(CGHeroInstance * hero, int slot)
 {
 	const int artmask = map->version == EMapFormat::ROE ? 0xff : 0xffff;
-	int aid;
+	ArtifactID aid;
 
 	if(map->version == EMapFormat::ROE)
 	{
-		aid = reader.readUInt8();
+		aid = ArtifactID(reader.readUInt8());
 	}
 	else
 	{
-		aid = reader.readUInt16();
+		aid = ArtifactID(reader.readUInt16());
 	}
 
 	bool isArt  =  aid != artmask;
@@ -1207,7 +1207,7 @@ void CMapLoaderH3M::readObjects()
 		case Obj::RANDOM_RELIC_ART:
 		case Obj::SPELL_SCROLL:
 			{
-				int artID = ArtifactID::NONE; //random, set later
+				auto artID = ArtifactID::NONE; //random, set later
 				int spellID = -1;
 				auto  art = new CGArtifact();
 				nobj = art;
@@ -1222,7 +1222,7 @@ void CMapLoaderH3M::readObjects()
 				else if(objTempl->id == Obj::ARTIFACT)
 				{
 					//specific artifact
-					artID = objTempl->subid;
+					artID = ArtifactID(objTempl->subid);
 				}
 
 				art->storedArtifact = CArtifactInstance::createArtifact(map, artID, spellID);
@@ -1754,7 +1754,7 @@ CGSeerHut * CMapLoaderH3M::readSeerHut()
 	else
 	{
 		//RoE
-		int artID = reader.readUInt8();
+		auto artID = ArtifactID(reader.readUInt8());
 		if (artID != 255)
 		{
 			//not none quest
@@ -1886,7 +1886,7 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard)
 			int artNumber = reader.readUInt8();
 			for(int yy = 0; yy < artNumber; ++yy)
 			{
-				int artid = reader.readUInt16();
+				auto artid = ArtifactID(reader.readUInt16());
 				guard->quest->addArtifactID(artid);
 				map->allowedArtifact[artid] = false; //these are unavailable for random generation
 			}

+ 2 - 2
lib/mapping/MapFormatJson.cpp

@@ -1152,7 +1152,7 @@ void CMapLoaderJson::MapObjectLoader::configure()
 
 	if(auto art = dynamic_cast<CGArtifact *>(instance))
 	{
-		int artID = ArtifactID::NONE;
+		auto artID = ArtifactID::NONE;
 		int spellID = -1;
 
 		if(art->ID == Obj::SPELL_SCROLL)
@@ -1168,7 +1168,7 @@ void CMapLoaderJson::MapObjectLoader::configure()
 		else if(art->ID  == Obj::ARTIFACT)
 		{
 			//specific artifact
-			artID = art->subID;
+			artID = ArtifactID(art->subID);
 		}
 
 		art->storedArtifact = CArtifactInstance::createArtifact(owner->map, artID, spellID);

+ 4 - 4
lib/spells/CSpellHandler.cpp

@@ -505,10 +505,10 @@ std::unique_ptr<spells::Mechanics> CSpell::battleMechanics(const spells::IBattle
 
 void CSpell::registerIcons(const IconRegistar & cb) const
 {
-	cb(getIndex(), "SPELLS", iconBook);
-	cb(getIndex()+1, "SPELLINT", iconEffect);
-	cb(getIndex(), "SPELLBON", iconScenarioBonus);
-	cb(getIndex(), "SPELLSCR", iconScroll);
+	cb(getIndex(), 0, "SPELLS", iconBook);
+	cb(getIndex()+1, 0, "SPELLINT", iconEffect);
+	cb(getIndex(), 0, "SPELLBON", iconScenarioBonus);
+	cb(getIndex(), 0, "SPELLSCR", iconScroll);
 }
 
 void CSpell::updateFrom(const JsonNode & data)

+ 35 - 2
mapeditor/Animation.cpp

@@ -19,7 +19,7 @@
 #include "../lib/filesystem/ISimpleResourceLoader.h"
 #include "../lib/JsonNode.h"
 #include "../lib/CRandomGenerator.h"
-
+#include "../lib/VCMIDirs.h"
 
 typedef std::map<size_t, std::vector<JsonNode>> source_map;
 
@@ -686,6 +686,39 @@ std::shared_ptr<QImage> Animation::getImage(size_t frame, size_t group, bool ver
 	return nullptr;
 }
 
+void Animation::exportBitmaps(const QDir & path) const
+{
+	if(images.empty())
+	{
+		logGlobal->error("Nothing to export, animation is empty");
+		return;
+	}
+
+	QString actualPath = path.absolutePath() + "/SPRITES/" + QString::fromStdString(name);
+	QDir().mkdir(actualPath);
+
+	size_t counter = 0;
+
+	for(const auto& groupPair : images)
+	{
+		size_t group = groupPair.first;
+
+		for(const auto& imagePair : groupPair.second)
+		{
+			size_t frame = imagePair.first;
+			const auto img = imagePair.second;
+
+			QString filename = QString("%1_%2_%3.png").arg(QString::fromStdString(name)).arg(group).arg(frame);
+			QString filePath = actualPath + "/" + filename;
+			img->save(filePath, "PNG");
+
+			counter++;
+		}
+	}
+
+	logGlobal->info("Exported %d frames to %s", counter, actualPath.toStdString());
+}
+
 void Animation::load()
 {
 	for (auto & elem : source)
@@ -774,4 +807,4 @@ void Animation::createFlippedGroup(const size_t sourceGroup, const size_t target
 		auto image = getImage(frame, targetGroup);
 		*image = image->transformed(QTransform::fromScale(1, -1));
 	}
-}
+}

+ 2 - 0
mapeditor/Animation.h

@@ -83,6 +83,8 @@ public:
 	void load  (size_t frame, size_t group = 0);
 	void unload(size_t frame, size_t group = 0);
 
+	void exportBitmaps(const QDir & path) const;
+
 	//total count of frames in group (including not loaded)
 	size_t size(size_t group = 0) const;
 

+ 3 - 1
mapeditor/CMakeLists.txt

@@ -24,6 +24,7 @@ set(editor_SRCS
 		inspector/messagewidget.cpp
 		inspector/rewardswidget.cpp
 		inspector/questwidget.cpp
+		resourceExtractor/ResourceConverter.cpp
 )
 
 set(editor_HEADERS
@@ -51,6 +52,7 @@ set(editor_HEADERS
 		inspector/messagewidget.h
 		inspector/rewardswidget.h
 		inspector/questwidget.h
+		resourceExtractor/ResourceConverter.h
 )
 
 set(editor_FORMS
@@ -110,7 +112,7 @@ if(WIN32)
 endif()
 
 if(APPLE)
-	# This makes Xcode project prettier by moving vcmilauncher_autogen directory into vcmiclient subfolder
+	# This makes Xcode project prettier by moving mapeditor_autogen directory into vcmiclient subfolder
 	set_property(GLOBAL PROPERTY AUTOGEN_TARGETS_FOLDER vcmieditor)
 endif()
 

+ 4 - 2
mapeditor/graphics.cpp

@@ -310,11 +310,13 @@ std::shared_ptr<Animation> Graphics::getAnimation(const std::shared_ptr<const Ob
 	return ret;
 }
 
-void Graphics::addImageListEntry(size_t index, const std::string & listName, const std::string & imageName)
+void Graphics::addImageListEntry(size_t index, size_t group, const std::string & listName, const std::string & imageName)
 {
 	if (!imageName.empty())
 	{
 		JsonNode entry;
+		if(group != 0)
+			entry["group"].Integer() = group;
 		entry["frame"].Integer() = index;
 		entry["file"].String() = imageName;
 		
@@ -324,7 +326,7 @@ void Graphics::addImageListEntry(size_t index, const std::string & listName, con
 
 void Graphics::addImageListEntries(const EntityService * service)
 {
-	auto cb = std::bind(&Graphics::addImageListEntry, this, _1, _2, _3);
+	auto cb = std::bind(&Graphics::addImageListEntry, this, _1, _2, _3, _4);
 	
 	auto loopCb = [&](const Entity * entity, bool & stop)
 	{

+ 1 - 1
mapeditor/graphics.h

@@ -27,7 +27,7 @@ class JsonNode;
 /// Handles fonts, hero images, town images, various graphics
 class Graphics
 {
-	void addImageListEntry(size_t index, const std::string & listName, const std::string & imageName);
+	void addImageListEntry(size_t index, size_t group, const std::string & listName, const std::string & imageName);
 	
 	void addImageListEntries(const EntityService * service);
 	

二进制
mapeditor/icons/brush-1.png


二进制
mapeditor/icons/brush-2.png


二进制
mapeditor/icons/brush-4.png


二进制
mapeditor/icons/brush-5.png


二进制
mapeditor/icons/document-new.png


二进制
mapeditor/icons/document-open.png


二进制
mapeditor/icons/document-save.png


二进制
mapeditor/icons/edit-clear.png


二进制
mapeditor/icons/edit-copy.png


二进制
mapeditor/icons/edit-cut.png


二进制
mapeditor/icons/edit-paste.png


二进制
mapeditor/icons/fill-obstacles.png


二进制
mapeditor/icons/toggle-grid.png


二进制
mapeditor/icons/toggle-pass.png


二进制
mapeditor/icons/toggle-underground.png


+ 1 - 1
mapeditor/inspector/questwidget.cpp

@@ -134,7 +134,7 @@ QString QuestWidget::commitChanges()
 			return QString("N/A");
 		case CQuest::Emission::MISSION_ART:
 			seerhut.quest->m5arts.clear();
-			seerhut.quest->m5arts.push_back(ui->targetId->currentIndex());
+			seerhut.quest->m5arts.push_back(ArtifactID(ui->targetId->currentIndex()));
 			//TODO: support multiple artifacts
 			return ui->targetId->currentText();
 		case CQuest::Emission::MISSION_ARMY:

+ 52 - 15
mapeditor/mainwindow.cpp

@@ -88,57 +88,94 @@ void MainWindow::saveUserSettings()
 	s.setValue(mainWindowPositionSetting, pos());
 }
 
-MainWindow::MainWindow(QWidget *parent) :
+void MainWindow::parseCommandLine(ExtractionOptions & extractionOptions)
+{
+	QCommandLineParser parser;
+	parser.addHelpOption();
+	parser.addPositionalArgument("map", QCoreApplication::translate("main", "Filepath of the map to open."));
+
+	parser.addOptions({
+		{"e", QCoreApplication::translate("main", "Extract original H3 archives into a separate folder.")},
+		{"s", QCoreApplication::translate("main", "From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's.")},
+		{"c", QCoreApplication::translate("main", "From an extracted archive, Converts single Images (found in Images folder) from .pcx to png.")},
+		{"d", QCoreApplication::translate("main", "Delete original files, for the ones splitted / converted.")},
+		});
+
+	parser.process(qApp->arguments());
+
+	const QStringList positionalArgs = parser.positionalArguments();
+
+	if(!positionalArgs.isEmpty())
+		mapFilePath = positionalArgs.at(0);
+
+	extractionOptions = {
+		parser.isSet("e"), {
+			parser.isSet("s"),
+			parser.isSet("c"),
+			parser.isSet("d")}};
+}
+
+MainWindow::MainWindow(QWidget* parent) :
 	QMainWindow(parent),
 	ui(new Ui::MainWindow),
 	controller(this)
 {
+	for(auto & string : VCMIDirs::get().dataPaths())
+		QDir::addSearchPath("icons", pathToQString(string / "mapeditor" / "icons"));
+	QDir::addSearchPath("icons", pathToQString(VCMIDirs::get().userDataPath() / "mapeditor" / "icons"));
+	
 	ui->setupUi(this);
 	loadUserSettings(); //For example window size
 	setTitle();
-	
+
 	// Set current working dir to executable folder.
 	// This is important on Mac for relative paths to work inside DMG.
 	QDir::setCurrent(QApplication::applicationDirPath());
 
+	ExtractionOptions extractionOptions;
+	parseCommandLine(extractionOptions);
+
 	//configure logging
 	const boost::filesystem::path logPath = VCMIDirs::get().userLogsPath() / "VCMI_Editor_log.txt";
 	console = new CConsoleHandler();
 	logConfig = new CBasicLogConfigurator(logPath, console);
 	logConfig->configureDefault();
 	logGlobal->info("The log file will be saved to %s", logPath);
-	
+
 	//init
-	preinitDLL(::console);
+	preinitDLL(::console, false, extractionOptions.extractArchives);
 	settings.init();
-	
+
 	// Initialize logging based on settings
 	logConfig->configure();
 	logGlobal->debug("settings = %s", settings.toJsonNode().toJson());
-	
+
 	// Some basic data validation to produce better error messages in cases of incorrect install
 	auto testFile = [](std::string filename, std::string message) -> bool
 	{
 		if (CResourceHandler::get()->existsResource(ResourceID(filename)))
 			return true;
-		
+
 		logGlobal->error("Error: %s was not found!", message);
 		return false;
 	};
-	
-	if(!testFile("DATA/HELP.TXT", "Heroes III data") ||
-	   !testFile("MODS/VCMI/MOD.JSON", "VCMI data"))
+
+	if (!testFile("DATA/HELP.TXT", "Heroes III data") ||
+		!testFile("MODS/VCMI/MOD.JSON", "VCMI data"))
 	{
 		QApplication::quit();
 	}
-	
+
 	conf.init();
 	logGlobal->info("Loading settings");
-	
+
 	init();
-	
+
 	graphics = new Graphics(); // should be before curh->init()
 	graphics->load();//must be after Content loading but should be in main thread
+
+	if (extractionOptions.extractArchives)
+		ResourceConverter::convertExtractedResourceFiles(extractionOptions.conversionOptions);
 	
 	ui->mapView->setScene(controller.scene(0));
 	ui->mapView->setController(&controller);
@@ -167,8 +204,8 @@ MainWindow::MainWindow(QWidget *parent) :
 	show();
 	
 	//Load map from command line
-	if(qApp->arguments().size() >= 2)
-		openMap(qApp->arguments().at(1));
+	if(!mapFilePath.isEmpty())
+		openMap(mapFilePath);
 }
 
 MainWindow::~MainWindow()

部分文件因为文件数量过多而无法显示