浏览代码

Merge remote-tracking branch 'origin/develop' into ban_stuff_on_water_maps

Tomasz Zieliński 2 年之前
父节点
当前提交
bda126a1fd
共有 76 个文件被更改,包括 1970 次插入925 次删除
  1. 4 11
      CCallback.cpp
  2. 123 0
      ChangeLog.md
  3. 2 1
      client/CMT.cpp
  4. 41 10
      client/CPlayerInterface.cpp
  5. 7 4
      client/adventureMap/AdventureMapInterface.cpp
  6. 12 15
      client/adventureMap/CList.cpp
  7. 11 4
      client/adventureMap/CList.h
  8. 3 1
      client/eventsSDL/InputSourceTouch.cpp
  9. 1 0
      client/gui/CIntObject.cpp
  10. 3 0
      client/render/IScreenHandler.h
  11. 3 3
      client/renderSDL/ScreenHandler.cpp
  12. 3 3
      client/renderSDL/ScreenHandler.h
  13. 1 4
      client/windows/CHeroWindow.cpp
  14. 14 6
      client/windows/CKingdomInterface.cpp
  15. 1 0
      client/windows/CKingdomInterface.h
  16. 19 9
      client/windows/GUIClasses.cpp
  17. 13 14
      client/windows/settings/GeneralOptionsTab.cpp
  18. 3 0
      cmake_modules/VCMI_lib.cmake
  19. 26 26
      config/battleStartpos.json
  20. 9 9
      config/filesystem.json
  21. 2 0
      config/gameConfig.json
  22. 2 2
      config/schemas/settings.json
  23. 2 2
      config/widgets/settings/battleOptionsTab.json
  24. 8 8
      launcher/firstLaunch/firstlaunch_moc.ui
  25. 2 0
      launcher/mainwindow_moc.cpp
  26. 13 6
      launcher/settingsView/csettingsview_moc.cpp
  27. 4 1
      launcher/settingsView/csettingsview_moc.h
  28. 11 3
      launcher/translation/chinese.ts
  29. 6 6
      launcher/translation/english.ts
  30. 13 5
      launcher/translation/french.ts
  31. 11 3
      launcher/translation/german.ts
  32. 11 3
      launcher/translation/polish.ts
  33. 11 3
      launcher/translation/russian.ts
  34. 12 4
      launcher/translation/spanish.ts
  35. 11 3
      launcher/translation/ukrainian.ts
  36. 1 1
      lib/CCreatureHandler.cpp
  37. 4 13
      lib/CGameInfoCallback.cpp
  38. 0 2
      lib/CGameInfoCallback.h
  39. 0 1
      lib/CPlayerState.cpp
  40. 0 2
      lib/CPlayerState.h
  41. 5 0
      lib/CRandomGenerator.cpp
  42. 3 0
      lib/CRandomGenerator.h
  43. 1 0
      lib/GameConstants.cpp
  44. 5 1
      lib/GameConstants.h
  45. 1 0
      lib/GameSettings.cpp
  46. 1 0
      lib/GameSettings.h
  47. 1 0
      lib/IGameCallback.cpp
  48. 2 2
      lib/NetPackVisitor.h
  49. 13 9
      lib/NetPacks.h
  50. 19 32
      lib/NetPacksLib.cpp
  51. 1 1
      lib/Rect.h
  52. 3 3
      lib/StartInfo.h
  53. 5 96
      lib/gameState/CGameState.cpp
  54. 5 20
      lib/gameState/CGameState.h
  55. 142 0
      lib/gameState/TavernHeroesPool.cpp
  56. 87 0
      lib/gameState/TavernHeroesPool.h
  57. 35 0
      lib/gameState/TavernSlot.h
  58. 2 2
      lib/mapObjectConstructors/CObjectClassesHandler.cpp
  59. 40 0
      lib/mapObjectConstructors/CommonConstructors.cpp
  60. 22 0
      lib/mapObjectConstructors/CommonConstructors.h
  61. 15 0
      lib/mapObjects/CGDwelling.cpp
  62. 3 3
      lib/mapping/CMap.h
  63. 3 1
      lib/registerTypes/RegisterTypes.h
  64. 1 0
      lib/registerTypes/TypesLobbyPacks.cpp
  65. 2 0
      lib/rmg/modificators/ObjectManager.cpp
  66. 1 1
      lib/spells/TargetCondition.cpp
  67. 4 1
      lib/spells/effects/Timed.cpp
  68. 35 544
      server/CGameHandler.cpp
  69. 23 11
      server/CGameHandler.h
  70. 4 0
      server/CMakeLists.txt
  71. 5 4
      server/CVCMIServer.cpp
  72. 397 0
      server/HeroPoolProcessor.cpp
  73. 66 0
      server/HeroPoolProcessor.h
  74. 7 6
      server/NetPacksServer.cpp
  75. 523 0
      server/PlayerMessageProcessor.cpp
  76. 65 0
      server/PlayerMessageProcessor.h

+ 4 - 11
CCallback.cpp

@@ -270,17 +270,10 @@ void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroIn
 {
 	assert(townOrTavern);
 	assert(hero);
-	ui8 i=0;
-	for(; i<gs->players[*player].availableHeroes.size(); i++)
-	{
-		if(gs->players[*player].availableHeroes[i] == hero)
-		{
-			HireHero pack(i, townOrTavern->id);
-			pack.player = *player;
-			sendRequest(&pack);
-			return;
-		}
-	}
+
+	HireHero pack(HeroTypeID(hero->subID), townOrTavern->id);
+	pack.player = *player;
+	sendRequest(&pack);
 }
 
 void CCallback::save( const std::string &fname )

+ 123 - 0
ChangeLog.md

@@ -1,6 +1,129 @@
 # 1.2.1 -> 1.3.0
 (unreleased)
 
+### GENERAL:
+* Implemented automatic interface scaling to any resolution supported by monitor
+* Implemented UI scaling option to scale game interface
+* Game resolution and UI scaling can now be changed without game restart
+* Fixed multiple issues with borderless fullscreen mode
+* On mobile systems game will now always run at native resolution with configurable UI scaling
+* Implemented support for Horn of the Abyss map format
+* Implemented option to replay results of quick combat
+* Added translations to French and Chinese
+* All in-game cheats are now case-insensitive
+* Added high-definition icon for Windows
+* Fix crash on connecting to server on FreeBSD and Flatpak builds
+
+### TOUCHSCREEN SUPPORT:
+* VCMI will now properly recognizes touch screen input
+* Implemented long tap gesture that shows popup window. Tap once more to close popup
+* Long tap gesture duration can now be configured in settings
+* Implemented swipe gesture for scrolling through lists
+* All windows that have sliders in UI can now be scrolled using swipe gesture
+* Implemented swipe gesture for attack direction selection: swipe from enemy position to position you want to attack from
+* Implemented pinch gesture for zooming adventure map
+* Implemented haptic feedback (vibration) for long press gesture
+
+### LAUNCHER:
+* Launcher will now attempt to automatically detect language of OS on first launch
+* Added "About" tab with information about project and environment
+* Added separate options for Allied AI and Enemy AI for adventure map
+* Patially fixed displaying of download progress for mods
+* Fixed potential crash on opening mod information for mods with a changelog
+
+### MAP EDITOR:
+* Fixed crash on cutting random town
+* Added option to export entire map as an image
+* Added validation for placing multiple heroes into starting town
+* It is now possible to have single player on a map
+* It is now possible to configure teams in editor
+
+### AI PLAYER:
+* Fixed potential crash on accessing market (VCAI)
+* Fixed potentially infinite turns (VCAI)
+
+### GAME MECHANICS
+* Implemented hero backpack limit (disabled by default)
+* Fixed Admiral's Hat movement points calculation
+* It is now possible to access Shipwrecks from coast
+* Hero path will now be correctly updated on equipping/unequipping Levitation Boots or Angel Wings
+* It is no longer possible to abort movement while hero is flying over water
+* Fixed digging for Grail
+* Implemented "Survive beyond a time limit" victory condition
+* Implemented "Defeat all monsters" victory condition
+* 100% damage resistance or damage reduction will make unit immune to a spell
+* Game will now randomly select obligatory skill for hero on levelup instead of always picking Fire Magic
+* Fixed duration of bonuses from visitable object such as Idol of Fortune
+* Rescued hero from prison will now correctly reveal map around him
+* Lighthouses will no longer give movement bonus on land
+
+### CAMPAIGNS:
+* Fixed transfer of artifacts into next scenario
+* Fixed crash on advancing to next scenario with heroes from mods
+* Fixed handling of "Start with building" campaign bonus
+* Fixed incorrect starting level of heroes in campaigns
+* Game will now play correct music track on scenario selection window
+* Dracon woll now correctly start without spellbook in Dragon Slayer campaign
+* Fixed frequent crash on moving to next scenario during campaign
+
+### RANDOM MAP GENERATOR:
+* Improved zone placement, shape and connections
+* Improved zone passability for better gameplay
+* Improved treasure distribution and treasure values to match SoD closely
+* RMG will now respect road settings set in menu
+* Tweaked many original templates so they allow new terrains and factions
+* Added "bannedTowns", "bannedTerrains", "bannedMonsters" zone properties
+* Added "road" property to connections
+* Support for "wide" connections
+* Support for new "fictive" and "repulsive" connections
+* RMG will now run faster, utilizing many CPU cores
+
+### INTERFACE:
+* Adventure map is now scalable and can be used with any resolution without mods
+* Adventure map interface is now correctly blocked during enemy turn
+* It is now possible to zoom in or out using mouse wheel or pinch gesture
+* It is now possible to reset zoom via Backspace hotkey
+* Receiving a message in chat will now play sound
+* Map grid will now correctly display on map start
+* Fixed multiple issues with incorrect updates of save/load game screen
+* Fixed missing fortifications level icon in town tooltip
+* Fixed positioning of resource label in Blacksmith window
+* Status bar on inactive windows will no longer show any tooltip from active window
+* Fixed highlighting of possible artifact placements when exchanging with allied hero
+* Implemented sound of flying movement (for Fly spell or Angel Wings)
+* Last symbol of entered cheat/chat message will no longer trigger hotkey
+
+### BATTLES:
+* Implemented Tower moat (Land Mines)
+* Implemented defence reduction for units in moat
+* Fixed movement through moat of double-hexed units
+* Fixed removal of Land Mines and Fire Walls
+* Obstacles will now corectly show up either below or above unit
+* It is now possible to teleport a unit through destroyed walls
+* Added distinct overlay image for showing movement range of highlighted unit
+* Added overlay for displaying shooting range penalties of units
+
+### MODDING:
+* Implemented initial version of VCMI campaign format
+* Implemented spell cast as possible reward for configurable object
+* Implemented support for configurable buildings in towns
+* Implemented support for placing prison, tavern and heroes on water
+* Implemented support for new boat types
+* It is now possible for boats to use other movement layers, such as "air"
+* It is now possible to use growing artifacts on artifacts that can be used by hero
+* It is now possible to configure town moat
+* Palette-cycling animation of terrains and rivers can now be configured in json
+* Game will now correctly resolve identifier in unexpected form (e.g. 'bless' vs 'spell.bless' vs 'core:bless')
+* Creature specialties that use short form ( "creature" : "pikeman" ) will now correctly affect all creature upgrades
+* It is now possible to configure spells for Shrines
+* It is now possible to configure upgrade costs per level for Hill Forts
+* It is now possible to configure boat type for Shipyards on adventure map and in town
+* Implemented support for HotA-style adventure map images for monsters, with offset
+* Replaced (SCHOOL)_SPELL_DMG_PREMY with SPELL_DAMAGE bonus (uses school as subtype).
+* Removed bonuses (SCHOOL)_SPELLS - replaced with SPELLS_OF_SCHOOL
+* Removed DIRECT_DAMAGE_IMMUNITY bonus - replaced by 100% spell damage resistance
+* MAGIC_SCHOOL_SKILL subtype has been changed for consistency with other spell school bonuses
+
 # 1.2.0 -> 1.2.1
 
 ### GENERAL:

+ 2 - 1
client/CMT.cpp

@@ -443,7 +443,8 @@ void playIntro()
 {
 	if(CCS->videoh->openAndPlayVideo("3DOLOGO.SMK", 0, 1, true, true))
 	{
-		CCS->videoh->openAndPlayVideo("AZVS.SMK", 0, 1, true, true);
+		if (CCS->videoh->openAndPlayVideo("NWCLOGO.SMK", 0, 1, true, true))
+			CCS->videoh->openAndPlayVideo("H3INTRO.SMK", 0, 1, true, true);
 	}
 }
 

+ 41 - 10
client/CPlayerInterface.cpp

@@ -398,7 +398,10 @@ void CPlayerInterface::heroKilled(const CGHeroInstance* hero)
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	LOG_TRACE_PARAMS(logGlobal, "Hero %s killed handler for player %s", hero->getNameTranslated() % playerID);
 
-	localState->removeWanderingHero(hero);
+	// if hero is not in town garrison
+	if (vstd::contains(localState->getWanderingHeroes(), hero))
+		localState->removeWanderingHero(hero);
+
 	adventureInt->onHeroChanged(hero);
 	localState->erasePath(hero);
 }
@@ -500,7 +503,7 @@ void CPlayerInterface::heroInGarrisonChange(const CGTownInstance *town)
 	if(town->garrisonHero) //wandering hero moved to the garrison
 	{
 		// This method also gets called on hero recruitment -> garrisoned hero is already in garrison
-		if(town->garrisonHero->tempOwner == playerID && !vstd::contains(localState->getWanderingHeroes(), town->visitingHero))
+		if(town->garrisonHero->tempOwner == playerID && vstd::contains(localState->getWanderingHeroes(), town->garrisonHero))
 			localState->removeWanderingHero(town->garrisonHero);
 	}
 
@@ -520,7 +523,9 @@ void CPlayerInterface::heroInGarrisonChange(const CGTownInstance *town)
 		castleInt->garr->setArmy(town->visitingHero, 1);
 		castleInt->garr->recreateSlots();
 		castleInt->heroes->update();
-		castleInt->redraw();
+
+		// Perform totalRedraw to update hero list on adventure map
+		GH.windows().totalRedraw();
 	}
 
 	for (auto ki : GH.windows().findWindows<CKingdomInterface>())
@@ -587,9 +592,11 @@ void CPlayerInterface::garrisonsChanged(std::vector<const CGObjectInstance *> ob
 void CPlayerInterface::buildChanged(const CGTownInstance *town, BuildingID buildingID, int what) //what: 1 - built, 2 - demolished
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
+	adventureInt->onTownChanged(town);
+
 	if (castleInt)
 	{
-		castleInt->townlist->update(town);
+		castleInt->townlist->updateElement(town);
 
 		if (castleInt->town == town)
 		{
@@ -604,8 +611,10 @@ void CPlayerInterface::buildChanged(const CGTownInstance *town, BuildingID build
 				break;
 			}
 		}
+
+		// Perform totalRedraw in order to force redraw of updated town list icon from adventure map
+		GH.windows().totalRedraw();
 	}
-	adventureInt->onTownChanged(town);
 }
 
 void CPlayerInterface::battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2)
@@ -1473,6 +1482,13 @@ void CPlayerInterface::objectRemovedAfter()
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	adventureInt->onMapTilesChanged(boost::none);
+
+	// visiting or garrisoned hero removed - recreate castle window
+	if (castleInt)
+		openTownWindow(castleInt->town);
+
+	for (auto ki : GH.windows().findWindows<CKingdomInterface>())
+		ki->heroRemoved();
 }
 
 void CPlayerInterface::playerBlocked(int reason, bool start)
@@ -1971,8 +1987,17 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path)
 		int soundChannel = -1;
 		std::string soundName;
 
-		auto getMovementSoundFor = [&](const CGHeroInstance * hero, int3 posPrev, int3 posNext) -> std::string
+		auto getMovementSoundFor = [&](const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType) -> std::string
 		{
+			if (moveType == EPathNodeAction::TELEPORT_BATTLE || moveType == EPathNodeAction::TELEPORT_BLOCKING_VISIT || moveType == EPathNodeAction::TELEPORT_NORMAL)
+				return "";
+
+			if (moveType == EPathNodeAction::EMBARK || moveType == EPathNodeAction::DISEMBARK)
+				return "";
+
+			if (moveType == EPathNodeAction::BLOCKING_VISIT)
+				return "";
+
 			// flying movement sound
 			if (hero->hasBonusOfType(BonusType::FLYING_MOVEMENT))
 				return "HORSE10.wav";
@@ -2024,8 +2049,11 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path)
 				}
 				if(i != path.nodes.size() - 1)
 				{
-					soundName = getMovementSoundFor(h, prevCoord, nextCoord);
-					soundChannel = CCS->soundh->playSound(soundName, -1);
+					soundName = getMovementSoundFor(h, prevCoord, nextCoord, path.nodes[i-1].action);
+					if (!soundName.empty())
+						soundChannel = CCS->soundh->playSound(soundName, -1);
+					else
+						soundChannel = -1;
 				}
 				continue;
 			}
@@ -2038,14 +2066,17 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path)
 
 			{
 				// Start a new sound for the hero movement or let the existing one carry on.
-				std::string newSoundName = getMovementSoundFor(h, prevCoord, nextCoord);
+				std::string newSoundName = getMovementSoundFor(h, prevCoord, nextCoord, path.nodes[i-1].action);
 
 				if(newSoundName != soundName)
 				{
 					soundName = newSoundName;
 
 					CCS->soundh->stopSound(soundChannel);
-					soundChannel = CCS->soundh->playSound(soundName, -1);
+					if (!soundName.empty())
+						soundChannel = CCS->soundh->playSound(soundName, -1);
+					else
+						soundChannel = -1;
 				}
 			}
 

+ 7 - 4
client/adventureMap/AdventureMapInterface.cpp

@@ -92,7 +92,7 @@ void AdventureMapInterface::onHeroMovementStarted(const CGHeroInstance * hero)
 
 void AdventureMapInterface::onHeroChanged(const CGHeroInstance *h)
 {
-	widget->getHeroList()->update(h);
+	widget->getHeroList()->updateElement(h);
 
 	if (h && h == LOCPLINT->localState->getCurrentHero() && !widget->getInfoBar()->showingComponents())
 		widget->getInfoBar()->showSelection();
@@ -102,7 +102,7 @@ void AdventureMapInterface::onHeroChanged(const CGHeroInstance *h)
 
 void AdventureMapInterface::onTownChanged(const CGTownInstance * town)
 {
-	widget->getTownList()->update(town);
+	widget->getTownList()->updateElement(town);
 
 	if (town && town == LOCPLINT->localState->getCurrentTown() && !widget->getInfoBar()->showingComponents())
 		widget->getInfoBar()->showSelection();
@@ -365,8 +365,8 @@ void AdventureMapInterface::onPlayerTurnStarted(PlayerColor playerID)
 		widget->getInfoBar()->showSelection();
 	}
 
-	widget->getHeroList()->update();
-	widget->getTownList()->update();
+	widget->getHeroList()->updateWidget();
+	widget->getTownList()->updateWidget();
 
 	const CGHeroInstance * heroToSelect = nullptr;
 
@@ -827,5 +827,8 @@ void AdventureMapInterface::onScreenResize()
 	widget->getMinimap()->update();
 	widget->getInfoBar()->showSelection();
 
+	if (LOCPLINT && LOCPLINT->localState->getCurrentArmy())
+		widget->getMapView()->onCenteredObject(LOCPLINT->localState->getCurrentArmy());
+
 	adjustActiveness();
 }

+ 12 - 15
client/adventureMap/CList.cpp

@@ -280,21 +280,15 @@ void CHeroList::select(const CGHeroInstance * hero)
 	selectIndex(vstd::find_pos(LOCPLINT->localState->getWanderingHeroes(), hero));
 }
 
-void CHeroList::update(const CGHeroInstance * hero)
+void CHeroList::updateElement(const CGHeroInstance * hero)
 {
-	//this hero is already present, update its status
-	for(auto & elem : listBox->getItems())
-	{
-		auto item = std::dynamic_pointer_cast<CHeroItem>(elem);
-		if(item && item->hero == hero && vstd::contains(LOCPLINT->localState->getWanderingHeroes(), hero))
-		{
-			item->update();
-			return;
-		}
-	}
-	//simplest solution for now: reset list and restore selection
+	updateWidget();
+}
 
+void CHeroList::updateWidget()
+{
 	listBox->resize(LOCPLINT->localState->getWanderingHeroes().size());
+	listBox->reset();
 	if (LOCPLINT->localState->getCurrentHero())
 		select(LOCPLINT->localState->getCurrentHero());
 
@@ -363,14 +357,17 @@ void CTownList::select(const CGTownInstance * town)
 	selectIndex(vstd::find_pos(LOCPLINT->localState->getOwnedTowns(), town));
 }
 
-void CTownList::update(const CGTownInstance *)
+void CTownList::updateElement(const CGTownInstance * town)
 {
-	//simplest solution for now: reset list and restore selection
+	updateWidget();
+}
 
+void CTownList::updateWidget()
+{
 	listBox->resize(LOCPLINT->localState->getOwnedTowns().size());
+	listBox->reset();
 	if (LOCPLINT->localState->getCurrentTown())
 		select(LOCPLINT->localState->getCurrentTown());
 
 	CList::update();
 }
-

+ 11 - 4
client/adventureMap/CList.h

@@ -77,6 +77,9 @@ protected:
 
 	virtual std::shared_ptr<CIntObject> createItem(size_t index) = 0;
 
+	/// should be called when list is invalidated
+	void update();
+
 public:
 	/// functions that will be called when selection changes
 	CFunctionList<void()> onSelect;
@@ -87,8 +90,6 @@ public:
 	void setScrollUpButton(std::shared_ptr<CButton> button);
 	void setScrollDownButton(std::shared_ptr<CButton> button);
 
-	/// should be called when list is invalidated
-	void update();
 
 	/// set of methods to switch selection
 	void selectIndex(int which);
@@ -137,7 +138,10 @@ public:
 	void select(const CGHeroInstance * hero = nullptr);
 
 	/// Update hero. Will add or remove it from the list if needed
-	void update(const CGHeroInstance * hero = nullptr);
+	void updateElement(const CGHeroInstance * hero);
+
+	/// Update all heroes
+	void updateWidget();
 };
 
 /// List of towns which is shown at the right of the adventure map screen or in the town screen
@@ -167,6 +171,9 @@ public:
 	void select(const CGTownInstance * town = nullptr);
 
 	/// Update town. Will add or remove it from the list if needed
-	void update(const CGTownInstance * town = nullptr);
+	void updateElement(const CGTownInstance * town);
+
+	/// Update all towns
+	void updateWidget();
 };
 

