浏览代码

Merge branch 'develop' into random_template_description

Ivan Savenko 1 年之前
父节点
当前提交
ef8eb330d6
共有 100 个文件被更改,包括 1888 次插入641 次删除
  1. 7 0
      .gitignore
  2. 1 1
      AI/BattleAI/BattleExchangeVariant.cpp
  3. 2 2
      AI/EmptyAI/CEmptyAI.h
  4. 1 1
      AI/Nullkiller/Behaviors/BuildingBehavior.h
  5. 1 1
      AI/Nullkiller/Behaviors/BuyArmyBehavior.h
  6. 1 1
      AI/Nullkiller/Behaviors/CaptureObjectsBehavior.h
  7. 2 2
      AI/Nullkiller/Behaviors/ClusterBehavior.h
  8. 1 1
      AI/Nullkiller/Behaviors/DefenceBehavior.h
  9. 2 2
      AI/Nullkiller/Behaviors/GatherArmyBehavior.h
  10. 2 2
      AI/Nullkiller/Behaviors/RecruitHeroBehavior.h
  11. 2 2
      AI/Nullkiller/Behaviors/StartupBehavior.h
  12. 2 2
      AI/Nullkiller/Behaviors/StayAtTownBehavior.h
  13. 1 1
      AI/Nullkiller/Goals/AdventureSpellCast.h
  14. 1 1
      AI/Nullkiller/Goals/Build.h
  15. 1 1
      AI/Nullkiller/Goals/BuildBoat.h
  16. 1 1
      AI/Nullkiller/Goals/BuildThis.h
  17. 2 2
      AI/Nullkiller/Goals/BuyArmy.h
  18. 4 4
      AI/Nullkiller/Goals/CGoal.h
  19. 3 3
      AI/Nullkiller/Goals/CaptureObject.h
  20. 3 3
      AI/Nullkiller/Goals/CompleteQuest.h
  21. 4 4
      AI/Nullkiller/Goals/Composition.h
  22. 1 1
      AI/Nullkiller/Goals/DigAtTile.h
  23. 1 1
      AI/Nullkiller/Goals/DismissHero.h
  24. 1 1
      AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h
  25. 2 2
      AI/Nullkiller/Goals/ExecuteHeroChain.h
  26. 1 1
      AI/Nullkiller/Goals/GatherArmy.h
  27. 2 2
      AI/Nullkiller/Goals/Invalid.h
  28. 1 1
      AI/Nullkiller/Goals/RecruitHero.h
  29. 1 1
      AI/Nullkiller/Goals/SaveResources.h
  30. 1 1
      AI/Nullkiller/Goals/StayAtTown.h
  31. 1 1
      AI/Nullkiller/Goals/Trade.h
  32. 1 1
      AI/Nullkiller/Markers/ArmyUpgrade.h
  33. 1 1
      AI/Nullkiller/Markers/DefendTown.h
  34. 1 1
      AI/Nullkiller/Markers/HeroExchange.h
  35. 1 1
      AI/Nullkiller/Markers/UnlockCluster.h
  36. 1 1
      AI/Nullkiller/Pathfinding/AINodeStorage.h
  37. 1 1
      AI/Nullkiller/Pathfinding/AIPathfinderConfig.h
  38. 2 2
      AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h
  39. 1 1
      AI/Nullkiller/Pathfinding/Actions/BattleAction.h
  40. 7 7
      AI/Nullkiller/Pathfinding/Actions/BoatActions.h
  41. 2 2
      AI/Nullkiller/Pathfinding/Actions/QuestAction.h
  42. 1 1
      AI/Nullkiller/Pathfinding/Actions/TownPortalAction.h
  43. 2 2
      AI/Nullkiller/Pathfinding/Actors.h
  44. 1 1
      AI/VCAI/Goals/AdventureSpellCast.h
  45. 1 1
      AI/VCAI/Goals/Build.h
  46. 1 1
      AI/VCAI/Goals/BuildBoat.h
  47. 1 1
      AI/VCAI/Goals/BuildThis.h
  48. 1 1
      AI/VCAI/Goals/BuyArmy.h
  49. 1 1
      AI/VCAI/Goals/CGoal.h
  50. 1 1
      AI/VCAI/Goals/ClearWayTo.h
  51. 1 1
      AI/VCAI/Goals/CollectRes.h
  52. 1 1
      AI/VCAI/Goals/CompleteQuest.h
  53. 1 1
      AI/VCAI/Goals/Conquer.h
  54. 1 1
      AI/VCAI/Goals/DigAtTile.h
  55. 1 1
      AI/VCAI/Goals/Explore.h
  56. 1 1
      AI/VCAI/Goals/FindObj.h
  57. 1 1
      AI/VCAI/Goals/GatherArmy.h
  58. 1 1
      AI/VCAI/Goals/GatherTroops.h
  59. 1 1
      AI/VCAI/Goals/GetArtOfType.h
  60. 1 1
      AI/VCAI/Goals/Invalid.h
  61. 1 1
      AI/VCAI/Goals/RecruitHero.h
  62. 1 1
      AI/VCAI/Goals/Trade.h
  63. 1 1
      AI/VCAI/Goals/VisitHero.h
  64. 1 1
      AI/VCAI/Goals/VisitObj.h
  65. 1 1
      AI/VCAI/Goals/VisitTile.h
  66. 1 1
      AI/VCAI/Goals/Win.h
  67. 1 1
      AI/VCAI/Pathfinding/AINodeStorage.h
  68. 1 1
      AI/VCAI/Pathfinding/AIPathfinderConfig.h
  69. 1 1
      CI/linux-qt6/before_install.sh
  70. 1 1
      CI/linux/before_install.sh
  71. 16 0
      CMakeLists.txt
  72. 72 51
      Mods/vcmi/config/vcmi/chinese.json
  73. 22 2
      Mods/vcmi/config/vcmi/english.json
  74. 24 3
      Mods/vcmi/config/vcmi/spanish.json
  75. 1 51
      client/CMT.cpp
  76. 13 0
      client/CMakeLists.txt
  77. 3 8
      client/CPlayerInterface.cpp
  78. 280 327
      client/CServerHandler.cpp
  79. 64 39
      client/CServerHandler.h
  80. 7 7
      client/Client.cpp
  81. 1 1
      client/Client.h
  82. 12 12
      client/ClientNetPackVisitors.h
  83. 13 12
      client/LobbyClientNetPackVisitors.h
  84. 2 2
      client/NetPacksClient.cpp
  85. 51 22
      client/NetPacksLobbyClient.cpp
  86. 10 3
      client/battle/BattleInterfaceClasses.cpp
  87. 1 1
      client/battle/BattleInterfaceClasses.h
  88. 2 2
      client/eventsSDL/InputHandler.cpp
  89. 9 1
      client/eventsSDL/InputSourceKeyboard.cpp
  90. 366 0
      client/globalLobby/GlobalLobbyClient.cpp
  91. 70 0
      client/globalLobby/GlobalLobbyClient.h
  92. 27 0
      client/globalLobby/GlobalLobbyDefines.h
  93. 128 0
      client/globalLobby/GlobalLobbyLoginWindow.cpp
  94. 45 0
      client/globalLobby/GlobalLobbyLoginWindow.h
  95. 142 0
      client/globalLobby/GlobalLobbyServerSetup.cpp
  96. 49 0
      client/globalLobby/GlobalLobbyServerSetup.h
  97. 154 0
      client/globalLobby/GlobalLobbyWidget.cpp
  98. 57 0
      client/globalLobby/GlobalLobbyWidget.h
  99. 105 0
      client/globalLobby/GlobalLobbyWindow.cpp
  100. 38 0
      client/globalLobby/GlobalLobbyWindow.h

+ 7 - 0
.gitignore

@@ -1,12 +1,15 @@
 /client/vcmiclient
 /server/vcmiserver
+/launcher/.lupdate
 /launcher/vcmilauncher