+ 3 - 1
client/eventsSDL/InputSourceTouch.cpp

@@ -223,10 +223,12 @@ void InputSourceTouch::handleUpdate()
 		if (currentTime > lastTapTimeTicks + params.longTouchTimeMilliseconds)
 		{
 			GH.events().dispatchShowPopup(GH.getCursorPosition());
-			hapticFeedback();
 
 			if (GH.windows().isTopWindowPopup())
+			{
+				hapticFeedback();
 				state = TouchState::TAP_DOWN_LONG;
+			}
 		}
 	}
 }

+ 1 - 0
client/gui/CIntObject.cpp

@@ -305,6 +305,7 @@ CKeyShortcut::CKeyShortcut()
 
 CKeyShortcut::CKeyShortcut(EShortcut key)
 	: assignedKey(key)
+	, shortcutPressed(false)
 {
 }
 

+ 3 - 0
client/render/IScreenHandler.h

@@ -37,4 +37,7 @@ public:
 
 	/// Converts provided rect from logical coordinates into coordinates within window, accounting for scaling and viewport
 	virtual Rect convertLogicalPointsToWindow(const Rect & input) const = 0;
+
+	/// Dimensions of render output
+	virtual Point getRenderResolution() const = 0;
 };

+ 3 - 3
client/renderSDL/ScreenHandler.cpp

@@ -46,7 +46,7 @@ std::tuple<int, int> ScreenHandler::getSupportedScalingRange() const
 	// arbitrary limit on *downscaling*. Allow some downscaling, if requested by user. Should be generally limited to 100+ for all but few devices
 	static const double minimalScaling = 50;
 
-	Point renderResolution = getActualRenderResolution();
+	Point renderResolution = getRenderResolution();
 	double reservedAreaWidth = settings["video"]["reservedWidth"].Float();
 	Point availableResolution = Point(renderResolution.x * (1 - reservedAreaWidth), renderResolution.y);
 
@@ -85,7 +85,7 @@ Rect ScreenHandler::convertLogicalPointsToWindow(const Rect & input) const
 
 Point ScreenHandler::getPreferredLogicalResolution() const
 {
-	Point renderResolution = getActualRenderResolution();
+	Point renderResolution = getRenderResolution();
 	double reservedAreaWidth = settings["video"]["reservedWidth"].Float();
 	Point availableResolution = Point(renderResolution.x * (1 - reservedAreaWidth), renderResolution.y);
 
@@ -99,7 +99,7 @@ Point ScreenHandler::getPreferredLogicalResolution() const
 	return logicalResolution;
 }
 
-Point ScreenHandler::getActualRenderResolution() const
+Point ScreenHandler::getRenderResolution() const
 {
 	assert(mainRenderer != nullptr);
 

+ 3 - 3
client/renderSDL/ScreenHandler.h

@@ -39,9 +39,6 @@ class ScreenHandler final : public IScreenHandler
 	/// This value is what player views as window size
 	Point getPreferredWindowResolution() const;
 
-	/// Dimensions of render output, usually same as window size except for high-DPI screens on macOS / iOS
-	Point getActualRenderResolution() const;
-
 	EWindowMode getPreferredWindowMode() const;
 
 	/// Returns index of display on which window should be created
@@ -86,6 +83,9 @@ public:
 	/// Fills screen with black color, erasing any existing content
 	void clearScreen() final;
 
+	/// Dimensions of render output, usually same as window size except for high-DPI screens on macOS / iOS
+	Point getRenderResolution() const final;
+
 	std::vector<Point> getSupportedResolutions() const final;
 	std::vector<Point> getSupportedResolutions(int displayIndex) const;
 	std::tuple<int, int> getSupportedScalingRange() const final;

+ 1 - 4
client/windows/CHeroWindow.cpp

@@ -319,9 +319,6 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded)
 				noDismiss = true;
 	}
 
-	for(auto ki : GH.windows().findWindows<CKingdomInterface>())
-		noDismiss = true;
-
 	//if player only have one hero and no towns
 	if(!LOCPLINT->cb->howManyTowns() && LOCPLINT->cb->howManyHeroes() == 1)
 		noDismiss = true;
@@ -329,7 +326,7 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded)
 	if(curHero->isMissionCritical())
 		noDismiss = true;
 
-	dismissButton->block(!!curHero->visitedTown || noDismiss);
+	dismissButton->block(noDismiss);
 
 	if(curHero->valOfBonuses(Selector::type()(BonusType::BEFORE_BATTLE_REPOSITION)) == 0)
 	{

+ 14 - 6
client/windows/CKingdomInterface.cpp

@@ -15,6 +15,7 @@
 
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
+#include "../PlayerLocalState.h"
 #include "../adventureMap/CResDataBar.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/Shortcut.h"
@@ -638,6 +639,11 @@ void CKingdomInterface::townChanged(const CGTownInstance *town)
 		townList->townChanged(town);
 }
 
+void CKingdomInterface::heroRemoved()
+{
+	tabArea->reset();
+}
+
 void CKingdomInterface::updateGarrisons()
 {
 	if(auto garrison = std::dynamic_pointer_cast<CGarrisonHolder>(tabArea->getItem()))
@@ -694,11 +700,12 @@ void CKingdHeroList::updateGarrisons()
 std::shared_ptr<CIntObject> CKingdHeroList::createHeroItem(size_t index)
 {
 	ui32 picCount = 4; // OVSLOT contains 4 images
-	size_t heroesCount = LOCPLINT->cb->howManyHeroes(false);
 
-	if(index < heroesCount)
+	auto heroesList = LOCPLINT->localState->getWanderingHeroes();
+
+	if(index < heroesList.size())
 	{
-		auto hero = std::make_shared<CHeroItem>(LOCPLINT->cb->getHeroBySerial((int)index, false));
+		auto hero = std::make_shared<CHeroItem>(heroesList[index]);
 		addSet(hero->heroArts);
 		return hero;
 	}
@@ -745,10 +752,11 @@ void CKingdTownList::updateGarrisons()
 std::shared_ptr<CIntObject> CKingdTownList::createTownItem(size_t index)
 {
 	ui32 picCount = 4; // OVSLOT contains 4 images
-	size_t townsCount = LOCPLINT->cb->howManyTowns();
 
-	if(index < townsCount)
-		return std::make_shared<CTownItem>(LOCPLINT->cb->getTownBySerial((int)index));
+	auto townsList = LOCPLINT->localState->getOwnedTowns();
+
+	if(index < townsList.size())
+		return std::make_shared<CTownItem>(townsList[index]);
 	else
 		return std::make_shared<CAnimImage>("OVSLOT", (index-2) % picCount );
 }

+ 1 - 0
client/windows/CKingdomInterface.h

@@ -246,6 +246,7 @@ public:
 	CKingdomInterface();
 
 	void townChanged(const CGTownInstance *town);
+	void heroRemoved();
 	void updateGarrisons() override;
 	void artifactRemoved(const ArtifactLocation &artLoc) override;
 	void artifactMoved(const ArtifactLocation &artLoc, const ArtifactLocation &destLoc, bool withRedraw) override;

+ 19 - 9
client/windows/GUIClasses.cpp

@@ -158,23 +158,33 @@ void CRecruitmentWindow::select(std::shared_ptr<CCreatureCard> card)
 void CRecruitmentWindow::buy()
 {
 	CreatureID crid =  selected->creature->getId();
-	SlotID dstslot = dst-> getSlotFor(crid);
+	SlotID dstslot = dst->getSlotFor(crid);
 
 	if(!dstslot.validSlot() && (selected->creature->warMachine == ArtifactID::NONE)) //no available slot
 	{
-		std::string txt;
-		if(dst->ID == Obj::HERO)
+		std::pair<SlotID, SlotID> toMerge;
+		bool allowMerge = CGI->settings()->getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED);
+
+		if (allowMerge && dst->mergableStacks(toMerge))
 		{
-			txt = CGI->generaltexth->allTexts[425]; //The %s would join your hero, but there aren't enough provisions to support them.
-			boost::algorithm::replace_first(txt, "%s", slider->getValue() > 1 ? CGI->creh->objects[crid]->getNamePluralTranslated() : CGI->creh->objects[crid]->getNameSingularTranslated());
+			LOCPLINT->cb->mergeStacks( dst, dst, toMerge.first, toMerge.second);
 		}
 		else
 		{
-			txt = CGI->generaltexth->allTexts[17]; //There is no room in the garrison for this army.
-		}
+			std::string txt;
+			if(dst->ID == Obj::HERO)
+			{
+				txt = CGI->generaltexth->allTexts[425]; //The %s would join your hero, but there aren't enough provisions to support them.
+				boost::algorithm::replace_first(txt, "%s", slider->getValue() > 1 ? CGI->creh->objects[crid]->getNamePluralTranslated() : CGI->creh->objects[crid]->getNameSingularTranslated());
+			}
+			else
+			{
+				txt = CGI->generaltexth->allTexts[17]; //There is no room in the garrison for this army.
+			}
 
-		LOCPLINT->showInfoDialog(txt);
-		return;
+			LOCPLINT->showInfoDialog(txt);
+			return;
+		}
 	}
 
 	onRecruit(crid, slider->getValue());

+ 13 - 14
client/windows/settings/GeneralOptionsTab.cpp

@@ -224,25 +224,19 @@ void GeneralOptionsTab::updateResolutionSelector()
 	std::shared_ptr<CButton> resolutionButton = widget<CButton>("resolutionButton");
 	std::shared_ptr<CLabel> resolutionLabel = widget<CLabel>("resolutionLabel");
 
-	if (settings["video"]["fullscreen"].Bool() && !settings["video"]["realFullscreen"].Bool())
+	if (resolutionButton)
 	{
-		if (resolutionButton)
+		if (settings["video"]["fullscreen"].Bool() && !settings["video"]["realFullscreen"].Bool())
 			resolutionButton->disable();
-
-		if (resolutionLabel)
-			resolutionLabel->setText(resolutionToLabelString(GH.screenDimensions().x, GH.screenDimensions().y));
-	}
-	else
-	{
-		const auto & currentResolution = settings["video"]["resolution"];
-
-		if (resolutionButton)
+		else
 			resolutionButton->enable();
-
-		if (resolutionLabel)
-			resolutionLabel->setText(resolutionToLabelString(currentResolution["width"].Integer(), currentResolution["height"].Integer()));
 	}
 
+	if (resolutionLabel)
+	{
+		Point resolution = GH.screenHandler().getRenderResolution();
+		resolutionLabel->setText(resolutionToLabelString(resolution.x, resolution.y));
+	}
 }
 
 void GeneralOptionsTab::selectGameResolution()
@@ -370,6 +364,11 @@ void GeneralOptionsTab::setGameScaling(int index)
 	gameRes["scaling"].Float() = scaling;
 
 	widget<CLabel>("scalingLabel")->setText(scalingToLabelString(scaling));
+
+	GH.dispatchMainThread([](){
+		boost::unique_lock<boost::recursive_mutex> lock(*CPlayerInterface::pim);
+		GH.onScreenResize();
+	});
 }
 
 void GeneralOptionsTab::selectLongTouchDuration()

+ 3 - 0
cmake_modules/VCMI_lib.cmake

@@ -68,6 +68,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 		${MAIN_LIB_DIR}/gameState/CGameState.cpp
 		${MAIN_LIB_DIR}/gameState/CGameStateCampaign.cpp
 		${MAIN_LIB_DIR}/gameState/InfoAboutArmy.cpp
+		${MAIN_LIB_DIR}/gameState/TavernHeroesPool.cpp
 
 		${MAIN_LIB_DIR}/logging/CBasicLogConfigurator.cpp
 		${MAIN_LIB_DIR}/logging/CLogger.cpp
@@ -394,6 +395,8 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 		${MAIN_LIB_DIR}/gameState/EVictoryLossCheckResult.h
 		${MAIN_LIB_DIR}/gameState/InfoAboutArmy.h
 		${MAIN_LIB_DIR}/gameState/SThievesGuildInfo.h
+		${MAIN_LIB_DIR}/gameState/TavernHeroesPool.h
+		${MAIN_LIB_DIR}/gameState/TavernSlot.h
 		${MAIN_LIB_DIR}/gameState/QuestInfo.h
 
 		${MAIN_LIB_DIR}/logging/CBasicLogConfigurator.h

+ 26 - 26
config/battleStartpos.json

@@ -4,12 +4,12 @@
 		{
 			"name" : "attackerLoose", // loose formation, attacker
 			"levels": [
-				[ 86 ],
-				[ 35, 137 ],
-				[ 35, 86, 137 ],
-				[ 1, 69, 103, 171 ],
-				[ 1, 35, 86, 137, 171 ],
-				[ 1, 35, 69, 103, 137, 171 ],
+				[            86                ],
+				[ 35,                 137      ],
+				[ 35,        86,      137      ],
+				[ 1,     69,     103,      171 ],
+				[ 1, 35,     86,      137, 171 ],
+				[ 1, 35, 69,     103, 137, 171 ],
 				[ 1, 35, 69, 86, 103, 137, 171 ]
 				]
 		},
@@ -17,12 +17,12 @@
 		{
 			"name" : "defenderLoose", // loose formation, defender
 			"levels": [
-				[ 100 ],
-				[ 49, 151 ],
-				[ 49, 100, 151 ],
-				[ 15, 83, 117, 185 ],
-				[ 15, 49, 100, 151, 185 ],
-				[ 15, 49, 83, 117, 151, 185 ],
+				[             100                ],
+				[     49,               151      ],
+				[     49,     100,      151      ],
+				[ 15,     83,      117,      185 ],
+				[ 15, 49,     100,      151, 185 ],
+				[ 15, 49, 83,      117, 151, 185 ],
 				[ 15, 49, 83, 100, 117, 151, 185 ]
 				]
 		},
@@ -30,26 +30,26 @@
 		{
 			"name" : "attackerTight", // tight formation, attacker
 			"levels": [
-				[ 86 ],
-				[ 69, 103 ],
-				[ 69, 86, 103 ],
-				[ 52, 69, 103, 120 ],
-				[ 52, 69, 86, 103, 120 ],
-				[ 35, 52, 69, 103, 120, 137 ],
-				[ 35, 52, 69, 86, 103, 120, 137 ]
+				[             86                ],
+				[         69,     103           ],
+				[         69, 86, 103           ],
+				[     35, 69,     103, 137      ],
+				[     35, 69, 86, 103, 137      ],
+				[  1, 35, 69,     103, 137, 171 ],
+				[  1, 35, 69, 86, 103, 137, 171 ]
 				]
 		},
 
 		{
 			"name" : "defenderTight", // tight formation, defender
 			"levels": [
-				[ 100 ],
-				[ 83, 117 ],
-				[ 83, 100, 117 ],
-				[ 66, 83, 117, 134 ],
-				[ 66, 83, 100, 117, 134 ],
-				[ 49, 66, 83, 117, 134, 151 ],
-				[ 49, 66, 83, 100, 117, 134, 151 ]
+				[             100                ],
+				[         83,      117           ],
+				[         83, 100, 117           ],
+				[     49, 83,      117, 151      ],
+				[     49, 83, 100, 117, 151      ],
+				[ 15, 49, 83,      117, 151, 185 ],
+				[ 15, 49, 83, 100, 117, 151, 185 ]
 				]
 		},
 

+ 9 - 9
config/filesystem.json

@@ -9,25 +9,25 @@
 	{
 		"DATA/" :
 		[
-			{"type" : "lod", "path" : "Data/H3ab_bmp.lod"},
-			{"type" : "lod", "path" : "Data/H3bitmap.lod"},
-			{"type" : "lod", "path" : "Data/h3abp_bm.lod"}, // Polish version of H3 only
-			{"type" : "lod", "path" : "Data/H3pbitma.lod"}, // Polish version of H3 only
+			{"type" : "lod", "path" : "Data/H3ab_bmp.lod"}, // Contains H3:AB data
+			{"type" : "lod", "path" : "Data/h3abp_bm.lod"}, // Localized versions only, contains H3:AB patch data
+			{"type" : "lod", "path" : "Data/H3bitmap.lod"}, // Contains H3:SoD data (overrides H3:AB data)
+			{"type" : "lod", "path" : "Data/H3pbitma.lod"}, // Localized versions only, contains H3:SoD patch data
 			{"type" : "dir",  "path" : "Data"}
 		],
 		"SPRITES/":
 		[
-			{"type" : "lod", "path" : "Data/H3ab_spr.lod"},
-			{"type" : "lod", "path" : "Data/H3sprite.lod"},
-			{"type" : "lod", "path" : "Data/h3abp_sp.lod"}, // Polish version of H3 only
-			{"type" : "lod", "path" : "Data/H3psprit.lod"}, // Polish version of H3 only
+			{"type" : "lod", "path" : "Data/H3ab_spr.lod"}, // Contains H3:AB data
+			{"type" : "lod", "path" : "Data/H3sprite.lod"}, // Localized versions only, contains H3:AB patch data
+			{"type" : "lod", "path" : "Data/h3abp_sp.lod"}, // Contains H3:SoD data (overrides H3:AB data)
+//			{"type" : "lod", "path" : "Data/H3psprit.lod"}, // Localized versions only, contains H3:SoD patch data. Unused? Has corrupted data, e.g. lock icon for artifacts
 			{"type" : "dir",  "path" : "Sprites"}
 		],
 		"SOUNDS/":
 		[
 			{"type" : "snd", "path" : "Data/H3ab_ahd.snd"},
-			{"type" : "snd", "path" : "Data/Heroes3-cd2.snd"},
 			{"type" : "snd", "path" : "Data/Heroes3.snd"},
+			{"type" : "snd", "path" : "Data/Heroes3-cd2.snd"},
 			//WoG have overriden sounds with .82m extension in Data
 			{"type" : "dir",  "path" : "Data", "depth": 0}
 		],

+ 2 - 0
config/gameConfig.json

@@ -311,6 +311,8 @@
 			"accumulateWhenNeutral" : false,
 			// if enabled, dwellings owned by players will accumulate creatures 
 			"accumulateWhenOwned" : false
+			// if enabled, game will attempt to merge slots in army on recruit if all slots in hero army are in use
+			"mergeOnRecruit" : true
 		},
 		
 		"markets" : 

+ 2 - 2
config/schemas/settings.json

@@ -59,12 +59,12 @@
 				},
 				"language" : {
 					"type" : "string",
-					"enum" : [ "english", "czech", "chinese", "german", "hungarian", "italian", "korean", "polish", "russian", "spanish", "ukrainian" ],
+					"enum" : [ "english", "czech", "chinese", "french", "german", "hungarian", "italian", "korean", "polish", "russian", "spanish", "ukrainian" ],
 					"default" : "english"
 				},
 				"gameDataLanguage" : {
 					"type" : "string",
-					"enum" : [ "auto", "english", "czech", "chinese", "german", "hungarian", "italian", "korean", "polish", "russian", "spanish", "ukrainian", "other_cp1250", "other_cp1251", "other_cp1252" ],
+					"enum" : [ "auto", "english", "czech", "chinese", "french", "german", "hungarian", "italian", "korean", "polish", "russian", "spanish", "ukrainian", "other_cp1250", "other_cp1251", "other_cp1252" ],
 					"default" : "auto"
 				},
 				"lastSave" : {

+ 2 - 2
config/widgets/settings/battleOptionsTab.json

@@ -245,7 +245,7 @@
 					]
 				},
 				{
-					"index": 12,
+					"index": 9,
 					"type": "toggleButton",
 					"image": "settingsWindow/button46",
 					"help": "vcmi.battleOptions.animationsSpeed5",
@@ -261,7 +261,7 @@
 					]
 				},
 				{
-					"index": 24,
+					"index": 18,
 					"type": "toggleButton",
 					"image": "settingsWindow/button46",
 					"help": "vcmi.battleOptions.animationsSpeed6",

+ 8 - 8
launcher/firstLaunch/firstlaunch_moc.ui

@@ -7,7 +7,7 @@
     <x>0</x>
     <y>0</y>
     <width>745</width>
-    <height>389</height>
+    <height>397</height>
    </rect>
   </property>
   <property name="windowTitle">
@@ -96,7 +96,7 @@
    <item>
     <widget class="QStackedWidget" name="installerTabs">
      <property name="currentIndex">
-      <number>0</number>
+      <number>2</number>
      </property>
      <widget class="QWidget" name="pageLanguageSelect">
       <layout class="QGridLayout" name="gridLayout_3">
@@ -616,7 +616,7 @@ Heroes® of Might and Magic® III HD is currently not supported!</string>
             <string>Horn of the Abyss</string>
            </property>
            <property name="wordWrap">
-            <bool>true</bool>
+            <bool>false</bool>
            </property>
           </widget>
          </item>
@@ -638,7 +638,7 @@ Heroes® of Might and Magic® III HD is currently not supported!</string>
             <string>Heroes III Translation</string>
            </property>
            <property name="wordWrap">
-            <bool>true</bool>
+            <bool>false</bool>
            </property>
           </widget>
          </item>
@@ -699,10 +699,10 @@ Heroes® of Might and Magic® III HD is currently not supported!</string>
             </font>
            </property>
            <property name="text">
-            <string>High Definition Support</string>
+            <string>Interface Improvements</string>
            </property>
            <property name="wordWrap">
-            <bool>true</bool>
+            <bool>false</bool>
            </property>
           </widget>
          </item>
@@ -724,7 +724,7 @@ Heroes® of Might and Magic® III HD is currently not supported!</string>
             <string>In The Wake of Gods</string>
            </property>
            <property name="wordWrap">
-            <bool>true</bool>
+            <bool>false</bool>
            </property>
           </widget>
          </item>
@@ -769,7 +769,7 @@ Heroes® of Might and Magic® III HD is currently not supported!</string>
             </sizepolicy>
            </property>
            <property name="text">
-            <string>Install support for playing Heroes III in resolutions higher than 800x600</string>
+            <string>Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles</string>
            </property>
            <property name="wordWrap">
             <bool>true</bool>

+ 2 - 0
launcher/mainwindow_moc.cpp

@@ -153,6 +153,7 @@ void MainWindow::enterSetup()
 	ui->startEditorButton->setEnabled(false);
 	ui->lobbyButton->setEnabled(false);
 	ui->settingsButton->setEnabled(false);
+	ui->aboutButton->setEnabled(false);
 	ui->modslistButton->setEnabled(false);
 	ui->tabListWidget->setCurrentIndex(TabRows::SETUP);
 }
@@ -166,6 +167,7 @@ void MainWindow::exitSetup()
 	ui->startEditorButton->setEnabled(true);
 	ui->lobbyButton->setEnabled(true);
 	ui->settingsButton->setEnabled(true);
+	ui->aboutButton->setEnabled(true);
 	ui->modslistButton->setEnabled(true);
 	ui->tabListWidget->setCurrentIndex(TabRows::MODS);
 }

+ 13 - 6
launcher/settingsView/csettingsview_moc.cpp

@@ -273,12 +273,6 @@ void CSettingsView::on_comboBoxDisplayIndex_currentIndexChanged(int index)
 	fillValidResolutionsForScreen(index);
 }
 
-void CSettingsView::on_comboBoxPlayerAI_currentTextChanged(const QString & arg1)
-{
-	Settings node = settings.write["server"]["playerAI"];
-	node->String() = arg1.toUtf8().data();
-}
-
 void CSettingsView::on_comboBoxFriendlyAI_currentTextChanged(const QString & arg1)
 {
 	Settings node = settings.write["server"]["friendlyAI"];
@@ -493,3 +487,16 @@ void CSettingsView::on_spinBoxFramerateLimit_valueChanged(int arg1)
 	node->Float() = arg1;
 }
 
+void CSettingsView::on_comboBoxEnemyPlayerAI_currentTextChanged(const QString &arg1)
+{
+	Settings node = settings.write["server"]["playerAI"];
+	node->String() = arg1.toUtf8().data();
+}
+
+
+void CSettingsView::on_comboBoxAlliedPlayerAI_currentTextChanged(const QString &arg1)
+{
+	Settings node = settings.write["server"]["alliedAI"];
+	node->String() = arg1.toUtf8().data();
+}
+

+ 4 - 1
launcher/settingsView/csettingsview_moc.h

@@ -35,7 +35,6 @@ public slots:
 private slots:
 	void on_comboBoxResolution_currentTextChanged(const QString & arg1);
 	void on_comboBoxFullScreen_currentIndexChanged(int index);
-	void on_comboBoxPlayerAI_currentTextChanged(const QString & arg1);
 	void on_comboBoxFriendlyAI_currentTextChanged(const QString & arg1);
 	void on_comboBoxNeutralAI_currentTextChanged(const QString & arg1);
 	void on_comboBoxEnemyAI_currentTextChanged(const QString & arg1);
@@ -63,6 +62,10 @@ private slots:
 
 	void on_spinBoxFramerateLimit_valueChanged(int arg1);
 
+	void on_comboBoxEnemyPlayerAI_currentTextChanged(const QString &arg1);
+
+	void on_comboBoxAlliedPlayerAI_currentTextChanged(const QString &arg1);
+
 private:
 	Ui::CSettingsView * ui;
 

+ 11 - 3
launcher/translation/chinese.ts

@@ -710,6 +710,11 @@ Heroes® of Might and Magic® III HD is currently not supported!</source>
         <source>The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually</source>
         <translation>自动检测英雄无敌3语言失败。请手动选择英雄无敌3语言</translation>
     </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="702"/>
+        <source>Interface Improvements</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="740"/>
         <source>Install a translation of Heroes III in your preferred language</source>
@@ -722,8 +727,12 @@ Heroes® of Might and Magic® III HD is currently not supported!</source>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="772"/>
+        <source>Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
         <source>Install support for playing Heroes III in resolutions higher than 800x600</source>
-        <translation>安装英雄无敌3的800x600以上分辨率支持</translation>
+        <translation type="vanished">安装英雄无敌3的800x600以上分辨率支持</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="788"/>
@@ -813,9 +822,8 @@ Heroes® of Might and Magic® III HD is currently not supported!</source>
         <translation>英雄无敌3翻译</translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.ui" line="702"/>
         <source>High Definition Support</source>
-        <translation>高分辨率支持</translation>
+        <translation type="vanished">高分辨率支持</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="724"/>

+ 6 - 6
launcher/translation/english.ts

@@ -678,6 +678,11 @@ Heroes® of Might and Magic® III HD is currently not supported!</source>
         <source>The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="702"/>
+        <source>Interface Improvements</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="740"/>
         <source>Install a translation of Heroes III in your preferred language</source>
@@ -690,7 +695,7 @@ Heroes® of Might and Magic® III HD is currently not supported!</source>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="772"/>
-        <source>Install support for playing Heroes III in resolutions higher than 800x600</source>
+        <source>Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
@@ -780,11 +785,6 @@ Heroes® of Might and Magic® III HD is currently not supported!</source>
         <source>Heroes III Translation</source>
         <translation type="unfinished"></translation>
     </message>
-    <message>
-        <location filename="../firstLaunch/firstlaunch_moc.ui" line="702"/>
-        <source>High Definition Support</source>
-        <translation type="unfinished"></translation>
-    </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="724"/>
         <source>In The Wake of Gods</source>

+ 13 - 5
launcher/translation/french.ts

@@ -257,7 +257,7 @@
         <translation>Impressions écran</translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.ui" line="323"/>
+        <location filename="../modManager/cmodlistview_moc.ui" line="324"/>
         <source> %p% (%v KB out of %m KB)</source>
         <translation> %p% (%v Ko sur %m Ko)</translation>
     </message>
@@ -780,9 +780,8 @@ Mode exclusif plein écran - le jeu couvrira l&quot;intégralité de votre écra
             </translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.ui" line="772"/>
         <source>Install support for playing Heroes III in resolutions higher than 800x600</source>
-        <translation>Installer un support pour jouer à Heroes III avec des résolutions supérieures à 800x600
+        <translation type="vanished">Installer un support pour jouer à Heroes III avec des résolutions supérieures à 800x600
             </translation>
     </message>
     <message>
@@ -860,7 +859,7 @@ Heroes® of Might and Magic® III HD n&quot;est actuellement pas pris en charge
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="346"/>
         <source>If you don&apos;t have a copy of Heroes III installed, you can use our automatic installation tool &apos;vcmibuilder&apos;, which only requires the GoG.com Heroes III installer. Please visit our wiki for detailed instructions.</source>
-        <translation>Si vous n&quot;avez pas installé de copie de Heroes III, vous pouvez utiliser notre outil d&quot;installation automatique "vcmibuilder", qui ne nécessite que le programme d&quot;installation de GoG.com Heroes III. Veuillez visiter notre wiki pour des instructions détaillées.</translation>
+        <translation>Si vous n&quot;avez pas installé de copie de Heroes III, vous pouvez utiliser notre outil d&quot;installation automatique &quot;vcmibuilder&quot;, qui ne nécessite que le programme d&quot;installation de GoG.com Heroes III. Veuillez visiter notre wiki pour des instructions détaillées.</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="362"/>
@@ -920,8 +919,12 @@ Heroes® of Might and Magic® III HD n&quot;est actuellement pas pris en charge
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="702"/>
+        <source>Interface Improvements</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
         <source>High Definition Support</source>
-        <translation>Support de Haute Définition</translation>
+        <translation type="vanished">Support de Haute Définition</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="724"/>
@@ -933,6 +936,11 @@ Heroes® of Might and Magic® III HD n&quot;est actuellement pas pris en charge
         <source>Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher</source>
         <translation>En option, vous pouvez installer des mods supplémentaires soit maintenant, soit à tout moment plus tard, à l&quot;aide du lanceur VCMI</translation>
     </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="772"/>
+        <source>Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="788"/>
         <source>Install compatible version of &quot;Horn of the Abyss&quot;, a fan-made Heroes III expansion ported by the VCMI team</source>

+ 11 - 3
launcher/translation/german.ts

@@ -714,6 +714,11 @@ Heroes III: HD Edition wird derzeit nicht unterstützt</translation>
         <source>The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually</source>
         <translation>Automatische Erkennung der Sprache fehlgeschlagen. Bitte wählen Sie die Sprache Ihrer Heroes III Kopie</translation>
     </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="702"/>
+        <source>Interface Improvements</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="740"/>
         <source>Install a translation of Heroes III in your preferred language</source>
@@ -726,8 +731,12 @@ Heroes III: HD Edition wird derzeit nicht unterstützt</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="772"/>
+        <source>Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
         <source>Install support for playing Heroes III in resolutions higher than 800x600</source>
-        <translation>Installieren Sie Unterstützung für das Spielen von Heroes III in anderen Auflösungen als 800x600</translation>
+        <translation type="vanished">Installieren Sie Unterstützung für das Spielen von Heroes III in anderen Auflösungen als 800x600</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="788"/>
@@ -817,9 +826,8 @@ Heroes III: HD Edition wird derzeit nicht unterstützt</translation>
         <translation>Heroes III Übersetzung</translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.ui" line="702"/>
         <source>High Definition Support</source>
-        <translation>Unterstützung für hohe Auflösungen</translation>
+        <translation type="vanished">Unterstützung für hohe Auflösungen</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="724"/>

+ 11 - 3
launcher/translation/polish.ts

@@ -714,6 +714,11 @@ Heroes III: HD Edition nie jest obecnie wspierane!</translation>
         <source>The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually</source>
         <translation>Automatyczna detekcja języka nie powiodła się. Proszę wybrać język twojego Heroes III</translation>
     </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="702"/>
+        <source>Interface Improvements</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="740"/>
         <source>Install a translation of Heroes III in your preferred language</source>
@@ -726,8 +731,12 @@ Heroes III: HD Edition nie jest obecnie wspierane!</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="772"/>
+        <source>Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
         <source>Install support for playing Heroes III in resolutions higher than 800x600</source>
-        <translation>Zainstaluj wsparcie dla grania w Heroes III w rozdzielczości innej niż 800x600</translation>
+        <translation type="vanished">Zainstaluj wsparcie dla grania w Heroes III w rozdzielczości innej niż 800x600</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="788"/>
@@ -817,9 +826,8 @@ Heroes III: HD Edition nie jest obecnie wspierane!</translation>
         <translation>Tłumaczenie Heroes III</translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.ui" line="702"/>
         <source>High Definition Support</source>
-        <translation>Wsparcie High Definition</translation>
+        <translation type="vanished">Wsparcie High Definition</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="724"/>

+ 11 - 3
launcher/translation/russian.ts

@@ -708,6 +708,11 @@ Heroes® of Might and Magic® III HD is currently not supported!</source>
         <source>The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually</source>
         <translation>Язык Героев III не был определен. Пожалуйста, выберите язык вашей копии Героев III</translation>
     </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="702"/>
+        <source>Interface Improvements</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="740"/>
         <source>Install a translation of Heroes III in your preferred language</source>
@@ -720,8 +725,12 @@ Heroes® of Might and Magic® III HD is currently not supported!</source>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="772"/>
+        <source>Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
         <source>Install support for playing Heroes III in resolutions higher than 800x600</source>
-        <translation>Установить поддержку запуска Героев III в разрешениях, отличных от 800x600</translation>
+        <translation type="vanished">Установить поддержку запуска Героев III в разрешениях, отличных от 800x600</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="788"/>
@@ -811,9 +820,8 @@ Heroes® of Might and Magic® III HD is currently not supported!</source>
         <translation>Перевод Героев III</translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.ui" line="702"/>
         <source>High Definition Support</source>
-        <translation>Поддержка высоких разрешений</translation>
+        <translation type="vanished">Поддержка высоких разрешений</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="724"/>

+ 12 - 4
launcher/translation/spanish.ts

@@ -702,6 +702,16 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
         <source>Your Heroes III language has been successfully detected.</source>
         <translation>Se ha detectado con éxito el idioma de tu Heroes III.</translation>
     </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="702"/>
+        <source>Interface Improvements</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="772"/>
+        <source>Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles</source>
+        <translation type="unfinished"></translation>
+    </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="127"/>
         <source>Select your language</source>
@@ -781,9 +791,8 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi
         <translation>Traducción de Heroes III.</translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.ui" line="702"/>
         <source>High Definition Support</source>
-        <translation>Soporte para resoluciones en Alta Definición</translation>
+        <translation type="vanished">Soporte para resoluciones en Alta Definición</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="724"/>
@@ -801,9 +810,8 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi
         <translation>Opcionalmente, puedes instalar mods adicionales ya sea ahora o en cualquier momento posterior, utilizando el lanzador de VCMI.</translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.ui" line="772"/>
         <source>Install support for playing Heroes III in resolutions higher than 800x600</source>
-        <translation>Instalar soporte para jugar Heroes III en resoluciones superiores a 800x600</translation>
+        <translation type="vanished">Instalar soporte para jugar Heroes III en resoluciones superiores a 800x600</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="788"/>

+ 11 - 3
launcher/translation/ukrainian.ts

@@ -718,6 +718,11 @@ Heroes® of Might and Magic® III HD наразі не підтримуєтьс
         <source>The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually</source>
         <translation>Не вдалося визначити мову гри. Будь ласка, виберіть мову вашої копії Heroes III</translation>
     </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="702"/>
+        <source>Interface Improvements</source>
+        <translation>Удосконалення нтерфейсу</translation>
+    </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="740"/>
         <source>Install a translation of Heroes III in your preferred language</source>
@@ -730,8 +735,12 @@ Heroes® of Might and Magic® III HD наразі не підтримуєтьс
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="772"/>
+        <source>Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles</source>
+        <translation>Встановити різноманітні покращення інтерфейсу, такі як покращений інтерфейс випадкових карт та вибір варіантів дій у боях</translation>
+    </message>
+    <message>
         <source>Install support for playing Heroes III in resolutions higher than 800x600</source>
-        <translation>Встановити підтримку для гри в Heroes III у роздільних здатностях, більших за 800x600</translation>
+        <translation type="vanished">Встановити підтримку для гри в Heroes III у роздільних здатностях, більших за 800x600</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="788"/>
@@ -821,9 +830,8 @@ Heroes® of Might and Magic® III HD наразі не підтримуєтьс
         <translation>Переклад Heroes III</translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.ui" line="702"/>
         <source>High Definition Support</source>
-        <translation>Підтримка високих роздільних здатностей</translation>
+        <translation type="vanished">Підтримка високих роздільних здатностей</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="724"/>

+ 1 - 1
lib/CCreatureHandler.cpp

@@ -1097,7 +1097,7 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars
 			case 'F':
 				b.type = BonusType::FLYING; break;
 			case 'm':
-				b.type = BonusType::MORALE; break;
+				b.type = BonusType::MORALE;
 				b.val = 1;
 				b.valType = BonusValueType::INDEPENDENT_MAX;
 				break;

+ 4 - 13
lib/CGameInfoCallback.cpp

@@ -13,6 +13,7 @@
 #include "gameState/CGameState.h"
 #include "gameState/InfoAboutArmy.h"
 #include "gameState/SThievesGuildInfo.h"
+#include "gameState/TavernHeroesPool.h"
 #include "CGeneralTextHandler.h"
 #include "StartInfo.h" // for StartInfo
 #include "battle/BattleInfo.h" // for BattleInfo
@@ -99,13 +100,6 @@ const PlayerState * CGameInfoCallback::getPlayerState(PlayerColor color, bool ve
 	}
 }
 
-const CTown * CGameInfoCallback::getNativeTown(PlayerColor color) const
-{
-	const PlayerSettings *ps = getPlayerSettings(color);
-	ERROR_RET_VAL_IF(!ps, "There is no such player!", nullptr);
-	return (*VLC->townh)[ps->castle]->town;
-}
-
 const CGObjectInstance * CGameInfoCallback::getObjByQuestIdentifier(int identifier) const
 {
 	if(gs->map->questIdentifierToId.empty())
@@ -486,13 +480,10 @@ std::vector<const CGHeroInstance *> CGameInfoCallback::getAvailableHeroes(const
 	//ERROR_RET_VAL_IF(!isOwnedOrVisited(townOrTavern), "Town or tavern must be owned or visited!", ret);
 	//TODO: town needs to be owned, advmap tavern needs to be visited; to be reimplemented when visit tracking is done
 	const CGTownInstance * town = getTown(townOrTavern->id);
+
 	if(townOrTavern->ID == Obj::TAVERN || (town && town->hasBuilt(BuildingID::TAVERN)))
-	{
-		range::copy(gs->players[*player].availableHeroes, std::back_inserter(ret));
-		vstd::erase_if(ret, [](const CGHeroInstance * h) {
-			return h == nullptr;
-		});
-	}
+		return gs->heroesPool->getHeroesFor(*player);
+
 	return ret;
 }
 

+ 0 - 2
lib/CGameInfoCallback.h

@@ -108,7 +108,6 @@ public:
 //	std::string getTavernRumor(const CGObjectInstance * townOrTavern) const;
 //	EBuildingState::EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID);//// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements
 //	virtual bool getTownInfo(const CGObjectInstance * town, InfoAboutTown & dest, const CGObjectInstance * selectedObject = nullptr) const;
-//	const CTown *getNativeTown(PlayerColor color) const;
 
 	//from gs
 //	const TeamState *getTeam(TeamID teamID) const;
@@ -206,7 +205,6 @@ public:
 	virtual std::string getTavernRumor(const CGObjectInstance * townOrTavern) const;
 	virtual EBuildingState::EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID);//// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements
 	virtual bool getTownInfo(const CGObjectInstance * town, InfoAboutTown & dest, const CGObjectInstance * selectedObject = nullptr) const;
-	virtual const CTown *getNativeTown(PlayerColor color) const;
 
 	//from gs
 	virtual const TeamState *getTeam(TeamID teamID) const;

+ 0 - 1
lib/CPlayerState.cpp

@@ -35,7 +35,6 @@ PlayerState::PlayerState(PlayerState && other) noexcept:
 	std::swap(visitedObjects, other.visitedObjects);
 	std::swap(heroes, other.heroes);
 	std::swap(towns, other.towns);
-	std::swap(availableHeroes, other.availableHeroes);
 	std::swap(dwellings, other.dwellings);
 	std::swap(quests, other.quests);
 }

+ 0 - 2
lib/CPlayerState.h

@@ -33,7 +33,6 @@ public:
 	std::set<ObjectInstanceID> visitedObjects; // as a std::set, since most accesses here will be from visited status checks
 	std::vector<ConstTransitivePtr<CGHeroInstance> > heroes;
 	std::vector<ConstTransitivePtr<CGTownInstance> > towns;
-	std::vector<ConstTransitivePtr<CGHeroInstance> > availableHeroes; //heroes available in taverns
 	std::vector<ConstTransitivePtr<CGDwelling> > dwellings; //used for town growth
 	std::vector<QuestInfo> quests; //store info about all received quests
 
@@ -74,7 +73,6 @@ public:
 		h & status;
 		h & heroes;
 		h & towns;
-		h & availableHeroes;
 		h & dwellings;
 		h & quests;
 		h & visitedObjects;

+ 5 - 0
lib/CRandomGenerator.cpp

@@ -20,6 +20,11 @@ CRandomGenerator::CRandomGenerator()
 	resetSeed();
 }
 
+CRandomGenerator::CRandomGenerator(int seed)
+{
+	setSeed(seed);
+}
+
 void CRandomGenerator::setSeed(int seed)
 {
 	rand.seed(seed);

+ 3 - 0
lib/CRandomGenerator.h

@@ -30,6 +30,9 @@ public:
 	/// current thread ID.
 	CRandomGenerator();
 
+	/// Seeds the generator with provided initial seed
+	explicit CRandomGenerator(int seed);
+
 	void setSeed(int seed);
 
 	/// Resets the seed to the product of the current time in milliseconds and the

+ 1 - 0
lib/GameConstants.cpp

@@ -40,6 +40,7 @@
 VCMI_LIB_NAMESPACE_BEGIN
 
 const HeroTypeID HeroTypeID::NONE = HeroTypeID(-1);
+const ObjectInstanceID ObjectInstanceID::NONE = ObjectInstanceID(-1);
 
 const SlotID SlotID::COMMANDER_SLOT_PLACEHOLDER = SlotID(-2);
 const SlotID SlotID::SUMMONED_SLOT_PLACEHOLDER = SlotID(-3);

+ 5 - 1
lib/GameConstants.h

@@ -313,6 +313,8 @@ class ObjectInstanceID : public BaseForID<ObjectInstanceID, si32>
 {
 	INSTID_LIKE_CLASS_COMMON(ObjectInstanceID, si32)
 
+	DLL_LINKAGE static const ObjectInstanceID NONE;
+
 	friend class CGameInfoCallback;
 	friend class CNonConstInfoCallback;
 };
@@ -357,9 +359,11 @@ class PlayerColor : public BaseForID<PlayerColor, ui8>
 
 	enum EPlayerColor
 	{
-		PLAYER_LIMIT_I = 8
+		PLAYER_LIMIT_I = 8,
 	};
 
+	using Mask = uint8_t;
+
 	DLL_LINKAGE static const PlayerColor SPECTATOR; //252
 	DLL_LINKAGE static const PlayerColor CANNOT_DETERMINE; //253
 	DLL_LINKAGE static const PlayerColor UNFLAGGABLE; //254 - neutral objects (pandora, banks)

+ 1 - 0
lib/GameSettings.cpp

@@ -68,6 +68,7 @@ void GameSettings::load(const JsonNode & input)
 		{EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT,        "creatures", "weeklyGrowthPercent"        },
 		{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL,      "dwellings", "accumulateWhenNeutral"      },
 		{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED,        "dwellings", "accumulateWhenOwned"        },
+		{EGameSettings::DWELLINGS_MERGE_ON_RECRUIT,             "dwellings", "mergeOnRecruit"             },
 		{EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP,           "heroes",    "perPlayerOnMapCap"          },
 		{EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP,            "heroes",    "perPlayerTotalCap"          },
 		{EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS,   "heroes",    "retreatOnWinWithoutTroops"  },

+ 1 - 0
lib/GameSettings.h

@@ -32,6 +32,7 @@ enum class EGameSettings
 	CREATURES_WEEKLY_GROWTH_PERCENT,
 	DWELLINGS_ACCUMULATE_WHEN_NEUTRAL,
 	DWELLINGS_ACCUMULATE_WHEN_OWNED,
+	DWELLINGS_MERGE_ON_RECRUIT,
 	HEROES_PER_PLAYER_ON_MAP_CAP,
 	HEROES_PER_PLAYER_TOTAL_CAP,
 	HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS,

+ 1 - 0
lib/IGameCallback.cpp

@@ -36,6 +36,7 @@
 #include "StartInfo.h"
 #include "gameState/CGameState.h"
 #include "gameState/CGameStateCampaign.h"
+#include "gameState/TavernHeroesPool.h"
 #include "mapping/CMap.h"
 #include "CPlayerState.h"
 #include "GameSettings.h"

+ 2 - 2
lib/NetPackVisitor.h

@@ -36,7 +36,7 @@ public:
 	virtual void visitSetMana(SetMana & pack) {}
 	virtual void visitSetMovePoints(SetMovePoints & pack) {}
 	virtual void visitFoWChange(FoWChange & pack) {}
-	virtual void visitSetAvailableHeroes(SetAvailableHeroes & pack) {}
+	virtual void visitSetAvailableHeroes(SetAvailableHero & pack) {}
 	virtual void visitGiveBonus(GiveBonus & pack) {}
 	virtual void visitChangeObjPos(ChangeObjPos & pack) {}
 	virtual void visitPlayerEndsGame(PlayerEndsGame & pack) {}
@@ -162,4 +162,4 @@ public:
 	virtual void visitLobbyShowMessage(LobbyShowMessage & pack) {}
 };
 