+/mapeditor/.lupdate
 /launcher/vcmilauncher_automoc.cpp
 /conan-*
 
 build/
 .cache/*
 out/
+/.qt
 *.dll
 *.exe
 *.depend
@@ -42,6 +45,7 @@ VCMI_VS11.opensdf
 .DS_Store
 CMakeUserPresets.json
 compile_commands.json
+fuzzylite.pc
 
 # Visual Studio
 *.suo
@@ -62,5 +66,8 @@ compile_commands.json
 /deps
 .vs/
 
+# Visual Studio Code
+/.vscode/
+
 # CLion
 .idea/

+ 1 - 1
AI/BattleAI/BattleExchangeVariant.cpp

@@ -843,7 +843,7 @@ bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb
 					}
 				}
 
-				if(!reachable && vstd::contains(reachabilityMap[hex], unit))
+				if(!reachable && std::count(reachabilityMap[hex].begin(), reachabilityMap[hex].end(), unit) > 1)
 				{
 					blockingScore += ratio * (enemyUnit ? BLOCKING_OWN_ATTACK_PENALTY : BLOCKING_OWN_MOVE_PENALTY);
 				}

+ 2 - 2
AI/EmptyAI/CEmptyAI.h

@@ -19,8 +19,8 @@ class CEmptyAI : public CGlobalAI
 	std::shared_ptr<CCallback> cb;
 
 public:
-	virtual void saveGame(BinarySerializer & h) override;
-	virtual void loadGame(BinaryDeserializer & h) override;
+	void saveGame(BinarySerializer & h) override;
+	void loadGame(BinaryDeserializer & h) override;
 
 	void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
 	void yourTurn(QueryID queryID) override;

+ 1 - 1
AI/Nullkiller/Behaviors/BuildingBehavior.h

@@ -27,7 +27,7 @@ namespace Goals
 
 		virtual Goals::TGoalVec decompose() const override;
 		virtual std::string toString() const override;
-		virtual bool operator==(const BuildingBehavior & other) const override
+		bool operator==(const BuildingBehavior & other) const override
 		{
 			return true;
 		}

+ 1 - 1
AI/Nullkiller/Behaviors/BuyArmyBehavior.h

@@ -26,7 +26,7 @@ namespace Goals
 
 		virtual Goals::TGoalVec decompose() const override;
 		virtual std::string toString() const override;
-		virtual bool operator==(const BuyArmyBehavior & other) const override
+		bool operator==(const BuyArmyBehavior & other) const override
 		{
 			return true;
 		}

+ 1 - 1
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.h

@@ -65,7 +65,7 @@ namespace Goals
 			return *this;
 		}
 
-		virtual bool operator==(const CaptureObjectsBehavior & other) const override;
+		bool operator==(const CaptureObjectsBehavior & other) const override;
 
 		static Goals::TGoalVec getVisitGoals(const std::vector<AIPath> & paths, const CGObjectInstance * objToVisit = nullptr);
 

+ 2 - 2
AI/Nullkiller/Behaviors/ClusterBehavior.h

@@ -28,10 +28,10 @@ namespace Goals
 		{
 		}
 
-		virtual TGoalVec decompose() const override;
+		TGoalVec decompose() const override;
 		virtual std::string toString() const override;
 
-		virtual bool operator==(const ClusterBehavior & other) const override
+		bool operator==(const ClusterBehavior & other) const override
 		{
 			return true;
 		}

+ 1 - 1
AI/Nullkiller/Behaviors/DefenceBehavior.h

@@ -32,7 +32,7 @@ namespace Goals
 		virtual Goals::TGoalVec decompose() const override;
 		virtual std::string toString() const override;
 
-		virtual bool operator==(const DefenceBehavior & other) const override
+		bool operator==(const DefenceBehavior & other) const override
 		{
 			return true;
 		}

+ 2 - 2
AI/Nullkiller/Behaviors/GatherArmyBehavior.h

@@ -25,10 +25,10 @@ namespace Goals
 		{
 		}
 
-		virtual TGoalVec decompose() const override;
+		TGoalVec decompose() const override;
 		virtual std::string toString() const override;
 
-		virtual bool operator==(const GatherArmyBehavior & other) const override
+		bool operator==(const GatherArmyBehavior & other) const override
 		{
 			return true;
 		}

+ 2 - 2
AI/Nullkiller/Behaviors/RecruitHeroBehavior.h

@@ -25,10 +25,10 @@ namespace Goals
 		{
 		}
 
-		virtual TGoalVec decompose() const override;
+		TGoalVec decompose() const override;
 		virtual std::string toString() const override;
 
-		virtual bool operator==(const RecruitHeroBehavior & other) const override
+		bool operator==(const RecruitHeroBehavior & other) const override
 		{
 			return true;
 		}

+ 2 - 2
AI/Nullkiller/Behaviors/StartupBehavior.h

@@ -25,10 +25,10 @@ namespace Goals
 		{
 		}
 
-		virtual TGoalVec decompose() const override;
+		TGoalVec decompose() const override;
 		virtual std::string toString() const override;
 
-		virtual bool operator==(const StartupBehavior & other) const override
+		bool operator==(const StartupBehavior & other) const override
 		{
 			return true;
 		}

+ 2 - 2
AI/Nullkiller/Behaviors/StayAtTownBehavior.h

@@ -25,10 +25,10 @@ namespace Goals
 		{
 		}
 
-		virtual TGoalVec decompose() const override;
+		TGoalVec decompose() const override;
 		virtual std::string toString() const override;
 
-		virtual bool operator==(const StayAtTownBehavior & other) const override
+		bool operator==(const StayAtTownBehavior & other) const override
 		{
 			return true;
 		}

+ 1 - 1
AI/Nullkiller/Goals/AdventureSpellCast.h

@@ -35,7 +35,7 @@ namespace Goals
 
 		void accept(AIGateway * ai) override;
 		std::string toString() const override;
-		virtual bool operator==(const AdventureSpellCast & other) const override;
+		bool operator==(const AdventureSpellCast & other) const override;
 	};
 }
 

+ 1 - 1
AI/Nullkiller/Goals/Build.h

@@ -32,7 +32,7 @@ namespace Goals
 		TSubgoal whatToDoToAchieve() override;
 		bool fulfillsMe(TSubgoal goal) override;
 
-		virtual bool operator==(const Build & other) const override
+		bool operator==(const Build & other) const override
 		{
 			return true;
 		}

+ 1 - 1
AI/Nullkiller/Goals/BuildBoat.h

@@ -29,7 +29,7 @@ namespace Goals
 
 		void accept(AIGateway * ai) override;
 		std::string toString() const override;
-		virtual bool operator==(const BuildBoat & other) const override;
+		bool operator==(const BuildBoat & other) const override;
 	};
 }
 

+ 1 - 1
AI/Nullkiller/Goals/BuildThis.h

@@ -39,7 +39,7 @@ namespace Goals
 		}
 		BuildThis(BuildingID Bid, const CGTownInstance * tid);
 
-		virtual bool operator==(const BuildThis & other) const override;
+		bool operator==(const BuildThis & other) const override;
 		virtual std::string toString() const override;
 		void accept(AIGateway * ai) override;
 	};

+ 2 - 2
AI/Nullkiller/Goals/BuyArmy.h

@@ -36,11 +36,11 @@ namespace Goals
 			priority = 3;//TODO: evaluate?
 		}
 
-		virtual bool operator==(const BuyArmy & other) const override;
+		bool operator==(const BuyArmy & other) const override;
 
 		virtual std::string toString() const override;
 
-		virtual void accept(AIGateway * ai) override;
+		void accept(AIGateway * ai) override;
 	};
 }
 

+ 4 - 4
AI/Nullkiller/Goals/CGoal.h

@@ -44,7 +44,7 @@ namespace Goals
 			//h & value & resID & objid & aid & tile & hero & town & bid;
 		}
 
-		virtual bool operator==(const AbstractGoal & g) const override
+		bool operator==(const AbstractGoal & g) const override
 		{
 			if(goalType != g.goalType)
 				return false;
@@ -54,7 +54,7 @@ namespace Goals
 
 		virtual bool operator==(const T & other) const = 0;
 
-		virtual TGoalVec decompose() const override
+		TGoalVec decompose() const override
 		{
 			TSubgoal single = decomposeSingle();
 
@@ -90,11 +90,11 @@ namespace Goals
 			return *((T *)this);
 		}
 
-		virtual bool isElementar() const override { return true; }
+		bool isElementar() const override { return true; }
 
 		virtual HeroPtr getHero() const override { return AbstractGoal::hero; }
 
-		virtual int getHeroExchangeCount() const override { return 0; }
+		int getHeroExchangeCount() const override { return 0; }
 	};
 }
 

+ 3 - 3
AI/Nullkiller/Goals/CaptureObject.h

@@ -34,11 +34,11 @@ namespace Goals
 			name = obj->getObjectName();
 		}
 
-		virtual bool operator==(const CaptureObject & other) const override;
+		bool operator==(const CaptureObject & other) const override;
 		virtual Goals::TGoalVec decompose() const override;
 		virtual std::string toString() const override;
-		virtual bool hasHash() const override { return true; }
-		virtual uint64_t getHash() const override;
+		bool hasHash() const override { return true; }
+		uint64_t getHash() const override;
 	};
 }
 

+ 3 - 3
AI/Nullkiller/Goals/CompleteQuest.h

@@ -31,10 +31,10 @@ namespace Goals
 
 		virtual Goals::TGoalVec decompose() const override;
 		virtual std::string toString() const override;
-		virtual bool hasHash() const override { return true; }
-		virtual uint64_t getHash() const override;
+		bool hasHash() const override { return true; }
+		uint64_t getHash() const override;
 
-		virtual bool operator==(const CompleteQuest & other) const override;
+		bool operator==(const CompleteQuest & other) const override;
 
 	private:
 		TGoalVec tryCompleteQuest() const;

+ 4 - 4
AI/Nullkiller/Goals/Composition.h

@@ -26,15 +26,15 @@ namespace Goals
 		{
 		}
 
-		virtual bool operator==(const Composition & other) const override;
+		bool operator==(const Composition & other) const override;
 		virtual std::string toString() const override;
 		void accept(AIGateway * ai) override;
 		Composition & addNext(const AbstractGoal & goal);
 		Composition & addNext(TSubgoal goal);
 		Composition & addNextSequence(const TGoalVec & taskSequence);
-		virtual TGoalVec decompose() const override;
-		virtual bool isElementar() const override;
-		virtual int getHeroExchangeCount() const override;
+		TGoalVec decompose() const override;
+		bool isElementar() const override;
+		int getHeroExchangeCount() const override;
 	};
 }
 

+ 1 - 1
AI/Nullkiller/Goals/DigAtTile.h

@@ -33,7 +33,7 @@ namespace Goals
 		{
 			tile = Tile;
 		}
-		virtual bool operator==(const DigAtTile & other) const override;
+		bool operator==(const DigAtTile & other) const override;
 
 	private:
 		//TSubgoal decomposeSingle() const override;

+ 1 - 1
AI/Nullkiller/Goals/DismissHero.h

@@ -26,7 +26,7 @@ namespace Goals
 
 		void accept(AIGateway * ai) override;
 		std::string toString() const override;
-		virtual bool operator==(const DismissHero & other) const override;
+		bool operator==(const DismissHero & other) const override;
 	};
 }
 

+ 1 - 1
AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h

@@ -31,7 +31,7 @@ namespace Goals
 
 		void accept(AIGateway * ai) override;
 		std::string toString() const override;
-		virtual bool operator==(const ExchangeSwapTownHeroes & other) const override;
+		bool operator==(const ExchangeSwapTownHeroes & other) const override;
 
 		const CGHeroInstance * getGarrisonHero() const { return garrisonHero; }
 		HeroLockedReason getLockingReason() const { return lockingReason; }

+ 2 - 2
AI/Nullkiller/Goals/ExecuteHeroChain.h

@@ -30,10 +30,10 @@ namespace Goals
 		
 		void accept(AIGateway * ai) override;
 		std::string toString() const override;
-		virtual bool operator==(const ExecuteHeroChain & other) const override;
+		bool operator==(const ExecuteHeroChain & other) const override;
 		const AIPath & getPath() const { return chainPath; }
 
-		virtual int getHeroExchangeCount() const override { return chainPath.exchangeCount; }
+		int getHeroExchangeCount() const override { return chainPath.exchangeCount; }
 
 	private:
 		bool moveHeroToTile(const CGHeroInstance * hero, const int3 & tile);

+ 1 - 1
AI/Nullkiller/Goals/GatherArmy.h

@@ -36,7 +36,7 @@ namespace Goals
 		TGoalVec getAllPossibleSubgoals() override;
 		TSubgoal whatToDoToAchieve() override;
 		std::string completeMessage() const override;
-		virtual bool operator==(const GatherArmy & other) const override;
+		bool operator==(const GatherArmy & other) const override;
 	};
 }
 

+ 2 - 2
AI/Nullkiller/Goals/Invalid.h

@@ -32,7 +32,7 @@ namespace Goals
 			return TGoalVec();
 		}
 
-		virtual bool operator==(const Invalid & other) const override
+		bool operator==(const Invalid & other) const override
 		{
 			return true;
 		}
@@ -42,7 +42,7 @@ namespace Goals
 			return "Invalid";
 		}
 
-		virtual void accept(AIGateway * ai) override
+		void accept(AIGateway * ai) override
 		{
 			throw cannotFulfillGoalException("Can not fulfill Invalid goal!");
 		}

+ 1 - 1
AI/Nullkiller/Goals/RecruitHero.h

@@ -38,7 +38,7 @@ namespace Goals
 		{
 		}
 
-		virtual bool operator==(const RecruitHero & other) const override
+		bool operator==(const RecruitHero & other) const override
 		{
 			return true;
 		}

+ 1 - 1
AI/Nullkiller/Goals/SaveResources.h

@@ -28,7 +28,7 @@ namespace Goals
 
 		void accept(AIGateway * ai) override;
 		std::string toString() const override;
-		virtual bool operator==(const SaveResources & other) const override;
+		bool operator==(const SaveResources & other) const override;
 	};
 }
 

+ 1 - 1
AI/Nullkiller/Goals/StayAtTown.h

@@ -26,7 +26,7 @@ namespace Goals
 	public:
 		StayAtTown(const CGTownInstance * town, AIPath & path);
 
-		virtual bool operator==(const StayAtTown & other) const override;
+		bool operator==(const StayAtTown & other) const override;
 		virtual std::string toString() const override;
 		void accept(AIGateway * ai) override;
 		float getMovementWasted() const { return movementWasted; }

+ 1 - 1
AI/Nullkiller/Goals/Trade.h

@@ -34,7 +34,7 @@ namespace Goals
 			value = val;
 			objid = Objid;
 		}
-		virtual bool operator==(const Trade & other) const override;
+		bool operator==(const Trade & other) const override;
 	};
 }
 

+ 1 - 1
AI/Nullkiller/Markers/ArmyUpgrade.h

@@ -29,7 +29,7 @@ namespace Goals
 		ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade);
 		ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade);
 
-		virtual bool operator==(const ArmyUpgrade & other) const override;
+		bool operator==(const ArmyUpgrade & other) const override;
 		virtual std::string toString() const override;
 
 		uint64_t getUpgradeValue() const { return upgradeValue; }

+ 1 - 1
AI/Nullkiller/Markers/DefendTown.h

@@ -30,7 +30,7 @@ namespace Goals
 		DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isCounterAttack = false);
 		DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const CGHeroInstance * defender);
 
-		virtual bool operator==(const DefendTown & other) const override;
+		bool operator==(const DefendTown & other) const override;
 		virtual std::string toString() const override;
 
 		const HitMapInfo & getTreat() const { return treat; }

+ 1 - 1
AI/Nullkiller/Markers/HeroExchange.h

@@ -28,7 +28,7 @@ namespace Goals
 			sethero(targetHero);
 		}
 
-		virtual bool operator==(const HeroExchange & other) const override;
+		bool operator==(const HeroExchange & other) const override;
 		virtual std::string toString() const override;
 
 		uint64_t getReinforcementArmyStrength() const;

+ 1 - 1
AI/Nullkiller/Markers/UnlockCluster.h

@@ -36,7 +36,7 @@ namespace Goals
 			sethero(pathToCenter.targetHero);
 		}
 
-		virtual bool operator==(const UnlockCluster & other) const override;
+		bool operator==(const UnlockCluster & other) const override;
 		virtual std::string toString() const override;
 		std::shared_ptr<ObjectCluster> getCluster() const { return cluster; }
 		const AIPath & getPathToCenter() { return pathToCenter; }

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

@@ -200,7 +200,7 @@ public:
 		const PathfinderConfig * pathfinderConfig,
 		const CPathfinderHelper * pathfinderHelper) override;
 
-	virtual void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override;
+	void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override;
 
 	void commit(
 		AIPathNode * destination,

+ 1 - 1
AI/Nullkiller/Pathfinding/AIPathfinderConfig.h

@@ -34,7 +34,7 @@ namespace AIPathfinding
 
 		~AIPathfinderConfig();
 
-		virtual CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override;
+		CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override;
 	};
 }
 

+ 2 - 2
AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h

@@ -29,7 +29,7 @@ namespace AIPathfinding
 	public:
 		AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero, DayFlags flagsToAdd = DayFlags::NONE);
 
-		virtual void execute(const CGHeroInstance * hero) const override;
+		void execute(const CGHeroInstance * hero) const override;
 
 		virtual void applyOnDestination(
 			const CGHeroInstance * hero,
@@ -38,7 +38,7 @@ namespace AIPathfinding
 			AIPathNode * dstMode,
 			const AIPathNode * srcNode) const override;
 
-		virtual bool canAct(const AIPathNode * source) const override;
+		bool canAct(const AIPathNode * source) const override;
 
 		virtual std::string toString() const override;
 	};

+ 1 - 1
AI/Nullkiller/Pathfinding/Actions/BattleAction.h

@@ -28,7 +28,7 @@ namespace AIPathfinding
 		{
 		}
 
-		virtual void execute(const CGHeroInstance * hero) const override;
+		void execute(const CGHeroInstance * hero) const override;
 
 		virtual std::string toString() const override;
 	};

+ 7 - 7
AI/Nullkiller/Pathfinding/Actions/BoatActions.h

@@ -25,7 +25,7 @@ namespace AIPathfinding
 	class SummonBoatAction : public VirtualBoatAction
 	{
 	public:
-		virtual void execute(const CGHeroInstance * hero) const override;
+		void execute(const CGHeroInstance * hero) const override;
 
 		virtual void applyOnDestination(
 			const CGHeroInstance * hero,
@@ -34,9 +34,9 @@ namespace AIPathfinding
 			AIPathNode * dstMode,
 			const AIPathNode * srcNode) const override;
 
-		virtual bool canAct(const AIPathNode * source) const override;
+		bool canAct(const AIPathNode * source) const override;
 
-		virtual const ChainActor * getActor(const ChainActor * sourceActor) const override;
+		const ChainActor * getActor(const ChainActor * sourceActor) const override;
 
 		virtual std::string toString() const override;
 
@@ -56,17 +56,17 @@ namespace AIPathfinding
 		{
 		}
 
-		virtual bool canAct(const AIPathNode * source) const override;
+		bool canAct(const AIPathNode * source) const override;
 
-		virtual void execute(const CGHeroInstance * hero) const override;
+		void execute(const CGHeroInstance * hero) const override;
 
 		virtual Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
 
-		virtual const ChainActor * getActor(const ChainActor * sourceActor) const override;
+		const ChainActor * getActor(const ChainActor * sourceActor) const override;
 
 		virtual std::string toString() const override;
 
-		virtual const CGObjectInstance * targetObject() const override;
+		const CGObjectInstance * targetObject() const override;
 	};
 }
 

+ 2 - 2
AI/Nullkiller/Pathfinding/Actions/QuestAction.h

@@ -28,11 +28,11 @@ namespace AIPathfinding
 		{
 		}
 
-		virtual bool canAct(const AIPathNode * node) const override;
+		bool canAct(const AIPathNode * node) const override;
 
 		virtual Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
 
-		virtual void execute(const CGHeroInstance * hero) const override;
+		void execute(const CGHeroInstance * hero) const override;
 
 		virtual std::string toString() const override;
 	};

+ 1 - 1
AI/Nullkiller/Pathfinding/Actions/TownPortalAction.h

@@ -29,7 +29,7 @@ namespace AIPathfinding
 		{
 		}
 
-		virtual void execute(const CGHeroInstance * hero) const override;
+		void execute(const CGHeroInstance * hero) const override;
 
 		virtual std::string toString() const override;
 	};

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

@@ -28,7 +28,7 @@ class HeroExchangeArmy : public CArmedInstance
 public:
 	TResources armyCost;
 	bool requireBuyArmy;
-	virtual bool needsLastStack() const override;
+	bool needsLastStack() const override;
 	std::shared_ptr<SpecialAction> getActorAction() const;
 
 	HeroExchangeArmy(): CArmedInstance(nullptr, true), requireBuyArmy(false) {}
@@ -126,7 +126,7 @@ public:
 	HeroActor(const ChainActor * carrier, const ChainActor * other, const HeroExchangeArmy * army, const Nullkiller * ai);
 
 protected:
-	virtual ExchangeResult tryExchangeNoLock(const ChainActor * specialActor, const ChainActor * other) const override;
+	ExchangeResult tryExchangeNoLock(const ChainActor * specialActor, const ChainActor * other) const override;
 };
 
 class ObjectActor : public ChainActor

+ 1 - 1
AI/VCAI/Goals/AdventureSpellCast.h

@@ -39,6 +39,6 @@ namespace Goals
 		void accept(VCAI * ai) override;
 		std::string name() const override;
 		std::string completeMessage() const override;
-		virtual bool operator==(const AdventureSpellCast & other) const override;
+		bool operator==(const AdventureSpellCast & other) const override;
 	};
 }

+ 1 - 1
AI/VCAI/Goals/Build.h

@@ -29,7 +29,7 @@ namespace Goals
 		TSubgoal whatToDoToAchieve() override;
 		bool fulfillsMe(TSubgoal goal) override;
 
-		virtual bool operator==(const Build & other) const override
+		bool operator==(const Build & other) const override
 		{
 			return true;
 		}

+ 1 - 1
AI/VCAI/Goals/BuildBoat.h

@@ -32,6 +32,6 @@ namespace Goals
 		void accept(VCAI * ai) override;
 		std::string name() const override;
 		std::string completeMessage() const override;
-		virtual bool operator==(const BuildBoat & other) const override;
+		bool operator==(const BuildBoat & other) const override;
 	};
 }

+ 1 - 1
AI/VCAI/Goals/BuildThis.h

@@ -43,6 +43,6 @@ namespace Goals
 		}
 		TSubgoal whatToDoToAchieve() override;
 		//bool fulfillsMe(TSubgoal goal) override;
-		virtual bool operator==(const BuildThis & other) const override;
+		bool operator==(const BuildThis & other) const override;
 	};
 }

+ 1 - 1
AI/VCAI/Goals/BuyArmy.h

@@ -36,6 +36,6 @@ namespace Goals
 
 		TSubgoal whatToDoToAchieve() override;
 		std::string completeMessage() const override;
-		virtual bool operator==(const BuyArmy & other) const override;
+		bool operator==(const BuyArmy & other) const override;
 	};
 }

+ 1 - 1
AI/VCAI/Goals/CGoal.h

@@ -76,7 +76,7 @@ namespace Goals
 			//h & value & resID & objid & aid & tile & hero & town & bid;
 		}
 
-		virtual bool operator==(const AbstractGoal & g) const override
+		bool operator==(const AbstractGoal & g) const override
 		{
 			if(goalType != g.goalType)
 				return false;

+ 1 - 1
AI/VCAI/Goals/ClearWayTo.h

@@ -40,6 +40,6 @@ namespace Goals
 		TGoalVec getAllPossibleSubgoals() override;
 		TSubgoal whatToDoToAchieve() override;
 		bool fulfillsMe(TSubgoal goal) override;
-		virtual bool operator==(const ClearWayTo & other) const override;
+		bool operator==(const ClearWayTo & other) const override;
 	};
 }

+ 1 - 1
AI/VCAI/Goals/CollectRes.h

@@ -35,6 +35,6 @@ namespace Goals
 		TSubgoal whatToDoToAchieve() override;
 		TSubgoal whatToDoToTrade();
 		bool fulfillsMe(TSubgoal goal) override; //TODO: Trade
-		virtual bool operator==(const CollectRes & other) const override;
+		bool operator==(const CollectRes & other) const override;
 	};
 }

+ 1 - 1
AI/VCAI/Goals/CompleteQuest.h

@@ -30,7 +30,7 @@ namespace Goals
 		TSubgoal whatToDoToAchieve() override;
 		std::string name() const override;
 		std::string completeMessage() const override;
-		virtual bool operator==(const CompleteQuest & other) const override;
+		bool operator==(const CompleteQuest & other) const override;
 
 	private:
 		TGoalVec tryCompleteQuest() const;

+ 1 - 1
AI/VCAI/Goals/Conquer.h

@@ -27,6 +27,6 @@ namespace Goals
 		}
 		TGoalVec getAllPossibleSubgoals() override;
 		TSubgoal whatToDoToAchieve() override;
-		virtual bool operator==(const Conquer & other) const override;
+		bool operator==(const Conquer & other) const override;
 	};
 }

+ 1 - 1
AI/VCAI/Goals/DigAtTile.h

@@ -36,6 +36,6 @@ namespace Goals
 			return TGoalVec();
 		}
 		TSubgoal whatToDoToAchieve() override;
-		virtual bool operator==(const DigAtTile & other) const override;
+		bool operator==(const DigAtTile & other) const override;
 	};
 }

+ 1 - 1
AI/VCAI/Goals/Explore.h

@@ -46,7 +46,7 @@ namespace Goals
 		TSubgoal whatToDoToAchieve() override;
 		std::string completeMessage() const override;
 		bool fulfillsMe(TSubgoal goal) override;
-		virtual bool operator==(const Explore & other) const override;
+		bool operator==(const Explore & other) const override;
 
 	private:
 		TSubgoal exploreNearestNeighbour(HeroPtr h) const;

+ 1 - 1
AI/VCAI/Goals/FindObj.h

@@ -42,6 +42,6 @@ namespace Goals
 		}
 		TSubgoal whatToDoToAchieve() override;
 		bool fulfillsMe(TSubgoal goal) override;
-		virtual bool operator==(const FindObj & other) const override;
+		bool operator==(const FindObj & other) const override;
 	};
 }

+ 1 - 1
AI/VCAI/Goals/GatherArmy.h

@@ -33,6 +33,6 @@ namespace Goals
 		TGoalVec getAllPossibleSubgoals() override;
 		TSubgoal whatToDoToAchieve() override;
 		std::string completeMessage() const override;
-		virtual bool operator==(const GatherArmy & other) const override;
+		bool operator==(const GatherArmy & other) const override;
 	};
 }

+ 1 - 1
AI/VCAI/Goals/GatherTroops.h

@@ -35,7 +35,7 @@ namespace Goals
 		TGoalVec getAllPossibleSubgoals() override;
 		TSubgoal whatToDoToAchieve() override;
 		bool fulfillsMe(TSubgoal goal) override;
-		virtual bool operator==(const GatherTroops & other) const override;
+		bool operator==(const GatherTroops & other) const override;
 
 	private:
 		int getCreaturesCount(const CArmedInstance * army);

+ 1 - 1
AI/VCAI/Goals/GetArtOfType.h

@@ -35,6 +35,6 @@ namespace Goals
 			return TGoalVec();
 		}
 		TSubgoal whatToDoToAchieve() override;
-		virtual bool operator==(const GetArtOfType & other) const override;
+		bool operator==(const GetArtOfType & other) const override;
 	};
 }

+ 1 - 1
AI/VCAI/Goals/Invalid.h

@@ -33,7 +33,7 @@ namespace Goals
 			return iAmElementar();
 		}
 
-		virtual bool operator==(const Invalid & other) const override
+		bool operator==(const Invalid & other) const override
 		{
 			return true;
 		}

+ 1 - 1
AI/VCAI/Goals/RecruitHero.h

@@ -33,7 +33,7 @@ namespace Goals
 
 		TSubgoal whatToDoToAchieve() override;
 
-		virtual bool operator==(const RecruitHero & other) const override
+		bool operator==(const RecruitHero & other) const override
 		{
 			return true;
 		}

+ 1 - 1
AI/VCAI/Goals/Trade.h

@@ -33,6 +33,6 @@ namespace Goals
 			priority = 3; //trading is instant, but picking resources is free
 		}
 		TSubgoal whatToDoToAchieve() override;
-		virtual bool operator==(const Trade & other) const override;
+		bool operator==(const Trade & other) const override;
 	};
 }

+ 1 - 1
AI/VCAI/Goals/VisitHero.h

@@ -37,6 +37,6 @@ namespace Goals
 		TSubgoal whatToDoToAchieve() override;
 		bool fulfillsMe(TSubgoal goal) override;
 		std::string completeMessage() const override;
-		virtual bool operator==(const VisitHero & other) const override;
+		bool operator==(const VisitHero & other) const override;
 	};
 }

+ 1 - 1
AI/VCAI/Goals/VisitObj.h

@@ -27,6 +27,6 @@ namespace Goals
 		TSubgoal whatToDoToAchieve() override;
 		bool fulfillsMe(TSubgoal goal) override;
 		std::string completeMessage() const override;
-		virtual bool operator==(const VisitObj & other) const override;
+		bool operator==(const VisitObj & other) const override;
 	};
 }

+ 1 - 1
AI/VCAI/Goals/VisitTile.h

@@ -32,6 +32,6 @@ namespace Goals
 		TGoalVec getAllPossibleSubgoals() override;
 		TSubgoal whatToDoToAchieve() override;
 		std::string completeMessage() const override;
-		virtual bool operator==(const VisitTile & other) const override;
+		bool operator==(const VisitTile & other) const override;
 	};
 }

+ 1 - 1
AI/VCAI/Goals/Win.h

@@ -31,7 +31,7 @@ namespace Goals
 		}
 		TSubgoal whatToDoToAchieve() override;
 
-		virtual bool operator==(const Win & other) const override
+		bool operator==(const Win & other) const override
 		{
 			return true;
 		}

+ 1 - 1
AI/VCAI/Pathfinding/AINodeStorage.h

@@ -99,7 +99,7 @@ public:
 		const PathfinderConfig * pathfinderConfig,
 		const CPathfinderHelper * pathfinderHelper) override;
 
-	virtual void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override;
+	void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override;
 
 	const AIPathNode * getAINode(const CGPathNode * node) const;
 	void updateAINode(CGPathNode * node, std::function<void (AIPathNode *)> updater);

+ 1 - 1
AI/VCAI/Pathfinding/AIPathfinderConfig.h

@@ -30,6 +30,6 @@ namespace AIPathfinding
 
 		~AIPathfinderConfig();
 
-		virtual CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override;
+		CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override;
 	};
 }

+ 1 - 1
CI/linux-qt6/before_install.sh

@@ -7,4 +7,4 @@ sudo apt-get install libboost-all-dev \
 libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \
 qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools \
 ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \
-libminizip-dev libfuzzylite-dev # Optional dependencies
+libminizip-dev libfuzzylite-dev libsqlite3-dev # Optional dependencies

+ 1 - 1
CI/linux/before_install.sh

@@ -7,4 +7,4 @@ sudo apt-get install libboost-all-dev \
 libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \
 qtbase5-dev \
 ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \
-libminizip-dev libfuzzylite-dev qttools5-dev # Optional dependencies
+libminizip-dev libfuzzylite-dev qttools5-dev libsqlite3-dev # Optional dependencies

+ 16 - 0
CMakeLists.txt

@@ -41,6 +41,7 @@ if(NOT CMAKE_BUILD_TYPE)
 	set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS Debug Release RelWithDebInfo)
 endif()
 
+set(buildLobby OFF)
 set(singleProcess OFF)
 set(staticAI OFF)
 if(ANDROID)
@@ -89,6 +90,14 @@ if(NOT APPLE_IOS AND NOT ANDROID)
 	option(ENABLE_MONOLITHIC_INSTALL "Install everything in single directory on Linux and Mac" OFF)
 endif()
 
+if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
+	set(buildLobby ON)
+endif()
+
+if(NOT APPLE_IOS AND NOT ANDROID)
+	option(ENABLE_LOBBY "Enable compilation of lobby server" ${buildLobby})
+endif()
+
 option(ENABLE_CCACHE "Speed up recompilation by caching previous compilations" OFF)
 if(ENABLE_CCACHE)
 	find_program(CCACHE ccache REQUIRED)
@@ -475,6 +484,10 @@ if(TARGET SDL2_ttf::SDL2_ttf)
 	add_library(SDL2::TTF ALIAS SDL2_ttf::SDL2_ttf)
 endif()
 
+if(ENABLE_LOBBY)
+	find_package(SQLite3 REQUIRED)
+endif()
+
 if(ENABLE_LAUNCHER OR ENABLE_EDITOR)
 	# Widgets finds its own dependencies (QtGui and QtCore).
 	find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets Network)
@@ -622,6 +635,9 @@ endif()
 if(ENABLE_EDITOR)
 	add_subdirectory(mapeditor)
 endif()
+if(ENABLE_LOBBY)
+	add_subdirectory(lobby)
+endif()
 add_subdirectory(client)
 add_subdirectory(server)
 if(ENABLE_TEST)

+ 72 - 51
Mods/vcmi/config/vcmi/chinese.json

@@ -20,6 +20,7 @@
 	"vcmi.adventureMap.playerAttacked"         : "玩家遭受攻击: %s",
 	"vcmi.adventureMap.moveCostDetails"        : "移动点数 - 花费: %TURNS 轮 + %POINTS 点移动力, 剩余移动力: %REMAINING",
 	"vcmi.adventureMap.moveCostDetailsNoTurns" : "移动点数 - 花费: %POINTS 点移动力, 剩余移动力: %REMAINING",
+	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "抱歉,重放对手行动功能目前暂未实现!",
 
 	"vcmi.capitalColors.0" : "红色",
 	"vcmi.capitalColors.1" : "蓝色",
@@ -70,7 +71,9 @@
 	"vcmi.lobby.mapPreview" : "地图预览",
 	"vcmi.lobby.noPreview" : "无地上部分",
 	"vcmi.lobby.noUnderground" : "无地下部分",
+	"vcmi.lobby.sortDate" : "以修改时间排序地图",
 
+	"vcmi.client.errors.invalidMap" : "{非法地图或战役}\n\n启动游戏失败,选择的地图或者战役,无效或被污染。原因:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{找不到数据文件}\n\n没有找到战役数据文件!你可能使用了不完整或损坏的英雄无敌3数据文件,请重新安装数据文件。",
 	"vcmi.server.errors.existingProcess"     : "一个VCMI进程已经在运行,启动新进程前请结束它。",
 	"vcmi.server.errors.modsToEnable"    : "{需要启用的mod列表}",
@@ -81,9 +84,9 @@
 	"vcmi.server.errors.unknownEntity" : "加载保存失败! 在保存的游戏中发现未知实体'%s'! 保存可能与当前安装的mod版本不兼容!",
 
 	"vcmi.settingsMainWindow.generalTab.hover"   : "常规",
-	"vcmi.settingsMainWindow.generalTab.help"    : "切换到“常规”选项卡 - 设置游戏客户端呈现",
+	"vcmi.settingsMainWindow.generalTab.help"    : "切换到“常规”选项卡 - 配置客户端常规内容",
 	"vcmi.settingsMainWindow.battleTab.hover"    : "战斗",
-	"vcmi.settingsMainWindow.battleTab.help"     : "切换到“战斗”选项卡 - 这些设置允许配置战斗界面和相关内容",
+	"vcmi.settingsMainWindow.battleTab.help"     : "切换到“战斗”选项卡 - 配置游戏战斗界面内容",
 	"vcmi.settingsMainWindow.adventureTab.hover" : "冒险地图",
 	"vcmi.settingsMainWindow.adventureTab.help"  : "切换到“冒险地图”选项卡 - 冒险地图即玩家能操作英雄移动的界面",
 
@@ -93,43 +96,43 @@
 	"vcmi.systemOptions.townsGroup" : "城镇画面",
 
 	"vcmi.systemOptions.fullscreenBorderless.hover" : "全屏 (无边框)",
-	"vcmi.systemOptions.fullscreenBorderless.help"  : "{全屏}\n\n选中时,VCMI将以无边框全屏模式运行。在这种模式下,游戏会使用和桌面一致的分辨率而非设置的分辨率。 ",
+	"vcmi.systemOptions.fullscreenBorderless.help"  : "{全屏}\n\n选中时,VCMI将以无边框全屏模式运行。该模式下,游戏会始终和桌面分辨率保持一致,无视设置的分辨率。 ",
 	"vcmi.systemOptions.fullscreenExclusive.hover"  : "全屏 (独占)",
-	"vcmi.systemOptions.fullscreenExclusive.help"   : "{全屏}\n\n选中时,VCMI将以独占全屏模式运行。在这种模式下,游戏会将显示器分辨率改变为设置值。",
-	"vcmi.systemOptions.resolutionButton.hover" : "分辨率",
-	"vcmi.systemOptions.resolutionButton.help"  : "{分辨率选择}\n\n改变游戏内的分辨率,更改后需要重启游戏使其生效。",
-	"vcmi.systemOptions.resolutionMenu.hover"   : "分辨率选择",
+	"vcmi.systemOptions.fullscreenExclusive.help"   : "{全屏}\n\n选中时,VCMI将以独占全屏模式运行。模式下,游戏会将显示器分辨率改变为设置值。",
+	"vcmi.systemOptions.resolutionButton.hover" : "分辨率: %wx%h",
+	"vcmi.systemOptions.resolutionButton.help"  : "{分辨率选择}\n\n改变游戏内的分辨率。",
+	"vcmi.systemOptions.resolutionMenu.hover"   : "选择分辨率",
 	"vcmi.systemOptions.resolutionMenu.help"    : "修改游戏运行时的分辨率。",
-	"vcmi.systemOptions.scalingButton.hover"   : "界面大小: %p%",
-	"vcmi.systemOptions.scalingButton.help"    : "{界面大小}\n\n改变界面的大小",
-	"vcmi.systemOptions.scalingMenu.hover"     : "选择界面大小",
-	"vcmi.systemOptions.scalingMenu.help"      : "改变游戏界面大小。",
-	"vcmi.systemOptions.longTouchButton.hover"   : "触控间距: %d 毫秒", // Translation note: "ms" = "milliseconds"
-	"vcmi.systemOptions.longTouchButton.help"    : "{触控间距}\n\n使用触摸屏时,触摸屏幕指定持续时间(以毫秒为单位)后将出现弹出窗口。",
-	"vcmi.systemOptions.longTouchMenu.hover"     : "选择触控间距",
-	"vcmi.systemOptions.longTouchMenu.help"      : "改变触控间距。",
+	"vcmi.systemOptions.scalingButton.hover"   : "界面缩放: %p%",
+	"vcmi.systemOptions.scalingButton.help"    : "{界面缩放}\n\n改变用户界面的缩放比例。",
+	"vcmi.systemOptions.scalingMenu.hover"     : "选择界面缩放",
+	"vcmi.systemOptions.scalingMenu.help"      : "改变游戏内界面缩放。",
+	"vcmi.systemOptions.longTouchButton.hover"   : "长触延迟: %d 毫秒", // Translation note: "ms" = "milliseconds"
+	"vcmi.systemOptions.longTouchButton.help"    : "{长触延迟}\n\n使用触摸屏时,长触屏幕一定时间后出现弹出窗口(单位:毫秒)。",
+	"vcmi.systemOptions.longTouchMenu.hover"     : "选择长触延迟",
+	"vcmi.systemOptions.longTouchMenu.help"      : "改变长触延迟。",
 	"vcmi.systemOptions.longTouchMenu.entry"     : "%d 毫秒",
 	"vcmi.systemOptions.framerateButton.hover"  : "显示FPS",
-	"vcmi.systemOptions.framerateButton.help"   : "{显示FPS}\n\n打开/关闭在游戏窗口角落的FPS指示器。",
+	"vcmi.systemOptions.framerateButton.help"   : "{显示FPS}\n\n切换在游戏窗口角落显FPS指示器。",
 	"vcmi.systemOptions.hapticFeedbackButton.hover"  : "触觉反馈",
 	"vcmi.systemOptions.hapticFeedbackButton.help"   : "{触觉反馈}\n\n切换触摸输入的触觉反馈。",
 	"vcmi.systemOptions.enableUiEnhancementsButton.hover"  : "界面增强",
-	"vcmi.systemOptions.enableUiEnhancementsButton.help"   : "{界面增强}\n\n显示所有界面增强内容,如大背包和魔法书等。",
-	"vcmi.systemOptions.enableLargeSpellbookButton.hover"  : "增大魔法书界面",
-	"vcmi.systemOptions.enableLargeSpellbookButton.help"   : "{增大魔法书界面}\n\n可以在魔法书单页中显示更多的魔法,从而获得更好的视觉效果。",
-	"vcmi.systemOptions.audioMuteFocus.hover"  : "切换窗口时静音",
-	"vcmi.systemOptions.audioMuteFocus.help"   : "{切换窗口时静音}\n\n快速切换窗口时将静音,在工作时,切换游戏窗口不会有声音。",
+	"vcmi.systemOptions.enableUiEnhancementsButton.help"   : "{界面增强}\n\n显示所有界面优化增强,如背包按钮等。关闭该项以贴近经典模式。",
+	"vcmi.systemOptions.enableLargeSpellbookButton.hover"  : "扩展魔法书",
+	"vcmi.systemOptions.enableLargeSpellbookButton.help"   : "{扩展魔法书}\n\n启用更大的魔法书界面,每页展示更多魔法,但魔法书翻页特效在该模式下无法呈现。",
+	"vcmi.systemOptions.audioMuteFocus.hover"  : "失去焦点时静音",
+	"vcmi.systemOptions.audioMuteFocus.help"   : "{失去焦点时静音}\n\n当窗口失去焦点时静音,游戏内消息提示和新回合提示除外。",
 
 	"vcmi.adventureOptions.infoBarPick.hover" : "在信息面板显示消息",
-	"vcmi.adventureOptions.infoBarPick.help" : "{在信息面板显示消息}\n\n来自访问地图物件的信息将显示在信息面板,而不是弹出窗口。",
+	"vcmi.adventureOptions.infoBarPick.help" : "{在信息面板显示消息}\n\n尽可能将来自访问地图物件的信息将显示在信息面板,而不是弹出窗口。",
 	"vcmi.adventureOptions.numericQuantities.hover" : "生物数量显示",
 	"vcmi.adventureOptions.numericQuantities.help" : "{生物数量显示}\n\n以数字 A-B 格式显示不准确的敌方生物数量。",
-	"vcmi.adventureOptions.forceMovementInfo.hover" : "在状态栏中显示移动力",
-	"vcmi.adventureOptions.forceMovementInfo.help" : "{在状态栏中显示移动力}\n\n不需要按ALT就可以显示移动力。",
+	"vcmi.adventureOptions.forceMovementInfo.hover" : "总是显示移动花费",
+	"vcmi.adventureOptions.forceMovementInfo.help" : "{总是显示移动花费}\n\n总是在状态栏中显示行动力花费(否则仅在按下ALT时显示)。",
 	"vcmi.adventureOptions.showGrid.hover" : "显示网格",
 	"vcmi.adventureOptions.showGrid.help" : "{显示网格}\n\n显示网格覆盖层,高亮冒险地图物件的边沿。",
-	"vcmi.adventureOptions.borderScroll.hover" : "滚动边界",
-	"vcmi.adventureOptions.borderScroll.help" : "{滚动边界}\n\n当光标靠近窗口边缘时滚动冒险地图。 可以通过按住 CTRL 键来禁用。",
+	"vcmi.adventureOptions.borderScroll.hover" : "边缘滚动",
+	"vcmi.adventureOptions.borderScroll.help" : "{边缘滚动}\n\n当光标靠近窗口边缘时滚动冒险地图。 可以通过按住 CTRL 键来禁用。",
 	"vcmi.adventureOptions.infoBarCreatureManagement.hover" : "信息面板生物管理",
 	"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{信息面板生物管理}\n\n允许在信息面板中重新排列生物,而不是在默认组件之间循环。",
 	"vcmi.adventureOptions.leftButtonDrag.hover" : "左键拖动地图",
@@ -137,13 +140,15 @@
 	"vcmi.adventureOptions.smoothDragging.hover" : "平滑地图拖动",
 	"vcmi.adventureOptions.smoothDragging.help" : "{平滑地图拖动}\n\n启用后,地图拖动会产生柔和的羽化效果。",
 	"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "关闭淡入淡出特效",
-	"vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{关闭淡入淡出特效}\n\n启用后,跳过物体淡出或类似特效(资源收集,登船等)。设置此项能在渲染开销重时能够加快UI的响应,尤其是在PvP对战中。当移动速度被设置为最大时忽略此项设置。",
+	"vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{关闭淡入淡出特效}\n\n启用后,跳过物体淡出或类似特效(资源收集,登船等)。设置此项能在渲染开销重时能够加快UI的响应,尤其是在PvP对战中。当移动速度被设置为最大时忽略此项设置。",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed1.help": "将地图卷动速度设置为非常慢",
 	"vcmi.adventureOptions.mapScrollSpeed5.help": "将地图卷动速度设置为非常快",
 	"vcmi.adventureOptions.mapScrollSpeed6.help": "将地图卷动速度设置为即刻。",
+	"vcmi.adventureOptions.hideBackground.hover" : "隐藏背景",
+	"vcmi.adventureOptions.hideBackground.help" : "{隐藏背景}\n\n隐藏冒险地图背景,以显示贴图代替。",
 
 	"vcmi.battleOptions.queueSizeLabel.hover": "回合顺序指示器",
 	"vcmi.battleOptions.queueSizeNoneButton.hover": "关闭",
@@ -168,6 +173,8 @@
 	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{显示英雄统计数据窗口}\n\n永久切换并显示主要统计数据和法术点的英雄统计数据窗口。",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "跳过战斗开始音乐",
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{跳过战斗开始音乐}\n\n战斗开始音乐播放期间,你也能够进行操作。",
+	"vcmi.battleOptions.endWithAutocombat.hover": "结束战斗",
+	"vcmi.battleOptions.endWithAutocombat.help": "{结束战斗}\n\n以自动战斗立即结束剩余战斗过程",
 	
 	"vcmi.adventureMap.revisitObject.hover" : "重新访问",
 	"vcmi.adventureMap.revisitObject.help" : "{重新访问}\n\n让当前英雄重新访问地图建筑或城镇。",
@@ -183,17 +190,22 @@
 	"vcmi.battleWindow.damageEstimation.damage.1" : "%d 伤害",
 	"vcmi.battleWindow.damageEstimation.kills" : "%d 将被消灭",
 	"vcmi.battleWindow.damageEstimation.kills.1" : "%d 将被消灭",
+	"vcmi.battleWindow.killed" : "已消灭",
+	"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s 死于精准射击",
+	"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s 死于精准射击",
+	"vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s 死于精准射击",
+	"vcmi.battleWindow.endWithAutocombat" : "您确定想以自动战斗立即结束吗?",
 
 	"vcmi.battleResultsWindow.applyResultsLabel" : "接受战斗结果",
 	
 	"vcmi.tutorialWindow.title" : "触摸屏介绍",
-	"vcmi.tutorialWindow.decription.RightClick" : "触摸并按住要右键单击的元素。 触摸可用区域以关闭。",
-	"vcmi.tutorialWindow.decription.MapPanning" : "用一根手指触摸并拖动来移动地图。",
-	"vcmi.tutorialWindow.decription.MapZooming" : "用两根手指捏合可更改地图缩放比例。",
+	"vcmi.tutorialWindow.decription.RightClick" : "长按要右键单击的元素。 触摸其他区域以关闭。",
+	"vcmi.tutorialWindow.decription.MapPanning" : "单指拖拽以移动地图。",
+	"vcmi.tutorialWindow.decription.MapZooming" : "两指开合更改地图缩放比例。",
 	"vcmi.tutorialWindow.decription.RadialWheel" : "滑动可打开径向轮以执行各种操作,例如生物/英雄管理和城镇排序。",
 	"vcmi.tutorialWindow.decription.BattleDirection" : "要从特定方向攻击,请向要进行攻击的方向滑动。",
-	"vcmi.tutorialWindow.decription.BattleDirectionAbort" : "如果手指距离足够远,可以取消攻击方向手势。",
-	"vcmi.tutorialWindow.decription.AbortSpell" : "触摸并按住可取消魔法。",
+	"vcmi.tutorialWindow.decription.BattleDirectionAbort" : "将长触状态的手指拉远足够距离,可以取消攻击方向手势。",
+	"vcmi.tutorialWindow.decription.AbortSpell" : "长触以取消魔法。",
 
 	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "显示可招募生物",
 	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{显示可招募生物}\n\n在城镇摘要(城镇屏幕的左下角)中显示可招募的生物数量,而不是增长。",
@@ -212,7 +224,7 @@
 	"vcmi.townHall.greetingDefence"         : "在%s中稍待片刻,富有战斗经验的战士会教你防御技巧(防御力+1)。",
 	"vcmi.townHall.hasNotProduced"          : "本周%s并没有产生什么资源。",
 	"vcmi.townHall.hasProduced"             : "本周%s产生了%d个%s。",
-	"vcmi.townHall.greetingCustomBonus"     : "当你的英雄访问%s 时,这个神奇的建筑使你的英雄 +%d %s%s。",
+	"vcmi.townHall.greetingCustomBonus"     : "%s 给予英雄 +%d %s%s。",
 	"vcmi.townHall.greetingCustomUntil"     : "直到下一场战斗。",
 	"vcmi.townHall.greetingInTownMagicWell" : "%s使你的魔法值恢复到最大值。",
 
@@ -225,13 +237,15 @@
 	"vcmi.heroWindow.openBackpack.hover" : "开启宝物背包界面",
 	"vcmi.heroWindow.openBackpack.help"  : "用更大的界面显示所有获得的宝物",
 
+	"vcmi.tavernWindow.inviteHero"  : "邀请英雄",
+
 	"vcmi.commanderWindow.artifactMessage" : "你要把这个宝物还给英雄吗?",
 
 	"vcmi.creatureWindow.showBonuses.hover"    : "属性视图",
 	"vcmi.creatureWindow.showBonuses.help"     : "显示指挥官的所有属性增益",
 	"vcmi.creatureWindow.showSkills.hover"     : "技能视图",
 	"vcmi.creatureWindow.showSkills.help"      : "显示指挥官的所有学习的技能",
-	"vcmi.creatureWindow.returnArtifact.hover" : "交换宝物",
+	"vcmi.creatureWindow.returnArtifact.hover" : "返还宝物",
 	"vcmi.creatureWindow.returnArtifact.help"  : "点击这个按钮将宝物反还到英雄的背包里",
 
 	"vcmi.questLog.hideComplete.hover" : "隐藏完成任务",
@@ -321,6 +335,7 @@
 	"vcmi.map.victoryCondition.collectArtifacts.message" : "获得所有三件宝物",
 	"vcmi.map.victoryCondition.angelicAlliance.toSelf" : "祝贺你!你取得了天使联盟且消灭了所有敌人,取得了胜利!",
 	"vcmi.map.victoryCondition.angelicAlliance.message" : "击败所有敌人并取得天使联盟",
+	"vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" : "功亏一篑,你已失去了天使联盟的一个组件。彻底的失败。",
 
 	// few strings from WoG used by vcmi
 	"vcmi.stackExperience.description" : "» 经 验 获 得 明 细 «\n\n生物类型 ................... : %s\n经验等级 ................. : %s (%i)\n经验点数 ............... : %i\n下一个等级所需经验 .. : %i\n每次战斗最大获得经验 ... : %i%% (%i)\n获得经验的生物数量 .... : %i\n最大招募数量\n不会丢失经验升级 .... : %i\n经验倍数 ........... : %.2f\n升级倍数 .............. : %.2f\n10级后经验值 ........ : %i\n最大招募数量下\n 升级到10级所需经验数量: %i",
@@ -374,8 +389,10 @@
 	"core.bonus.ENCHANTER.description": "每回合群体施放${subtype.spell}",
 	"core.bonus.ENCHANTED.name": "法术加持",
 	"core.bonus.ENCHANTED.description": "永久处于${subtype.spell}影响",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.name": "忽略攻击 (${val}%)",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.description": "被攻击时,进攻方${val}%的攻击力将被无视。",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.name": "忽略防御 (${val}%)",
-	"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "当攻击时,目标生物${val}%的防御力将被无视。",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "发动攻击时,防御方${val}%的防御力将被无视。",
 	"core.bonus.FIRE_IMMUNITY.name": "火系免疫",
 	"core.bonus.FIRE_IMMUNITY.description": "免疫所有火系魔法。",
 	"core.bonus.FIRE_SHIELD.name": "烈火神盾 (${val}%)",
@@ -386,7 +403,9 @@
 	"core.bonus.FEAR.description": "使得敌方一只部队恐惧",
 	"core.bonus.FEARLESS.name": "无惧",
 	"core.bonus.FEARLESS.description": "免疫恐惧特质",
-	"core.bonus.FLYING.name": "飞行兵种",
+	"core.bonus.FEROCITY.name": "凶猛追击",
+	"core.bonus.FEROCITY.description": "杀死任意生物后额外攻击${val}次",
+	"core.bonus.FLYING.name": "飞行能力",
 	"core.bonus.FLYING.description": "以飞行的方式移动(无视障碍)",
 	"core.bonus.FREE_SHOOTING.name": "近身射击",
 	"core.bonus.FREE_SHOOTING.description": "能在近战范围内进行射击",
@@ -396,50 +415,52 @@
 	"core.bonus.GENERAL_DAMAGE_REDUCTION.description": "减少从远程和近战中遭受的物理伤害",
 	"core.bonus.HATE.name": "${subtype.creature}的死敌",
 	"core.bonus.HATE.description": "对${subtype.creature}造成额外${val}%伤害",
-	"core.bonus.HEALER.name": "治疗",
+	"core.bonus.HEALER.name": "治疗",
 	"core.bonus.HEALER.description": "可以治疗友军单位",
 	"core.bonus.HP_REGENERATION.name": "再生",
 	"core.bonus.HP_REGENERATION.description": "每回合恢复${val}点生命值",
-	"core.bonus.JOUSTING.name": "冲锋",
+	"core.bonus.JOUSTING.name": "勇士冲锋",
 	"core.bonus.JOUSTING.description": "每移动一格 +${val}%伤害",
-	"core.bonus.KING.name": "顶级怪物",
+	"core.bonus.KING.name": "王牌",
 	"core.bonus.KING.description": "受${val}级或更高级屠戮成性影响",
 	"core.bonus.LEVEL_SPELL_IMMUNITY.name": "免疫1-${val}级魔法",
 	"core.bonus.LEVEL_SPELL_IMMUNITY.description": "免疫1-${val}级的魔法",
-	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "受限射击距离",
-	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "无法以${val}格外的单位为射击目标",
+	"core.bonus.LIMITED_SHOOTING_RANGE.name": "射程限制",
+	"core.bonus.LIMITED_SHOOTING_RANGE.description": "无法瞄准${val}格以外的单位",
 	"core.bonus.LIFE_DRAIN.name": "吸取生命 (${val}%)",
 	"core.bonus.LIFE_DRAIN.description": "吸取${val}%伤害回复自身",
-	"core.bonus.MANA_CHANNELING.name": "法虹吸${val}%",
+	"core.bonus.MANA_CHANNELING.name": "法虹吸${val}%",
 	"core.bonus.MANA_CHANNELING.description": "使你的英雄有${val}%几率获得敌人施法的魔法值",
-	"core.bonus.MANA_DRAIN.name": "吸取力",
+	"core.bonus.MANA_DRAIN.name": "吸取力",
 	"core.bonus.MANA_DRAIN.description": "每回合吸取${val}魔法值",
 	"core.bonus.MAGIC_MIRROR.name": "魔法神镜 (${val}%)",
 	"core.bonus.MAGIC_MIRROR.description": "${val}%几率将进攻性魔法导向一个敌人单位",
 	"core.bonus.MAGIC_RESISTANCE.name": "魔法抵抗 (${val}%)",
 	"core.bonus.MAGIC_RESISTANCE.description": "${val}%几率抵抗敌人的魔法",
-	"core.bonus.MIND_IMMUNITY.name": "免疫心智",
-	"core.bonus.MIND_IMMUNITY.description": "不受心智魔法影响",
-	"core.bonus.NO_DISTANCE_PENALTY.name": "无视距离惩罚",
-	"core.bonus.NO_DISTANCE_PENALTY.description": "任意距离均造成全额伤害",
+	"core.bonus.MIND_IMMUNITY.name": "免疫心智魔法",
+	"core.bonus.MIND_IMMUNITY.description": "不受心智相关的魔法影响",
+	"core.bonus.NO_DISTANCE_PENALTY.name": "无视射程惩罚",
+	"core.bonus.NO_DISTANCE_PENALTY.description": "任意射程造成全额伤害",
 	"core.bonus.NO_MELEE_PENALTY.name": "无近战惩罚",
 	"core.bonus.NO_MELEE_PENALTY.description": "该生物没有近战伤害惩罚",
 	"core.bonus.NO_MORALE.name": "无士气",
 	"core.bonus.NO_MORALE.description": "生物不受士气影响",
-	"core.bonus.NO_WALL_PENALTY.name": "无城墙影响",
-	"core.bonus.NO_WALL_PENALTY.description": "攻城战中不被城墙阻挡造成全额伤害",
+	"core.bonus.NO_WALL_PENALTY.name": "无城墙惩罚",
+	"core.bonus.NO_WALL_PENALTY.description": "攻城战中无视城墙阻挡,造成全额伤害",
 	"core.bonus.NON_LIVING.name": "无生命",
 	"core.bonus.NON_LIVING.description": "免疫大多数的效果",
 	"core.bonus.RANDOM_SPELLCASTER.name": "随机施法",
 	"core.bonus.RANDOM_SPELLCASTER.description": "可以施放随机魔法",
 	"core.bonus.RANGED_RETALIATION.name": "远程反击",
 	"core.bonus.RANGED_RETALIATION.description": "可以对远程攻击进行反击",
-	"core.bonus.RECEPTIVE.name": "接",
+	"core.bonus.RECEPTIVE.name": "接",
 	"core.bonus.RECEPTIVE.description": "不会免疫有益魔法",
 	"core.bonus.REBIRTH.name": "复生 (${val}%)",
 	"core.bonus.REBIRTH.description": "当整支部队死亡后${val}%会复活",
 	"core.bonus.RETURN_AFTER_STRIKE.name": "攻击后返回",
 	"core.bonus.RETURN_AFTER_STRIKE.description": "近战攻击后回到初始位置",
+	"core.bonus.REVENGE.name": "复仇",
+	"core.bonus.REVENGE.description": "根据攻击者在战斗中失去的生命值造成额外伤害",
 	"core.bonus.SHOOTER.name": "远程攻击",
 	"core.bonus.SHOOTER.description": "生物可以射击",
 	"core.bonus.SHOOTS_ALL_ADJACENT.name": "范围远程攻击",
@@ -471,7 +492,7 @@
 	"core.bonus.TRANSMUTATION.name": "变形术",
 	"core.bonus.TRANSMUTATION.description": "${val}%机会将被攻击单位变成其他生物",
 	"core.bonus.UNDEAD.name": "不死生物",
-	"core.bonus.UNDEAD.description": "该生物属于丧尸",
+	"core.bonus.UNDEAD.description": "该生物属于不死生物",
 	"core.bonus.UNLIMITED_RETALIATIONS.name": "无限反击",
 	"core.bonus.UNLIMITED_RETALIATIONS.description": "每回合可以无限反击敌人",
 	"core.bonus.WATER_IMMUNITY.name": "水系免疫",

+ 22 - 2
Mods/vcmi/config/vcmi/english.json

@@ -63,7 +63,6 @@
 	"vcmi.mainMenu.serverClosing" : "Closing...",
 	"vcmi.mainMenu.hostTCP" : "Host TCP/IP game",
 	"vcmi.mainMenu.joinTCP" : "Join TCP/IP game",
-	"vcmi.mainMenu.playerName" : "Player",
 	
 	"vcmi.lobby.filepath" : "File path",
 	"vcmi.lobby.creationDate" : "Creation date",
@@ -72,13 +71,34 @@
 	"vcmi.lobby.noPreview" : "no preview",
 	"vcmi.lobby.noUnderground" : "no underground",
 	"vcmi.lobby.sortDate" : "Sorts maps by change date",
+	
+	"vcmi.lobby.login.title" : "VCMI Lobby",
+	"vcmi.lobby.login.username" : "Username:",
+	"vcmi.lobby.login.connecting" : "Connecting...",
+	"vcmi.lobby.login.error" : "Connection error: %s",
+	"vcmi.lobby.login.create" : "New Account",
+	"vcmi.lobby.login.login" : "Login",
+
+	"vcmi.lobby.room.create" : "Create Room",
+	"vcmi.lobby.room.players.limit" : "Players Limit",
+	"vcmi.lobby.room.public" : "Public",
+	"vcmi.lobby.room.private" : "Private",
+	"vcmi.lobby.room.description.public" : "Any player can join public room.",
+	"vcmi.lobby.room.description.private" : "Only invited players can join private room.",
+	"vcmi.lobby.room.description.new" : "To start the game, select a scenario or set up a random map.",
+	"vcmi.lobby.room.description.load" : "To start the game, use one of your saved games.",
+	"vcmi.lobby.room.description.limit" : "Up to %d players can enter your room, including you.",
+	"vcmi.lobby.room.new" : "New Game",
+	"vcmi.lobby.room.load" : "Load Game",
+	"vcmi.lobby.room.type" : "Room Type",
+	"vcmi.lobby.room.mode" : "Game Mode",
 
 	"vcmi.client.errors.invalidMap" : "{Invalid map or campaign}\n\nFailed to start game! Selected map or campaign might be invalid or corrupted. Reason:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{Missing data files}\n\nCampaigns data files were not found! You may be using incomplete or corrupted Heroes 3 data files. Please reinstall game data.",
+	"vcmi.server.errors.disconnected" : "{Network Error}\n\nConnection to game server has been lost!",
 	"vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.",
 	"vcmi.server.errors.modsToEnable"    : "{Following mods are required}",
 	"vcmi.server.errors.modsToDisable"   : "{Following mods must be disabled}",
-	"vcmi.server.confirmReconnect"       : "Do you want to reconnect to the last session?",
 	"vcmi.server.errors.modNoDependency" : "Failed to load mod {'%s'}!\n It depends on mod {'%s'} which is not active!\n",
 	"vcmi.server.errors.modConflict" : "Failed to load mod {'%s'}!\n Conflicts with active mod {'%s'}!\n",
 	"vcmi.server.errors.unknownEntity" : "Failed to load save! Unknown entity '%s' found in saved game! Save may not be compatible with currently installed version of mods!",

+ 24 - 3
Mods/vcmi/config/vcmi/spanish.json

@@ -20,6 +20,7 @@
 	"vcmi.adventureMap.playerAttacked"         : "El jugador ha sido atacado: %s",
 	"vcmi.adventureMap.moveCostDetails"        : "Puntos de movimiento - Coste: %TURNS turnos + %POINTS puntos, Puntos restantes: %REMAINING",
 	"vcmi.adventureMap.moveCostDetailsNoTurns" : "Puntos de movimiento - Coste: %POINTS puntos, Puntos restantes: %REMAINING",
+	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Disculpe, la repetición del turno del oponente aún no está implementada.",
 
 	"vcmi.capitalColors.0" : "Rojo",
 	"vcmi.capitalColors.1" : "Azul",
@@ -70,7 +71,9 @@
 	"vcmi.lobby.mapPreview" : "Vista previa del mapa",
 	"vcmi.lobby.noPreview" : "sin vista previa",
 	"vcmi.lobby.noUnderground" : "sin subterráneo",
+	"vcmi.lobby.sortDate" : "Ordena los mapas por la fecha de modificación",
 
+	"vcmi.client.errors.invalidMap" : "{Mapa o Campaña invalido}\n\n¡No se pudo iniciar el juego! El mapa o la campaña seleccionados pueden no ser válidos o estar dañados. Motivo:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{Archivos de datos faltantes}\n\n¡No se encontraron los archivos de datos de las campañas! Quizás estés utilizando archivos de datos incompletos o dañados de Heroes 3. Por favor, reinstala los datos del juego.",
 	"vcmi.server.errors.existingProcess" : "Otro servidor VCMI está en ejecución. Por favor, termínalo antes de comenzar un nuevo juego.",
 	"vcmi.server.errors.modsToEnable"    : "{Se requieren los siguientes mods}",
@@ -144,6 +147,8 @@
 	"vcmi.adventureOptions.mapScrollSpeed1.help": "Establece la velocidad de desplazamiento del mapa como muy lenta",
 	"vcmi.adventureOptions.mapScrollSpeed5.help": "Establece la velocidad de desplazamiento del mapa como muy rápida",
 	"vcmi.adventureOptions.mapScrollSpeed6.help": "Establece la velocidad de desplazamiento del mapa como instantánea.",
+	"vcmi.adventureOptions.hideBackground.hover" : "Ocultar fondo",
+	"vcmi.adventureOptions.hideBackground.help" : "{Ocultar fondo}\n\nOculta el mapa de aventuras en el fondo y muestra una textura en su lugar..",
 
 	"vcmi.battleOptions.queueSizeLabel.hover": "Mostrar orden de turno de criaturas",
 	"vcmi.battleOptions.queueSizeNoneButton.hover": "APAGADO",
@@ -168,6 +173,8 @@
 	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Mostrar ventanas de estadísticas de héroes}\n\nAlternar permanentemente las ventanas de estadísticas de héroes que muestran estadísticas primarias y puntos de hechizo.",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Omitir música de introducción",
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Omitir música de introducción}\n\nPermitir acciones durante la música de introducción que se reproduce al comienzo de cada batalla.",
+	"vcmi.battleOptions.endWithAutocombat.hover": "Finaliza la batalla",
+	"vcmi.battleOptions.endWithAutocombat.help": "{Finaliza la batalla}\n\nAutomatiza la batalla y la finaliza al instante",
 
 	"vcmi.adventureMap.revisitObject.hover" : "Revisitar objeto",
 	"vcmi.adventureMap.revisitObject.help" : "{Revisitar objeto}\n\nSi un héroe se encuentra actualmente en un objeto del mapa, puede volver a visitar la ubicación.",
@@ -183,6 +190,11 @@
 	"vcmi.battleWindow.damageEstimation.damage.1" : "%d daño",
 	"vcmi.battleWindow.damageEstimation.kills" : "%d perecerán",
 	"vcmi.battleWindow.damageEstimation.kills.1" : "%d perecerá",
+	"vcmi.battleWindow.killed" : "Eliminados",
+	"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s han sido eliminados por disparos certeros",
+	"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s ha sido eliminado por un disparo certero",
+	"vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s han sido eliminados por disparos certeros",
+	"vcmi.battleWindow.endWithAutocombat" : "¿Quieres finalizar la batalla con combate automatizado?",
 
 	"vcmi.battleResultsWindow.applyResultsLabel" : "Aplicar resultado de la batalla",
 
@@ -225,6 +237,8 @@
 	"vcmi.heroWindow.openBackpack.hover" : "Abrir ventana de mochila de artefactos",
 	"vcmi.heroWindow.openBackpack.help"  : "Abre la ventana que facilita la gestión de la mochila de artefactos.",
 
+	"vcmi.tavernWindow.inviteHero"  : "Invitar heroe",
+
 	"vcmi.commanderWindow.artifactMessage" : "¿Quieres devolver este artefacto al héroe?",
 
 	"vcmi.creatureWindow.showBonuses.hover"    : "Cambiar a vista de bonificaciones",
@@ -321,6 +335,7 @@
 	"vcmi.map.victoryCondition.collectArtifacts.message" : "Adquirir tres artefactos",
 	"vcmi.map.victoryCondition.angelicAlliance.toSelf" : "¡Felicidades! Todos tus enemigos han sido derrotados y tienes la Alianza Angelical. ¡La victoria es tuya!",
 	"vcmi.map.victoryCondition.angelicAlliance.message" : "Derrota a todos los enemigos y crea la Alianza Angelical",
+	"vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" : "Por desgracia, has perdido parte de la Alianza Angélica. Todo se ha perdido.",
 
 	// few strings from WoG used by vcmi
 	"vcmi.stackExperience.description" : "» D e t a l l e s  d e  E x p e r i e n c i a  d e l  G r u p o «\n\nTipo de Criatura ................ : %s\nRango de Experiencia ............ : %s (%i)\nPuntos de Experiencia ............ : %i\nPuntos de Experiencia para el\nSiguiente Rango ............... : %i\nExperiencia Máxima por Batalla .. : %i%% (%i)\nNúmero de Criaturas en el grupo .. : %i\nMáximo de Nuevos Reclutas sin\nPerder el Rango Actual ......... : %i\nMultiplicador de Experiencia .... : %.2f\nMultiplicador de Actualización .. : %.2f\nExperiencia después del Rango 10 : %i\nMáximo de Nuevos Reclutas para\nMantener el Rango 10 si\nEstá en la Experiencia Máxima : %i",
@@ -374,6 +389,8 @@
 	"core.bonus.ENCHANTER.description": "Puede lanzar ${subtype.spell} masivo cada turno",
 	"core.bonus.ENCHANTED.name": "Encantado",
 	"core.bonus.ENCHANTED.description": "Afectado por el hechizo permanente ${subtype.spell}",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.name": "Ignorar ataque (${val}%)",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.description": "Al ser atacado, ${val}% del daño del atacante es ignorado",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignorar Defensa (${val}%)",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Ignora una parte de la defensa al atacar",
 	"core.bonus.FIRE_IMMUNITY.name": "Inmunidad al Fuego",
@@ -386,6 +403,8 @@
 	"core.bonus.FEAR.description": "Causa miedo a un grupo enemigo",
 	"core.bonus.FEARLESS.name": "Inmune al miedo",
 	"core.bonus.FEARLESS.description": "Inmune a la habilidad de miedo",
+	"core.bonus.FEROCITY.name": "Ferocidad",
+	"core.bonus.FEROCITY.description": "Ataca ${val} veces adicionales en caso de eliminar a alguien",
 	"core.bonus.FLYING.name": "Volar",
 	"core.bonus.FLYING.description": "Puede volar (ignora obstáculos)",
 	"core.bonus.FREE_SHOOTING.name": "Disparo cercano",
@@ -416,8 +435,8 @@
 	"core.bonus.MANA_DRAIN.description": "Drena ${val} de maná cada turno",
 	"core.bonus.MAGIC_MIRROR.name": "Espejo mágico (${val}%)",
 	"core.bonus.MAGIC_MIRROR.description": "Tiene una probabilidad del ${val}% de redirigir un hechizo ofensivo al enemigo",
-	"core.bonus.MAGIC_RESISTANCE.name": "Resistencia mágica (${MR}%)",
-	"core.bonus.MAGIC_RESISTANCE.description": "Tiene una probabilidad del ${MR}% de resistir el hechizo del enemigo",
+	"core.bonus.MAGIC_RESISTANCE.name": "Resistencia mágica (${val}%)",
+	"core.bonus.MAGIC_RESISTANCE.description": "Tiene una probabilidad del ${val}% de resistir el hechizo del enemigo",
 	"core.bonus.MIND_IMMUNITY.name": "Inmunidad a hechizos mentales",
 	"core.bonus.MIND_IMMUNITY.description": "Inmune a hechizos de tipo mental",
 	"core.bonus.NO_DISTANCE_PENALTY.name": "Sin penalización por distancia",
@@ -440,10 +459,12 @@
 	"core.bonus.REBIRTH.description": "El ${val}% del grupo resucitará después de la muerte",
 	"core.bonus.RETURN_AFTER_STRIKE.name": "Atacar y volver",
 	"core.bonus.RETURN_AFTER_STRIKE.description": "Regresa después de un ataque cuerpo a cuerpo",
+	"core.bonus.REVENGE.name": "Venganza",
+	"core.bonus.REVENGE.description": "Inflige daño adicional según la salud perdida del atacante en la batalla.",
 	"core.bonus.SHOOTER.name": "A distancia",
 	"core.bonus.SHOOTER.description": "La criatura puede disparar",
 	"core.bonus.SHOOTS_ALL_ADJACENT.name": "Dispara en todas direcciones",
-	"core.bonus.SHOOTS_ALL_ADJACENT.description": "Los ataques a distancia de esta criatura impactan a todos los objetivos en un área pequeña",
+	"core.bonus.SHOOTS_ALL_ADJACENT.description": "Los ataques a distancia de esta criatura impactan a todos los objetivos en un área reducida",
 	"core.bonus.SOUL_STEAL.name": "Roba almas",
 	"core.bonus.SOUL_STEAL.description": "Gana ${val} nuevas criaturas por cada enemigo eliminado",
 	"core.bonus.SPELLCASTER.name": "Lanzador de hechizos",

+ 1 - 51
client/CMT.cpp

@@ -138,16 +138,7 @@ int main(int argc, char * argv[])
 		("nointro,i", "skips intro movies")
 		("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")
-		("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")
-		("lobby-username", po::value<std::string>(), "player name")
-		("lobby-gamemode", po::value<ui16>(), "use 0 for new game and 1 for load game")
-		("uuid", po::value<std::string>(), "uuid for the client");
+		("savefrequency", po::value<si64>(), "limit auto save creation to each N days");
 
 	if(argc > 1)
 	{
@@ -371,46 +362,6 @@ int main(int argc, char * argv[])
 	}
 	
 	std::vector<std::string> names;
-	session["lobby"].Bool() = false;
-	if(vm.count("lobby"))
-	{
-		session["lobby"].Bool() = true;
-		session["host"].Bool() = false;
-		session["address"].String() = vm["lobby-address"].as<std::string>();
-		if(vm.count("lobby-username"))
-			session["username"].String() = vm["lobby-username"].as<std::string>();
-		else
-			session["username"].String() = settings["launcher"]["lobbyUsername"].String();
-		if(vm.count("lobby-gamemode"))
-			session["gamemode"].Integer() = vm["lobby-gamemode"].as<ui16>();
-		else
-			session["gamemode"].Integer() = 0;
-		CSH->uuid = vm["uuid"].as<std::string>();
-		session["port"].Integer() = vm["lobby-port"].as<ui16>();
-		logGlobal->info("Remote lobby mode at %s:%d, uuid is %s", session["address"].String(), session["port"].Integer(), CSH->uuid);
-		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;
-		
-		//start lobby immediately
-		names.push_back(session["username"].String());
-		ESelectionScreen sscreen = session["gamemode"].Integer() == 0 ? ESelectionScreen::newGame : ESelectionScreen::loadGame;
-		CMM->openLobby(sscreen, session["host"].Bool(), &names, ELoadMode::MULTI);
-	}
-	
-	// Restore remote session - start game immediately
-	if(settings["server"]["reconnect"].Bool())
-	{
-		CSH->restoreLastSession();
-	}
 
 	if(!settings["session"]["headless"].Bool())
 	{
@@ -451,7 +402,6 @@ static void mainLoop()
 	while(1) //main SDL events loop
 	{
 		GH.input().fetchEvents();
-		CSH->applyPacksOnLobbyScreen();
 		GH.renderFrame();
 	}
 }

+ 13 - 0
client/CMakeLists.txt

@@ -95,6 +95,12 @@ set(client_SRCS
 	renderSDL/ScreenHandler.cpp
 	renderSDL/SDL_Extensions.cpp
 
+	globalLobby/GlobalLobbyClient.cpp
+	globalLobby/GlobalLobbyLoginWindow.cpp
+	globalLobby/GlobalLobbyServerSetup.cpp
+	globalLobby/GlobalLobbyWidget.cpp
+	globalLobby/GlobalLobbyWindow.cpp
+
 	widgets/Buttons.cpp
 	widgets/CArtifactHolder.cpp
 	widgets/CComponent.cpp
@@ -270,6 +276,13 @@ set(client_HEADERS
 	renderSDL/SDL_Extensions.h
 	renderSDL/SDL_PixelAccess.h
 
+	globalLobby/GlobalLobbyClient.h
+	globalLobby/GlobalLobbyDefines.h
+	globalLobby/GlobalLobbyLoginWindow.h
+	globalLobby/GlobalLobbyServerSetup.h
+	globalLobby/GlobalLobbyWidget.h
+	globalLobby/GlobalLobbyWindow.h
+
 	widgets/Buttons.h
 	widgets/CArtifactHolder.h
 	widgets/CComponent.h

+ 3 - 8
client/CPlayerInterface.cpp

@@ -1870,14 +1870,9 @@ void CPlayerInterface::proposeLoadingGame()
 		CGI->generaltexth->allTexts[68],
 		[]()
 		{
-			GH.dispatchMainThread(
-				[]()
-				{
-					CSH->endGameplay();
-					GH.defActionsDef = 63;
-					CMM->menu->switchToTab("load");
-				}
-			);
+			CSH->endGameplay();
+			GH.defActionsDef = 63;
+			CMM->menu->switchToTab("load");
 		},
 		nullptr
 	);

+ 280 - 327
client/CServerHandler.cpp

@@ -16,6 +16,7 @@
 #include "gui/CGuiHandler.h"
 #include "gui/WindowHandler.h"
 
+#include "globalLobby/GlobalLobbyClient.h"
 #include "lobby/CSelectionBase.h"
 #include "lobby/CLobbyScreen.h"
 #include "windows/InfoWindows.h"
@@ -46,16 +47,15 @@
 #include "../lib/mapObjects/MiscObjects.h"
 #include "../lib/modding/ModIncompatibility.h"
 #include "../lib/rmg/CMapGenOptions.h"
+#include "../lib/serializer/Connection.h"
 #include "../lib/filesystem/Filesystem.h"
 #include "../lib/registerTypes/RegisterTypesLobbyPacks.h"
-#include "../lib/serializer/Connection.h"
 #include "../lib/serializer/CMemorySerializer.h"
 #include "../lib/UnlockGuard.h"
 
 #include <boost/uuid/uuid.hpp>
 #include <boost/uuid/uuid_io.hpp>
 #include <boost/uuid/uuid_generators.hpp>
-#include <boost/asio.hpp>
 #include "../lib/serializer/Cast.h"
 #include "LobbyClientNetPackVisitors.h"
 
@@ -67,8 +67,6 @@
 
 template<typename T> class CApplyOnLobby;
 
-const std::string CServerHandler::localhostAddress{"127.0.0.1"};
-
 #if defined(VCMI_ANDROID) && !defined(SINGLE_PROCESS_APP)
 extern std::atomic_bool androidTestServerReadyFlag;
 #endif
@@ -76,8 +74,8 @@ extern std::atomic_bool androidTestServerReadyFlag;
 class CBaseForLobbyApply
 {
 public:
-	virtual bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const = 0;
-	virtual void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, void * pack) const = 0;
+	virtual bool applyOnLobbyHandler(CServerHandler * handler, CPackForLobby & pack) const = 0;
+	virtual void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, CPackForLobby & pack) const = 0;
 	virtual ~CBaseForLobbyApply(){};
 	template<typename U> static CBaseForLobbyApply * getApplier(const U * t = nullptr)
 	{
@@ -88,124 +86,137 @@ public:
 template<typename T> class CApplyOnLobby : public CBaseForLobbyApply
 {
 public:
-	bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const override
+	bool applyOnLobbyHandler(CServerHandler * handler, CPackForLobby & pack) const override
 	{
-		boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
-
-		T * ptr = static_cast<T *>(pack);
+		auto & ref = static_cast<T&>(pack);
 		ApplyOnLobbyHandlerNetPackVisitor visitor(*handler);
 
-		logNetwork->trace("\tImmediately apply on lobby: %s", typeid(ptr).name());
-		ptr->visit(visitor);
+		logNetwork->trace("\tImmediately apply on lobby: %s", typeid(ref).name());
+		ref.visit(visitor);
 
 		return visitor.getResult();
 	}
 
-	void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, void * pack) const override
+	void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, CPackForLobby & pack) const override
 	{
-		T * ptr = static_cast<T *>(pack);
+		auto & ref = static_cast<T &>(pack);
 		ApplyOnLobbyScreenNetPackVisitor visitor(*handler, lobby);
 
-		logNetwork->trace("\tApply on lobby from queue: %s", typeid(ptr).name());
-		ptr->visit(visitor);
+		logNetwork->trace("\tApply on lobby from queue: %s", typeid(ref).name());
+		ref.visit(visitor);
 	}
 };
 
 template<> class CApplyOnLobby<CPack>: public CBaseForLobbyApply
 {
 public:
-	bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const override
+	bool applyOnLobbyHandler(CServerHandler * handler, CPackForLobby & pack) const override
 	{
 		logGlobal->error("Cannot apply plain CPack!");
 		assert(0);
 		return false;
 	}
 
-	void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, void * pack) const override
+	void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, CPackForLobby & pack) const override
 	{
 		logGlobal->error("Cannot apply plain CPack!");
 		assert(0);
 	}
 };
 
-static const std::string NAME_AFFIX = "client";
-static const std::string NAME = GameConstants::VCMI_VERSION + std::string(" (") + NAME_AFFIX + ')'; //application name
+CServerHandler::~CServerHandler()
+{
+	networkHandler->stop();
+	try
+	{
+		threadNetwork.join();
+	}
+	catch (const std::runtime_error & e)
+	{
+		logGlobal->error("Failed to shut down network thread! Reason: %s", e.what());
+		assert(0);
+	}
+}
 
 CServerHandler::CServerHandler()
-	: state(EClientState::NONE), mx(std::make_shared<boost::recursive_mutex>()), client(nullptr), loadMode(0), campaignStateToSend(nullptr), campaignServerRestartLock(false)
+	: networkHandler(INetworkHandler::createHandler())
+	, lobbyClient(std::make_unique<GlobalLobbyClient>())
+	, applier(std::make_unique<CApplier<CBaseForLobbyApply>>())
+	, threadNetwork(&CServerHandler::threadRunNetwork, this)
+	, state(EClientState::NONE)
+	, serverPort(0)
+	, campaignStateToSend(nullptr)
+	, screenType(ESelectionScreen::unknown)
+	, serverMode(EServerMode::NONE)
+	, loadMode(ELoadMode::NONE)
+	, client(nullptr)
 {
 	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);
 }
 
-CServerHandler::~CServerHandler() = default;
+void CServerHandler::threadRunNetwork()
+{
+	logGlobal->info("Starting network thread");
+	setThreadName("runNetwork");
+	networkHandler->run();
+	logGlobal->info("Ending network thread");
+}
 
-void CServerHandler::resetStateForLobby(const StartInfo::EMode mode, const std::vector<std::string> * names)
+void CServerHandler::resetStateForLobby(EStartMode mode, ESelectionScreen screen, EServerMode newServerMode, const std::vector<std::string> & names)
 {
 	hostClientId = -1;
-	state = EClientState::NONE;
+	setState(EClientState::NONE);
+	serverMode = newServerMode;
 	mapToStart = nullptr;
 	th = std::make_unique<CStopWatch>();
-	packsForLobbyScreen.clear();
-	c.reset();
+	logicConnection.reset();
 	si = std::make_shared<StartInfo>();
 	playerNames.clear();
 	si->difficulty = 1;
 	si->mode = mode;
+	screenType = screen;
 	myNames.clear();
-	if(names && !names->empty()) //if have custom set of player names - use it
-		myNames = *names;
+	if(!names.empty()) //if have custom set of player names - use it
+		myNames = names;
 	else
 		myNames.push_back(settings["general"]["playerName"].String());
 }
 
-void CServerHandler::startLocalServerAndConnect()
+GlobalLobbyClient & CServerHandler::getGlobalLobby()
+{
+	return *lobbyClient;
+}
+
+INetworkHandler & CServerHandler::getNetworkHandler()
+{
+	return *networkHandler;
+}
+
+void CServerHandler::startLocalServerAndConnect(bool connectToLobby)
 {
-	if(threadRunLocalServer)
-		threadRunLocalServer->join();
+	if(threadRunLocalServer.joinable())
+		threadRunLocalServer.join();
 
 	th->update();
-	
-	auto errorMsg = CGI->generaltexth->translate("vcmi.server.errors.existingProcess");
-	try
-	{
-		CConnection testConnection(localhostAddress, getDefaultPort(), NAME, uuid);
-		logNetwork->error("Port is busy, check if another instance of vcmiserver is working");
-		CInfoWindow::showInfoDialog(errorMsg, {});
-		return;
-	}
-	catch(std::runtime_error & error)
-	{
-		//no connection means that port is not busy and we can start local server
-	}
-	
+
 #if defined(SINGLE_PROCESS_APP)
 	boost::condition_variable cond;
-	std::vector<std::string> args{"--uuid=" + uuid, "--port=" + std::to_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=" + std::to_string(settings["session"]["port"].Integer()));
-		args.push_back("--lobby-uuid=" + settings["session"]["hostUuid"].String());
-	}
-	threadRunLocalServer = std::make_shared<boost::thread>([&cond, args, this] {
+	std::vector<std::string> args{"--port=" + std::to_string(getLocalPort())};
+	if(connectToLobby)
+		args.push_back("--lobby");
+
+	threadRunLocalServer = boost::thread([&cond, args] {
 		setThreadName("CVCMIServer");
 		CVCMIServer::create(&cond, args);
-		onServerFinished();
 	});
-	threadRunLocalServer->detach();
 #elif defined(VCMI_ANDROID)
 	{
 		CAndroidVMHelper envHelper;
 		envHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "startServer", true);
 	}
 #else
-	threadRunLocalServer = std::make_shared<boost::thread>(&CServerHandler::threadRunServer, this); //runs server executable;
+	threadRunLocalServer = boost::thread(&CServerHandler::threadRunServer, this, connectToLobby); //runs server executable;
 #endif
 	logNetwork->trace("Setting up thread calling server: %d ms", th->getDiff());
 
@@ -236,7 +247,7 @@ void CServerHandler::startLocalServerAndConnect()
 	while(!androidTestServerReadyFlag.load())
 	{
 		logNetwork->info("still waiting...");
-		boost::this_thread::sleep_for(boost::chrono::milliseconds(1000));
+		boost::this_thread::sleep_for(boost::chrono::milliseconds(100));
 	}
 	logNetwork->info("waiting for server finished...");
 	androidTestServerReadyFlag = false;
@@ -245,151 +256,165 @@ void CServerHandler::startLocalServerAndConnect()
 
 	th->update(); //put breakpoint here to attach to server before it does something stupid
 
-	justConnectToServer(localhostAddress, 0);
+	connectToServer(getLocalHostname(), getLocalPort());
 
 	logNetwork->trace("\tConnecting to the server: %d ms", th->getDiff());
 }
 
-void CServerHandler::justConnectToServer(const std::string & addr, const ui16 port)
+void CServerHandler::connectToServer(const std::string & addr, const ui16 port)
 {
-	state = EClientState::CONNECTING;
-	while(!c && state != EClientState::CONNECTION_CANCELLED)
-	{
-		try
-		{
-			logNetwork->info("Establishing connection...");
-			c = std::make_shared<CConnection>(
-					addr.size() ? addr : getHostAddress(),
-					port ? port : getHostPort(),
-					NAME, uuid);
-
-			nextClient = std::make_unique<CClient>();
-			c->iser.cb = nextClient.get();
-		}
-		catch(std::runtime_error & error)
-		{
-			logNetwork->warn("\nCannot establish connection. %s Retrying in 1 second", error.what());
-			boost::this_thread::sleep_for(boost::chrono::milliseconds(1000));
-		}
-	}
+	logNetwork->info("Establishing connection to %s:%d...", addr, port);
+	setState(EClientState::CONNECTING);
+	serverHostname = addr;
+	serverPort = port;
 
-	if(state == EClientState::CONNECTION_CANCELLED)
+	if (!isServerLocal())
 	{
-		logNetwork->info("Connection aborted by player!");
-		return;
+		Settings remoteAddress = settings.write["server"]["remoteHostname"];
+		remoteAddress->String() = addr;
+
+		Settings remotePort = settings.write["server"]["remotePort"];
+		remotePort->Integer() = port;
 	}
 
-	c->handler = std::make_shared<boost::thread>(&CServerHandler::threadHandleConnection, this);
+	networkHandler->connectToRemote(*this, addr, port);
+}
+
+void CServerHandler::onConnectionFailed(const std::string & errorMessage)
+{
+	assert(getState() == EClientState::CONNECTING);
+	boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
 
-	if(!addr.empty() && addr != getHostAddress())
+	if (isServerLocal())
 	{
-		Settings serverAddress = settings.write["server"]["server"];
-		serverAddress->String() = addr;
+		// retry - local server might be still starting up
+		logNetwork->debug("\nCannot establish connection. %s. Retrying...", errorMessage);
+		networkHandler->createTimer(*this, std::chrono::milliseconds(100));
 	}
-	if(port && port != getHostPort())
+	else
 	{
-		Settings serverPort = settings.write["server"]["port"];
-		serverPort->Integer() = port;
+		// remote server refused connection - show error message
+		setState(EClientState::NONE);
+		CInfoWindow::showInfoDialog(CGI->generaltexth->translate("vcmi.mainMenu.serverConnectionFailed"), {});
 	}
 }
 
-void CServerHandler::applyPacksOnLobbyScreen()
+void CServerHandler::onTimer()
 {
-	if(!c || !c->handler)
-		return;
+	boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
 
-	boost::unique_lock<boost::recursive_mutex> lock(*mx);
-	while(!packsForLobbyScreen.empty())
+	if(getState() == EClientState::CONNECTION_CANCELLED)
 	{
-		boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
-		CPackForLobby * pack = packsForLobbyScreen.front();
-		packsForLobbyScreen.pop_front();
-		CBaseForLobbyApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(pack)); //find the applier
-		apply->applyOnLobbyScreen(dynamic_cast<CLobbyScreen *>(SEL), this, pack);
-		GH.windows().totalRedraw();
-		delete pack;
+		logNetwork->info("Connection aborted by player!");
+		return;
 	}
+
+	assert(isServerLocal());
+	networkHandler->connectToRemote(*this, getLocalHostname(), getLocalPort());
 }
 
-void CServerHandler::stopServerConnection()
+void CServerHandler::onConnectionEstablished(const NetworkConnectionPtr & netConnection)
 {
-	if(c->handler)
+	assert(getState() == EClientState::CONNECTING);
+
+	boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
+
+	networkConnection = netConnection;
+
+	logNetwork->info("Connection established");
+
+	if (serverMode == EServerMode::LOBBY_GUEST)
 	{
-		while(!c->handler->timed_join(boost::chrono::milliseconds(50)))
-			applyPacksOnLobbyScreen();
-		c->handler->join();
+		// say hello to lobby to switch connection to proxy mode
+		getGlobalLobby().sendProxyConnectionLogin(netConnection);
 	}
+
+	logicConnection = std::make_shared<CConnection>(netConnection);
+	logicConnection->uuid = uuid;
+	logicConnection->enterLobbyConnectionMode();
+	sendClientConnecting();
 }
 
-std::set<PlayerColor> CServerHandler::getHumanColors()
+void CServerHandler::applyPackOnLobbyScreen(CPackForLobby & pack)
 {
-	return clientHumanColors(c->connectionID);
+	const CBaseForLobbyApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(&pack)); //find the applier
+	apply->applyOnLobbyScreen(dynamic_cast<CLobbyScreen *>(SEL), this, pack);
+	GH.windows().totalRedraw();
 }
 
+std::set<PlayerColor> CServerHandler::getHumanColors()
+{
+	return clientHumanColors(logicConnection->connectionID);
+}
 
 PlayerColor CServerHandler::myFirstColor() const
 {
-	return clientFirstColor(c->connectionID);
+	return clientFirstColor(logicConnection->connectionID);
 }
 
 bool CServerHandler::isMyColor(PlayerColor color) const
 {
-	return isClientColor(c->connectionID, color);
+	return isClientColor(logicConnection->connectionID, color);
 }
 
 ui8 CServerHandler::myFirstId() const
 {
-	return clientFirstId(c->connectionID);
+	return clientFirstId(logicConnection->connectionID);
 }
 
-bool CServerHandler::isServerLocal() const
+EClientState CServerHandler::getState() const
 {
-	if(threadRunLocalServer)
-		return true;
+	return state;
+}
 
-	return false;
+void CServerHandler::setState(EClientState newState)
+{
+	state = newState;
+}
+
+bool CServerHandler::isServerLocal() const
+{
+	return threadRunLocalServer.joinable();
 }
 
 bool CServerHandler::isHost() const
 {
-	return c && hostClientId == c->connectionID;
+	return logicConnection && hostClientId == logicConnection->connectionID;
 }
 
 bool CServerHandler::isGuest() const
 {
-	return !c || hostClientId != c->connectionID;
+	return !logicConnection || hostClientId != logicConnection->connectionID;
 }
 
-ui16 CServerHandler::getDefaultPort()
+const std::string & CServerHandler::getLocalHostname() const
 {
-	return static_cast<ui16>(settings["server"]["port"].Integer());
+	return settings["server"]["localHostname"].String();
 }
 
-std::string CServerHandler::getDefaultPortStr()
+ui16 CServerHandler::getLocalPort() const
 {
-	return std::to_string(getDefaultPort());
+	return settings["server"]["localPort"].Integer();
 }
 
-std::string CServerHandler::getHostAddress() const
+const std::string & CServerHandler::getRemoteHostname() 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();
+	return settings["server"]["remoteHostname"].String();
 }
 
-ui16 CServerHandler::getHostPort() const
+ui16 CServerHandler::getRemotePort() const
 {
-	if(settings["session"]["lobby"].isNull() || !settings["session"]["lobby"].Bool())
-		return getDefaultPort();
-	
-	if(settings["session"]["host"].Bool())
-		return getDefaultPort();
-	
-	return settings["session"]["port"].Integer();
+	return settings["server"]["remotePort"].Integer();
+}
+
+const std::string & CServerHandler::getCurrentHostname() const
+{
+	return serverHostname;
+}
+
+ui16 CServerHandler::getCurrentPort() const
+{
+	return serverPort;
 }
 
 void CServerHandler::sendClientConnecting() const
@@ -404,13 +429,16 @@ void CServerHandler::sendClientConnecting() const
 void CServerHandler::sendClientDisconnecting()
 {
 	// FIXME: This is workaround needed to make sure client not trying to sent anything to non existed server
-	if(state == EClientState::DISCONNECTING)
+	if(getState() == EClientState::DISCONNECTING)
+	{
+		assert(0);
 		return;
+	}
 
-	state = EClientState::DISCONNECTING;
+	setState(EClientState::DISCONNECTING);
 	mapToStart = nullptr;
 	LobbyClientDisconnected lcd;
-	lcd.clientId = c->connectionID;
+	lcd.clientId = logicConnection->connectionID;
 	logNetwork->info("Connection has been requested to be closed.");
 	if(isServerLocal())
 	{
@@ -422,18 +450,14 @@ void CServerHandler::sendClientDisconnecting()
 		logNetwork->info("Sent leaving signal to the server");
 	}
 	sendLobbyPack(lcd);
-	
-	{
-		// Network thread might be applying network pack at this moment
-		auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex);
-		c->close();
-		c.reset();
-	}
+	networkConnection->close();
+	networkConnection.reset();
+	logicConnection.reset();
 }
 
 void CServerHandler::setCampaignState(std::shared_ptr<CampaignState> newCampaign)
 {
-	state = EClientState::LOBBY_CAMPAIGN;
+	setState(EClientState::LOBBY_CAMPAIGN);
 	LobbySetCampaign lsc;
 	lsc.ourCampaign = newCampaign;
 	sendLobbyPack(lsc);
@@ -441,7 +465,7 @@ void CServerHandler::setCampaignState(std::shared_ptr<CampaignState> newCampaign
 
 void CServerHandler::setCampaignMap(CampaignScenarioID mapId) const
 {
-	if(state == EClientState::GAMEPLAY) // FIXME: UI shouldn't sent commands in first place
+	if(getState() == EClientState::GAMEPLAY) // FIXME: UI shouldn't sent commands in first place
 		return;
 
 	LobbySetCampaignMap lscm;
@@ -451,7 +475,7 @@ void CServerHandler::setCampaignMap(CampaignScenarioID mapId) const
 
 void CServerHandler::setCampaignBonus(int bonusId) const
 {
-	if(state == EClientState::GAMEPLAY) // FIXME: UI shouldn't sent commands in first place
+	if(getState() == EClientState::GAMEPLAY) // FIXME: UI shouldn't sent commands in first place
 		return;
 
 	LobbySetCampaignBonus lscb;
@@ -575,9 +599,7 @@ void CServerHandler::sendRestartGame() const
 {
 	GH.windows().createAndPushWindow<CLoadingScreen>();
 	
-	LobbyEndGame endGame;
-	endGame.closeConnection = false;
-	endGame.restart = true;
+	LobbyRestartGame endGame;
 	sendLobbyPack(endGame);
 }
 
@@ -621,17 +643,18 @@ void CServerHandler::sendStartGame(bool allowOnlyAI) const
 	if(!settings["session"]["headless"].Bool())
 		GH.windows().createAndPushWindow<CLoadingScreen>();
 	
+	LobbyPrepareStartGame lpsg;
+	sendLobbyPack(lpsg);
+
 	LobbyStartGame lsg;
 	if(client)
 	{
 		lsg.initializedStartInfo = std::make_shared<StartInfo>(* const_cast<StartInfo *>(client->getStartInfo(true)));
-		lsg.initializedStartInfo->mode = StartInfo::NEW_GAME;
+		lsg.initializedStartInfo->mode = EStartMode::NEW_GAME;
 		lsg.initializedStartInfo->seedToBeUsed = lsg.initializedStartInfo->seedPostInit = 0;
 		* si = * lsg.initializedStartInfo;
 	}
 	sendLobbyPack(lsg);
-	c->enterLobbyConnectionMode();
-	c->disableStackSendingByID();
 }
 
 void CServerHandler::startMapAfterConnection(std::shared_ptr<CMapInfo> to)
@@ -644,83 +667,54 @@ void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameSta
 	if(CMM)
 		CMM->disable();
 
-	std::swap(client, nextClient);
-
 	highScoreCalc = nullptr;
 
 	switch(si->mode)
 	{
-	case StartInfo::NEW_GAME:
+	case EStartMode::NEW_GAME:
 		client->newGame(gameState);
 		break;
-	case StartInfo::CAMPAIGN:
+	case EStartMode::CAMPAIGN:
 		client->newGame(gameState);
 		break;
-	case StartInfo::LOAD_GAME:
+	case EStartMode::LOAD_GAME:
 		client->loadGame(gameState);
 		break;
 	default:
 		throw std::runtime_error("Invalid mode");
 	}
 	// After everything initialized we can accept CPackToClient netpacks
-	c->enterGameplayConnectionMode(client->gameState());
-	state = EClientState::GAMEPLAY;
-	
-	//store settings to continue game
-	if(!isServerLocal() && isGuest())
-	{
-		Settings saveSession = settings.write["server"]["reconnect"];
-		saveSession->Bool() = true;
-		Settings saveUuid = settings.write["server"]["uuid"];
-		saveUuid->String() = uuid;
-		Settings saveNames = settings.write["server"]["names"];
-		saveNames->Vector().clear();
-		for(auto & name : myNames)
-		{
-			JsonNode jsonName;
-			jsonName.String() = name;
-			saveNames->Vector().push_back(jsonName);
-		}
-	}
+	logicConnection->enterGameplayConnectionMode(client->gameState());
+	setState(EClientState::GAMEPLAY);
 }
 
-void CServerHandler::endGameplay(bool closeConnection, bool restart)
+void CServerHandler::endGameplay()
 {
-	if(closeConnection)
-	{
-		// Game is ending
-		// Tell the network thread to reach a stable state
-		CSH->sendClientDisconnecting();
-		logNetwork->info("Closed connection.");
-	}
+	// Game is ending
+	// Tell the network thread to reach a stable state
+	CSH->sendClientDisconnecting();
+	logNetwork->info("Closed connection.");
 
 	client->endGame();
 	client.reset();
 
-	if(!restart)
+	if(CMM)
 	{
-		if(CMM)
-		{
-			GH.curInt = CMM.get();
-			CMM->enable();
-		}
-		else
-		{
-			GH.curInt = CMainMenu::create().get();
-		}
+		GH.curInt = CMM.get();
+		CMM->enable();
 	}
-	
-	if(c)
+	else
 	{
-		nextClient = std::make_unique<CClient>();
-		c->iser.cb = nextClient.get();
-		c->enterLobbyConnectionMode();
-		c->disableStackSendingByID();
+		GH.curInt = CMainMenu::create().get();
 	}
-	
-	//reset settings
-	Settings saveSession = settings.write["server"]["reconnect"];
-	saveSession->Bool() = false;
+}
+
+void CServerHandler::restartGameplay()
+{
+	client->endGame();
+	client.reset();
+
+	logicConnection->enterLobbyConnectionMode();
 }
 
 void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared_ptr<CampaignState> cs)
@@ -741,7 +735,6 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared
 
 	GH.dispatchMainThread([ourCampaign, this]()
 	{
-		CSH->campaignServerRestartLock.set(true);
 		CSH->endGameplay();
 
 		auto & epilogue = ourCampaign->scenario(*ourCampaign->lastScenario()).epilog;
@@ -764,13 +757,14 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared
 				GH.windows().createAndPushWindow<CHighScoreInputScreen>(true, *highScoreCalc);
 			}
 		};
+
+		threadRunLocalServer.join();
 		if(epilogue.hasPrologEpilog)
 		{
 			GH.windows().createAndPushWindow<CPrologEpilogVideo>(epilogue, finisher);
 		}
 		else
 		{
-			CSH->campaignServerRestartLock.waitUntil(false);
 			finisher();
 		}
 	});
@@ -796,15 +790,15 @@ int CServerHandler::howManyPlayerInterfaces()
 	return playerInts;
 }
 
-ui8 CServerHandler::getLoadMode()
+ELoadMode CServerHandler::getLoadMode()
 {
-	if(loadMode != ELoadMode::TUTORIAL && state == EClientState::GAMEPLAY)
+	if(loadMode != ELoadMode::TUTORIAL && getState() == EClientState::GAMEPLAY)
 	{
 		if(si->campState)
 			return ELoadMode::CAMPAIGN;
 		for(auto pn : playerNames)
 		{
-			if(pn.second.connection != c->connectionID)
+			if(pn.second.connection != logicConnection->connectionID)
 				return ELoadMode::MULTI;
 		}
 		if(howManyPlayerInterfaces() > 1)  //this condition will work for hotseat mode OR multiplayer with allowed more than 1 color per player to control
@@ -815,48 +809,24 @@ ui8 CServerHandler::getLoadMode()
 	return loadMode;
 }
 
-void CServerHandler::restoreLastSession()
-{
-	auto loadSession = [this]()
-	{
-		uuid = settings["server"]["uuid"].String();
-		for(auto & name : settings["server"]["names"].Vector())
-			myNames.push_back(name.String());
-		resetStateForLobby(StartInfo::LOAD_GAME, &myNames);
-		screenType = ESelectionScreen::loadGame;
-		justConnectToServer(settings["server"]["server"].String(), settings["server"]["port"].Integer());
-	};
-	
-	auto cleanUpSession = []()
-	{
-		//reset settings
-		Settings saveSession = settings.write["server"]["reconnect"];
-		saveSession->Bool() = false;
-	};
-	
-	CInfoWindow::showYesNoDialog(VLC->generaltexth->translate("vcmi.server.confirmReconnect"), {}, loadSession, cleanUpSession);
-}
-
 void CServerHandler::debugStartTest(std::string filename, bool save)
 {
 	logGlobal->info("Starting debug test with file: %s", filename);
 	auto mapInfo = std::make_shared<CMapInfo>();
 	if(save)
 	{
-		resetStateForLobby(StartInfo::LOAD_GAME);
+		resetStateForLobby(EStartMode::LOAD_GAME, ESelectionScreen::loadGame, EServerMode::LOCAL, {});
 		mapInfo->saveInit(ResourcePath(filename, EResType::SAVEGAME));
-		screenType = ESelectionScreen::loadGame;
 	}
 	else
 	{
-		resetStateForLobby(StartInfo::NEW_GAME);
+		resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOCAL, {});
 		mapInfo->mapInit(filename);
-		screenType = ESelectionScreen::newGame;
 	}
 	if(settings["session"]["donotstartserver"].Bool())
-		justConnectToServer(localhostAddress, 3030);
+		connectToServer(getLocalHostname(), getLocalPort());
 	else
-		startLocalServerAndConnect();
+		startLocalServerAndConnect(false);
 
 	boost::this_thread::sleep_for(boost::chrono::milliseconds(100));
 
@@ -899,91 +869,71 @@ public:
 	{
 	}
 
-	virtual bool callTyped() override { return false; }
+	bool callTyped() override { return false; }
 
-	virtual void visitForLobby(CPackForLobby & lobbyPack) override
+	void visitForLobby(CPackForLobby & lobbyPack) override
 	{
 		handler.visitForLobby(lobbyPack);
 	}
 
-	virtual void visitForClient(CPackForClient & clientPack) override
+	void visitForClient(CPackForClient & clientPack) override
 	{
 		handler.visitForClient(clientPack);
 	}
 };
 
-void CServerHandler::threadHandleConnection()
+void CServerHandler::onPacketReceived(const std::shared_ptr<INetworkConnection> &, const std::vector<std::byte> & message)
 {
-	setThreadName("handleConnection");
-	c->enterLobbyConnectionMode();
+	boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
 
-	try
+	if(getState() == EClientState::DISCONNECTING)
 	{
-		sendClientConnecting();
-		while(c && c->connected)
-		{
-			while(state == EClientState::STARTING)
-				boost::this_thread::sleep_for(boost::chrono::milliseconds(10));
-
-			CPack * pack = c->retrievePack();
-			if(state == EClientState::DISCONNECTING)
-			{
-				// FIXME: server shouldn't really send netpacks after it's tells client to disconnect
-				// Though currently they'll be delivered and might cause crash.
-				vstd::clear_pointer(pack);
-			}
-			else
-			{
-				ServerHandlerCPackVisitor visitor(*this);
-				pack->visit(visitor);
-			}
-		}
+		assert(0); //Should not be possible - socket must be closed at this point
+		return;
 	}
-	//catch only asio exceptions
-	catch(const boost::system::system_error & e)
+
+	CPack * pack = logicConnection->retrievePack(message);
+	ServerHandlerCPackVisitor visitor(*this);
+	pack->visit(visitor);
+}
+
+void CServerHandler::onDisconnected(const std::shared_ptr<INetworkConnection> & connection, const std::string & errorMessage)
+{
+	if(getState() == EClientState::DISCONNECTING)
 	{
-		if(state == EClientState::DISCONNECTING)
-		{
-			logNetwork->info("Successfully closed connection to server, ending listening thread!");
-		}
-		else
-		{
-			if (e.code() == boost::asio::error::eof)
-				logNetwork->error("Lost connection to server, ending listening thread! Connection has been closed");
-			else
-				logNetwork->error("Lost connection to server, ending listening thread! Reason: %s", e.what());
+		assert(networkConnection == nullptr);
+		// Note: this branch can be reached on app shutdown, when main thread holds mutex till destruction
+		logNetwork->info("Successfully closed connection to server!");
+		return;
+	}
 
-			if(client)
-			{
-				state = EClientState::DISCONNECTING;
-
-				GH.dispatchMainThread([]()
-				{
-					CSH->endGameplay();
-					GH.defActionsDef = 63;
-					CMM->menu->switchToTab("main");
-				});
-			}
-			else
-			{
-				auto lcd = new LobbyClientDisconnected();
-				lcd->clientId = c->connectionID;
-				boost::unique_lock<boost::recursive_mutex> lock(*mx);
-				packsForLobbyScreen.push_back(lcd);
-			}
-		}
+	boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
+
+	logNetwork->error("Lost connection to server! Connection has been closed");
+
+	if(client)
+	{
+		CSH->endGameplay();
+		GH.defActionsDef = 63;
+		CMM->menu->switchToTab("main");
+		CSH->showServerError(CGI->generaltexth->translate("vcmi.server.errors.disconnected"));
+	}
+	else
+	{
+		LobbyClientDisconnected lcd;
+		lcd.clientId = logicConnection->connectionID;
+		applyPackOnLobbyScreen(lcd);
 	}
+
+	networkConnection.reset();
 }
 
 void CServerHandler::visitForLobby(CPackForLobby & lobbyPack)
 {
-	if(applier->getApplier(CTypeList::getInstance().getTypeID(&lobbyPack))->applyOnLobbyHandler(this, &lobbyPack))
+	if(applier->getApplier(CTypeList::getInstance().getTypeID(&lobbyPack))->applyOnLobbyHandler(this, lobbyPack))
 	{
 		if(!settings["session"]["headless"].Bool())
-		{
-			boost::unique_lock<boost::recursive_mutex> lock(*mx);
-			packsForLobbyScreen.push_back(&lobbyPack);
-		}
+			applyPackOnLobbyScreen(lobbyPack);
 	}
 }
 
@@ -992,22 +942,16 @@ void CServerHandler::visitForClient(CPackForClient & clientPack)
 	client->handlePack(&clientPack);
 }
 
-void CServerHandler::threadRunServer()
+void CServerHandler::threadRunServer(bool connectToLobby)
 {
 #if !defined(VCMI_MOBILE)
 	setThreadName("runServer");
 	const std::string logName = (VCMIDirs::get().userLogsPath() / "server_log.txt").string();
 	std::string comm = VCMIDirs::get().serverPath().string()
-		+ " --port=" + std::to_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=" + std::to_string(settings["session"]["port"].Integer());
-		comm += " --lobby-uuid=" + settings["session"]["hostUuid"].String();
-	}
+		+ " --port=" + std::to_string(getLocalPort())
+		+ " --run-by-client";
+	if(connectToLobby)
+		comm += " --lobby";
 
 	comm += " > \"" + logName + '\"';
 	logGlobal->info("Server command line: %s", comm);
@@ -1035,22 +979,31 @@ void CServerHandler::threadRunServer()
 	}
 	else
 	{
+		boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
+
+		if (getState() == EClientState::CONNECTING)
+		{
+			showServerError(CGI->generaltexth->translate("vcmi.server.errors.existingProcess"));
+			setState(EClientState::CONNECTION_CANCELLED); // stop attempts to reconnect
+		}
 		logNetwork->error("Error: server failed to close correctly or crashed!");
 		logNetwork->error("Check %s for more info", logName);
 	}
-	onServerFinished();
 #endif
 }
 
-void CServerHandler::onServerFinished()
+void CServerHandler::sendLobbyPack(const CPackForLobby & pack) const
 {
-	threadRunLocalServer.reset();
-	if (CSH)
-		CSH->campaignServerRestartLock.setn(false);
+	if(getState() != EClientState::STARTING)
+		logicConnection->sendPack(&pack);
 }
 
-void CServerHandler::sendLobbyPack(const CPackForLobby & pack) const
+bool CServerHandler::inLobbyRoom() const
+{
+	return CSH->serverMode == EServerMode::LOBBY_HOST || CSH->serverMode == EServerMode::LOBBY_GUEST;
+}
+
+bool CServerHandler::inGame() const
 {
-	if(state != EClientState::STARTING)
-		c->sendPack(&pack);
+	return logicConnection != nullptr;
 }

+ 64 - 39
client/CServerHandler.h

@@ -11,6 +11,7 @@
 
 #include "../lib/CStopWatch.h"
 
+#include "../lib/network/NetworkInterface.h"
 #include "../lib/StartInfo.h"
 #include "../lib/CondSh.h"
 
@@ -34,10 +35,14 @@ VCMI_LIB_NAMESPACE_END
 
 class CClient;
 class CBaseForLobbyApply;
+class GlobalLobbyClient;
 
 class HighScoreCalculation;
 class HighScoreParameter;
 
+enum class ESelectionScreen : ui8;
+enum class ELoadMode : ui8;
+
 // TODO: Add mutex so we can't set CONNECTION_CANCELLED if client already connected, but thread not setup yet
 enum class EClientState : ui8
 {
@@ -49,7 +54,14 @@ enum class EClientState : ui8
 	STARTING, // Gameplay interfaces being created, we pause netpacks retrieving
 	GAMEPLAY, // In-game, used by some UI
 	DISCONNECTING, // We disconnecting, drop all netpacks
-	CONNECTION_FAILED // We could not connect to server
+};
+
+enum class EServerMode : uint8_t
+{
+	NONE = 0,
+	LOCAL, // no global lobby
+	LOBBY_HOST, // We are hosting global server available via global lobby
+	LOBBY_GUEST // Connecting to a remote server via proxy provided by global lobby
 };
 
 class IServerAPI
@@ -80,63 +92,68 @@ public:
 };
 
 /// structure to handle running server and connecting to it
-class CServerHandler : public IServerAPI, public LobbyInfo
+class CServerHandler final : public IServerAPI, public LobbyInfo, public INetworkClientListener, public INetworkTimerListener, boost::noncopyable
 {
 	friend class ApplyOnLobbyHandlerNetPackVisitor;
-	
-	std::shared_ptr<CApplier<CBaseForLobbyApply>> applier;
 
-	std::shared_ptr<boost::recursive_mutex> mx;
-	std::list<CPackForLobby *> packsForLobbyScreen; //protected by mx
-	
+	std::unique_ptr<INetworkHandler> networkHandler;
+	std::shared_ptr<INetworkConnection> networkConnection;
+	std::unique_ptr<GlobalLobbyClient> lobbyClient;
+	std::unique_ptr<CApplier<CBaseForLobbyApply>> applier;
 	std::shared_ptr<CMapInfo> mapToStart;
-
 	std::vector<std::string> myNames;
-
 	std::shared_ptr<HighScoreCalculation> highScoreCalc;
 
-	/// temporary helper member that exists while game in lobby mode
-	/// required to correctly deserialize gamestate using client-side game callback
-	std::unique_ptr<CClient> nextClient;
+	boost::thread threadRunLocalServer;
+	boost::thread threadNetwork;
+
+	std::atomic<EClientState> state;
+
+	void threadRunNetwork();
+	void threadRunServer(bool connectToLobby);
 
-	void threadHandleConnection();
-	void threadRunServer();
-	void onServerFinished();
 	void sendLobbyPack(const CPackForLobby & pack) const override;
 
+	void onPacketReceived(const NetworkConnectionPtr &, const std::vector<std::byte> & message) override;
+	void onConnectionFailed(const std::string & errorMessage) override;
+	void onConnectionEstablished(const NetworkConnectionPtr &) override;
+	void onDisconnected(const NetworkConnectionPtr &, const std::string & errorMessage) override;
+	void onTimer() override;
+
+	void applyPackOnLobbyScreen(CPackForLobby & pack);
+
+	std::string serverHostname;
+	ui16 serverPort;
+
+	bool isServerLocal() const;
+
 public:
-	std::atomic<EClientState> state;
+	/// High-level connection overlay that is capable of (de)serializing network data
+	std::shared_ptr<CConnection> logicConnection;
+
 	////////////////////
 	// FIXME: Bunch of crutches to glue it all together
 
 	// For starting non-custom campaign and continue to next mission
 	std::shared_ptr<CampaignState> campaignStateToSend;
 
-	ui8 screenType; // To create lobby UI only after server is setup
-	ui8 loadMode; // For saves filtering in SelectionTab
+	ESelectionScreen screenType; // To create lobby UI only after server is setup
+	EServerMode serverMode;
+	ELoadMode loadMode; // For saves filtering in SelectionTab
 	////////////////////
 
 	std::unique_ptr<CStopWatch> th;
-	std::shared_ptr<boost::thread> threadRunLocalServer;
-
-	std::shared_ptr<CConnection> c;
 	std::unique_ptr<CClient> client;
 
-	CondSh<bool> campaignServerRestartLock;
-
-	static const std::string localhostAddress;
-
 	CServerHandler();
 	~CServerHandler();
 	
-	std::string getHostAddress() const;
-	ui16 getHostPort() const;
+	void resetStateForLobby(EStartMode mode, ESelectionScreen screen, EServerMode serverMode, const std::vector<std::string> & names);
+	void startLocalServerAndConnect(bool connectToLobby);
+	void connectToServer(const std::string & addr, const ui16 port);
 
-	void resetStateForLobby(const StartInfo::EMode mode, const std::vector<std::string> * names = nullptr);
-	void startLocalServerAndConnect();
-	void justConnectToServer(const std::string & addr, const ui16 port);
-	void applyPacksOnLobbyScreen();
-	void stopServerConnection();
+	GlobalLobbyClient & getGlobalLobby();
+	INetworkHandler & getNetworkHandler();
 
 	// Helpers for lobby state access
 	std::set<PlayerColor> getHumanColors();
@@ -144,12 +161,21 @@ public:
 	bool isMyColor(PlayerColor color) const;
 	ui8 myFirstId() const; // Used by chat only!
 
-	bool isServerLocal() const;
+	EClientState getState() const;
+	void setState(EClientState newState);
+
 	bool isHost() const;
 	bool isGuest() const;
+	bool inLobbyRoom() const;
+	bool inGame() const;
+
+	const std::string & getCurrentHostname() const;
+	const std::string & getLocalHostname() const;
+	const std::string & getRemoteHostname() const;
 
-	static ui16 getDefaultPort();
-	static std::string getDefaultPortStr();
+	ui16 getCurrentPort() const;
+	ui16 getLocalPort() const;
+	ui16 getRemotePort() const;
 
 	// Lobby server API for UI
 	void sendClientConnecting() const override;
@@ -175,15 +201,14 @@ public:
 	void debugStartTest(std::string filename, bool save = false);
 
 	void startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState = nullptr);
-	void endGameplay(bool closeConnection = true, bool restart = false);
+	void endGameplay();
+	void restartGameplay();
 	void startCampaignScenario(HighScoreParameter param, std::shared_ptr<CampaignState> cs = {});
 	void showServerError(const std::string & txt) const;
 
 	// TODO: LobbyState must be updated within game so we should always know how many player interfaces our client handle
 	int howManyPlayerInterfaces();
-	ui8 getLoadMode();
-
-	void restoreLastSession();
+	ELoadMode getLoadMode();
 
 	void visitForLobby(CPackForLobby & lobbyPack);
 	void visitForClient(CPackForClient & clientPack);

+ 7 - 7
client/Client.cpp

@@ -30,11 +30,12 @@
 #include "../lib/UnlockGuard.h"
 #include "../lib/battle/BattleInfo.h"
 #include "../lib/serializer/BinaryDeserializer.h"
+#include "../lib/serializer/BinarySerializer.h"
+#include "../lib/serializer/Connection.h"
 #include "../lib/mapping/CMapService.h"
 #include "../lib/pathfinder/CGPathNode.h"
 #include "../lib/filesystem/Filesystem.h"
 #include "../lib/registerTypes/RegisterTypesClientPacks.h"
-#include "../lib/serializer/Connection.h"
 
 #include <memory>
 #include <vcmi/events/EventBus.h>
@@ -297,7 +298,7 @@ void CClient::serialize(BinaryDeserializer & h)
 
 		bool shouldResetInterface = true;
 		// Client no longer handle this player at all
-		if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), pid))
+		if(!vstd::contains(CSH->getAllClientPlayers(CSH->logicConnection->connectionID), pid))
 		{
 			logGlobal->trace("Player %s is not belong to this client. Destroying interface", pid);
 		}
@@ -397,7 +398,7 @@ void CClient::initPlayerEnvironments()
 {
 	playerEnvironments.clear();
 
-	auto allPlayers = CSH->getAllClientPlayers(CSH->c->connectionID);
+	auto allPlayers = CSH->getAllClientPlayers(CSH->logicConnection->connectionID);
 	bool hasHumanPlayer = false;
 	for(auto & color : allPlayers)
 	{
@@ -427,7 +428,7 @@ void CClient::initPlayerInterfaces()
 	for(auto & playerInfo : gs->scenarioOps->playerInfos)
 	{
 		PlayerColor color = playerInfo.first;
-		if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), color))
+		if(!vstd::contains(CSH->getAllClientPlayers(CSH->logicConnection->connectionID), color))
 			continue;
 
 		if(!vstd::contains(playerint, color))
@@ -457,7 +458,7 @@ void CClient::initPlayerInterfaces()
 		installNewPlayerInterface(std::make_shared<CPlayerInterface>(PlayerColor::SPECTATOR), PlayerColor::SPECTATOR, true);
 	}
 
-	if(CSH->getAllClientPlayers(CSH->c->connectionID).count(PlayerColor::NEUTRAL))
+	if(CSH->getAllClientPlayers(CSH->logicConnection->connectionID).count(PlayerColor::NEUTRAL))
 		installNewBattleInterface(CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].String()), PlayerColor::NEUTRAL);
 
 	logNetwork->trace("Initialized player interfaces %d ms", CSH->th->getDiff());
@@ -520,7 +521,6 @@ void CClient::handlePack(CPack * pack)
 	CBaseForCLApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(pack)); //find the applier
 	if(apply)
 	{
-		boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
 		apply->applyOnClBefore(this, pack);
 		logNetwork->trace("\tMade first apply on cl: %s", typeid(pack).name());
 		gs->apply(pack);
@@ -545,7 +545,7 @@ int CClient::sendRequest(const CPackForServer * request, PlayerColor player)
 	waitingRequest.pushBack(requestID);
 	request->requestID = requestID;
 	request->player = player;
-	CSH->c->sendPack(request);
+	CSH->logicConnection->sendPack(request);
 	if(vstd::contains(playerint, player))
 		playerint[player]->requestSent(request, requestID);
 

+ 1 - 1
client/Client.h

@@ -173,7 +173,7 @@ public:
 	void showTeleportDialog(TeleportDialog * iw) override {};
 	void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override {};
 	void giveResource(PlayerColor player, GameResID which, int val) override {};
-	virtual void giveResources(PlayerColor player, TResources resources) override {};
+	void giveResources(PlayerColor player, TResources resources) override {};
 
 	void giveCreatures(const CArmedInstance * objid, const CGHeroInstance * h, const CCreatureSet & creatures, bool remove) override {};
 	void takeCreatures(ObjectInstanceID objid, const std::vector<CStackBasicDescriptor> & creatures) override {};

+ 12 - 12
client/ClientNetPackVisitors.h

@@ -118,16 +118,16 @@ public:
 	{
 	}
 
-	virtual void visitChangeObjPos(ChangeObjPos & pack) override;
-	virtual void visitRemoveObject(RemoveObject & pack) override;
-	virtual void visitTryMoveHero(TryMoveHero & pack) override;
-	virtual void visitGiveHero(GiveHero & pack) override;
-	virtual void visitBattleStart(BattleStart & pack) override;
-	virtual void visitBattleNextRound(BattleNextRound & pack) override;
-	virtual void visitBattleUpdateGateState(BattleUpdateGateState & pack) override;
-	virtual void visitBattleResult(BattleResult & pack) override;
-	virtual void visitBattleStackMoved(BattleStackMoved & pack) override;
-	virtual void visitBattleAttack(BattleAttack & pack) override;
-	virtual void visitStartAction(StartAction & pack) override;
-	virtual void visitSetObjectProperty(SetObjectProperty & pack) override;
+	void visitChangeObjPos(ChangeObjPos & pack) override;
+	void visitRemoveObject(RemoveObject & pack) override;
+	void visitTryMoveHero(TryMoveHero & pack) override;
+	void visitGiveHero(GiveHero & pack) override;
+	void visitBattleStart(BattleStart & pack) override;
+	void visitBattleNextRound(BattleNextRound & pack) override;
+	void visitBattleUpdateGateState(BattleUpdateGateState & pack) override;
+	void visitBattleResult(BattleResult & pack) override;
+	void visitBattleStackMoved(BattleStackMoved & pack) override;
+	void visitBattleAttack(BattleAttack & pack) override;
+	void visitStartAction(StartAction & pack) override;
+	void visitSetObjectProperty(SetObjectProperty & pack) override;
 };

+ 13 - 12
client/LobbyClientNetPackVisitors.h

@@ -32,11 +32,12 @@ public:
 
 	bool getResult() const { return result; }
 
-	virtual void visitLobbyClientConnected(LobbyClientConnected & pack) override;
-	virtual void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override;
-	virtual void visitLobbyEndGame(LobbyEndGame & pack) override;
-	virtual void visitLobbyStartGame(LobbyStartGame & pack) override;
-	virtual void visitLobbyUpdateState(LobbyUpdateState & pack) override;
+	void visitLobbyClientConnected(LobbyClientConnected & pack) override;
+	void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override;
+	void visitLobbyRestartGame(LobbyRestartGame & pack) override;
+	void visitLobbyPrepareStartGame(LobbyPrepareStartGame & pack) override;
+	void visitLobbyStartGame(LobbyStartGame & pack) override;
+	void visitLobbyUpdateState(LobbyUpdateState & pack) override;
 };
 
 class ApplyOnLobbyScreenNetPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor)
@@ -51,11 +52,11 @@ public:
 	{
 	}
 
-	virtual void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override;
-	virtual void visitLobbyChatMessage(LobbyChatMessage & pack) override;
-	virtual void visitLobbyGuiAction(LobbyGuiAction & pack) override;
-	virtual void visitLobbyStartGame(LobbyStartGame & pack) override;
-	virtual void visitLobbyLoadProgress(LobbyLoadProgress & pack) override;
-	virtual void visitLobbyUpdateState(LobbyUpdateState & pack) override;
-	virtual void visitLobbyShowMessage(LobbyShowMessage & pack) override;
+	void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override;
+	void visitLobbyChatMessage(LobbyChatMessage & pack) override;
+	void visitLobbyGuiAction(LobbyGuiAction & pack) override;
+	void visitLobbyStartGame(LobbyStartGame & pack) override;
+	void visitLobbyLoadProgress(LobbyLoadProgress & pack) override;
+	void visitLobbyUpdateState(LobbyUpdateState & pack) override;
+	void visitLobbyShowMessage(LobbyShowMessage & pack) override;
 };

+ 2 - 2
client/NetPacksClient.cpp

@@ -27,8 +27,8 @@
 #include "../CCallback.h"
 #include "../lib/filesystem/Filesystem.h"
 #include "../lib/filesystem/FileInfo.h"
-#include "../lib/serializer/Connection.h"
 #include "../lib/serializer/BinarySerializer.h"
+#include "../lib/serializer/Connection.h"
 #include "../lib/CGeneralTextHandler.h"
 #include "../lib/CHeroHandler.h"
 #include "../lib/VCMI_Lib.h"
@@ -424,7 +424,7 @@ void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface
 			cl.initPlayerEnvironments();
 			initInterfaces();
 		}
-		else if(pack.playerConnectionId == CSH->c->connectionID)
+		else if(pack.playerConnectionId == CSH->logicConnection->connectionID)
 		{
 			plSettings.connectedPlayerIDs.insert(pack.playerConnectionId);
 			cl.playerint.clear();

+ 51 - 22
client/NetPacksLobbyClient.cpp

@@ -19,9 +19,13 @@
 #include "lobby/ExtraOptionsTab.h"
 #include "lobby/SelectionTab.h"
 #include "lobby/CBonusSelection.h"
+#include "globalLobby/GlobalLobbyWindow.h"
+#include "globalLobby/GlobalLobbyServerSetup.h"
+#include "globalLobby/GlobalLobbyClient.h"
 
 #include "CServerHandler.h"
 #include "CGameInfo.h"
+#include "Client.h"
 #include "gui/CGuiHandler.h"
 #include "gui/WindowHandler.h"
 #include "widgets/Buttons.h"
@@ -36,26 +40,50 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientCon
 	result = false;
 
 	// Check if it's LobbyClientConnected for our client
-	if(pack.uuid == handler.c->uuid)
+	if(pack.uuid == handler.logicConnection->uuid)
 	{
-		handler.c->connectionID = pack.clientId;
+		handler.logicConnection->connectionID = pack.clientId;
 		if(handler.mapToStart)
+		{
 			handler.setMapInfo(handler.mapToStart);
+		}
 		else if(!settings["session"]["headless"].Bool())
-			GH.windows().createAndPushWindow<CLobbyScreen>(static_cast<ESelectionScreen>(handler.screenType));
-		handler.state = EClientState::LOBBY;
+		{
+			if (GH.windows().topWindow<CSimpleJoinScreen>())
+				GH.windows().popWindows(1);
+
+			if (!GH.windows().findWindows<GlobalLobbyServerSetup>().empty())
+			{
+				assert(handler.serverMode == EServerMode::LOBBY_HOST);
+				// announce opened game room
+				// TODO: find better approach?
+				int roomType = settings["lobby"]["roomType"].Integer();
+
+				if (roomType != 0)
+					handler.getGlobalLobby().sendOpenPrivateRoom();
+				else
+					handler.getGlobalLobby().sendOpenPublicRoom();
+			}
+
+			while (!GH.windows().findWindows<GlobalLobbyWindow>().empty())
+			{
+				// if global lobby is open, pop all dialogs on top of it as well as lobby itself
+				GH.windows().popWindows(1);
+			}
+
+			GH.windows().createAndPushWindow<CLobbyScreen>(handler.screenType);
+		}
+		handler.setState(EClientState::LOBBY);
 	}
 }
 
 void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientDisconnected(LobbyClientDisconnected & pack)
 {
-	if(pack.clientId != handler.c->connectionID)
+	if(pack.clientId != handler.logicConnection->connectionID)
 	{
 		result = false;
 		return;
 	}
-
-	handler.stopServerConnection();
 }
 
 void ApplyOnLobbyScreenNetPackVisitor::visitLobbyClientDisconnected(LobbyClientDisconnected & pack)
@@ -106,30 +134,31 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyGuiAction(LobbyGuiAction & pack
 	}
 }
 
-void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyEndGame(LobbyEndGame & pack)
+void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyRestartGame(LobbyRestartGame & pack)
 {
-	if(handler.state == EClientState::GAMEPLAY)
-	{
-		handler.endGameplay(pack.closeConnection, pack.restart);
-	}
-	
-	if(pack.restart)
-	{
-		if (handler.validateGameStart())
-			handler.sendStartGame();
-	}
+	assert(handler.getState() == EClientState::GAMEPLAY);
+
+	handler.restartGameplay();
+	handler.sendStartGame();
+}
+
+void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyPrepareStartGame(LobbyPrepareStartGame & pack)
+{
+	handler.client = std::make_unique<CClient>();
+	handler.logicConnection->enterLobbyConnectionMode();
+	handler.logicConnection->setCallback(handler.client.get());
 }
 
 void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack)
 {
-	if(pack.clientId != -1 && pack.clientId != handler.c->connectionID)
+	if(pack.clientId != -1 && pack.clientId != handler.logicConnection->connectionID)
 	{
 		result = false;
 		return;
 	}
 	
-	handler.state = EClientState::STARTING;
-	if(handler.si->mode != StartInfo::LOAD_GAME || pack.clientId == handler.c->connectionID)
+	handler.setState(EClientState::STARTING);
+	if(handler.si->mode != EStartMode::LOAD_GAME || pack.clientId == handler.logicConnection->connectionID)
 	{
 		auto modeBackup = handler.si->mode;
 		handler.si = pack.initializedStartInfo;
@@ -174,7 +203,7 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState &
 	if(!lobby) //stub: ignore message for game mode
 		return;
 		
-	if(!lobby->bonusSel && handler.si->campState && handler.state == EClientState::LOBBY_CAMPAIGN)
+	if(!lobby->bonusSel && handler.si->campState && handler.getState() == EClientState::LOBBY_CAMPAIGN)
 	{
 		lobby->bonusSel = std::make_shared<CBonusSelection>();
 		GH.windows().pushWindow(lobby->bonusSel);

+ 10 - 3
client/battle/BattleInterfaceClasses.cpp

@@ -852,12 +852,19 @@ StackQueue::StackQueue(bool Embedded, BattleInterface & owner)
 	owner(owner)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
+
+	uint32_t queueSize = QUEUE_SIZE_BIG;
+
 	if(embedded)
 	{
-		pos.w = QUEUE_SIZE * 41;
+		int32_t queueSmallOutsideYOffset = 65;
+		bool queueSmallOutside = settings["battle"]["queueSmallOutside"].Bool() && (pos.y - queueSmallOutsideYOffset) >= 0;
+		queueSize = std::clamp(static_cast<int>(settings["battle"]["queueSmallSlots"].Float()), 1, queueSmallOutside ? GH.screenDimensions().x / 41 : 19);
+
+		pos.w = queueSize * 41;
 		pos.h = 49;
 		pos.x += parent->pos.w/2 - pos.w/2;
-		pos.y += 10;
+		pos.y += queueSmallOutside ? -queueSmallOutsideYOffset : 10;
 
 		icons = GH.renderHandler().loadAnimation(AnimationPath::builtin("CPRSMALL"));
 		stateIcons = GH.renderHandler().loadAnimation(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL"));
@@ -878,7 +885,7 @@ StackQueue::StackQueue(bool Embedded, BattleInterface & owner)
 	}
 	stateIcons->preload();
 
-	stackBoxes.resize(QUEUE_SIZE);
+	stackBoxes.resize(queueSize);
 	for (int i = 0; i < stackBoxes.size(); i++)
 	{
 		stackBoxes[i] = std::make_shared<StackBox>(this);

+ 1 - 1
client/battle/BattleInterfaceClasses.h

@@ -239,7 +239,7 @@ class StackQueue : public CIntObject
 		std::optional<uint32_t> getBoundUnitID() const;
 	};
 
-	static const int QUEUE_SIZE = 10;
+	static const int QUEUE_SIZE_BIG = 10;
 	std::shared_ptr<CFilledTexture> background;
 	std::vector<std::shared_ptr<StackBox>> stackBoxes;
 	BattleInterface & owner;

+ 2 - 2
client/eventsSDL/InputHandler.cpp

@@ -145,7 +145,7 @@ void InputHandler::preprocessEvent(const SDL_Event & ev)
 			Settings full = settings.write["video"]["fullscreen"];
 			full->Bool() = !full->Bool();
 
-			GH.onScreenResize();
+			GH.onScreenResize(false);
 			return;
 		}
 	}
@@ -163,7 +163,7 @@ void InputHandler::preprocessEvent(const SDL_Event & ev)
 #ifndef VCMI_IOS
 			{
 				boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
-				GH.onScreenResize();
+				GH.onScreenResize(false);
 			}
 #endif
 			break;

+ 9 - 1
client/eventsSDL/InputSourceKeyboard.cpp

@@ -16,6 +16,8 @@
 #include "../gui/CGuiHandler.h"
 #include "../gui/EventDispatcher.h"
 #include "../gui/ShortcutHandler.h"
+#include "../CServerHandler.h"
+#include "../globalLobby/GlobalLobbyClient.h"
 
 #include <SDL_clipboard.h>
 #include <SDL_events.h>
@@ -31,6 +33,8 @@ InputSourceKeyboard::InputSourceKeyboard()
 
 void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key)
 {
+	assert(key.state == SDL_PRESSED);
+
 	if (SDL_IsTextInputActive() == SDL_TRUE)
 	{
 		if(key.keysym.sym == SDLK_v && isKeyboardCtrlDown()) 
@@ -51,7 +55,11 @@ void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key)
 			return; // ignore periodic event resends
 	}
 
-	assert(key.state == SDL_PRESSED);
+
+	if(key.keysym.sym == SDLK_TAB && isKeyboardCtrlDown())
+	{
+		CSH->getGlobalLobby().activateInterface();
+	}
 
 	if(key.keysym.sym >= SDLK_F1 && key.keysym.sym <= SDLK_F15 && settings["session"]["spectate"].Bool())
 	{

+ 366 - 0
client/globalLobby/GlobalLobbyClient.cpp

@@ -0,0 +1,366 @@
+/*
+ * GlobalLobbyClient.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 "GlobalLobbyClient.h"
+
+#include "GlobalLobbyLoginWindow.h"
+#include "GlobalLobbyWindow.h"
+
+#include "../gui/CGuiHandler.h"
+#include "../gui/WindowHandler.h"
+#include "../windows/InfoWindows.h"
+#include "../CServerHandler.h"
+#include "../mainmenu/CMainMenu.h"
+
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/MetaString.h"
+#include "../../lib/TextOperations.h"
+
+GlobalLobbyClient::GlobalLobbyClient() = default;
+GlobalLobbyClient::~GlobalLobbyClient() = default;
+
+static std::string getCurrentTimeFormatted(int timeOffsetSeconds = 0)
+{
+	// FIXME: better/unified way to format date
+	auto timeNowChrono = std::chrono::system_clock::now();
+	timeNowChrono += std::chrono::seconds(timeOffsetSeconds);
+
+	return TextOperations::getFormattedTimeLocal(std::chrono::system_clock::to_time_t(timeNowChrono));
+}
+
+void GlobalLobbyClient::onPacketReceived(const std::shared_ptr<INetworkConnection> &, const std::vector<std::byte> & message)
+{
+	boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
+
+	JsonNode json(message.data(), message.size());
+
+	if(json["type"].String() == "accountCreated")
+		return receiveAccountCreated(json);
+
+	if(json["type"].String() == "operationFailed")
+		return receiveOperationFailed(json);
+
+	if(json["type"].String() == "loginSuccess")
+		return receiveLoginSuccess(json);
+
+	if(json["type"].String() == "chatHistory")
+		return receiveChatHistory(json);
+
+	if(json["type"].String() == "chatMessage")
+		return receiveChatMessage(json);
+
+	if(json["type"].String() == "activeAccounts")
+		return receiveActiveAccounts(json);
+
+	if(json["type"].String() == "activeGameRooms")
+		return receiveActiveGameRooms(json);
+
+	if(json["type"].String() == "joinRoomSuccess")
+		return receiveJoinRoomSuccess(json);
+
+	if(json["type"].String() == "inviteReceived")
+		return receiveInviteReceived(json);
+
+	logGlobal->error("Received unexpected message from lobby server: %s", json["type"].String());
+}
+
+void GlobalLobbyClient::receiveAccountCreated(const JsonNode & json)
+{
+	auto loginWindowPtr = loginWindow.lock();
+
+	if(!loginWindowPtr || !GH.windows().topWindow<GlobalLobbyLoginWindow>())
+		throw std::runtime_error("lobby connection finished without active login window!");
+
+	{
+		Settings configID = settings.write["lobby"]["accountID"];
+		configID->String() = json["accountID"].String();
+
+		Settings configName = settings.write["lobby"]["displayName"];
+		configName->String() = json["displayName"].String();
+
+		Settings configCookie = settings.write["lobby"]["accountCookie"];
+		configCookie->String() = json["accountCookie"].String();
+	}
+
+	sendClientLogin();
+}
+
+void GlobalLobbyClient::receiveOperationFailed(const JsonNode & json)
+{
+	auto loginWindowPtr = loginWindow.lock();
+
+	if(loginWindowPtr)
+		loginWindowPtr->onConnectionFailed(json["reason"].String());
+
+	// TODO: handle errors in lobby menu
+}
+
+void GlobalLobbyClient::receiveLoginSuccess(const JsonNode & json)
+{
+	{
+		Settings configCookie = settings.write["lobby"]["accountCookie"];
+		configCookie->String() = json["accountCookie"].String();
+
+		Settings configName = settings.write["lobby"]["displayName"];
+		configName->String() = json["displayName"].String();
+	}
+
+	auto loginWindowPtr = loginWindow.lock();
+
+	if(!loginWindowPtr || !GH.windows().topWindow<GlobalLobbyLoginWindow>())
+		throw std::runtime_error("lobby connection finished without active login window!");
+
+	loginWindowPtr->onConnectionSuccess();
+}
+
+void GlobalLobbyClient::receiveChatHistory(const JsonNode & json)
+{
+	for(const auto & entry : json["messages"].Vector())
+	{
+		std::string accountID = entry["accountID"].String();
+		std::string displayName = entry["displayName"].String();
+		std::string messageText = entry["messageText"].String();
+		int ageSeconds = entry["ageSeconds"].Integer();
+		std::string timeFormatted = getCurrentTimeFormatted(-ageSeconds);
+
+		auto lobbyWindowPtr = lobbyWindow.lock();
+		if(lobbyWindowPtr)
+			lobbyWindowPtr->onGameChatMessage(displayName, messageText, timeFormatted);
+	}
+}
+
+void GlobalLobbyClient::receiveChatMessage(const JsonNode & json)
+{
+	std::string accountID = json["accountID"].String();
+	std::string displayName = json["displayName"].String();
+	std::string messageText = json["messageText"].String();
+	std::string timeFormatted = getCurrentTimeFormatted();
+	auto lobbyWindowPtr = lobbyWindow.lock();
+	if(lobbyWindowPtr)
+		lobbyWindowPtr->onGameChatMessage(displayName, messageText, timeFormatted);
+}
+
+void GlobalLobbyClient::receiveActiveAccounts(const JsonNode & json)
+{
+	activeAccounts.clear();
+
+	for (auto const & jsonEntry : json["accounts"].Vector())
+	{
+		GlobalLobbyAccount account;
+
+		account.accountID = jsonEntry["accountID"].String();
+		account.displayName = jsonEntry["displayName"].String();
+		account.status = jsonEntry["status"].String();
+
+		activeAccounts.push_back(account);
+	}
+
+	auto lobbyWindowPtr = lobbyWindow.lock();
+	if(lobbyWindowPtr)
+		lobbyWindowPtr->onActiveAccounts(activeAccounts);
+}
+
+void GlobalLobbyClient::receiveActiveGameRooms(const JsonNode & json)
+{
+	activeRooms.clear();
+
+	for (auto const & jsonEntry : json["gameRooms"].Vector())
+	{
+		GlobalLobbyRoom room;
+
+		room.gameRoomID = jsonEntry["gameRoomID"].String();
+		room.hostAccountID = jsonEntry["hostAccountID"].String();
+		room.hostAccountDisplayName = jsonEntry["hostAccountDisplayName"].String();
+		room.description = jsonEntry["description"].String();
+		room.playersCount = jsonEntry["playersCount"].Integer();
+		room.playersLimit = jsonEntry["playersLimit"].Integer();
+
+		activeRooms.push_back(room);
+	}
+
+	auto lobbyWindowPtr = lobbyWindow.lock();
+	if(lobbyWindowPtr)
+		lobbyWindowPtr->onActiveRooms(activeRooms);
+}
+
+void GlobalLobbyClient::receiveInviteReceived(const JsonNode & json)
+{
+	assert(0); //TODO
+}
+
+void GlobalLobbyClient::receiveJoinRoomSuccess(const JsonNode & json)
+{
+	Settings configRoom = settings.write["lobby"]["roomID"];
+	configRoom->String() = json["gameRoomID"].String();
+
+	if (json["proxyMode"].Bool())
+	{
+		CSH->resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOBBY_GUEST, {});
+		CSH->loadMode = ELoadMode::MULTI;
+
+		std::string hostname = settings["lobby"]["hostname"].String();
+		int16_t port = settings["lobby"]["port"].Integer();
+		CSH->connectToServer(hostname, port);
+	}
+}
+
+void GlobalLobbyClient::onConnectionEstablished(const std::shared_ptr<INetworkConnection> & connection)
+{
+	boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
+	networkConnection = connection;
+
+	auto loginWindowPtr = loginWindow.lock();
+
+	if(!loginWindowPtr || !GH.windows().topWindow<GlobalLobbyLoginWindow>())
+		throw std::runtime_error("lobby connection established without active login window!");
+
+	loginWindowPtr->onConnectionSuccess();
+}
+
+void GlobalLobbyClient::sendClientRegister(const std::string & accountName)
+{
+	JsonNode toSend;
+	toSend["type"].String() = "clientRegister";
+	toSend["displayName"].String() = accountName;
+	sendMessage(toSend);
+}
+
+void GlobalLobbyClient::sendClientLogin()
+{
+	JsonNode toSend;
+	toSend["type"].String() = "clientLogin";
+	toSend["accountID"] = settings["lobby"]["accountID"];
+	toSend["accountCookie"] = settings["lobby"]["accountCookie"];
+	sendMessage(toSend);
+}
+
+void GlobalLobbyClient::onConnectionFailed(const std::string & errorMessage)
+{
+	boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
+
+	auto loginWindowPtr = loginWindow.lock();
+
+	if(!loginWindowPtr || !GH.windows().topWindow<GlobalLobbyLoginWindow>())
+		throw std::runtime_error("lobby connection failed without active login window!");
+
+	logGlobal->warn("Connection to game lobby failed! Reason: %s", errorMessage);
+	loginWindowPtr->onConnectionFailed(errorMessage);
+}
+
+void GlobalLobbyClient::onDisconnected(const std::shared_ptr<INetworkConnection> & connection, const std::string & errorMessage)
+{
+	boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
+
+	assert(connection == networkConnection);
+	networkConnection.reset();
+
+	while (!GH.windows().findWindows<GlobalLobbyWindow>().empty())
+	{
+		// if global lobby is open, pop all dialogs on top of it as well as lobby itself
+		GH.windows().popWindows(1);
+	}
+
+	CInfoWindow::showInfoDialog("Connection to game lobby was lost!", {});
+}
+
+void GlobalLobbyClient::sendMessage(const JsonNode & data)
+{
+	networkConnection->sendPacket(data.toBytes(true));
+}
+
+void GlobalLobbyClient::sendOpenPublicRoom()
+{
+	JsonNode toSend;
+	toSend["type"].String() = "openGameRoom";
+	toSend["hostAccountID"] = settings["lobby"]["accountID"];
+	toSend["roomType"].String() = "public";
+	sendMessage(toSend);
+}
+
+void GlobalLobbyClient::sendOpenPrivateRoom()
+{
+	JsonNode toSend;
+	toSend["type"].String() = "openGameRoom";
+	toSend["hostAccountID"] = settings["lobby"]["accountID"];
+	toSend["roomType"].String() = "private";
+	sendMessage(toSend);
+}
+
+void GlobalLobbyClient::connect()
+{
+	std::string hostname = settings["lobby"]["hostname"].String();
+	int16_t port = settings["lobby"]["port"].Integer();
+	CSH->getNetworkHandler().connectToRemote(*this, hostname, port);
+}
+
+bool GlobalLobbyClient::isConnected() const
+{
+	return networkConnection != nullptr;
+}
+
+std::shared_ptr<GlobalLobbyLoginWindow> GlobalLobbyClient::createLoginWindow()
+{
+	auto loginWindowPtr = loginWindow.lock();
+	if(loginWindowPtr)
+		return loginWindowPtr;
+
+	auto loginWindowNew = std::make_shared<GlobalLobbyLoginWindow>();
+	loginWindow = loginWindowNew;
+
+	return loginWindowNew;
+}
+
+std::shared_ptr<GlobalLobbyWindow> GlobalLobbyClient::createLobbyWindow()
+{
+	auto lobbyWindowPtr = lobbyWindow.lock();
+	if(lobbyWindowPtr)
+		return lobbyWindowPtr;
+
+	lobbyWindowPtr = std::make_shared<GlobalLobbyWindow>();
+	lobbyWindow = lobbyWindowPtr;
+	lobbyWindowLock = lobbyWindowPtr;
+	return lobbyWindowPtr;
+}
+
+const std::vector<GlobalLobbyAccount> & GlobalLobbyClient::getActiveAccounts() const
+{
+	return activeAccounts;
+}
+
+const std::vector<GlobalLobbyRoom> & GlobalLobbyClient::getActiveRooms() const
+{
+	return activeRooms;
+}
+
+void GlobalLobbyClient::activateInterface()
+{
+	if (!GH.windows().findWindows<GlobalLobbyWindow>().empty())
+		return;
+
+	if (!GH.windows().findWindows<GlobalLobbyLoginWindow>().empty())
+		return;
+
+	if (isConnected())
+		GH.windows().pushWindow(createLobbyWindow());
+	else
+		GH.windows().pushWindow(createLoginWindow());
+}
+
+void GlobalLobbyClient::sendProxyConnectionLogin(const NetworkConnectionPtr & netConnection)
+{
+	JsonNode toSend;
+	toSend["type"].String() = "clientProxyLogin";
+	toSend["accountID"] = settings["lobby"]["accountID"];
+	toSend["accountCookie"] = settings["lobby"]["accountCookie"];
+	toSend["gameRoomID"] = settings["lobby"]["roomID"];
+
+	netConnection->sendPacket(toSend.toBytes(true));
+}

+ 70 - 0
client/globalLobby/GlobalLobbyClient.h

@@ -0,0 +1,70 @@
+/*
+ * GlobalLobbyClient.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 "GlobalLobbyDefines.h"
+#include "../../lib/network/NetworkInterface.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+class JsonNode;
+VCMI_LIB_NAMESPACE_END
+
+class GlobalLobbyLoginWindow;
+class GlobalLobbyWindow;
+
+class GlobalLobbyClient final : public INetworkClientListener, boost::noncopyable
+{
+	std::vector<GlobalLobbyAccount> activeAccounts;
+	std::vector<GlobalLobbyRoom> activeRooms;
+
+	std::shared_ptr<INetworkConnection> networkConnection;
+
+	std::weak_ptr<GlobalLobbyLoginWindow> loginWindow;
+	std::weak_ptr<GlobalLobbyWindow> lobbyWindow;
+	std::shared_ptr<GlobalLobbyWindow> lobbyWindowLock; // helper strong reference to prevent window destruction on closing
+
+	void onPacketReceived(const std::shared_ptr<INetworkConnection> &, const std::vector<std::byte> & message) override;
+	void onConnectionFailed(const std::string & errorMessage) override;
+	void onConnectionEstablished(const std::shared_ptr<INetworkConnection> &) override;
+	void onDisconnected(const std::shared_ptr<INetworkConnection> &, const std::string & errorMessage) override;
+
+	void receiveAccountCreated(const JsonNode & json);
+	void receiveOperationFailed(const JsonNode & json);
+	void receiveLoginSuccess(const JsonNode & json);
+	void receiveChatHistory(const JsonNode & json);
+	void receiveChatMessage(const JsonNode & json);
+	void receiveActiveAccounts(const JsonNode & json);
+	void receiveActiveGameRooms(const JsonNode & json);
+	void receiveJoinRoomSuccess(const JsonNode & json);
+	void receiveInviteReceived(const JsonNode & json);
+
+	std::shared_ptr<GlobalLobbyLoginWindow> createLoginWindow();
+	std::shared_ptr<GlobalLobbyWindow> createLobbyWindow();
+
+public:
+	explicit GlobalLobbyClient();
+	~GlobalLobbyClient();
+
+	const std::vector<GlobalLobbyAccount> & getActiveAccounts() const;
+	const std::vector<GlobalLobbyRoom> & getActiveRooms() const;
+
+	/// Activate interface and pushes lobby UI as top window
+	void activateInterface();
+	void sendMessage(const JsonNode & data);
+	void sendClientRegister(const std::string & accountName);
+	void sendClientLogin();
+	void sendOpenPublicRoom();
+	void sendOpenPrivateRoom();
+
+	void sendProxyConnectionLogin(const NetworkConnectionPtr & netConnection);
+
+	void connect();
+	bool isConnected() const;
+};

+ 27 - 0
client/globalLobby/GlobalLobbyDefines.h

@@ -0,0 +1,27 @@
+/*
+ * GlobalLobbyClient.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
+
+struct GlobalLobbyAccount
+{
+	std::string accountID;
+	std::string displayName;
+	std::string status;
+};
+
+struct GlobalLobbyRoom
+{
+	std::string gameRoomID;
+	std::string hostAccountID;
+	std::string hostAccountDisplayName;
+	std::string description;
+	int playersCount;
+	int playersLimit;
+};

+ 128 - 0
client/globalLobby/GlobalLobbyLoginWindow.cpp

@@ -0,0 +1,128 @@
+/*
+ * GlobalLobbyLoginWindow.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 "GlobalLobbyLoginWindow.h"
+
+#include "GlobalLobbyClient.h"
+#include "GlobalLobbyWindow.h"
+
+#include "../CGameInfo.h"
+#include "../CServerHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/WindowHandler.h"
+#include "../widgets/Buttons.h"
+#include "../widgets/Images.h"
+#include "../widgets/MiscWidgets.h"
+#include "../widgets/TextControls.h"
+
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/MetaString.h"
+
+GlobalLobbyLoginWindow::GlobalLobbyLoginWindow()
+	: CWindowObject(BORDERED)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	pos.w = 284;
+	pos.h = 220;
+
+	filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
+	labelTitle = std::make_shared<CLabel>( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.login.title"));
+	labelUsername = std::make_shared<CLabel>( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.lobby.login.username"));
+	backgroundUsername = std::make_shared<TransparentFilledRectangle>(Rect(10, 90, 264, 20), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64));
+	inputUsername = std::make_shared<CTextInput>(Rect(15, 93, 260, 16), FONT_SMALL, nullptr, ETextAlignment::TOPLEFT, true);
+	buttonLogin = std::make_shared<CButton>(Point(10, 180), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onLogin(); });
+	buttonClose = std::make_shared<CButton>(Point(210, 180), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); });
+	labelStatus = std::make_shared<CTextBox>( "", Rect(15, 115, 255, 60), 1, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
+
+	auto buttonRegister = std::make_shared<CToggleButton>(Point(10, 40),  AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0);
+	auto buttonLogin = std::make_shared<CToggleButton>(Point(146, 40), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0);
+	buttonRegister->addTextOverlay(CGI->generaltexth->translate("vcmi.lobby.login.create"), EFonts::FONT_SMALL, Colors::YELLOW);
+	buttonLogin->addTextOverlay(CGI->generaltexth->translate("vcmi.lobby.login.login"), EFonts::FONT_SMALL, Colors::YELLOW);
+
+	toggleMode = std::make_shared<CToggleGroup>(nullptr);
+	toggleMode->addToggle(0, buttonRegister);
+	toggleMode->addToggle(1, buttonLogin);
+	toggleMode->setSelected(settings["lobby"]["roomType"].Integer());
+	toggleMode->addCallback([this](int index){onLoginModeChanged(index);});
+
+	if (settings["lobby"]["accountID"].String().empty())
+	{
+		buttonLogin->block(true);
+		toggleMode->setSelected(0);
+	}
+	else
+		toggleMode->setSelected(1);
+
+	filledBackground->playerColored(PlayerColor(1));
+	inputUsername->cb += [this](const std::string & text)
+	{
+		this->buttonLogin->block(text.empty());
+	};
+
+	center();
+}
+
+void GlobalLobbyLoginWindow::onLoginModeChanged(int value)
+{
+	if (value == 0)
+	{
+		inputUsername->setText("");
+	}
+	else
+	{
+		inputUsername->setText(settings["lobby"]["displayName"].String());
+	}
+}
+
+void GlobalLobbyLoginWindow::onClose()
+{
+	close();
+	// TODO: abort ongoing connection attempt, if any
+}
+
+void GlobalLobbyLoginWindow::onLogin()
+{
+	labelStatus->setText(CGI->generaltexth->translate("vcmi.lobby.login.connecting"));
+	if(!CSH->getGlobalLobby().isConnected())
+		CSH->getGlobalLobby().connect();
+	else
+		onConnectionSuccess();
+
+	buttonClose->block(true);
+}
+
+void GlobalLobbyLoginWindow::onConnectionSuccess()
+{
+	std::string accountID = settings["lobby"]["accountID"].String();
+
+	if(toggleMode->getSelected() == 0)
+		CSH->getGlobalLobby().sendClientRegister(inputUsername->getText());
+	else
+		CSH->getGlobalLobby().sendClientLogin();
+}
+
+void GlobalLobbyLoginWindow::onLoginSuccess()
+{
+	close();
+	CSH->getGlobalLobby().activateInterface();
+}
+
+void GlobalLobbyLoginWindow::onConnectionFailed(const std::string & reason)
+{
+	MetaString formatter;
+	formatter.appendTextID("vcmi.lobby.login.error");
+	formatter.replaceRawString(reason);
+
+	labelStatus->setText(formatter.toString());
+	buttonClose->block(false);
+}

+ 45 - 0
client/globalLobby/GlobalLobbyLoginWindow.h

@@ -0,0 +1,45 @@
+/*
+ * GlobalLobbyLoginWindow.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 "../windows/CWindowObject.h"
+
+class CLabel;
+class CTextBox;
+class CTextInput;
+class CToggleGroup;
+class FilledTexturePlayerColored;
+class TransparentFilledRectangle;
+class CButton;
+
+class GlobalLobbyLoginWindow : public CWindowObject
+{
+	std::shared_ptr<FilledTexturePlayerColored> filledBackground;
+	std::shared_ptr<CLabel> labelTitle;
+	std::shared_ptr<CLabel> labelUsername;
+	std::shared_ptr<CTextBox> labelStatus;
+	std::shared_ptr<TransparentFilledRectangle> backgroundUsername;
+	std::shared_ptr<CTextInput> inputUsername;
+
+	std::shared_ptr<CButton> buttonLogin;
+	std::shared_ptr<CButton> buttonClose;
+	std::shared_ptr<CToggleGroup> toggleMode; // create account or use existing
+
+	void onLoginModeChanged(int value);
+	void onClose();
+	void onLogin();
+
+public:
+	GlobalLobbyLoginWindow();
+
+	void onConnectionSuccess();
+	void onLoginSuccess();
+	void onConnectionFailed(const std::string & reason);
+};

+ 142 - 0
client/globalLobby/GlobalLobbyServerSetup.cpp

@@ -0,0 +1,142 @@
+/*
+ * GlobalLobbyServerSetup.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 "GlobalLobbyServerSetup.h"
+
+#include "../CGameInfo.h"
+#include "../CServerHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../mainmenu/CMainMenu.h"
+#include "../widgets/Buttons.h"
+#include "../widgets/Images.h"
+#include "../widgets/TextControls.h"
+
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/MetaString.h"
+
+GlobalLobbyServerSetup::GlobalLobbyServerSetup()
+	: CWindowObject(BORDERED)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	pos.w = 284;
+	pos.h = 340;
+
+	filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
+	labelTitle = std::make_shared<CLabel>( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.create"));
+	labelPlayerLimit = std::make_shared<CLabel>( pos.w / 2, 48, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.players.limit"));
+	labelRoomType = std::make_shared<CLabel>( pos.w / 2, 108, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.type"));
+	labelGameMode = std::make_shared<CLabel>( pos.w / 2, 158, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.mode"));
+
+	togglePlayerLimit = std::make_shared<CToggleGroup>(nullptr);
+	togglePlayerLimit->addToggle(2, std::make_shared<CToggleButton>(Point(10 + 39*0, 60), AnimationPath::builtin("RanNum2"), CButton::tooltip(), 0));
+	togglePlayerLimit->addToggle(3, std::make_shared<CToggleButton>(Point(10 + 39*1, 60), AnimationPath::builtin("RanNum3"), CButton::tooltip(), 0));
+	togglePlayerLimit->addToggle(4, std::make_shared<CToggleButton>(Point(10 + 39*2, 60), AnimationPath::builtin("RanNum4"), CButton::tooltip(), 0));
+	togglePlayerLimit->addToggle(5, std::make_shared<CToggleButton>(Point(10 + 39*3, 60), AnimationPath::builtin("RanNum5"), CButton::tooltip(), 0));
+	togglePlayerLimit->addToggle(6, std::make_shared<CToggleButton>(Point(10 + 39*4, 60), AnimationPath::builtin("RanNum6"), CButton::tooltip(), 0));
+	togglePlayerLimit->addToggle(7, std::make_shared<CToggleButton>(Point(10 + 39*5, 60), AnimationPath::builtin("RanNum7"), CButton::tooltip(), 0));
+	togglePlayerLimit->addToggle(8, std::make_shared<CToggleButton>(Point(10 + 39*6, 60), AnimationPath::builtin("RanNum8"), CButton::tooltip(), 0));
+	togglePlayerLimit->setSelected(settings["lobby"]["roomPlayerLimit"].Integer());
+	togglePlayerLimit->addCallback([this](int index){onPlayerLimitChanged(index);});
+
+	auto buttonPublic  = std::make_shared<CToggleButton>(Point(10, 120),  AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0);
+	auto buttonPrivate = std::make_shared<CToggleButton>(Point(146, 120), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0);
+	buttonPublic->addTextOverlay(CGI->generaltexth->translate("vcmi.lobby.room.public"), EFonts::FONT_SMALL, Colors::YELLOW);
+	buttonPrivate->addTextOverlay(CGI->generaltexth->translate("vcmi.lobby.room.private"), EFonts::FONT_SMALL, Colors::YELLOW);
+
+	toggleRoomType = std::make_shared<CToggleGroup>(nullptr);
+	toggleRoomType->addToggle(0, buttonPublic);
+	toggleRoomType->addToggle(1, buttonPrivate);
+	toggleRoomType->setSelected(settings["lobby"]["roomType"].Integer());
+	toggleRoomType->addCallback([this](int index){onRoomTypeChanged(index);});
+
+	auto buttonNewGame = std::make_shared<CToggleButton>(Point(10, 170),  AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0);
+	auto buttonLoadGame = std::make_shared<CToggleButton>(Point(146, 170), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0);
+	buttonNewGame->addTextOverlay(CGI->generaltexth->translate("vcmi.lobby.room.new"), EFonts::FONT_SMALL, Colors::YELLOW);
+	buttonLoadGame->addTextOverlay(CGI->generaltexth->translate("vcmi.lobby.room.load"), EFonts::FONT_SMALL, Colors::YELLOW);
+
+	toggleGameMode = std::make_shared<CToggleGroup>(nullptr);
+	toggleGameMode->addToggle(0, buttonNewGame);
+	toggleGameMode->addToggle(1, buttonLoadGame);
+	toggleGameMode->setSelected(settings["lobby"]["roomMode"].Integer());
+	toggleGameMode->addCallback([this](int index){onGameModeChanged(index);});
+
+	labelDescription = std::make_shared<CTextBox>("", Rect(10, 195, pos.w - 20, 80), 1, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
+
+	buttonCreate = std::make_shared<CButton>(Point(10, 300), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onCreate(); });
+	buttonClose = std::make_shared<CButton>(Point(210, 300), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); });
+
+	filledBackground->playerColored(PlayerColor(1));
+
+	updateDescription();
+	center();
+}
+
+void GlobalLobbyServerSetup::updateDescription()
+{
+	MetaString description;
+	description.appendRawString("%s %s %s");
+	if(toggleRoomType->getSelected() == 0)
+		description.replaceTextID("vcmi.lobby.room.description.public");
+	else
+		description.replaceTextID("vcmi.lobby.room.description.private");
+
+	if(toggleGameMode->getSelected() == 0)
+		description.replaceTextID("vcmi.lobby.room.description.new");
+	else
+		description.replaceTextID("vcmi.lobby.room.description.load");
+
+	description.replaceTextID("vcmi.lobby.room.description.limit");
+	description.replaceNumber(togglePlayerLimit->getSelected());
+
+	labelDescription->setText(description.toString());
+}
+
+void GlobalLobbyServerSetup::onPlayerLimitChanged(int value)
+{
+	Settings config = settings.write["lobby"]["roomPlayerLimit"];
+	config->Integer() = value;
+	updateDescription();
+}
+
+void GlobalLobbyServerSetup::onRoomTypeChanged(int value)
+{
+	Settings config = settings.write["lobby"]["roomType"];
+	config->Integer() = value;
+	updateDescription();
+}
+
+void GlobalLobbyServerSetup::onGameModeChanged(int value)
+{
+	Settings config = settings.write["lobby"]["roomMode"];
+	config->Integer() = value;
+	updateDescription();
+}
+
+void GlobalLobbyServerSetup::onCreate()
+{
+	if(toggleGameMode->getSelected() == 0)
+		CSH->resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOBBY_HOST, {});
+	else
+		CSH->resetStateForLobby(EStartMode::LOAD_GAME, ESelectionScreen::loadGame, EServerMode::LOBBY_HOST, {});
+
+	CSH->loadMode = ELoadMode::MULTI;
+	CSH->startLocalServerAndConnect(true);
+
+	buttonCreate->block(true);
+	buttonClose->block(true);
+}
+
+void GlobalLobbyServerSetup::onClose()
+{
+	close();
+}

+ 49 - 0
client/globalLobby/GlobalLobbyServerSetup.h

@@ -0,0 +1,49 @@
+/*
+ * GlobalLobbyServerSetup.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 "../windows/CWindowObject.h"
+
+class CLabel;
+class CTextBox;
+class FilledTexturePlayerColored;
+class CButton;
+class CToggleGroup;
+
+class GlobalLobbyServerSetup : public CWindowObject
+{
+	std::shared_ptr<FilledTexturePlayerColored> filledBackground;
+	std::shared_ptr<CLabel> labelTitle;
+
+	std::shared_ptr<CLabel> labelPlayerLimit;
+	std::shared_ptr<CLabel> labelRoomType;
+	std::shared_ptr<CLabel> labelGameMode;
+
+	std::shared_ptr<CToggleGroup> togglePlayerLimit; // 2-8
+	std::shared_ptr<CToggleGroup> toggleRoomType; // public or private
+	std::shared_ptr<CToggleGroup> toggleGameMode; // new game or load game
+
+	std::shared_ptr<CTextBox> labelDescription;
+	std::shared_ptr<CTextBox> labelStatus;
+
+	std::shared_ptr<CButton> buttonCreate;
+	std::shared_ptr<CButton> buttonClose;
+
+	void updateDescription();
+	void onPlayerLimitChanged(int value);
+	void onRoomTypeChanged(int value);
+	void onGameModeChanged(int value);
+
+	void onCreate();
+	void onClose();
+
+public:
+	GlobalLobbyServerSetup();
+};

+ 154 - 0
client/globalLobby/GlobalLobbyWidget.cpp

@@ -0,0 +1,154 @@
+/*
+ * GlobalLobbyWidget.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 "GlobalLobbyWidget.h"
+
+#include "GlobalLobbyClient.h"
+#include "GlobalLobbyWindow.h"
+
+#include "../CServerHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/WindowHandler.h"
+#include "../widgets/Buttons.h"
+#include "../widgets/MiscWidgets.h"
+#include "../widgets/ObjectLists.h"
+#include "../widgets/TextControls.h"
+
+#include "../../lib/MetaString.h"
+GlobalLobbyWidget::GlobalLobbyWidget(GlobalLobbyWindow * window)
+	: window(window)
+{
+	addCallback("closeWindow", [](int) { GH.windows().popWindows(1); });
+	addCallback("sendMessage", [this](int) { this->window->doSendChatMessage(); });
+	addCallback("createGameRoom", [this](int) { this->window->doCreateGameRoom(); });
+
+	REGISTER_BUILDER("accountList", &GlobalLobbyWidget::buildAccountList);
+	REGISTER_BUILDER("roomList", &GlobalLobbyWidget::buildRoomList);
+
+	const JsonNode config(JsonPath::builtin("config/widgets/lobbyWindow.json"));
+	build(config);
+}
+
+std::shared_ptr<CIntObject> GlobalLobbyWidget::buildAccountList(const JsonNode & config) const
+{
+	const auto & createCallback = [this](size_t index) -> std::shared_ptr<CIntObject>
+	{
+		const auto & accounts = CSH->getGlobalLobby().getActiveAccounts();
+
+		if(index < accounts.size())
+			return std::make_shared<GlobalLobbyAccountCard>(this->window, accounts[index]);
+		return std::make_shared<CIntObject>();
+	};
+
+	auto position = readPosition(config["position"]);
+	auto itemOffset = readPosition(config["itemOffset"]);
+	auto sliderPosition = readPosition(config["sliderPosition"]);
+	auto sliderSize = readPosition(config["sliderSize"]);
+	size_t visibleSize = 4; // FIXME: how many items can fit into UI?
+	size_t totalSize = 4; //FIXME: how many items are there in total
+	int sliderMode = 1 | 4; //  present, vertical, blue
+	int initialPos = 0;
+
+	return std::make_shared<CListBox>(createCallback, position, itemOffset, visibleSize, totalSize, initialPos, sliderMode, Rect(sliderPosition, sliderSize) );
+}
+
+std::shared_ptr<CIntObject> GlobalLobbyWidget::buildRoomList(const JsonNode & config) const
+{
+	const auto & createCallback = [this](size_t index) -> std::shared_ptr<CIntObject>
+	{
+		const auto & rooms = CSH->getGlobalLobby().getActiveRooms();
+
+		if(index < rooms.size())
+			return std::make_shared<GlobalLobbyRoomCard>(this->window, rooms[index]);
+		return std::make_shared<CIntObject>();
+	};
+
+	auto position = readPosition(config["position"]);
+	auto itemOffset = readPosition(config["itemOffset"]);
+	auto sliderPosition = readPosition(config["sliderPosition"]);
+	auto sliderSize = readPosition(config["sliderSize"]);
+	size_t visibleSize = 4; // FIXME: how many items can fit into UI?
+	size_t totalSize = 4; //FIXME: how many items are there in total
+	int sliderMode = 1 | 4; //  present, vertical, blue
+	int initialPos = 0;
+
+	return std::make_shared<CListBox>(createCallback, position, itemOffset, visibleSize, totalSize, initialPos, sliderMode, Rect(sliderPosition, sliderSize) );
+}
+
+std::shared_ptr<CLabel> GlobalLobbyWidget::getAccountNameLabel()
+{
+	return widget<CLabel>("accountNameLabel");
+}
+
+std::shared_ptr<CTextInput> GlobalLobbyWidget::getMessageInput()
+{
+	return widget<CTextInput>("messageInput");
+}
+
+std::shared_ptr<CTextBox> GlobalLobbyWidget::getGameChat()
+{
+	return widget<CTextBox>("gameChat");
+}
+
+std::shared_ptr<CListBox> GlobalLobbyWidget::getAccountList()
+{
+	return widget<CListBox>("accountList");
+}
+
+std::shared_ptr<CListBox> GlobalLobbyWidget::getRoomList()
+{
+	return widget<CListBox>("roomList");
+}
+
+GlobalLobbyAccountCard::GlobalLobbyAccountCard(GlobalLobbyWindow * window, const GlobalLobbyAccount & accountDescription)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	const auto & onInviteClicked = [window, accountID=accountDescription.accountID]()
+	{
+		window->doInviteAccount(accountID);
+	};
+
+	pos.w = 130;
+	pos.h = 40;
+
+	backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64));
+	labelName = std::make_shared<CLabel>( 5, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, accountDescription.displayName);
+	labelStatus = std::make_shared<CLabel>( 5, 20, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, accountDescription.status);
+
+	if (CSH->inLobbyRoom())
+		buttonInvite = std::make_shared<CButton>(Point(95, 8), AnimationPath::builtin("settingsWindow/button32"), CButton::tooltip(), onInviteClicked);
+}
+
+GlobalLobbyRoomCard::GlobalLobbyRoomCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & roomDescription)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	const auto & onJoinClicked = [window, roomID=roomDescription.gameRoomID]()
+	{
+		window->doJoinRoom(roomID);
+	};
+
+	auto roomSizeText = MetaString::createFromRawString("%d/%d");
+	roomSizeText.replaceNumber(roomDescription.playersCount);
+	roomSizeText.replaceNumber(roomDescription.playersLimit);
+
+	pos.w = 230;
+	pos.h = 40;
+
+	backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64));
+	labelName = std::make_shared<CLabel>( 5, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, roomDescription.hostAccountDisplayName);
+	labelStatus = std::make_shared<CLabel>( 5, 20, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, roomDescription.description);
+	labelRoomSize = std::make_shared<CLabel>( 160, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, roomSizeText.toString());
+
+	if (!CSH->inGame())
+		buttonJoin = std::make_shared<CButton>(Point(195, 8), AnimationPath::builtin("settingsWindow/button32"), CButton::tooltip(), onJoinClicked);
+}

+ 57 - 0
client/globalLobby/GlobalLobbyWidget.h

@@ -0,0 +1,57 @@
+/*
+ * GlobalLobbyWidget.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 "../gui/InterfaceObjectConfigurable.h"
+
+class GlobalLobbyWindow;
+struct GlobalLobbyAccount;
+struct GlobalLobbyRoom;
+class CListBox;
+
+class GlobalLobbyWidget : public InterfaceObjectConfigurable
+{
+	GlobalLobbyWindow * window;
+
+	std::shared_ptr<CIntObject> buildAccountList(const JsonNode &) const;
+	std::shared_ptr<CIntObject> buildRoomList(const JsonNode &) const;
+
+public:
+	explicit GlobalLobbyWidget(GlobalLobbyWindow * window);
+
+	std::shared_ptr<CLabel> getAccountNameLabel();
+	std::shared_ptr<CTextInput> getMessageInput();
+	std::shared_ptr<CTextBox> getGameChat();
+	std::shared_ptr<CListBox> getAccountList();
+	std::shared_ptr<CListBox> getRoomList();
+};
+
+class GlobalLobbyAccountCard : public CIntObject
+{
+public:
+	GlobalLobbyAccountCard(GlobalLobbyWindow * window, const GlobalLobbyAccount & accountDescription);
+
+	std::shared_ptr<TransparentFilledRectangle> backgroundOverlay;
+	std::shared_ptr<CLabel> labelName;
+	std::shared_ptr<CLabel> labelStatus;
+	std::shared_ptr<CButton> buttonInvite;
+};
+
+class GlobalLobbyRoomCard : public CIntObject
+{
+public:
+	GlobalLobbyRoomCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & roomDescription);
+
+	std::shared_ptr<TransparentFilledRectangle> backgroundOverlay;
+	std::shared_ptr<CLabel> labelName;
+	std::shared_ptr<CLabel> labelRoomSize;
+	std::shared_ptr<CLabel> labelStatus;
+	std::shared_ptr<CButton> buttonJoin;
+};

+ 105 - 0
client/globalLobby/GlobalLobbyWindow.cpp

@@ -0,0 +1,105 @@
+/*
+ * GlobalLobbyWindow.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 "GlobalLobbyWindow.h"
+
+#include "GlobalLobbyClient.h"
+#include "GlobalLobbyServerSetup.h"
+#include "GlobalLobbyWidget.h"
+
+#include "../CServerHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/WindowHandler.h"
+#include "../widgets/TextControls.h"
+#include "../widgets/ObjectLists.h"
+
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/MetaString.h"
+
+GlobalLobbyWindow::GlobalLobbyWindow()
+	: CWindowObject(BORDERED)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	widget = std::make_shared<GlobalLobbyWidget>(this);
+	pos = widget->pos;
+	center();
+
+	widget->getAccountNameLabel()->setText(settings["lobby"]["displayName"].String());
+}
+
+void GlobalLobbyWindow::doSendChatMessage()
+{
+	std::string messageText = widget->getMessageInput()->getText();
+
+	JsonNode toSend;
+	toSend["type"].String() = "sendChatMessage";
+	toSend["messageText"].String() = messageText;
+
+	CSH->getGlobalLobby().sendMessage(toSend);
+
+	widget->getMessageInput()->setText("");
+}
+
+void GlobalLobbyWindow::doCreateGameRoom()
+{
+	GH.windows().createAndPushWindow<GlobalLobbyServerSetup>();
+}
+
+void GlobalLobbyWindow::doInviteAccount(const std::string & accountID)
+{
+	JsonNode toSend;
+	toSend["type"].String() = "sendInvite";
+	toSend["accountID"].String() = accountID;
+
+	CSH->getGlobalLobby().sendMessage(toSend);
+}
+
+void GlobalLobbyWindow::doJoinRoom(const std::string & roomID)
+{
+	JsonNode toSend;
+	toSend["type"].String() = "joinGameRoom";
+	toSend["gameRoomID"].String() = roomID;
+
+	CSH->getGlobalLobby().sendMessage(toSend);
+}
+
+void GlobalLobbyWindow::onGameChatMessage(const std::string & sender, const std::string & message, const std::string & when)
+{
+	MetaString chatMessageFormatted;
+	chatMessageFormatted.appendRawString("[%s] {%s}: %s\n");
+	chatMessageFormatted.replaceRawString(when);
+	chatMessageFormatted.replaceRawString(sender);
+	chatMessageFormatted.replaceRawString(message);
+
+	chatHistory += chatMessageFormatted.toString();
+
+	widget->getGameChat()->setText(chatHistory);
+}
+
+void GlobalLobbyWindow::onActiveAccounts(const std::vector<GlobalLobbyAccount> & accounts)
+{
+	widget->getAccountList()->reset();
+}
+
+void GlobalLobbyWindow::onActiveRooms(const std::vector<GlobalLobbyRoom> & rooms)
+{
+	widget->getRoomList()->reset();
+}
+
+void GlobalLobbyWindow::onJoinedRoom()
+{
+	widget->getAccountList()->reset();
+}
+
+void GlobalLobbyWindow::onLeftRoom()
+{
+	widget->getAccountList()->reset();
+}

+ 38 - 0
client/globalLobby/GlobalLobbyWindow.h

@@ -0,0 +1,38 @@
+/*
+ * GlobalLobbyWindow.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 "../windows/CWindowObject.h"
+
+class GlobalLobbyWidget;
+struct GlobalLobbyAccount;
+struct GlobalLobbyRoom;
+
+class GlobalLobbyWindow : public CWindowObject
+{
+	std::string chatHistory;
+
+	std::shared_ptr<GlobalLobbyWidget> widget;
+
+public:
+	GlobalLobbyWindow();
+
+	void doSendChatMessage();
+	void doCreateGameRoom();
+
+	void doInviteAccount(const std::string & accountID);
+	void doJoinRoom(const std::string & roomID);
+
+	void onGameChatMessage(const std::string & sender, const std::string & message, const std::string & when);
+	void onActiveAccounts(const std::vector<GlobalLobbyAccount> & accounts);
+	void onActiveRooms(const std::vector<GlobalLobbyRoom> & rooms);
+	void onJoinedRoom();
+	void onLeftRoom();
+};

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