-VCMI_LIB_NAMESPACE_END
+VCMI_LIB_NAMESPACE_END

+ 13 - 9
lib/NetPacks.h

@@ -19,6 +19,7 @@
 #include "battle/BattleAction.h"
 #include "battle/CObstacleInstance.h"
 #include "gameState/EVictoryLossCheckResult.h"
+#include "gameState/TavernSlot.h"
 #include "gameState/QuestInfo.h"
 #include "mapObjects/CGHeroInstance.h"
 #include "mapping/CMapDefines.h"
@@ -330,23 +331,26 @@ struct DLL_LINKAGE FoWChange : public CPackForClient
 	}
 };
 
-struct DLL_LINKAGE SetAvailableHeroes : public CPackForClient
+struct DLL_LINKAGE SetAvailableHero : public CPackForClient
 {
-	SetAvailableHeroes()
+	SetAvailableHero()
 	{
-		for(auto & i : army)
-			i.clear();
+		army.clear();
 	}
 	void applyGs(CGameState * gs);
 
+	TavernHeroSlot slotID;
+	TavernSlotRole roleID;
 	PlayerColor player;
-	si32 hid[GameConstants::AVAILABLE_HEROES_PER_PLAYER]; //-1 if no hero
-	CSimpleArmy army[GameConstants::AVAILABLE_HEROES_PER_PLAYER];
+	HeroTypeID hid; //HeroTypeID::NONE if no hero
+	CSimpleArmy army;
 
 	virtual void visitTyped(ICPackVisitor & visitor) override;
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & slotID;
+		h & roleID;
 		h & player;
 		h & hid;
 		h & army;
@@ -692,7 +696,7 @@ struct DLL_LINKAGE HeroRecruited : public CPackForClient
 {
 	void applyGs(CGameState * gs) const;
 
-	si32 hid = -1; //subID of hero
+	HeroTypeID hid; //subID of hero
 	ObjectInstanceID tid;
 	ObjectInstanceID boatId;
 	int3 tile;
@@ -2437,12 +2441,12 @@ struct DLL_LINKAGE SetFormation : public CPackForServer
 struct DLL_LINKAGE HireHero : public CPackForServer
 {
 	HireHero() = default;
-	HireHero(si32 HID, const ObjectInstanceID & TID)
+	HireHero(HeroTypeID HID, const ObjectInstanceID & TID)
 		: hid(HID)
 		, tid(TID)
 	{
 	}
-	si32 hid = 0; //available hero serial
+	HeroTypeID hid; //available hero serial
 	ObjectInstanceID tid; //town (tavern) id
 	PlayerColor player;
 

+ 19 - 32
lib/NetPacksLib.cpp

@@ -20,6 +20,7 @@
 #include "spells/CSpellHandler.h"
 #include "CCreatureHandler.h"
 #include "gameState/CGameState.h"
+#include "gameState/TavernHeroesPool.h"
 #include "CStack.h"
 #include "battle/BattleInfo.h"
 #include "CTownHandler.h"
@@ -151,7 +152,7 @@ void FoWChange::visitTyped(ICPackVisitor & visitor)
 	visitor.visitFoWChange(*this);
 }
 
-void SetAvailableHeroes::visitTyped(ICPackVisitor & visitor)
+void SetAvailableHero::visitTyped(ICPackVisitor & visitor)
 {
 	visitor.visitSetAvailableHeroes(*this);
 }
@@ -939,18 +940,9 @@ void FoWChange::applyGs(CGameState *gs)
 	}
 }
 
-void SetAvailableHeroes::applyGs(CGameState *gs)
+void SetAvailableHero::applyGs(CGameState *gs)
 {
-	PlayerState *p = gs->getPlayerState(player);
-	p->availableHeroes.clear();
-
-	for (int i = 0; i < GameConstants::AVAILABLE_HEROES_PER_PLAYER; i++)
-	{
-		CGHeroInstance *h = (hid[i]>=0 ?  gs->hpool.heroesPool[hid[i]].get() : nullptr);
-		if(h && army[i])
-			h->setToArmy(army[i]);
-		p->availableHeroes.emplace_back(h);
-	}
+	gs->heroesPool->setHeroForPlayer(player, slotID, hid, army, roleID);
 }
 
 void GiveBonus::applyGs(CGameState *gs)
@@ -1132,7 +1124,16 @@ void RemoveObject::applyGs(CGameState *gs)
 		PlayerState * p = gs->getPlayerState(beatenHero->tempOwner);
 		gs->map->heroesOnMap -= beatenHero;
 		p->heroes -= beatenHero;
-		beatenHero->detachFrom(*beatenHero->whereShouldBeAttachedOnSiege(gs));
+
+
+		auto * siegeNode = beatenHero->whereShouldBeAttachedOnSiege(gs);
+
+		// FIXME: workaround:
+		// hero should be attached to siegeNode after battle
+		// however this code might also be called on dismissing hero while in town
+		if (siegeNode && vstd::contains(beatenHero->getParentNodes(), siegeNode))
+				beatenHero->detachFrom(*siegeNode);
+
 		beatenHero->tempOwner = PlayerColor::NEUTRAL; //no one owns beaten hero
 		vstd::erase_if(beatenHero->artifactsInBackpack, [](const ArtSlotInfo& asi)
 		{
@@ -1150,11 +1151,8 @@ void RemoveObject::applyGs(CGameState *gs)
 			beatenHero->inTownGarrison = false;
 		}
 		//return hero to the pool, so he may reappear in tavern
-		gs->hpool.heroesPool[beatenHero->subID] = beatenHero;
-
-		if(!vstd::contains(gs->hpool.pavailable, beatenHero->subID))
-			gs->hpool.pavailable[beatenHero->subID] = 0xff;
 
+		gs->heroesPool->addHeroToPool(beatenHero);
 		gs->map->objects[id.getNum()] = nullptr;
 
 		//If hero on Boat is removed, the Boat disappears
@@ -1379,8 +1377,7 @@ void SetHeroesInTown::applyGs(CGameState * gs) const
 
 void HeroRecruited::applyGs(CGameState * gs) const
 {
-	assert(vstd::contains(gs->hpool.heroesPool, hid));
-	CGHeroInstance *h = gs->hpool.heroesPool[hid];
+	CGHeroInstance *h = gs->heroesPool->takeHeroFromPool(hid);
 	CGTownInstance *t = gs->getTown(tid);
 	PlayerState *p = gs->getPlayerState(player);
 
@@ -1411,7 +1408,6 @@ void HeroRecruited::applyGs(CGameState * gs) const
 		}
 	}
 
-	gs->hpool.heroesPool.erase(hid);
 	if(h->id == ObjectInstanceID())
 	{
 		h->id = ObjectInstanceID(static_cast<si32>(gs->map->objects.size()));
@@ -2021,26 +2017,17 @@ void NewTurn::applyGs(CGameState *gs)
 	{
 		CGHeroInstance *hero = gs->getHero(h.id);
 		if(!hero)
-		{
-			// retreated or surrendered hero who has not been reset yet
-			for(auto& hp : gs->hpool.heroesPool)
-			{
-				if(hp.second->id == h.id)
-				{
-					hero = hp.second;
-					break;
-				}
-			}
-		}
-		if(!hero)
 		{
 			logGlobal->error("Hero %d not found in NewTurn::applyGs", h.id.getNum());
 			continue;
 		}
+
 		hero->setMovementPoints(h.move);
 		hero->mana = h.mana;
 	}
 
+	gs->heroesPool->onNewDay();
+
 	for(const auto & re : res)
 	{
 		assert(re.first < PlayerColor::PLAYER_LIMIT);

+ 1 - 1
lib/Rect.h

@@ -160,7 +160,7 @@ public:
 		h & x;
 		h & y;
 		h & w;
-		h & h;
+		h & this->h;
 	}
 };
 

+ 3 - 3
lib/StartInfo.h

@@ -35,9 +35,9 @@ struct DLL_LINKAGE PlayerSettings
 	};
 
 	Ebonus bonus;
-	si16 castle;
-	si32 hero,
-		 heroPortrait; //-1 if default, else ID
+	FactionID castle;
+	HeroTypeID hero;
+	HeroTypeID heroPortrait; //-1 if default, else ID
 
 	std::string heroName;
 	PlayerColor color; //from 0 -

+ 5 - 96
lib/gameState/CGameState.cpp

@@ -12,6 +12,7 @@
 
 #include "EVictoryLossCheckResult.h"
 #include "InfoAboutArmy.h"
+#include "TavernHeroesPool.h"
 #include "CGameStateCampaign.h"
 #include "SThievesGuildInfo.h"
 
@@ -102,81 +103,6 @@ static CGObjectInstance * createObject(const Obj & id, int subid, const int3 & p
 	return nobj;
 }
 
-CGHeroInstance * CGameState::HeroesPool::pickHeroFor(bool native,
-													 const PlayerColor & player,
-													 const CTown * town,
-													 std::map<ui32, ConstTransitivePtr<CGHeroInstance>> & available,
-													 CRandomGenerator & rand,
-													 const CHeroClass * bannedClass) const
-{
-	CGHeroInstance *ret = nullptr;
-
-	if(player>=PlayerColor::PLAYER_LIMIT)
-	{
-		logGlobal->error("Cannot pick hero for faction %s. Wrong owner!", town->faction->getJsonKey());
-		return nullptr;
-	}
-
-	std::vector<CGHeroInstance *> pool;
-
-	if(native)
-	{
-		for(auto & elem : available)
-		{
-			if(pavailable.find(elem.first)->second & 1<<player.getNum()
-				&& elem.second->type->heroClass->faction == town->faction->getIndex())
-			{
-				pool.push_back(elem.second); //get all available heroes
-			}
-		}
-		if(pool.empty())
-		{
-			logGlobal->error("Cannot pick native hero for %s. Picking any...", player.getStr());
-			return pickHeroFor(false, player, town, available, rand);
-		}
-		else
-		{
-			ret = *RandomGeneratorUtil::nextItem(pool, rand);
-		}
-	}
-	else
-	{
-		int sum = 0;
-		int r;
-
-		for(auto & elem : available)
-		{
-			if (pavailable.find(elem.first)->second & (1<<player.getNum()) &&    // hero is available
-			    ( !bannedClass || elem.second->type->heroClass != bannedClass) ) // and his class is not same as other hero
-			{
-				pool.push_back(elem.second);
-				sum += elem.second->type->heroClass->selectionProbability[town->faction->getId()]; //total weight
-			}
-		}
-		if(pool.empty() || sum == 0)
-		{
-			logGlobal->error("There are no heroes available for player %s!", player.getStr());
-			return nullptr;
-		}
-
-		r = rand.nextInt(sum - 1);
-		for (auto & elem : pool)
-		{
-			r -= elem->type->heroClass->selectionProbability[town->faction->getId()];
-			if(r < 0)
-			{
-				ret = elem;
-				break;
-			}
-		}
-		if(!ret)
-			ret = pool.back();
-	}
-
-	available.erase(ret->subID);
-	return ret;
-}
-
 HeroTypeID CGameState::pickNextHeroType(const PlayerColor & owner)
 {
 	const PlayerSettings &ps = scenarioOps->getIthPlayersSettings(owner);
@@ -459,6 +385,7 @@ int CGameState::getDate(Date::EDateType mode) const
 CGameState::CGameState()
 {
 	gs = this;
+	heroesPool = std::make_unique<TavernHeroesPool>();
 	applier = std::make_shared<CApplier<CBaseForGSApply>>();
 	registerTypesClientPacks1(*applier);
 	registerTypesClientPacks2(*applier);
@@ -469,9 +396,6 @@ CGameState::~CGameState()
 {
 	map.dellNull();
 	curB.dellNull();
-
-	for(auto ptr : hpool.heroesPool) // clean hero pool
-		ptr.second.dellNull();
 }
 
 void CGameState::preInit(Services * services)
@@ -951,8 +875,7 @@ void CGameState::initHeroes()
 		if(!vstd::contains(heroesToCreate, HeroTypeID(ph->subID)))
 			continue;
 		ph->initHero(getRandomGenerator());
-		hpool.heroesPool[ph->subID] = ph;
-		hpool.pavailable[ph->subID] = 0xff;
+		heroesPool->addHeroToPool(ph);
 		heroesToCreate.erase(ph->type->getId());
 
 		map->allHeroes[ph->subID] = ph;
@@ -965,14 +888,11 @@ void CGameState::initHeroes()
 
 		int typeID = htype.getNum();
 		map->allHeroes[typeID] = vhi;
-		hpool.heroesPool[typeID] = vhi;
-		hpool.pavailable[typeID] = 0xff;
+		heroesPool->addHeroToPool(vhi);
 	}
 
 	for(auto & elem : map->disposedHeroes)
-	{
-		hpool.pavailable[elem.heroId] = elem.players;
-	}
+		heroesPool->setAvailability(elem.heroId, elem.players);
 
 	if (campaign)
 		campaign->initHeroes();
@@ -2067,17 +1987,6 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level)
 #undef FILL_FIELD
 }
 
-std::map<ui32, ConstTransitivePtr<CGHeroInstance> > CGameState::unusedHeroesFromPool()
-{
-	std::map<ui32, ConstTransitivePtr<CGHeroInstance> > pool = hpool.heroesPool;
-	for(const auto & player : players)
-		for(auto availableHero : player.second.availableHeroes)
-			if(availableHero)
-				pool.erase((*availableHero).subID);
-
-	return pool;
-}
-
 void CGameState::buildBonusSystemTree()
 {
 	buildGlobalTeamPlayerTree();

+ 5 - 20
lib/gameState/CGameState.h

@@ -29,6 +29,7 @@ struct EventCondition;
 struct CampaignTravel;
 class CStackInstance;
 class CGameStateCampaign;
+class TavernHeroesPool;
 struct SThievesGuildInfo;
 
 template<typename T> class CApplier;
@@ -78,25 +79,10 @@ DLL_LINKAGE std::ostream & operator<<(std::ostream & os, const EVictoryLossCheck
 class DLL_LINKAGE CGameState : public CNonConstInfoCallback
 {
 	friend class CGameStateCampaign;
+
 public:
-	struct DLL_LINKAGE HeroesPool
-	{
-		std::map<ui32, ConstTransitivePtr<CGHeroInstance> > heroesPool; //[subID] - heroes available to buy; nullptr if not available
-		std::map<ui32,ui8> pavailable; // [subid] -> which players can recruit hero (binary flags)
-
-		CGHeroInstance * pickHeroFor(bool native,
-									 const PlayerColor & player,
-									 const CTown * town,
-									 std::map<ui32, ConstTransitivePtr<CGHeroInstance>> & available,
-									 CRandomGenerator & rand,
-									 const CHeroClass * bannedClass = nullptr) const;
-
-		template <typename Handler> void serialize(Handler &h, const int version)
-		{
-			h & heroesPool;
-			h & pavailable;
-		}
-	} hpool; //we have here all heroes available on this map that are not hired
+	//we have here all heroes available on this map that are not hired
+	std::unique_ptr<TavernHeroesPool> heroesPool;
 
 	CGameState();
 	virtual ~CGameState();
@@ -142,7 +128,6 @@ public:
 	bool checkForStandardLoss(const PlayerColor & player) const; //checks if given player lost the game
 
 	void obtainPlayersStats(SThievesGuildInfo & tgi, int level); //fills tgi with info about other players that is available at given level of thieves' guild
-	std::map<ui32, ConstTransitivePtr<CGHeroInstance> > unusedHeroesFromPool(); //heroes pool without heroes that are available in taverns
 
 	bool isVisible(int3 pos, const std::optional<PlayerColor> & player) const override;
 	bool isVisible(const CGObjectInstance * obj, const std::optional<PlayerColor> & player) const override;
@@ -169,7 +154,7 @@ public:
 		h & map;
 		h & players;
 		h & teams;
-		h & hpool;
+		h & heroesPool;
 		h & globalEffects;
 		h & rand;
 		h & rumor;

+ 142 - 0
lib/gameState/TavernHeroesPool.cpp

@@ -0,0 +1,142 @@
+/*
+ * TavernHeroesPool.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 "TavernHeroesPool.h"
+
+#include "../mapObjects/CGHeroInstance.h"
+#include "../CHeroHandler.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+TavernHeroesPool::~TavernHeroesPool()
+{
+	for(const auto & ptr : heroesPool) // clean hero pool
+		delete ptr.second;
+}
+
+std::map<HeroTypeID, CGHeroInstance*> TavernHeroesPool::unusedHeroesFromPool() const
+{
+	std::map<HeroTypeID, CGHeroInstance*> pool = heroesPool;
+	for(const auto & slot : currentTavern)
+		pool.erase(HeroTypeID(slot.hero->subID));
+
+	return pool;
+}
+
+TavernSlotRole TavernHeroesPool::getSlotRole(HeroTypeID hero) const
+{
+	for (auto const & slot : currentTavern)
+	{
+		if (HeroTypeID(slot.hero->subID) == hero)
+			return slot.role;
+	}
+	return TavernSlotRole::NONE;
+}
+
+void TavernHeroesPool::setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role)
+{
+	vstd::erase_if(currentTavern, [&](const TavernSlot & entry){
+		return entry.player == player && entry.slot == slot;
+	});
+
+	if (hero == HeroTypeID::NONE)
+		return;
+
+	CGHeroInstance * h = heroesPool[hero];
+
+	if (h && army)
+		h->setToArmy(army);
+
+	TavernSlot newSlot;
+	newSlot.hero = h;
+	newSlot.player = player;
+	newSlot.role = role;
+	newSlot.slot = slot;
+
+	currentTavern.push_back(newSlot);
+
+	boost::range::sort(currentTavern, [](const TavernSlot & left, const TavernSlot & right)
+	{
+		if (left.slot == right.slot)
+			return left.player < right.player;
+		else
+			return left.slot < right.slot;
+	});
+}
+
+bool TavernHeroesPool::isHeroAvailableFor(HeroTypeID hero, PlayerColor color) const
+{
+	if (perPlayerAvailability.count(hero))
+		return perPlayerAvailability.at(hero) & (1 << color.getNum());
+
+	return true;
+}
+
+std::vector<const CGHeroInstance *> TavernHeroesPool::getHeroesFor(PlayerColor color) const
+{
+	std::vector<const CGHeroInstance *> result;
+
+	for(const auto & slot : currentTavern)
+	{
+		if (slot.player == color)
+			result.push_back(slot.hero);
+	}
+
+	return result;
+}
+
+CGHeroInstance * TavernHeroesPool::takeHeroFromPool(HeroTypeID hero)
+{
+	assert(heroesPool.count(hero));
+
+	CGHeroInstance * result = heroesPool[hero];
+	heroesPool.erase(hero);
+
+	vstd::erase_if(currentTavern, [&](const TavernSlot & entry){
+		return entry.hero->type->getId() == hero;
+	});
+
+	assert(result);
+	return result;
+}
+
+void TavernHeroesPool::onNewDay()
+{
+	for(auto & hero : heroesPool)
+	{
+		assert(hero.second);
+		if(!hero.second)
+			continue;
+
+		hero.second->setMovementPoints(hero.second->movementPointsLimit(true));
+		hero.second->mana = hero.second->manaLimit();
+	}
+
+	for (auto & slot : currentTavern)
+	{
+		if (slot.role == TavernSlotRole::RETREATED_TODAY)
+			slot.role = TavernSlotRole::RETREATED;
+
+		if (slot.role == TavernSlotRole::SURRENDERED_TODAY)
+			slot.role = TavernSlotRole::SURRENDERED;
+	}
+}
+
+void TavernHeroesPool::addHeroToPool(CGHeroInstance * hero)
+{
+	heroesPool[HeroTypeID(hero->subID)] = hero;
+}
+
+void TavernHeroesPool::setAvailability(HeroTypeID hero, PlayerColor::Mask mask)
+{
+	perPlayerAvailability[hero] = mask;
+}
+
+VCMI_LIB_NAMESPACE_END

+ 87 - 0
lib/gameState/TavernHeroesPool.h

@@ -0,0 +1,87 @@
+/*
+ * TavernHeroesPool.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 "../GameConstants.h"
+#include "TavernSlot.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class CGHeroInstance;
+class CTown;
+class CRandomGenerator;
+class CHeroClass;
+class CGameState;
+class CSimpleArmy;
+
+class DLL_LINKAGE TavernHeroesPool
+{
+	struct TavernSlot
+	{
+		CGHeroInstance * hero;
+		TavernHeroSlot slot;
+		TavernSlotRole role;
+		PlayerColor player;
+
+		template <typename Handler> void serialize(Handler &h, const int version)
+		{
+			h & hero;
+			h & slot;
+			h & role;
+			h & player;
+		}
+	};
+
+	/// list of all heroes in pool, including those currently present in taverns
+	std::map<HeroTypeID, CGHeroInstance* > heroesPool;
+
+	/// list of which players are able to purchase specific hero
+	/// if hero is not present in list, he is available for everyone
+	std::map<HeroTypeID, PlayerColor::Mask> perPlayerAvailability;
+
+	/// list of heroes currently available in taverns
+	std::vector<TavernSlot> currentTavern;
+
+public:
+	~TavernHeroesPool();
+
+	/// Returns heroes currently availabe in tavern of a specific player
+	std::vector<const CGHeroInstance *> getHeroesFor(PlayerColor color) const;
+
+	/// returns heroes in pool without heroes that are available in taverns
+	std::map<HeroTypeID, CGHeroInstance* > unusedHeroesFromPool() const;
+
+	/// Returns true if hero is available to a specific player
+	bool isHeroAvailableFor(HeroTypeID hero, PlayerColor color) const;
+
+	TavernSlotRole getSlotRole(HeroTypeID hero) const;
+
+	CGHeroInstance * takeHeroFromPool(HeroTypeID hero);
+
+	/// reset mana and movement points for all heroes in pool
+	void onNewDay();
+
+	void addHeroToPool(CGHeroInstance * hero);
+
+	/// Marks hero as available to only specific set of players
+	void setAvailability(HeroTypeID hero, PlayerColor::Mask mask);
+
+	/// Makes hero available in tavern of specified player
+	void setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role);
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+		h & heroesPool;
+		h & perPlayerAvailability;
+		h & currentTavern;
+	}
+};
+
+VCMI_LIB_NAMESPACE_END

+ 35 - 0
lib/gameState/TavernSlot.h

@@ -0,0 +1,35 @@
+/*
+ * TavernSlot.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+enum class TavernHeroSlot : int8_t
+{
+	NONE = -1,
+	NATIVE, // 1st / left slot in tavern, contains hero native to player's faction on new week
+	RANDOM  // 2nd / right slot in tavern, contains hero of random class
+};
+
+enum class TavernSlotRole : int8_t
+{
+	NONE = -1,
+
+	SINGLE_UNIT, // hero was added after buying hero from this slot, and only has 1 creature in army
+	FULL_ARMY, // hero was added to tavern on new week and still has full army
+
+	RETREATED, // hero was owned by player before, but have retreated from battle and only has 1 creature in army
+	RETREATED_TODAY,
+
+	SURRENDERED, // hero was owned by player before, but have surrendered in battle and kept some troops
+	SURRENDERED_TODAY,
+};
+
+VCMI_LIB_NAMESPACE_END

+ 2 - 2
lib/mapObjectConstructors/CObjectClassesHandler.cpp

@@ -57,6 +57,8 @@ CObjectClassesHandler::CObjectClassesHandler()
 	SET_HANDLER_CLASS("shrine", ShrineInstanceConstructor);
 	SET_HANDLER_CLASS("hillFort", HillFortInstanceConstructor);
 	SET_HANDLER_CLASS("shipyard", ShipyardInstanceConstructor);
+	SET_HANDLER_CLASS("monster", CreatureInstanceConstructor);
+	SET_HANDLER_CLASS("resource", ResourceInstanceConstructor);
 
 	SET_HANDLER_CLASS("static", CObstacleConstructor);
 	SET_HANDLER_CLASS("", CObstacleConstructor);
@@ -73,7 +75,6 @@ CObjectClassesHandler::CObjectClassesHandler()
 	SET_HANDLER("artifact", CGArtifact);
 	SET_HANDLER("borderGate", CGBorderGate);
 	SET_HANDLER("borderGuard", CGBorderGuard);
-	SET_HANDLER("monster", CGCreature);
 	SET_HANDLER("denOfThieves", CGDenOfthieves);
 	SET_HANDLER("event", CGEvent);
 	SET_HANDLER("garrison", CGGarrison);
@@ -87,7 +88,6 @@ CObjectClassesHandler::CObjectClassesHandler()
 	SET_HANDLER("pandora", CGPandoraBox);
 	SET_HANDLER("prison", CGHeroInstance);
 	SET_HANDLER("questGuard", CGQuestGuard);
-	SET_HANDLER("resource", CGResource);
 	SET_HANDLER("scholar", CGScholar);
 	SET_HANDLER("seerHut", CGSeerHut);
 	SET_HANDLER("sign", CGSignBottle);

+ 40 - 0
lib/mapObjectConstructors/CommonConstructors.cpp

@@ -35,6 +35,26 @@ bool CObstacleConstructor::isStaticObject()
 	return true;
 }
 
+bool CreatureInstanceConstructor::hasNameTextID() const
+{
+	return true;
+}
+
+std::string CreatureInstanceConstructor::getNameTextID() const
+{
+	return VLC->creatures()->getByIndex(getSubIndex())->getNamePluralTextID();
+}
+
+bool ResourceInstanceConstructor::hasNameTextID() const
+{
+	return true;
+}
+
+std::string ResourceInstanceConstructor::getNameTextID() const
+{
+	return TextIdentifier("core", "restypes", getSubIndex()).get();
+}
+
 void CTownInstanceConstructor::initTypeData(const JsonNode & input)
 {
 	VLC->modh->identifiers.requestIdentifier("faction", input["faction"], [&](si32 index)
@@ -86,6 +106,16 @@ void CTownInstanceConstructor::randomizeObject(CGTownInstance * object, CRandomG
 		object->appearance = templ;
 }
 
+bool CTownInstanceConstructor::hasNameTextID() const
+{
+	return true;
+}
+
+std::string CTownInstanceConstructor::getNameTextID() const
+{
+	return faction->getNameTextID();
+}
+
 void CHeroInstanceConstructor::initTypeData(const JsonNode & input)
 {
 	VLC->modh->identifiers.requestIdentifier(
@@ -133,6 +163,16 @@ void CHeroInstanceConstructor::randomizeObject(CGHeroInstance * object, CRandomG
 
 }
 
+bool CHeroInstanceConstructor::hasNameTextID() const
+{
+	return true;
+}
+
+std::string CHeroInstanceConstructor::getNameTextID() const
+{
+	return heroClass->getNameTextID();
+}
+
 void BoatInstanceConstructor::initTypeData(const JsonNode & input)
 {
 	layer = EPathfindingLayer::SAIL;

+ 22 - 0
lib/mapObjectConstructors/CommonConstructors.h

@@ -13,6 +13,7 @@
 #include "../LogicalExpression.h"
 
 #include "../mapObjects/MiscObjects.h"
+#include "../mapObjects/CGCreature.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -22,6 +23,7 @@ class CGTownInstance;
 class CGHeroInstance;
 class CGMarket;
 class CHeroClass;
+class CGCreature;
 class CBank;
 class CGBoat;
 class CFaction;
@@ -33,6 +35,20 @@ public:
 	bool isStaticObject() override;
 };
 
+class CreatureInstanceConstructor : public CDefaultObjectTypeHandler<CGCreature>
+{
+public:
+	bool hasNameTextID() const override;
+	std::string getNameTextID() const override;
+};
+
+class ResourceInstanceConstructor : public CDefaultObjectTypeHandler<CGResource>
+{
+public:
+	bool hasNameTextID() const override;
+	std::string getNameTextID() const override;
+};
+
 class CTownInstanceConstructor : public CDefaultObjectTypeHandler<CGTownInstance>
 {
 	JsonNode filtersJson;
@@ -48,6 +64,9 @@ public:
 	void randomizeObject(CGTownInstance * object, CRandomGenerator & rng) const override;
 	void afterLoadFinalization() override;
 
+	bool hasNameTextID() const override;
+	std::string getNameTextID() const override;
+
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
 		h & filtersJson;
@@ -72,6 +91,9 @@ public:
 	void randomizeObject(CGHeroInstance * object, CRandomGenerator & rng) const override;
 	void afterLoadFinalization() override;
 
+	bool hasNameTextID() const override;
+	std::string getNameTextID() const override;
+
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
 		h & filtersJson;

+ 15 - 0
lib/mapObjects/CGDwelling.cpp

@@ -324,6 +324,21 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const
 	{
 		if(count) //there are available creatures
 		{
+
+			if (VLC->settings()->getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED))
+			{
+				SlotID testSlot = h->getSlotFor(crid);
+				if(!testSlot.validSlot()) //no available slot - try merging army of visiting hero
+				{
+					std::pair<SlotID, SlotID> toMerge;
+					if (h->mergableStacks(toMerge))
+					{
+						cb->moveStack(StackLocation(h, toMerge.first), StackLocation(h, toMerge.second), -1); //merge toMerge.first into toMerge.second
+						assert(!h->hasStackAtSlot(toMerge.first)); //we have now a new free slot
+					}
+				}
+			}
+
 			SlotID slot = h->getSlotFor(crid);
 			if(!slot.validSlot()) //no available slot
 			{

+ 3 - 3
lib/mapping/CMap.h

@@ -56,10 +56,10 @@ struct DLL_LINKAGE DisposedHero
 {
 	DisposedHero();
 
-	ui32 heroId;
-	ui32 portrait; /// The portrait id of the hero, -1 is default.
+	HeroTypeID heroId;
+	HeroTypeID portrait; /// The portrait id of the hero, -1 is default.
 	std::string name;
-	ui8 players; /// Who can hire this hero (bitfield).
+	PlayerColor::Mask players; /// Who can hire this hero (bitfield).
 
 	template <typename Handler>
 	void serialize(Handler & h, const int version)

+ 3 - 1
lib/registerTypes/RegisterTypes.h

@@ -107,6 +107,8 @@ void registerTypesMapObjectTypes(Serializer &s)
 	s.template registerType<AObjectTypeHandler, ShrineInstanceConstructor>();
 	s.template registerType<AObjectTypeHandler, ShipyardInstanceConstructor>();
 	s.template registerType<AObjectTypeHandler, HillFortInstanceConstructor>();
+	s.template registerType<AObjectTypeHandler, CreatureInstanceConstructor>();
+	s.template registerType<AObjectTypeHandler, ResourceInstanceConstructor>();
 
 #define REGISTER_GENERIC_HANDLER(TYPENAME) s.template registerType<AObjectTypeHandler, CDefaultObjectTypeHandler<TYPENAME> >()
 
@@ -239,7 +241,7 @@ void registerTypesClientPacks1(Serializer &s)
 	s.template registerType<CPackForClient, SetMana>();
 	s.template registerType<CPackForClient, SetMovePoints>();
 	s.template registerType<CPackForClient, FoWChange>();
-	s.template registerType<CPackForClient, SetAvailableHeroes>();
+	s.template registerType<CPackForClient, SetAvailableHero>();
 	s.template registerType<CPackForClient, GiveBonus>();
 	s.template registerType<CPackForClient, ChangeObjPos>();
 	s.template registerType<CPackForClient, PlayerEndsGame>();

+ 1 - 0
lib/registerTypes/TypesLobbyPacks.cpp

@@ -14,6 +14,7 @@
 #include "../StartInfo.h"
 #include "../gameState/CGameState.h"
 #include "../gameState/CGameStateCampaign.h"
+#include "../gameState/TavernHeroesPool.h"
 #include "../mapping/CMap.h"
 #include "../CModHandler.h"
 #include "../mapObjects/CObjectHandler.h"

+ 2 - 0
lib/rmg/modificators/ObjectManager.cpp

@@ -606,6 +606,8 @@ bool ObjectManager::addGuard(rmg::Object & object, si32 strength, bool zoneGuard
 	auto & instance = object.addInstance(*guard);
 	instance.setPosition(guardPos - object.getPosition());
 	instance.setAnyTemplate(); //terrain is irrelevant for monsters, but monsters need some template now
+	//Make up for extra offset in HotA creature templates
+	instance.setPosition(instance.getPosition() + instance.object().getVisitableOffset());
 		
 	return true;
 }

+ 1 - 1
lib/spells/TargetCondition.cpp

@@ -541,7 +541,7 @@ void TargetCondition::loadConditions(const JsonNode & source, bool exclusive, bo
 
 			CModHandler::parseIdentifier(keyValue.first, scope, type, identifier);
 
-			item = itemFactory->createConfigurable(scope, type, identifier);
+			item = itemFactory->createConfigurable(keyValue.second.meta, type, identifier);
 		}
 
 		if(item)

+ 4 - 1
lib/spells/effects/Timed.cpp

@@ -250,7 +250,10 @@ void Timed::serializeJsonUnitEffect(JsonSerializeFormat & handler)
 			auto guard = handler.enterStruct(p.first);
 			const JsonNode & bonusNode = handler.getCurrent();
 			auto b = JsonUtils::parseBonus(bonusNode);
-			bonus.push_back(b);
+			if (b)
+				bonus.push_back(b);
+			else
+				logMod->error("Failed to parse bonus '%s'!", p.first);
 		}
 	}
 }

+ 35 - 544
server/CGameHandler.cpp

@@ -8,6 +8,14 @@
  *
  */
 #include "StdInc.h"
+#include "CGameHandler.h"
+
+#include "HeroPoolProcessor.h"
+#include "ServerNetPackVisitors.h"
+#include "ServerSpellCastEnvironment.h"
+#include "CVCMIServer.h"
+
+#include "PlayerMessageProcessor.h"
 
 #include "../lib/filesystem/Filesystem.h"
 #include "../lib/filesystem/FileInfo.h"
@@ -35,7 +43,6 @@
 #include "../lib/GameSettings.h"
 #include "../lib/battle/BattleInfo.h"
 #include "../lib/CondSh.h"
-#include "ServerNetPackVisitors.h"
 #include "../lib/VCMI_Lib.h"
 #include "../lib/mapping/CMap.h"
 #include "../lib/mapping/CMapService.h"
@@ -44,9 +51,6 @@
 #include "../lib/ScopeGuard.h"
 #include "../lib/CSoundBase.h"
 #include "../lib/TerrainHandler.h"
-#include "CGameHandler.h"
-#include "ServerSpellCastEnvironment.h"
-#include "CVCMIServer.h"
 #include "../lib/CCreatureSet.h"
 #include "../lib/CThreadHelper.h"
 #include "../lib/GameConstants.h"
@@ -294,6 +298,11 @@ events::EventBus * CGameHandler::eventBus() const
 	return serverEventBus.get();
 }
 
+CVCMIServer * CGameHandler::gameLobby() const
+{
+	return lobby;
+}
+
 void CGameHandler::levelUpHero(const CGHeroInstance * hero, SecondarySkill skill)
 {
 	changeSecSkill(hero, skill, 1, 0);
@@ -868,24 +877,12 @@ void CGameHandler::battleAfterLevelUp(const BattleResult &result)
 	std::set<PlayerColor> playerColors = {finishingBattle->loser, finishingBattle->victor};
 	checkVictoryLossConditions(playerColors);
 
-	if (result.result == BattleResult::SURRENDER || result.result == BattleResult::ESCAPE) //loser has escaped or surrendered
-	{
-		SetAvailableHeroes sah;
-		sah.player = finishingBattle->loser;
-		sah.hid[0] = finishingBattle->loserHero->subID;
-		if (result.result == BattleResult::ESCAPE) //retreat
-		{
-			sah.army[0].clear();
-			sah.army[0].setCreature(SlotID(0), finishingBattle->loserHero->type->initialArmy.at(0).creature, 1);
-		}
+	if (result.result == BattleResult::SURRENDER)
+		heroPool->onHeroSurrendered(finishingBattle->loser, finishingBattle->loserHero);
 
-		if (const CGHeroInstance *another = getPlayerState(finishingBattle->loser)->availableHeroes.at(0))
-			sah.hid[1] = another->subID;
-		else
-			sah.hid[1] = -1;
+	if (result.result == BattleResult::ESCAPE)
+		heroPool->onHeroEscaped(finishingBattle->loser, finishingBattle->loserHero);
 
-		sendAndApply(&sah);
-	}
 	if (result.winner != 2 && finishingBattle->winnerHero && finishingBattle->winnerHero->stacks.empty()
 		&& (!finishingBattle->winnerHero->commander || !finishingBattle->winnerHero->commander->alive))
 	{
@@ -893,20 +890,7 @@ void CGameHandler::battleAfterLevelUp(const BattleResult &result)
 		sendAndApply(&ro);
 
 		if (VLC->settings()->getBoolean(EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS))
-		{
-			SetAvailableHeroes sah;
-			sah.player = finishingBattle->victor;
-			sah.hid[0] = finishingBattle->winnerHero->subID;
-			sah.army[0].clear();
-			sah.army[0].setCreature(SlotID(0), finishingBattle->winnerHero->type->initialArmy.at(0).creature, 1);
-
-			if (const CGHeroInstance *another = getPlayerState(finishingBattle->victor)->availableHeroes.at(0))
-				sah.hid[1] = another->subID;
-			else
-				sah.hid[1] = -1;
-
-			sendAndApply(&sah);
-		}
+			heroPool->onHeroEscaped(finishingBattle->victor, finishingBattle->winnerHero);
 	}
 	
 	finishingBattle.reset();
@@ -1240,7 +1224,7 @@ void CGameHandler::handleClientDisconnection(std::shared_ptr<CConnection> c)
 		if(playerConnection != playerConnections.second.end())
 		{
 			std::string messageText = boost::str(boost::format("%s (cid %d) was disconnected") % playerSettings->name % c->connectionID);
-			playerMessage(playerId, messageText, ObjectInstanceID{});
+			playerMessages->broadcastMessage(playerId, messageText);
 		}
 	}
 }
@@ -1576,6 +1560,8 @@ int CGameHandler::moveStack(int stack, BattleHex dest)
 
 CGameHandler::CGameHandler(CVCMIServer * lobby)
 	: lobby(lobby)
+	, heroPool(std::make_unique<HeroPoolProcessor>(this))
+	, playerMessages(std::make_unique<PlayerMessageProcessor>(this))
 	, complainNoCreatures("No creatures to split")
 	, complainNotEnoughCreatures("Cannot split that stack, not enough creatures!")
 	, complainInvalidSlot("Invalid slot accessed!")
@@ -1765,27 +1751,6 @@ void CGameHandler::newTurn()
 		}
 	}
 
-	std::map<ui32, ConstTransitivePtr<CGHeroInstance> > pool = gs->hpool.heroesPool;
-
-	for (auto& hp : pool)
-	{
-		auto hero = hp.second;
-		if (hero->isInitialized() && hero->stacks.size())
-		{
-			// reset retreated or surrendered heroes
-			auto maxmove = hero->movementPointsLimit(true);
-			// if movement is greater than maxmove, we should decrease it
-			if (hero->movementPointsRemaining() != maxmove || hero->mana < hero->manaLimit())
-			{
-				NewTurn::Hero hth;
-				hth.id = hero->id;
-				hth.move = maxmove;
-				hth.mana = hero->getManaNewTurn();
-				n.heroes.insert(hth);
-			}
-		}
-	}
-
 	for (auto & elem : gs->players)
 	{
 		if (elem.first == PlayerColor::NEUTRAL)
@@ -1797,29 +1762,7 @@ void CGameHandler::newTurn()
 		hadGold.insert(playerGold);
 
 		if (newWeek) //new heroes in tavern
-		{
-			SetAvailableHeroes sah;
-			sah.player = elem.first;
-
-			//pick heroes and their armies
-			CHeroClass *banned = nullptr;
-			for (int j = 0; j < GameConstants::AVAILABLE_HEROES_PER_PLAYER; j++)
-			{
-				//first hero - native if possible, second hero -> any other class
-				if (CGHeroInstance *h = gs->hpool.pickHeroFor(j == 0, elem.first, getNativeTown(elem.first), pool, getRandomGenerator(), banned))
-				{
-					sah.hid[j] = h->subID;
-					h->initArmy(getRandomGenerator(), &sah.army[j]);
-					banned = h->type->heroClass;
-				}
-				else
-				{
-					sah.hid[j] = -1;
-				}
-			}
-
-			sendAndApply(&sah);
-		}
+			heroPool->onNewWeek(elem.first);
 
 		n.res[elem.first] = elem.second.resources;
 
@@ -2709,14 +2652,6 @@ void CGameHandler::changeSpells(const CGHeroInstance * hero, bool give, const st
 	sendAndApply(&cs);
 }
 
-void CGameHandler::sendMessageTo(std::shared_ptr<CConnection> c, const std::string &message)
-{
-	SystemMessage sm;
-	sm.text = message;
-	boost::unique_lock<boost::mutex> lock(*c->mutexWrite);
-	*(c.get()) << &sm;
-}
-
 void CGameHandler::giveHeroBonus(GiveBonus * bonus)
 {
 	sendAndApply(bonus);
@@ -2927,10 +2862,8 @@ bool CGameHandler::isPlayerOwns(CPackForServer * pack, ObjectInstanceID id)
 void CGameHandler::throwNotAllowedAction(CPackForServer * pack)
 {
 	if(pack->c)
-	{
-		SystemMessage temp_message("You are not allowed to perform this action!");
-		pack->c->sendPack(&temp_message);
-	}
+		playerMessages->sendSystemMessage(pack->c, "You are not allowed to perform this action!");
+
 	logNetwork->error("Player is not allowed to perform this action!");
 	throw ExceptionNotAllowedAction();
 }
@@ -2940,11 +2873,9 @@ void CGameHandler::wrongPlayerMessage(CPackForServer * pack, PlayerColor expecte
 	std::ostringstream oss;
 	oss << "You were identified as player " << getPlayerAt(pack->c) << " while expecting " << expectedplayer;
 	logNetwork->error(oss.str());
+
 	if(pack->c)
-	{
-		SystemMessage temp_message(oss.str());
-		pack->c->sendPack(&temp_message);
-	}
+		playerMessages->sendSystemMessage(pack->c, oss.str());
 }
 
 void CGameHandler::throwOnWrongOwner(CPackForServer * pack, ObjectInstanceID id)
@@ -3659,13 +3590,6 @@ bool CGameHandler::razeStructure (ObjectInstanceID tid, BuildingID bid)
 	return true;
 }
 
-void CGameHandler::sendMessageToAll(const std::string &message)
-{
-	SystemMessage sm;
-	sm.text = message;
-	sendToAllClients(&sm);
-}
-
 bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dstid, CreatureID crid, ui32 cram, si32 fromLvl)
 {
 	const CGDwelling * dw = static_cast<const CGDwelling *>(getObj(objid));
@@ -4383,94 +4307,6 @@ bool CGameHandler::setFormation(ObjectInstanceID hid, ui8 formation)
 	return true;
 }
 
-bool CGameHandler::hireHero(const CGObjectInstance *obj, ui8 hid, PlayerColor player)
-{
-	const PlayerState * p = getPlayerState(player);
-	const CGTownInstance * t = getTown(obj->id);
-
-	//common preconditions
-//	if ((p->resources.at(EGameResID::GOLD)<GOLD_NEEDED  && complain("Not enough gold for buying hero!"))
-//		|| (getHeroCount(player, false) >= GameConstants::MAX_HEROES_PER_PLAYER && complain("Cannot hire hero, only 8 wandering heroes are allowed!")))
-	if ((p->resources[EGameResID::GOLD] < GameConstants::HERO_GOLD_COST && complain("Not enough gold for buying hero!"))
-		|| ((getHeroCount(player, false) >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP) && complain("Cannot hire hero, too many wandering heroes already!")))
-		|| ((getHeroCount(player, true) >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP) && complain("Cannot hire hero, too many heroes garrizoned and wandering already!"))))
-	{
-		return false;
-	}
-
-	if (t) //tavern in town
-	{
-		if ((!t->hasBuilt(BuildingID::TAVERN) && complain("No tavern!"))
-			 || (t->visitingHero  && complain("There is visiting hero - no place!")))
-		{
-			return false;
-		}
-	}
-	else if (obj->ID == Obj::TAVERN)
-	{
-		if (getTile(obj->visitablePos())->visitableObjects.back() != obj && complain("Tavern entry must be unoccupied!"))
-		{
-			return false;
-		}
-	}
-
-	const CGHeroInstance *nh = p->availableHeroes.at(hid);
-	if (!nh)
-	{
-		complain ("Hero is not available for hiring!");
-		return false;
-	}
-
-	HeroRecruited hr;
-	hr.tid = obj->id;
-	hr.hid = nh->subID;
-	hr.player = player;
-	hr.tile = nh->convertFromVisitablePos(obj->visitablePos());
-	if (getTile(hr.tile)->isWater())
-	{
-		//Create a new boat for hero
-		createObject(obj->visitablePos(), Obj::BOAT, nh->getBoatType().getNum());
-
-		hr.boatId = getTopObj(hr.tile)->id;
-	}
-	sendAndApply(&hr);
-
-	std::map<ui32, ConstTransitivePtr<CGHeroInstance> > pool = gs->unusedHeroesFromPool();
-
-	const CGHeroInstance *theOtherHero = p->availableHeroes.at(!hid);
-	const CGHeroInstance *newHero = nullptr;
-	if (theOtherHero) //on XXL maps all heroes can be imprisoned :(
-	{
-		newHero = gs->hpool.pickHeroFor(false, player, getNativeTown(player), pool, getRandomGenerator(), theOtherHero->type->heroClass);
-	}
-
-	SetAvailableHeroes sah;
-	sah.player = player;
-
-	if (newHero)
-	{
-		sah.hid[hid] = newHero->subID;
-		sah.army[hid].clear();
-		sah.army[hid].setCreature(SlotID(0), newHero->type->initialArmy[0].creature, 1);
-	}
-	else
-	{
-		sah.hid[hid] = -1;
-	}
-
-	sah.hid[!hid] = theOtherHero ? theOtherHero->subID : -1;
-	sendAndApply(&sah);
-
-	giveResource(player, EGameResID::GOLD, -GameConstants::HERO_GOLD_COST);
-
-	if(t)
-	{
-		visitCastleObjects(t, nh);
-		giveSpells (t,nh);
-	}
-	return true;
-}
-
 bool CGameHandler::queryReply(QueryID qid, const JsonNode & answer, PlayerColor player)
 {
 	boost::unique_lock<boost::recursive_mutex> lock(gsm);
@@ -4978,140 +4814,6 @@ bool CGameHandler::makeBattleAction(BattleAction &ba)
 	return ok;
 }
 
-void CGameHandler::playerMessage(PlayerColor player, const std::string &message, ObjectInstanceID currObj)
-{
-	bool cheated = false;
-	
-	std::vector<std::string> words;
-	boost::split(words, message, boost::is_any_of(" "));
-	
-	bool isHost = false;
-	for(auto & c : connections[player])
-		if(lobby->isClientHost(c->connectionID))
-			isHost = true;
-	
-	if(isHost && words.size() >= 2 && words[0] == "game")
-	{
-		if(words[1] == "exit" || words[1] == "quit" || words[1] == "end")
-		{
-			SystemMessage temp_message("game was terminated");
-			sendAndApply(&temp_message);
-			lobby->state = EServerState::SHUTDOWN;
-			return;
-		}
-		if(words.size() == 3 && words[1] == "save")
-		{
-			save("Saves/" + words[2]);
-			SystemMessage temp_message("game saved as " + words[2]);
-			sendAndApply(&temp_message);
-			return;
-		}
-		if(words.size() == 3 && words[1] == "kick")
-		{
-			auto playername = words[2];
-			PlayerColor playerToKick(PlayerColor::CANNOT_DETERMINE);
-			if(std::all_of(playername.begin(), playername.end(), ::isdigit))
-				playerToKick = PlayerColor(std::stoi(playername));
-			else
-			{
-				for(auto & c : connections)
-				{
-					if(c.first.getStr(false) == playername)
-						playerToKick = c.first;
-				}
-			}
-			
-			if(playerToKick != PlayerColor::CANNOT_DETERMINE)
-			{
-				PlayerCheated pc;
-				pc.player = playerToKick;
-				pc.losingCheatCode = true;
-				sendAndApply(&pc);
-				checkVictoryLossConditionsForPlayer(playerToKick);
-			}
-			return;
-		}
-	}
-	
-	int obj = 0;
-	if (words.size() == 2 && words[0] != "vcmiexp" && words[0] != "vcmiolorin")
-	{
-		obj = std::atoi(words[1].c_str());
-		if (obj)
-			currObj = ObjectInstanceID(obj);
-	}
-
-	const CGHeroInstance * hero = getHero(currObj);
-	const CGTownInstance * town = getTown(currObj);
-	if (!town && hero)
-		town = hero->visitedTown;
-
-	if(words.size() > 1 && (words[0] == "vcmiarmy" || words[0] == "vcminissi" || words[0] == "vcmiexp" || words[0] == "vcmiolorin"))
-	{
-		std::string cheatCodeWithOneParameter = std::string(words[0]) + " " + words[1];
-		handleCheatCode(cheatCodeWithOneParameter, player, hero, town, cheated);
-	}
-	else if (words.size() == 1 || obj)
-	{
-		handleCheatCode(words[0], player, hero, town, cheated);
-	}
-	else
-	{
-		for (const auto & i : gs->players)
-		{
-			if (i.first == PlayerColor::NEUTRAL)
-				continue;
-			if (words[1] == "ai")
-			{
-				if (i.second.human)
-					continue;
-			}
-			else if (words[1] != "all" && words[1] != i.first.getStr())
-				continue;
-
-			if (words[0] == "vcmiformenos" || words[0] == "vcmieagles" || words[0] == "vcmiungoliant"
-				|| words[0] == "vcmiresources" || words[0] == "vcmimap" || words[0] == "vcmihidemap")
-			{
-				handleCheatCode(words[0], i.first, nullptr, nullptr, cheated);
-			}
-			else if (words[0] == "vcmiarmenelos" || words[0] == "vcmibuild")
-			{
-				for (const auto & t : i.second.towns)
-				{
-					handleCheatCode(words[0], i.first, nullptr, t, cheated);
-				}
-			}
-			else
-			{
-				for (const auto & h : i.second.heroes)
-				{
-					handleCheatCode(words[0], i.first, h, nullptr, cheated);
-				}
-			}
-		}
-	}
-
-	if (cheated)
-	{
-		if(!getPlayerSettings(player)->isControlledByAI())
-		{
-			SystemMessage temp_message(VLC->generaltexth->allTexts[260]);
-			sendAndApply(&temp_message);
-		}
-
-		if(!player.isSpectator())
-			checkVictoryLossConditionsForPlayer(player);//Player enter win code or got required art\creature
-	}
-	else
-	{
-		if(!getPlayerSettings(player)->isControlledByAI())
-		{
-			PlayerMessageClient temp_message(player, message);
-			sendAndApply(&temp_message);
-		}
-	}
-}
-
 bool CGameHandler::makeCustomAction(BattleAction & ba)
 {
 	switch(ba.actionType)
@@ -5474,7 +5176,7 @@ void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n)
 
 bool CGameHandler::complain(const std::string &problem)
 {
-	sendMessageToAll("Server encountered a problem: " + problem);
+	playerMessages->broadcastSystemMessage("Server encountered a problem: " + problem);
 	logGlobal->error(problem);
 	return true;
 }
@@ -6866,225 +6568,6 @@ void CGameHandler::spawnWanderingMonsters(CreatureID creatureID)
 	}
 }
 
-void CGameHandler::handleCheatCode(std::string & cheat, PlayerColor player, const CGHeroInstance * hero, const CGTownInstance * town, bool & cheated)
-{
-	//Make cheat case-insensitive
-	std::transform(cheat.begin(), cheat.end(), cheat.begin(), [](unsigned char c){ return std::tolower(c); });
-	
-	if (cheat == "vcmiistari" || cheat == "vcmispells")
-	{
-		cheated = true;
-		if (!hero) return;
-		///Give hero spellbook
-		if (!hero->hasSpellbook())
-			giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK);
-
-		///Give all spells with bonus (to allow banned spells)
-		GiveBonus giveBonus(GiveBonus::ETarget::HERO);
-		giveBonus.id = hero->id.getNum();
-		giveBonus.bonus = Bonus(BonusDuration::PERMANENT, BonusType::SPELLS_OF_LEVEL, BonusSource::OTHER, 0, 0);
-		//start with level 0 to skip abilities
-		for (int level = 1; level <= GameConstants::SPELL_LEVELS; level++)
-		{
-			giveBonus.bonus.subtype = level;
-			sendAndApply(&giveBonus);
-		}
-
-		///Give mana
-		SetMana sm;
-		sm.hid = hero->id;
-		sm.val = 999;
-		sm.absolute = true;
-		sendAndApply(&sm);
-	}
-	else if (cheat == "vcmiarmenelos" || cheat == "vcmibuild")
-	{
-		cheated = true;
-		if (!town) return;
-		///Build all buildings in selected town
-		for (auto & build : town->town->buildings)
-		{
-			if (!town->hasBuilt(build.first)
-				&& !build.second->getNameTranslated().empty()
-				&& build.first != BuildingID::SHIP)
-			{
-				buildStructure(town->id, build.first, true);
-			}
-		}
-	}
-	else if (cheat == "vcmiainur" || cheat == "vcmiangband" || cheat == "vcmiglaurung" || cheat == "vcmiarchangel"
-		|| cheat == "vcmiblackknight" || cheat == "vcmicrystal" || cheat == "vcmiazure" || cheat == "vcmifaerie")
-	{
-		cheated = true;
-		if (!hero) return;
-		///Gives N creatures into each slot
-		std::map<std::string, std::pair<std::string, int>> creatures;
-		creatures.insert(std::make_pair("vcmiainur", std::make_pair("archangel", 5))); //5 archangels
-		creatures.insert(std::make_pair("vcmiangband", std::make_pair("blackKnight", 10))); //10 black knights
-		creatures.insert(std::make_pair("vcmiglaurung", std::make_pair("crystalDragon", 5000))); //5000 crystal dragons
-		creatures.insert(std::make_pair("vcmiarchangel", std::make_pair("archangel", 5))); //5 archangels
-		creatures.insert(std::make_pair("vcmiblackknight", std::make_pair("blackKnight", 10))); //10 black knights
-		creatures.insert(std::make_pair("vcmicrystal", std::make_pair("crystalDragon", 5000))); //5000 crystal dragons
-		creatures.insert(std::make_pair("vcmiazure", std::make_pair("azureDragon", 5000))); //5000 azure dragons
-		creatures.insert(std::make_pair("vcmifaerie", std::make_pair("fairieDragon", 5000))); //5000 faerie dragons
-
-		const int32_t creatureIdentifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "creature", creatures[cheat].first, false).value();
-		const CCreature * creature = VLC->creh->objects.at(creatureIdentifier);
-		for (int i = 0; i < GameConstants::ARMY_SIZE; i++)
-			if (!hero->hasStackAtSlot(SlotID(i)))
-				insertNewStack(StackLocation(hero, SlotID(i)), creature, creatures[cheat].second);
-	}
-	else if (boost::starts_with(cheat, "vcmiarmy") || boost::starts_with(cheat, "vcminissi"))
-	{
-		cheated = true;
-		if (!hero) return;
-
-		std::vector<std::string> words;
-		boost::split(words, cheat, boost::is_any_of(" "));
-
-		if(words.size() < 2)
-			return;
-
-		std::string creatureIdentifier = words[1];
-
-		std::optional<int32_t> creatureId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "creature", creatureIdentifier, false);
-
-		if(creatureId.has_value())
-		{
-			const auto * creature = CreatureID(creatureId.value()).toCreature();
-
-			for (int i = 0; i < GameConstants::ARMY_SIZE; i++)
-				if (!hero->hasStackAtSlot(SlotID(i)))
-					insertNewStack(StackLocation(hero, SlotID(i)), creature, 5 * std::pow(10, i));
-		}
-	}
-	else if (cheat == "vcminoldor" || cheat == "vcmimachines")
-	{
-		cheated = true;
-		if (!hero) return;
-		///Give all war machines to hero
-		if (!hero->getArt(ArtifactPosition::MACH1))
-			giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::BALLISTA], ArtifactPosition::MACH1);
-		if (!hero->getArt(ArtifactPosition::MACH2))
-			giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::AMMO_CART], ArtifactPosition::MACH2);
-		if (!hero->getArt(ArtifactPosition::MACH3))
-			giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::FIRST_AID_TENT], ArtifactPosition::MACH3);
-	}
-	else if (cheat == "vcmiforgeofnoldorking" || cheat == "vcmiartifacts")
-	{
-		cheated = true;
-		if (!hero) return;
-		///Give hero all artifacts except war machines, spell scrolls and spell book
-		for(int g = 7; g < VLC->arth->objects.size(); ++g) //including artifacts from mods
-		{
-			if(VLC->arth->objects[g]->canBePutAt(hero))
-				giveHeroNewArtifact(hero, VLC->arth->objects[g], ArtifactPosition::FIRST_AVAILABLE);
-		}
-	}
-	else if (cheat == "vcmiglorfindel" || cheat == "vcmilevel")
-	{
-		cheated = true;
-		if (!hero) return;
-		///selected hero gains a new level
-		changePrimSkill(hero, PrimarySkill::EXPERIENCE, VLC->heroh->reqExp(hero->level + 1) - VLC->heroh->reqExp(hero->level));
-	}
-	else if (boost::starts_with(cheat, "vcmiexp") || boost::starts_with(cheat, "vcmiolorin"))
-	{
-		cheated = true;
-		if (!hero) return;
-
-		std::vector<std::string> words;
-		boost::split(words, cheat, boost::is_any_of(" "));
-
-		if(words.size() < 2)
-			return;
-
-		std::string expAmount = words[1];
-		long expAmountProcessed = 0;
-
-		try
-		{
-			expAmountProcessed = std::stol(expAmount);
-		}
-		catch(std::exception&)
-		{
-			logGlobal->error("Could not parse experience amount for vcmiexp cheat");
-		}
-
-		if(expAmountProcessed > 1)
-		{
-			changePrimSkill(hero, PrimarySkill::EXPERIENCE, expAmountProcessed);
-		}
-	}
-	else if (cheat == "vcminahar" || cheat == "vcmimove")
-	{
-		cheated = true;
-		if (!hero) return;
-		///Give 1000000 movement points to hero
-		SetMovePoints smp;
-		smp.hid = hero->id;
-		smp.val = 1000000;
-		sendAndApply(&smp);
-
-		GiveBonus gb(GiveBonus::ETarget::HERO);
-		gb.bonus.type = BonusType::FREE_SHIP_BOARDING;
-		gb.bonus.duration = BonusDuration::ONE_DAY;
-		gb.bonus.source = BonusSource::OTHER;
-		gb.id = hero->id.getNum();
-		giveHeroBonus(&gb);
-	}
-	else if (cheat == "vcmiformenos" || cheat == "vcmiresources")
-	{
-		cheated = true;
-		///Give resources to player
-		TResources resources;
-		resources[EGameResID::GOLD] = 100000;
-		for (GameResID i = EGameResID::WOOD; i < EGameResID::GOLD; ++i)
-			resources[i] = 100;
-
-		giveResources(player, resources);
-	}
-	else if (cheat == "vcmisilmaril" || cheat == "vcmiwin")
-	{
-		cheated = true;
-		///Player wins
-		PlayerCheated pc;
-		pc.player = player;
-		pc.winningCheatCode = true;
-		sendAndApply(&pc);
-	}
-	else if (cheat == "vcmimelkor" || cheat == "vcmilose")
-	{
-		cheated = true;
-		///Player looses
-		PlayerCheated pc;
-		pc.player = player;
-		pc.losingCheatCode = true;
-		sendAndApply(&pc);
-	}
-	else if (cheat == "vcmieagles" || cheat == "vcmiungoliant" || cheat == "vcmimap" || cheat == "vcmihidemap")
-	{
-		cheated = true;
-		///Reveal or conceal FoW
-		FoWChange fc;
-		fc.mode = ((cheat == "vcmieagles" || cheat == "vcmimap") ? 1 : 0);
-		fc.player = player;
-		const auto & fowMap = gs->getPlayerTeam(player)->fogOfWarMap;
-		auto hlp_tab = new int3[gs->map->width * gs->map->height * (gs->map->levels())];
-		int lastUnc = 0;
-
-		for(int z = 0; z < gs->map->levels(); z++)
-			for(int x = 0; x < gs->map->width; x++)
-				for(int y = 0; y < gs->map->height; y++)
-					if(!(*fowMap)[z][x][y] || !fc.mode)
-						hlp_tab[lastUnc++] = int3(x, y, z);
-
-		fc.tiles.insert(hlp_tab, hlp_tab + lastUnc);
-		delete [] hlp_tab;
-		sendAndApply(&fc);
-	}
-}
-
 void CGameHandler::removeObstacle(const CObstacleInstance & obstacle)
 {
 	BattleObstaclesChanged obsRem;
@@ -7395,3 +6878,11 @@ void CGameHandler::createObject(const int3 & visitablePosition, Obj type, int32_
 	no.targetPos = visitablePosition;
 	sendAndApply(&no);
 }
+
+void CGameHandler::deserializationFix()
+{
+	//FIXME: pointer to GameHandler itself can't be deserialized at the moment since GameHandler is top-level entity in serialization
+	// restore any places that requires such pointer manually
+	heroPool->gameHandler = this;
+	playerMessages->gameHandler = this;
+}

+ 23 - 11
server/CGameHandler.h

@@ -46,9 +46,11 @@ template<typename T> class CApplier;
 
 VCMI_LIB_NAMESPACE_END
 
+class HeroPoolProcessor;
 class CGameHandler;
 class CVCMIServer;
 class CBaseForGHApply;
+class PlayerMessageProcessor;
 
 struct PlayerStatus
 {
@@ -97,13 +99,18 @@ class CGameHandler : public IGameCallback, public CBattleInfoCallback, public En
 	CVCMIServer * lobby;
 	std::shared_ptr<CApplier<CBaseForGHApply>> applier;
 	std::unique_ptr<boost::thread> battleThread;
+
 public:
+	std::unique_ptr<HeroPoolProcessor> heroPool;
+
 	using FireShieldInfo = std::vector<std::pair<const CStack *, int64_t>>;
 	//use enums as parameters, because doMove(sth, true, false, true) is not readable
 	enum EGuardLook {CHECK_FOR_GUARDS, IGNORE_GUARDS};
 	enum EVisitDest {VISIT_DEST, DONT_VISIT_DEST};
 	enum ELEaveTile {LEAVING_TILE, REMAINING_ON_TILE};
 
+	std::unique_ptr<PlayerMessageProcessor> playerMessages;
+
 	std::map<PlayerColor, std::set<std::shared_ptr<CConnection>>> connections; //player color -> connection to client with interface of that player
 	PlayerStatuses states; //player color -> player state
 
@@ -119,6 +126,7 @@ public:
 	const GameCb * game() const override;
 	vstd::CLoggerBase * logger() const override;
 	events::EventBus * eventBus() const override;
+	CVCMIServer * gameLobby() const;
 
 	bool isValidObject(const CGObjectInstance *obj) const;
 	bool isBlockedByQueries(const CPack *pack, PlayerColor player);
@@ -145,6 +153,7 @@ public:
 	void setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town);
 	void setBattleResult(BattleResult::EResult resultType, int victoriusSide);
 
+	CGameHandler() = default;
 	CGameHandler(CVCMIServer * lobby);
 	~CGameHandler();
 
@@ -230,7 +239,6 @@ public:
 	PlayerColor getPlayerAt(std::shared_ptr<CConnection> c) const;
 	bool hasPlayerAt(PlayerColor player, std::shared_ptr<CConnection> c) const;
 
-	void playerMessage(PlayerColor player, const std::string &message, ObjectInstanceID currObj);
 	void updateGateState();
 	bool makeBattleAction(BattleAction &ba);
 	bool makeAutomaticAction(const CStack *stack, BattleAction &ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack)
@@ -240,7 +248,6 @@ public:
 
 	void removeObstacle(const CObstacleInstance &obstacle);
 	bool queryReply( QueryID qid, const JsonNode & answer, PlayerColor player );
-	bool hireHero( const CGObjectInstance *obj, ui8 hid, PlayerColor player );
 	bool buildBoat( ObjectInstanceID objid, PlayerColor player );
 	bool setFormation( ObjectInstanceID hid, ui8 formation );
 	bool tradeResources(const IMarket *market, ui32 val, PlayerColor player, ui32 id1, ui32 id2);
@@ -283,7 +290,12 @@ public:
 		h & QID;
 		h & states;
 		h & finishingBattle;
+		h & heroPool;
 		h & getRandomGenerator();
+		h & playerMessages;
+
+		if (!h.saving)
+			deserializationFix();
 
 #if SCRIPTING_ENABLED
 		JsonNode scriptsState;
@@ -295,8 +307,6 @@ public:
 #endif
 	}
 
-	void sendMessageToAll(const std::string &message);
-	void sendMessageTo(std::shared_ptr<CConnection> c, const std::string &message);
 	void sendToAllClients(CPackForClient * pack);
 	void sendAndApply(CPackForClient * pack) override;
 	void applyAndSend(CPackForClient * pack);
@@ -346,7 +356,11 @@ public:
 	void attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender);
 	bool sacrificeArtifact(const IMarket * m, const CGHeroInstance * hero, const std::vector<ArtifactPosition> & slot);
 	void spawnWanderingMonsters(CreatureID creatureID);
-	void handleCheatCode(std::string & cheat, PlayerColor player, const CGHeroInstance * hero, const CGTownInstance * town, bool & cheated);
+
+	// Check for victory and loss conditions
+	void checkVictoryLossConditionsForPlayer(PlayerColor player);
+	void checkVictoryLossConditions(const std::set<PlayerColor> & playerColors);
+	void checkVictoryLossConditionsForAll();
 
 	CRandomGenerator & getRandomGenerator();
 
@@ -355,6 +369,8 @@ public:
 	scripting::Pool * getContextPool() const override;
 #endif
 
+	std::list<PlayerColor> generatePlayerTurnOrder() const;
+
 	friend class CVCMIServer;
 private:
 	std::unique_ptr<events::EventBus> serverEventBus;
@@ -363,16 +379,12 @@ private:
 #endif
 
 	void reinitScripting();
+	void deserializationFix();
+
 
-	std::list<PlayerColor> generatePlayerTurnOrder() const;
 	void makeStackDoNothing(const CStack * next);
 	void getVictoryLossMessage(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult, InfoWindow & out) const;
 
-	// Check for victory and loss conditions
-	void checkVictoryLossConditionsForPlayer(PlayerColor player);
-	void checkVictoryLossConditions(const std::set<PlayerColor> & playerColors);
-	void checkVictoryLossConditionsForAll();
-
 	const std::string complainNoCreatures;
 	const std::string complainNotEnoughCreatures;
 	const std::string complainInvalidSlot;

+ 4 - 0
server/CMakeLists.txt

@@ -2,6 +2,8 @@ set(server_SRCS
 		StdInc.cpp
 
 		CGameHandler.cpp
+		HeroPoolProcessor.cpp
+		PlayerMessageProcessor.cpp
 		ServerSpellCastEnvironment.cpp
 		CQuery.cpp
 		CVCMIServer.cpp
@@ -13,6 +15,8 @@ set(server_HEADERS
 		StdInc.h
 
 		CGameHandler.h
+		HeroPoolProcessor.h
+		PlayerMessageProcessor.h
 		ServerSpellCastEnvironment.h
 		CQuery.h
 		CVCMIServer.h

+ 5 - 4
server/CVCMIServer.cpp

@@ -39,6 +39,7 @@
 #include "../lib/VCMI_Lib.h"
 #include "../lib/VCMIDirs.h"
 #include "CGameHandler.h"
+#include "PlayerMessageProcessor.h"
 #include "../lib/mapping/CMapInfo.h"
 #include "../lib/GameConstants.h"
 #include "../lib/logging/CBasicLogConfigurator.h"
@@ -605,7 +606,7 @@ void CVCMIServer::clientDisconnected(std::shared_ptr<CConnection> c)
 		
 		if(gh && si && state == EServerState::GAMEPLAY)
 		{
-			gh->playerMessage(playerSettings->color, playerLeftMsgText, ObjectInstanceID{});
+			gh->playerMessages->broadcastMessage(playerSettings->color, playerLeftMsgText);
 			gh->connections[playerSettings->color].insert(hostClient);
 			startAiPack.players.push_back(playerSettings->color);
 		}
@@ -633,7 +634,7 @@ void CVCMIServer::reconnectPlayer(int connId)
 				continue;
 			
 			std::string messageText = boost::str(boost::format("%s (cid %d) is connected") % playerSettings->name % connId);
-			gh->playerMessage(playerSettings->color, messageText, ObjectInstanceID{});
+			gh->playerMessages->broadcastMessage(playerSettings->color, messageText);
 			
 			startAiPack.players.push_back(playerSettings->color);
 		}
@@ -822,7 +823,7 @@ void CVCMIServer::setPlayer(PlayerColor clickedColor)
 void CVCMIServer::optionNextCastle(PlayerColor player, int dir)
 {
 	PlayerSettings & s = si->playerInfos[player];
-	si16 & cur = s.castle;
+	FactionID & cur = s.castle;
 	auto & allowed = getPlayerInfo(player.getNum()).allowedFactions;
 	const bool allowRandomTown = getPlayerInfo(player.getNum()).isFactionRandom;
 
@@ -856,7 +857,7 @@ void CVCMIServer::optionNextCastle(PlayerColor player, int dir)
 		else
 		{
 			assert(dir >= -1 && dir <= 1); //othervice std::advance may go out of range
-			auto iter = allowed.find(FactionID(cur));
+			auto iter = allowed.find(cur);
 			std::advance(iter, dir);
 			cur = *iter;
 		}

+ 397 - 0
server/HeroPoolProcessor.cpp

@@ -0,0 +1,397 @@
+/*
+ * HeroPoolProcessor.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 "HeroPoolProcessor.h"
+
+#include "CGameHandler.h"
+
+#include "../lib/CHeroHandler.h"
+#include "../lib/CPlayerState.h"
+#include "../lib/GameSettings.h"
+#include "../lib/NetPacks.h"
+#include "../lib/StartInfo.h"
+#include "../lib/mapObjects/CGTownInstance.h"
+#include "../lib/gameState/CGameState.h"
+#include "../lib/gameState/TavernHeroesPool.h"
+#include "../lib/gameState/TavernSlot.h"
+
+HeroPoolProcessor::HeroPoolProcessor()
+	: gameHandler(nullptr)
+{
+}
+
+HeroPoolProcessor::HeroPoolProcessor(CGameHandler * gameHandler)
+	: gameHandler(gameHandler)
+{
+}
+
+bool HeroPoolProcessor::playerEndedTurn(const PlayerColor & player)
+{
+	// our player is acting right now and have not ended turn
+	if (player == gameHandler->gameState()->currentPlayer)
+		return false;
+
+	auto turnOrder = gameHandler->generatePlayerTurnOrder();
+
+	for (auto const & entry : turnOrder)
+	{
+		// our player is yet to start turn
+		if (entry == gameHandler->gameState()->currentPlayer)
+			return false;
+
+		// our player have finished turn
+		if (entry == player)
+			return true;
+	}
+
+	assert(false);
+	return false;
+}
+
+TavernHeroSlot HeroPoolProcessor::selectSlotForRole(const PlayerColor & player, TavernSlotRole roleID)
+{
+	const auto & heroesPool = gameHandler->gameState()->heroesPool;
+
+	const auto & heroes = heroesPool->getHeroesFor(player);
+
+	// if tavern has empty slot - use it
+	if (heroes.size() == 0)
+		return TavernHeroSlot::NATIVE;
+
+	if (heroes.size() == 1)
+		return TavernHeroSlot::RANDOM;
+
+	// try to find "better" slot to overwrite
+	// we want to avoid overwriting retreated heroes when tavern still has slot with random hero
+	// as well as avoid overwriting surrendered heroes if we can overwrite retreated hero
+	auto roleLeft = heroesPool->getSlotRole(HeroTypeID(heroes[0]->subID));
+	auto roleRight = heroesPool->getSlotRole(HeroTypeID(heroes[1]->subID));
+
+	if (roleLeft > roleRight)
+		return TavernHeroSlot::RANDOM;
+
+	if (roleLeft < roleRight)
+		return TavernHeroSlot::NATIVE;
+
+	// both slots are equal in "value", so select randomly
+	if (getRandomGenerator(player).nextInt(100) > 50)
+		return TavernHeroSlot::RANDOM;
+	else
+		return TavernHeroSlot::NATIVE;
+}
+
+void HeroPoolProcessor::onHeroSurrendered(const PlayerColor & color, const CGHeroInstance * hero)
+{
+	SetAvailableHero sah;
+	if (playerEndedTurn(color))
+		sah.roleID = TavernSlotRole::SURRENDERED_TODAY;
+	else
+		sah.roleID = TavernSlotRole::SURRENDERED;
+
+	sah.slotID = selectSlotForRole(color, sah.roleID);
+	sah.player = color;
+	sah.hid = hero->subID;
+	gameHandler->sendAndApply(&sah);
+}
+
+void HeroPoolProcessor::onHeroEscaped(const PlayerColor & color, const CGHeroInstance * hero)
+{
+	SetAvailableHero sah;
+	if (playerEndedTurn(color))
+		sah.roleID = TavernSlotRole::RETREATED_TODAY;
+	else
+		sah.roleID = TavernSlotRole::RETREATED;
+
+	sah.slotID = selectSlotForRole(color, sah.roleID);
+	sah.player = color;
+	sah.hid = hero->subID;
+	sah.army.clear();
+	sah.army.setCreature(SlotID(0), hero->type->initialArmy.at(0).creature, 1);
+
+	gameHandler->sendAndApply(&sah);
+}
+
+void HeroPoolProcessor::clearHeroFromSlot(const PlayerColor & color, TavernHeroSlot slot)
+{
+	SetAvailableHero sah;
+	sah.player = color;
+	sah.roleID = TavernSlotRole::NONE;
+	sah.slotID = slot;
+	sah.hid = HeroTypeID::NONE;
+	gameHandler->sendAndApply(&sah);
+}
+
+void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHeroSlot slot, bool needNativeHero, bool giveArmy)
+{
+	SetAvailableHero sah;
+	sah.player = color;
+	sah.slotID = slot;
+
+	CGHeroInstance *newHero = pickHeroFor(needNativeHero, color);
+
+	if (newHero)
+	{
+		sah.hid = newHero->subID;
+
+		if (giveArmy)
+		{
+			sah.roleID = TavernSlotRole::FULL_ARMY;
+			newHero->initArmy(getRandomGenerator(color), &sah.army);
+		}
+		else
+		{
+			sah.roleID = TavernSlotRole::SINGLE_UNIT;
+			sah.army.clear();
+			sah.army.setCreature(SlotID(0), newHero->type->initialArmy[0].creature, 1);
+		}
+	}
+	else
+	{
+		sah.hid = -1;
+	}
+	gameHandler->sendAndApply(&sah);
+}
+
+void HeroPoolProcessor::onNewWeek(const PlayerColor & color)
+{
+	const auto & heroesPool = gameHandler->gameState()->heroesPool;
+	const auto & heroes = heroesPool->getHeroesFor(color);
+
+	const auto nativeSlotRole = heroes.size() < 1 ? TavernSlotRole::NONE : heroesPool->getSlotRole(heroes[0]->type->getId());
+	const auto randomSlotRole = heroes.size() < 2 ? TavernSlotRole::NONE : heroesPool->getSlotRole(heroes[1]->type->getId());
+
+	bool resetNativeSlot = nativeSlotRole != TavernSlotRole::RETREATED_TODAY && nativeSlotRole != TavernSlotRole::SURRENDERED_TODAY;
+	bool resetRandomSlot = randomSlotRole != TavernSlotRole::RETREATED_TODAY && randomSlotRole != TavernSlotRole::SURRENDERED_TODAY;
+
+	if (resetNativeSlot)
+		clearHeroFromSlot(color, TavernHeroSlot::NATIVE);
+
+	if (resetRandomSlot)
+		clearHeroFromSlot(color, TavernHeroSlot::RANDOM);
+
+	if (resetNativeSlot)
+		selectNewHeroForSlot(color, TavernHeroSlot::NATIVE, true, true);
+
+	if (resetRandomSlot)
+		selectNewHeroForSlot(color, TavernHeroSlot::RANDOM, false, true);
+}
+
+bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTypeID & heroToRecruit, const PlayerColor & player)
+{
+	const PlayerState * playerState = gameHandler->getPlayerState(player);
+	const CGObjectInstance * mapObject = gameHandler->getObj(objectID);
+	const CGTownInstance * town = gameHandler->getTown(objectID);
+
+	if (!mapObject && gameHandler->complain("Invalid map object!"))
+		return false;
+
+	if (!playerState && gameHandler->complain("Invalid player!"))
+		return false;
+
+	if (playerState->resources[EGameResID::GOLD] < GameConstants::HERO_GOLD_COST && gameHandler->complain("Not enough gold for buying hero!"))
+		return false;
+
+	if (gameHandler->getHeroCount(player, false) >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP) && gameHandler->complain("Cannot hire hero, too many wandering heroes already!"))
+		return false;
+
+	if (gameHandler->getHeroCount(player, true) >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP) && gameHandler->complain("Cannot hire hero, too many heroes garrizoned and wandering already!"))
+		return false;
+
+	if(town) //tavern in town
+	{
+		if(gameHandler->getPlayerRelations(mapObject->tempOwner, player) == PlayerRelations::ENEMIES && gameHandler->complain("Can't buy hero in enemy town!"))
+			return false;
+
+		if(!town->hasBuilt(BuildingID::TAVERN) && gameHandler->complain("No tavern!"))
+			return false;
+
+		if(town->visitingHero && gameHandler->complain("There is visiting hero - no place!"))
+			return false;
+	}
+
+	if(mapObject->ID == Obj::TAVERN)
+	{
+		if(gameHandler->getTile(mapObject->visitablePos())->visitableObjects.back() != mapObject && gameHandler->complain("Tavern entry must be unoccupied!"))
+			return false;
+	}
+
+	auto recruitableHeroes = gameHandler->gameState()->heroesPool->getHeroesFor(player);
+
+	const CGHeroInstance * recruitedHero = nullptr;
+
+	for(const auto & hero : recruitableHeroes)
+	{
+		if(hero->subID == heroToRecruit)
+			recruitedHero = hero;
+	}
+
+	if(!recruitedHero)
+	{
+		gameHandler->complain("Hero is not available for hiring!");
+		return false;
+	}
+
+	HeroRecruited hr;
+	hr.tid = mapObject->id;
+	hr.hid = recruitedHero->subID;
+	hr.player = player;
+	hr.tile = recruitedHero->convertFromVisitablePos(mapObject->visitablePos());
+	if(gameHandler->getTile(hr.tile)->isWater())
+	{
+		//Create a new boat for hero
+		gameHandler->createObject(mapObject->visitablePos(), Obj::BOAT, recruitedHero->getBoatType().getNum());
+
+		hr.boatId = gameHandler->getTopObj(hr.tile)->id;
+	}
+
+	// apply netpack -> this will remove hired hero from pool
+	gameHandler->sendAndApply(&hr);
+
+	if(recruitableHeroes[0] == recruitedHero)
+		selectNewHeroForSlot(player, TavernHeroSlot::NATIVE, false, false);
+	else
+		selectNewHeroForSlot(player, TavernHeroSlot::RANDOM, false, false);
+
+	gameHandler->giveResource(player, EGameResID::GOLD, -GameConstants::HERO_GOLD_COST);
+
+	if(town)
+	{
+		gameHandler->visitCastleObjects(town, recruitedHero);
+		gameHandler->giveSpells(town, recruitedHero);
+	}
+	return true;
+}
+
+std::vector<const CHeroClass *> HeroPoolProcessor::findAvailableClassesFor(const PlayerColor & player) const
+{
+	std::vector<const CHeroClass *> result;
+
+	const auto & heroesPool = gameHandler->gameState()->heroesPool;
+	FactionID factionID = gameHandler->getPlayerSettings(player)->castle;
+
+	for(auto & elem : heroesPool->unusedHeroesFromPool())
+	{
+		if (vstd::contains(result, elem.second->type->heroClass))
+			continue;
+
+		bool heroAvailable = heroesPool->isHeroAvailableFor(elem.first, player);
+		bool heroClassBanned = elem.second->type->heroClass->selectionProbability[factionID] == 0;
+
+		if(heroAvailable && !heroClassBanned)
+			result.push_back(elem.second->type->heroClass);
+	}
+
+	return result;
+}
+
+std::vector<CGHeroInstance *> HeroPoolProcessor::findAvailableHeroesFor(const PlayerColor & player, const CHeroClass * heroClass) const
+{
+	std::vector<CGHeroInstance *> result;
+
+	const auto & heroesPool = gameHandler->gameState()->heroesPool;
+
+	for(auto & elem : heroesPool->unusedHeroesFromPool())
+	{
+		assert(!vstd::contains(result, elem.second));
+
+		bool heroAvailable = heroesPool->isHeroAvailableFor(elem.first, player);
+		bool heroClassMatches = elem.second->type->heroClass == heroClass;
+
+		if(heroAvailable && heroClassMatches)
+			result.push_back(elem.second);
+	}
+
+	return result;
+}
+
+const CHeroClass * HeroPoolProcessor::pickClassFor(bool isNative, const PlayerColor & player)
+{
+	if(player >= PlayerColor::PLAYER_LIMIT)
+	{
+		logGlobal->error("Cannot pick hero for player %d. Wrong owner!", player.getStr());
+		return nullptr;
+	}
+
+	FactionID factionID = gameHandler->getPlayerSettings(player)->castle;
+	const auto & heroesPool = gameHandler->gameState()->heroesPool;
+	const auto & currentTavern = heroesPool->getHeroesFor(player);
+
+	std::vector<const CHeroClass *> potentialClasses = findAvailableClassesFor(player);
+	std::vector<const CHeroClass *> possibleClasses;
+
+	if(potentialClasses.empty())
+	{
+		logGlobal->error("There are no heroes available for player %s!", player.getStr());
+		return nullptr;
+	}
+
+	for(const auto & heroClass : potentialClasses)
+	{
+		if (isNative && heroClass->faction != factionID)
+			continue;
+
+		bool hasSameClass = vstd::contains_if(currentTavern, [&](const CGHeroInstance * hero){
+			return hero->type->heroClass == heroClass;
+		});
+
+		if (hasSameClass)
+			continue;
+
+		possibleClasses.push_back(heroClass);
+	}
+
+	if (possibleClasses.empty())
+	{
+		logGlobal->error("Cannot pick native hero for %s. Picking any...", player.getStr());
+		possibleClasses = potentialClasses;
+	}
+
+	int totalWeight = 0;
+	for(const auto & heroClass : possibleClasses)
+		totalWeight += heroClass->selectionProbability.at(factionID);
+
+	int roll = getRandomGenerator(player).nextInt(totalWeight - 1);
+
+	for(const auto & heroClass : possibleClasses)
+	{
+		roll -= heroClass->selectionProbability.at(factionID);
+		if(roll < 0)
+			return heroClass;
+	}
+
+	return *possibleClasses.rbegin();
+}
+
+CGHeroInstance * HeroPoolProcessor::pickHeroFor(bool isNative, const PlayerColor & player)
+{
+	const CHeroClass * heroClass = pickClassFor(isNative, player);
+
+	if(!heroClass)
+		return nullptr;
+
+	std::vector<CGHeroInstance *> possibleHeroes = findAvailableHeroesFor(player, heroClass);
+
+	assert(!possibleHeroes.empty());
+	if(possibleHeroes.empty())
+		return nullptr;
+
+	return *RandomGeneratorUtil::nextItem(possibleHeroes, getRandomGenerator(player));
+}
+
+CRandomGenerator & HeroPoolProcessor::getRandomGenerator(const PlayerColor & player)
+{
+	if (playerSeed.count(player) == 0)
+	{
+		int seed = gameHandler->getRandomGenerator().nextInt();
+		playerSeed.emplace(player, std::make_unique<CRandomGenerator>(seed));
+	}
+
+	return *playerSeed.at(player);
+}

+ 66 - 0
server/HeroPoolProcessor.h

@@ -0,0 +1,66 @@
+/*
+ * HeroPoolProcessor.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+enum class TavernHeroSlot : int8_t;
+enum class TavernSlotRole : int8_t;
+class PlayerColor;
+class CGHeroInstance;
+class HeroTypeID;
+class ObjectInstanceID;
+class CRandomGenerator;
+class CHeroClass;
+
+VCMI_LIB_NAMESPACE_END
+
+class CGameHandler;
+
+class HeroPoolProcessor : boost::noncopyable
+{
+	/// per-player random generators
+	std::map<PlayerColor, std::unique_ptr<CRandomGenerator>> playerSeed;
+
+	void clearHeroFromSlot(const PlayerColor & color, TavernHeroSlot slot);
+	void selectNewHeroForSlot(const PlayerColor & color, TavernHeroSlot slot, bool needNativeHero, bool giveStartingArmy);
+
+	std::vector<const CHeroClass *> findAvailableClassesFor(const PlayerColor & player) const;
+	std::vector<CGHeroInstance *> findAvailableHeroesFor(const PlayerColor & player, const CHeroClass * heroClass) const;
+
+	const CHeroClass * pickClassFor(bool isNative, const PlayerColor & player);
+
+	CGHeroInstance * pickHeroFor(bool isNative, const PlayerColor & player);
+
+	CRandomGenerator & getRandomGenerator(const PlayerColor & player);
+
+	TavernHeroSlot selectSlotForRole(const PlayerColor & player, TavernSlotRole roleID);
+
+	bool playerEndedTurn(const PlayerColor & player);
+public:
+	CGameHandler * gameHandler;
+
+	HeroPoolProcessor();
+	HeroPoolProcessor(CGameHandler * gameHandler);
+
+	void onHeroSurrendered(const PlayerColor & color, const CGHeroInstance * hero);
+	void onHeroEscaped(const PlayerColor & color, const CGHeroInstance * hero);
+
+	void onNewWeek(const PlayerColor & color);
+
+	/// Incoming net pack handling
+	bool hireHero(const ObjectInstanceID & objectID, const HeroTypeID & hid, const PlayerColor & player);
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+		// h & gameHandler; // FIXME: make this work instead of using deserializationFix in gameHandler
+		h & playerSeed;
+	}
+};

+ 7 - 6
server/NetPacksServer.cpp

@@ -11,6 +11,9 @@
 #include "ServerNetPackVisitors.h"
 
 #include "CGameHandler.h"
+#include "HeroPoolProcessor.h"
+#include "PlayerMessageProcessor.h"
+
 #include "../lib/IGameCallback.h"
 #include "../lib/mapObjects/CGTownInstance.h"
 #include "../lib/gameState/CGameState.h"
@@ -246,12 +249,10 @@ void ApplyGhNetPackVisitor::visitSetFormation(SetFormation & pack)
 
 void ApplyGhNetPackVisitor::visitHireHero(HireHero & pack)
 {
-	const CGObjectInstance * obj = gh.getObj(pack.tid);
-	const CGTownInstance * town = dynamic_ptr_cast<CGTownInstance>(obj);
-	if(town && PlayerRelations::ENEMIES == gh.getPlayerRelations(obj->tempOwner, gh.getPlayerAt(pack.c)))
-		gh.throwAndComplain(&pack, "Can't buy hero in enemy town!");
+	if (!gh.hasPlayerAt(pack.player, pack.c))
+		gh.throwAndComplain(&pack, "No such pack.player!");
 
-	result = gh.hireHero(obj, pack.hid, pack.player);
+	result = gh.heroPool->hireHero(pack.tid, pack.hid, pack.player);
 }
 
 void ApplyGhNetPackVisitor::visitBuildBoat(BuildBoat & pack)
@@ -352,6 +353,6 @@ void ApplyGhNetPackVisitor::visitPlayerMessage(PlayerMessage & pack)
 	if(!pack.player.isSpectator()) // TODO: clearly not a great way to verify permissions
 		gh.throwOnWrongPlayer(&pack, pack.player);
 	
-	gh.playerMessage(pack.player, pack.text, pack.currObj);
+	gh.playerMessages->playerMessage(pack.player, pack.text, pack.currObj);
 	result = true;
 }

+ 523 - 0
server/PlayerMessageProcessor.cpp

@@ -0,0 +1,523 @@
+/*
+ * CGameHandler.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 "PlayerMessageProcessor.h"
+
+#include "CGameHandler.h"
+#include "CVCMIServer.h"
+
+#include "../lib/serializer/Connection.h"
+#include "../lib/CGeneralTextHandler.h"
+#include "../lib/CHeroHandler.h"
+#include "../lib/CModHandler.h"
+#include "../lib/CPlayerState.h"
+#include "../lib/GameConstants.h"
+#include "../lib/NetPacks.h"
+#include "../lib/StartInfo.h"
+#include "../lib/gameState/CGameState.h"
+#include "../lib/mapObjects/CGTownInstance.h"
+
+PlayerMessageProcessor::PlayerMessageProcessor()
+	:gameHandler(nullptr)
+{
+}
+
+PlayerMessageProcessor::PlayerMessageProcessor(CGameHandler * gameHandler)
+	:gameHandler(gameHandler)
+{
+}
+
+void PlayerMessageProcessor::playerMessage(PlayerColor player, const std::string &message, ObjectInstanceID currObj)
+{
+	if (handleHostCommand(player, message))
+		return;
+
+	if (handleCheatCode(message, player, currObj))
+	{
+		if(!gameHandler->getPlayerSettings(player)->isControlledByAI())
+			broadcastSystemMessage(VLC->generaltexth->allTexts[260]);
+
+		if(!player.isSpectator())
+			gameHandler->checkVictoryLossConditionsForPlayer(player);//Player enter win code or got required art\creature
+
+		return;
+	}
+
+	broadcastMessage(player, message);
+}
+
+bool PlayerMessageProcessor::handleHostCommand(PlayerColor player, const std::string &message)
+{
+	std::vector<std::string> words;
+	boost::split(words, message, boost::is_any_of(" "));
+
+	bool isHost = false;
+	for(auto & c : gameHandler->connections[player])
+		if(gameHandler->gameLobby()->isClientHost(c->connectionID))
+			isHost = true;
+
+	if(!isHost || words.size() < 2 || words[0] != "game")
+		return false;
+
+	if(words[1] == "exit" || words[1] == "quit" || words[1] == "end")
+	{
+		broadcastSystemMessage("game was terminated");
+		gameHandler->gameLobby()->state = EServerState::SHUTDOWN;
+
+		return true;
+	}
+	if(words.size() == 3 && words[1] == "save")
+	{
+		gameHandler->save("Saves/" + words[2]);
+		broadcastSystemMessage("game saved as " + words[2]);
+
+		return true;
+	}
+	if(words.size() == 3 && words[1] == "kick")
+	{
+		auto playername = words[2];
+		PlayerColor playerToKick(PlayerColor::CANNOT_DETERMINE);
+		if(std::all_of(playername.begin(), playername.end(), ::isdigit))
+			playerToKick = PlayerColor(std::stoi(playername));
+		else
+		{
+			for(auto & c : gameHandler->connections)
+			{
+				if(c.first.getStr(false) == playername)
+					playerToKick = c.first;
+			}
+		}
+
+		if(playerToKick != PlayerColor::CANNOT_DETERMINE)
+		{
+			PlayerCheated pc;
+			pc.player = playerToKick;
+			pc.losingCheatCode = true;
+			gameHandler->sendAndApply(&pc);
+			gameHandler->checkVictoryLossConditionsForPlayer(playerToKick);
+		}
+		return true;
+	}
+	if(words.size() == 2 && words[1] == "cheaters")
+	{
+		if (cheaters.empty())
+			broadcastSystemMessage("No cheaters registered!");
+
+		for (auto const & entry : cheaters)
+			broadcastSystemMessage("Player " + entry.getStr() + " is cheater!");
+
+		return true;
+	}
+
+	return false;
+}
+
+void PlayerMessageProcessor::cheatGiveSpells(PlayerColor player, const CGHeroInstance * hero)
+{
+	if (!hero)
+		return;
+
+	///Give hero spellbook
+	if (!hero->hasSpellbook())
+		gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK);
+
+	///Give all spells with bonus (to allow banned spells)
+	GiveBonus giveBonus(GiveBonus::ETarget::HERO);
+	giveBonus.id = hero->id.getNum();
+	giveBonus.bonus = Bonus(BonusDuration::PERMANENT, BonusType::SPELLS_OF_LEVEL, BonusSource::OTHER, 0, 0);
+	//start with level 0 to skip abilities
+	for (int level = 1; level <= GameConstants::SPELL_LEVELS; level++)
+	{
+		giveBonus.bonus.subtype = level;
+		gameHandler->sendAndApply(&giveBonus);
+	}
+
+	///Give mana
+	SetMana sm;
+	sm.hid = hero->id;
+	sm.val = 999;
+	sm.absolute = true;
+	gameHandler->sendAndApply(&sm);
+}
+
+void PlayerMessageProcessor::cheatBuildTown(PlayerColor player, const CGTownInstance * town)
+{
+	if (!town)
+		return;
+
+	for (auto & build : town->town->buildings)
+	{
+		if (!town->hasBuilt(build.first)
+			&& !build.second->getNameTranslated().empty()
+			&& build.first != BuildingID::SHIP)
+		{
+			gameHandler->buildStructure(town->id, build.first, true);
+		}
+	}
+}
+
+void PlayerMessageProcessor::cheatGiveArmy(PlayerColor player, const CGHeroInstance * hero, std::vector<std::string> words)
+{
+	if (!hero)
+		return;
+
+	std::string creatureIdentifier = words.empty() ? "archangel" : words[0];
+	std::optional<int> amountPerSlot;
+
+	try
+	{
+		amountPerSlot = std::stol(words.at(1));
+	}
+	catch(std::exception&)
+	{
+	}
+
+	std::optional<int32_t> creatureId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "creature", creatureIdentifier, false);
+
+	if(creatureId.has_value())
+	{
+		const auto * creature = CreatureID(creatureId.value()).toCreature();
+
+		for (int i = 0; i < GameConstants::ARMY_SIZE; i++)
+		{
+			if (!hero->hasStackAtSlot(SlotID(i)))
+			{
+				if (amountPerSlot.has_value())
+					gameHandler->insertNewStack(StackLocation(hero, SlotID(i)), creature, *amountPerSlot);
+				else
+					gameHandler->insertNewStack(StackLocation(hero, SlotID(i)), creature, 5 * std::pow(10, i));
+			}
+		}
+	}
+}
+
+void PlayerMessageProcessor::cheatGiveMachines(PlayerColor player, const CGHeroInstance * hero)
+{
+	if (!hero)
+		return;
+
+	if (!hero->getArt(ArtifactPosition::MACH1))
+		gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::BALLISTA], ArtifactPosition::MACH1);
+	if (!hero->getArt(ArtifactPosition::MACH2))
+		gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::AMMO_CART], ArtifactPosition::MACH2);
+	if (!hero->getArt(ArtifactPosition::MACH3))
+		gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::FIRST_AID_TENT], ArtifactPosition::MACH3);
+}
+
+void PlayerMessageProcessor::cheatGiveArtifacts(PlayerColor player, const CGHeroInstance * hero)
+{
+	if (!hero)
+		return;
+
+	for(int g = 7; g < VLC->arth->objects.size(); ++g) //including artifacts from mods
+	{
+		if(VLC->arth->objects[g]->canBePutAt(hero))
+			gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[g], ArtifactPosition::FIRST_AVAILABLE);
+	}
+}
+
+void PlayerMessageProcessor::cheatLevelup(PlayerColor player, const CGHeroInstance * hero, std::vector<std::string> words)
+{
+	if (!hero)
+		return;
+
+	int levelsToGain;
+	try
+	{
+		levelsToGain = std::stol(words.at(0));
+	}
+	catch(std::exception&)
+	{
+		levelsToGain = 1;
+	}
+
+	gameHandler->changePrimSkill(hero, PrimarySkill::EXPERIENCE, VLC->heroh->reqExp(hero->level + levelsToGain) - VLC->heroh->reqExp(hero->level));
+}
+
+void PlayerMessageProcessor::cheatExperience(PlayerColor player, const CGHeroInstance * hero, std::vector<std::string> words)
+{
+	if (!hero)
+		return;
+
+	int expAmountProcessed;
+
+	try
+	{
+		expAmountProcessed = std::stol(words.at(0));
+	}
+	catch(std::exception&)
+	{
+		expAmountProcessed = 10000;
+	}
+
+	gameHandler->changePrimSkill(hero, PrimarySkill::EXPERIENCE, expAmountProcessed);
+}
+
+void PlayerMessageProcessor::cheatMovement(PlayerColor player, const CGHeroInstance * hero, std::vector<std::string> words)
+{
+	if (!hero)
+		return;
+
+	SetMovePoints smp;
+	smp.hid = hero->id;
+	try
+	{
+		smp.val = std::stol(words.at(0));;
+	}
+	catch(std::exception&)
+	{
+		smp.val = 1000000;
+	}
+
+	gameHandler->sendAndApply(&smp);
+
+	GiveBonus gb(GiveBonus::ETarget::HERO);
+	gb.bonus.type = BonusType::FREE_SHIP_BOARDING;
+	gb.bonus.duration = BonusDuration::ONE_DAY;
+	gb.bonus.source = BonusSource::OTHER;
+	gb.id = hero->id.getNum();
+	gameHandler->giveHeroBonus(&gb);
+}
+
+void PlayerMessageProcessor::cheatResources(PlayerColor player, std::vector<std::string> words)
+{
+	int baseResourceAmount;
+	try
+	{
+		baseResourceAmount = std::stol(words.at(0));;
+	}
+	catch(std::exception&)
+	{
+		baseResourceAmount = 100;
+	}
+
+	TResources resources;
+	resources[EGameResID::GOLD] = baseResourceAmount * 100;
+	for (GameResID i = EGameResID::WOOD; i < EGameResID::GOLD; ++i)
+		resources[i] = baseResourceAmount;
+
+	gameHandler->giveResources(player, resources);
+}
+
+void PlayerMessageProcessor::cheatVictory(PlayerColor player)
+{
+	PlayerCheated pc;
+	pc.player = player;
+	pc.winningCheatCode = true;
+	gameHandler->sendAndApply(&pc);
+}
+
+void PlayerMessageProcessor::cheatDefeat(PlayerColor player)
+{
+	PlayerCheated pc;
+	pc.player = player;
+	pc.losingCheatCode = true;
+	gameHandler->sendAndApply(&pc);
+}
+
+void PlayerMessageProcessor::cheatMapReveal(PlayerColor player, bool reveal)
+{
+	FoWChange fc;
+	fc.mode = reveal;
+	fc.player = player;
+	const auto & fowMap = gameHandler->gameState()->getPlayerTeam(player)->fogOfWarMap;
+	const auto & mapSize = gameHandler->gameState()->getMapSize();
+	auto hlp_tab = new int3[mapSize.x * mapSize.y * mapSize.z];
+	int lastUnc = 0;
+
+	for(int z = 0; z < mapSize.z; z++)
+		for(int x = 0; x < mapSize.x; x++)
+			for(int y = 0; y < mapSize.y; y++)
+				if(!(*fowMap)[z][x][y] || !fc.mode)
+					hlp_tab[lastUnc++] = int3(x, y, z);
+
+	fc.tiles.insert(hlp_tab, hlp_tab + lastUnc);
+	delete [] hlp_tab;
+	gameHandler->sendAndApply(&fc);
+}
+
+bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerColor player, ObjectInstanceID currObj)
+{
+	std::vector<std::string> words;
+	boost::split(words, cheat, boost::is_any_of("\t\r\n "));
+
+	if (words.empty())
+		return false;
+
+	//Make cheat name case-insensitive, but keep words/parameters (e.g. creature name) as it
+	std::string cheatName = boost::to_lower_copy(words[0]);
+	words.erase(words.begin());
+
+	std::vector<std::string> townTargetedCheats = { "vcmiarmenelos", "vcmibuild", "nwczion" };
+	std::vector<std::string> playerTargetedCheats = {
+		"vcmiformenos",  "vcmiresources", "nwctheconstruct",
+		"vcmimelkor",    "vcmilose",      "nwcbluepill",
+		"vcmisilmaril",  "vcmiwin",       "nwcredpill",
+		"vcmieagles",    "vcmimap",       "nwcwhatisthematrix",
+		"vcmiungoliant", "vcmihidemap",   "nwcignoranceisbliss"
+	};
+	std::vector<std::string> heroTargetedCheats = {
+		"vcmiainur",             "vcmiarchangel",   "nwctrinity",
+		"vcmiangband",           "vcmiblackknight", "nwcagents",
+		"vcmiglaurung",          "vcmicrystal",     "vcmiazure",
+		"vcmifaerie",            "vcmiarmy",        "vcminissi",
+		"vcmiistari",            "vcmispells",      "nwcthereisnospoon",
+		"vcminoldor",            "vcmimachines",     "nwclotsofguns",
+		"vcmiglorfindel",        "vcmilevel",       "nwcneo",
+		"vcminahar",             "vcmimove",        "nwcnebuchadnezzar",
+		"vcmiforgeofnoldorking", "vcmiartifacts",
+		"vcmiolorin",            "vcmiexp",
+	};
+
+	if (!vstd::contains(townTargetedCheats, cheatName) && !vstd::contains(playerTargetedCheats, cheatName) && !vstd::contains(heroTargetedCheats, cheatName))
+		return false;
+
+	bool playerTargetedCheat = false;
+
+	for (const auto & i : gameHandler->gameState()->players)
+	{
+		if (words.empty())
+			break;
+
+		if (i.first == PlayerColor::NEUTRAL)
+			continue;
+
+		if (words.front() == "ai" && i.second.human)
+			continue;
+
+		if (words.front() != "all" && words.front() != i.first.getStr())
+			continue;
+
+		std::vector<std::string> parameters = words;
+
+		cheaters.insert(i.first);
+		playerTargetedCheat = true;
+		parameters.erase(parameters.begin());
+
+		if (vstd::contains(playerTargetedCheats, cheatName))
+			executeCheatCode(cheatName, i.first, ObjectInstanceID::NONE, parameters);
+
+		if (vstd::contains(townTargetedCheats, cheatName))
+			for (const auto & t : i.second.towns)
+				executeCheatCode(cheatName, i.first, t->id, parameters);
+
+		if (vstd::contains(heroTargetedCheats, cheatName))
+			for (const auto & h : i.second.heroes)
+				executeCheatCode(cheatName, i.first, h->id, parameters);
+	}
+
+	if (!playerTargetedCheat)
+		executeCheatCode(cheatName, player, currObj, words);
+
+	cheaters.insert(player);
+	return true;
+}
+
+void PlayerMessageProcessor::executeCheatCode(const std::string & cheatName, PlayerColor player, ObjectInstanceID currObj, const std::vector<std::string> & words)
+{
+	const CGHeroInstance * hero = gameHandler->getHero(currObj);
+	const CGTownInstance * town = gameHandler->getTown(currObj);
+	if (!town && hero)
+		town = hero->visitedTown;
+
+	const auto & doCheatGiveSpells = [&]() { cheatGiveSpells(player, hero); };
+	const auto & doCheatBuildTown = [&]() { cheatBuildTown(player, town); };
+	const auto & doCheatGiveArmyCustom = [&]() { cheatGiveArmy(player, hero, words); };
+	const auto & doCheatGiveArmyFixed = [&](std::vector<std::string> customWords) { cheatGiveArmy(player, hero, customWords); };
+	const auto & doCheatGiveMachines = [&]() { cheatGiveMachines(player, hero); };
+	const auto & doCheatGiveArtifacts = [&]() { cheatGiveArtifacts(player, hero); };
+	const auto & doCheatLevelup = [&]() { cheatLevelup(player, hero, words); };
+	const auto & doCheatExperience = [&]() { cheatExperience(player, hero, words); };
+	const auto & doCheatMovement = [&]() { cheatMovement(player, hero, words); };
+	const auto & doCheatResources = [&]() { cheatResources(player, words); };
+	const auto & doCheatVictory = [&]() { cheatVictory(player); };
+	const auto & doCheatDefeat = [&]() { cheatDefeat(player); };
+	const auto & doCheatMapReveal = [&]() { cheatMapReveal(player, true); };
+	const auto & doCheatMapHide = [&]() { cheatMapReveal(player, false); };
+
+	// Unimplemented H3 cheats:
+	// nwcfollowthewhiterabbit - The currently selected hero permanently gains maximum luck.
+	// nwcmorpheus - The currently selected hero permanently gains maximum morale.
+	// nwcoracle - The puzzle map is permanently revealed.
+	// nwcphisherprice - Changes and brightens the game colors.
+
+	std::map<std::string, std::function<void()>> callbacks = {
+		{"vcmiainur",            [&] () {doCheatGiveArmyFixed({ "archangel", "5" });} },
+		{"nwctrinity",           [&] () {doCheatGiveArmyFixed({ "archangel", "5" });} },
+		{"vcmiangband",          [&] () {doCheatGiveArmyFixed({ "blackKnight", "10" });} },
+		{"vcmiglaurung",         [&] () {doCheatGiveArmyFixed({ "crystalDragon", "5000" });} },
+		{"vcmiarchangel",        [&] () {doCheatGiveArmyFixed({ "archangel", "5" });} },
+		{"nwcagents",            [&] () {doCheatGiveArmyFixed({ "blackKnight", "10" });} },
+		{"vcmiblackknight",      [&] () {doCheatGiveArmyFixed({ "blackKnight", "10" });} },
+		{"vcmicrystal",          [&] () {doCheatGiveArmyFixed({ "crystalDragon", "5000" });} },
+		{"vcmiazure",            [&] () {doCheatGiveArmyFixed({ "azureDragon", "5000" });} },
+		{"vcmifaerie",           [&] () {doCheatGiveArmyFixed({ "fairieDragon", "5000" });} },
+		{"vcmiarmy",              doCheatGiveArmyCustom },
+		{"vcminissi",             doCheatGiveArmyCustom },
+		{"vcmiistari",            doCheatGiveSpells     },
+		{"vcmispells",            doCheatGiveSpells     },
+		{"nwcthereisnospoon",     doCheatGiveSpells     },
+		{"vcmiarmenelos",         doCheatBuildTown      },
+		{"vcmibuild",             doCheatBuildTown      },
+		{"nwczion",               doCheatBuildTown      },
+		{"vcminoldor",            doCheatGiveMachines   },
+		{"vcmimachines",          doCheatGiveMachines   },
+		{"nwclotsofguns",         doCheatGiveMachines   },
+		{"vcmiforgeofnoldorking", doCheatGiveArtifacts  },
+		{"vcmiartifacts",         doCheatGiveArtifacts  },
+		{"vcmiglorfindel",        doCheatLevelup        },
+		{"vcmilevel",             doCheatLevelup        },
+		{"nwcneo",                doCheatLevelup        },
+		{"vcmiolorin",            doCheatExperience     },
+		{"vcmiexp",               doCheatExperience     },
+		{"vcminahar",             doCheatMovement       },
+		{"vcmimove",              doCheatMovement       },
+		{"nwcnebuchadnezzar",     doCheatMovement       },
+		{"vcmiformenos",          doCheatResources      },
+		{"vcmiresources",         doCheatResources      },
+		{"nwctheconstruct",       doCheatResources      },
+		{"nwcbluepill",           doCheatDefeat         },
+		{"vcmimelkor",            doCheatDefeat         },
+		{"vcmilose",              doCheatDefeat         },
+		{"nwcredpill",            doCheatVictory        },
+		{"vcmisilmaril",          doCheatVictory        },
+		{"vcmiwin",               doCheatVictory        },
+		{"nwcwhatisthematrix",    doCheatMapReveal      },
+		{"vcmieagles",            doCheatMapReveal      },
+		{"vcmimap",               doCheatMapReveal      },
+		{"vcmiungoliant",         doCheatMapHide        },
+		{"vcmihidemap",           doCheatMapHide        },
+		{"nwcignoranceisbliss",   doCheatMapHide        },
+	};
+
+	assert(callbacks.count(cheatName));
+	if (callbacks.count(cheatName))
+		callbacks.at(cheatName)();
+}
+
+void PlayerMessageProcessor::sendSystemMessage(std::shared_ptr<CConnection> connection, const std::string & message)
+{
+	SystemMessage sm;
+	sm.text = message;
+	connection->sendPack(&sm);
+}
+
+void PlayerMessageProcessor::broadcastSystemMessage(const std::string & message)
+{
+	SystemMessage sm;
+	sm.text = message;
+	gameHandler->sendToAllClients(&sm);
+}
+
+void PlayerMessageProcessor::broadcastMessage(PlayerColor playerSender, const std::string & message)
+{
+	PlayerMessageClient temp_message(playerSender, message);
+	gameHandler->sendAndApply(&temp_message);
+}

+ 65 - 0
server/PlayerMessageProcessor.h

@@ -0,0 +1,65 @@
+/*
+ * CGameHandler.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 "../lib/GameConstants.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+class CGHeroInstance;
+class CGTownInstance;
+class CConnection;
+VCMI_LIB_NAMESPACE_END
+
+class CGameHandler;
+
+class PlayerMessageProcessor
+{
+	std::set<PlayerColor> cheaters;
+
+	void executeCheatCode(const std::string & cheatName, PlayerColor player, ObjectInstanceID currObj, const std::vector<std::string> & arguments );
+	bool handleCheatCode(const std::string & cheatFullCommand, PlayerColor player, ObjectInstanceID currObj);
+	bool handleHostCommand(PlayerColor player, const std::string & message);
+
+	void cheatGiveSpells(PlayerColor player, const CGHeroInstance * hero);
+	void cheatBuildTown(PlayerColor player, const CGTownInstance * town);
+	void cheatGiveArmy(PlayerColor player, const CGHeroInstance * hero, std::vector<std::string> words);
+	void cheatGiveMachines(PlayerColor player, const CGHeroInstance * hero);
+	void cheatGiveArtifacts(PlayerColor player, const CGHeroInstance * hero);
+	void cheatLevelup(PlayerColor player, const CGHeroInstance * hero, std::vector<std::string> words);
+	void cheatExperience(PlayerColor player, const CGHeroInstance * hero, std::vector<std::string> words);
+	void cheatMovement(PlayerColor player, const CGHeroInstance * hero, std::vector<std::string> words);
+	void cheatResources(PlayerColor player, std::vector<std::string> words);
+	void cheatVictory(PlayerColor player);
+	void cheatDefeat(PlayerColor player);
+	void cheatMapReveal(PlayerColor player, bool reveal);
+
+public:
+	CGameHandler * gameHandler;
+
+	PlayerMessageProcessor();
+	PlayerMessageProcessor(CGameHandler * gameHandler);
+
+	/// incoming NetPack handling
+	void playerMessage(PlayerColor player, const std::string & message, ObjectInstanceID currObj);
+
+	/// Send message to specific client with "System" as sender
+	void sendSystemMessage(std::shared_ptr<CConnection> connection, const std::string & message);
+
+	/// Send message to all players with "System" as sender
+	void broadcastSystemMessage(const std::string & message);
+
+	/// Send message from specific player to all other players
+	void broadcastMessage(PlayerColor playerSender, const std::string & message);
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+		h & cheaters;
+	}
+};