Browse Source

Merge branch 'vcmi:develop' into bug_fixes

MichalZ 1 year ago
parent
commit
e769e0a856
76 changed files with 1409 additions and 969 deletions
  1. 1 3
      AI/Nullkiller/Analyzers/BuildAnalyzer.cpp
  2. 1 1
      client/CPlayerInterface.cpp
  3. 1 1
      client/Client.h
  4. 8 0
      client/NetPacksClient.cpp
  5. 6 1
      client/eventsSDL/InputSourceMouse.cpp
  6. 7 6
      client/mainmenu/CMainMenu.cpp
  7. 101 90
      client/windows/CCastleInterface.cpp
  8. 3 2
      client/windows/CCastleInterface.h
  9. 2 4
      client/windows/CKingdomInterface.cpp
  10. 2 3
      client/windows/GUIClasses.cpp
  11. 1 1
      client/windows/GUIClasses.h
  12. 43 1
      config/buildingsLibrary.json
  13. 2 3
      config/factions/castle.json
  14. 4 5
      config/factions/conflux.json
  15. 3 5
      config/factions/dungeon.json
  16. 2 3
      config/factions/fortress.json
  17. 2 3
      config/factions/inferno.json
  18. 3 4
      config/factions/necropolis.json
  19. 2 3
      config/factions/rampart.json
  20. 4 5
      config/factions/stronghold.json
  21. 3 4
      config/factions/tower.json
  22. 1 5
      config/schemas/faction.json
  23. 9 2
      config/schemas/townBuilding.json
  24. 4 2
      docs/modders/Bonus/Bonus_Types.md
  25. 0 3
      docs/modders/Entities_Format/Faction_Format.md
  26. 3 2
      docs/modders/Entities_Format/Town_Building_Format.md
  27. 1 1
      lib/CArtHandler.cpp
  28. 15 45
      lib/CGameInfoCallback.cpp
  29. 1 2
      lib/CGameInfoCallback.h
  30. 1 0
      lib/CMakeLists.txt
  31. 53 0
      lib/CPlayerState.cpp
  32. 33 9
      lib/CPlayerState.h
  33. 1 1
      lib/IGameCallback.h
  34. 6 0
      lib/ResourceSet.cpp
  35. 1 0
      lib/ResourceSet.h
  36. 1 1
      lib/bonuses/BonusEnum.h
  37. 10 7
      lib/constants/Enumerations.h
  38. 0 7
      lib/constants/StringConstants.h
  39. 1 0
      lib/entities/building/CBuilding.h
  40. 1 1
      lib/entities/faction/CTown.h
  41. 15 23
      lib/entities/faction/CTownHandler.cpp
  42. 0 2
      lib/entities/faction/CTownHandler.h
  43. 30 90
      lib/gameState/CGameState.cpp
  44. 5 1
      lib/gameState/CGameState.h
  45. 1 1
      lib/gameState/CGameStateCampaign.cpp
  46. 21 13
      lib/gameState/GameStatistics.cpp
  47. 2 1
      lib/gameState/GameStatistics.h
  48. 2 2
      lib/gameState/HighScore.cpp
  49. 22 17
      lib/mapObjects/CGDwelling.cpp
  50. 6 1
      lib/mapObjects/CGDwelling.h
  51. 23 0
      lib/mapObjects/CGHeroInstance.cpp
  52. 6 1
      lib/mapObjects/CGHeroInstance.h
  53. 5 0
      lib/mapObjects/CGObjectInstance.cpp
  54. 2 0
      lib/mapObjects/CGObjectInstance.h
  55. 47 33
      lib/mapObjects/CGTownInstance.cpp
  56. 9 2
      lib/mapObjects/CGTownInstance.h
  57. 3 0
      lib/mapObjects/IObjectInterface.h
  58. 31 0
      lib/mapObjects/IOwnableObject.h
  59. 43 12
      lib/mapObjects/MiscObjects.cpp
  60. 17 7
      lib/mapObjects/MiscObjects.h
  61. 5 0
      lib/mapObjects/TownBuildingInstance.cpp
  62. 1 0
      lib/mapObjects/TownBuildingInstance.h
  63. 26 26
      lib/networkPacks/NetPacksLib.cpp
  64. 47 44
      lib/networkPacks/PacksForClient.h
  65. 1 2
      lib/pathfinder/CPathfinder.cpp
  66. 2 1
      lib/serializer/ESerializationVersion.h
  67. 2 2
      lib/spells/AdventureSpellMechanics.cpp
  68. 60 443
      server/CGameHandler.cpp
  69. 3 3
      server/CGameHandler.h
  70. 2 0
      server/CMakeLists.txt
  71. 1 1
      server/battles/BattleResultProcessor.cpp
  72. 570 0
      server/processors/NewTurnProcessor.cpp
  73. 51 0
      server/processors/NewTurnProcessor.h
  74. 2 2
      server/processors/PlayerMessageProcessor.cpp
  75. 2 2
      server/processors/TurnOrderProcessor.cpp
  76. 1 1
      test/mock/mock_IGameCallback.h

+ 1 - 3
AI/Nullkiller/Analyzers/BuildAnalyzer.cpp

@@ -314,9 +314,7 @@ void BuildAnalyzer::updateDailyIncome()
 		const CGMine* mine = dynamic_cast<const CGMine*>(obj);
 
 		if(mine)
-		{
-			dailyIncome[mine->producedResource.getNum()] += mine->getProducedQuantity();
-		}
+			dailyIncome += mine->dailyIncome();
 	}
 
 	for(const CGTownInstance* town : towns)

+ 1 - 1
client/CPlayerInterface.cpp

@@ -1673,7 +1673,7 @@ void CPlayerInterface::showUniversityWindow(const IMarket *market, const CGHeroI
 	auto onWindowClosed = [this, queryID](){
 		cb->selectionMade(0, queryID);
 	};
-	GH.windows().createAndPushWindow<CUniversityWindow>(visitor, market, onWindowClosed);
+	GH.windows().createAndPushWindow<CUniversityWindow>(visitor, BuildingID::NONE, market, onWindowClosed);
 }
 
 void CPlayerInterface::showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor)

+ 1 - 1
client/Client.h

@@ -207,7 +207,7 @@ public:
 	void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {};
 
 	void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override {}
-	void changeFogOfWar(std::unordered_set<int3> & tiles, PlayerColor player, ETileVisibility mode) override {}
+	void changeFogOfWar(const std::unordered_set<int3> & tiles, PlayerColor player, ETileVisibility mode) override {}
 
 	void setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value) override {};
 	void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) override {};

+ 8 - 0
client/NetPacksClient.cpp

@@ -361,6 +361,14 @@ void ApplyClientNetPackVisitor::visitHeroVisit(HeroVisit & pack)
 void ApplyClientNetPackVisitor::visitNewTurn(NewTurn & pack)
 {
 	cl.invalidatePaths();
+
+	if (pack.newWeekNotification)
+	{
+		const auto & newWeek = *pack.newWeekNotification;
+
+		std::string str = newWeek.text.toString();
+		callAllInterfaces(cl, &CGameInterface::showInfoDialog, newWeek.type, str, newWeek.components,(soundBase::soundID)newWeek.soundID);
+	}
 }
 
 void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack)

+ 6 - 1
client/eventsSDL/InputSourceMouse.cpp

@@ -23,6 +23,7 @@
 
 #include <SDL_events.h>
 #include <SDL_hints.h>
+#include <SDL_version.h>
 
 InputSourceMouse::InputSourceMouse()
 	:mouseToleranceDistance(settings["input"]["mouseToleranceDistance"].Integer())
@@ -69,7 +70,11 @@ void InputSourceMouse::handleEventMouseButtonDown(const SDL_MouseButtonEvent & b
 
 void InputSourceMouse::handleEventMouseWheel(const SDL_MouseWheelEvent & wheel)
 {
-	GH.events().dispatchMouseScrolled(Point(wheel.x, wheel.y) / GH.screenHandler().getScalingFactor(), GH.getCursorPosition());
+#if SDL_VERSION_ATLEAST(2,26,0)
+	GH.events().dispatchMouseScrolled(Point(wheel.x, wheel.y), Point(wheel.mouseX, wheel.mouseY) / GH.screenHandler().getScalingFactor());
+#else
+	GH.events().dispatchMouseScrolled(Point(wheel.x, wheel.y), GH.getCursorPosition());
+#endif
 }
 
 void InputSourceMouse::handleEventMouseButtonUp(const SDL_MouseButtonEvent & button)

+ 7 - 6
client/mainmenu/CMainMenu.cpp

@@ -80,6 +80,12 @@ CMenuScreen::CMenuScreen(const JsonNode & configNode)
 
 	pos = background->center();
 
+	if(!config["video"].isNull())
+	{
+		Point videoPosition(config["video"]["x"].Integer(), config["video"]["y"].Integer());
+		videoPlayer = std::make_shared<VideoWidget>(videoPosition, VideoPath::fromJson(config["video"]["name"]), false);
+	}
+
 	for(const JsonNode & node : config["items"].Vector())
 		menuNameToEntry.push_back(node["name"].String());
 
@@ -90,12 +96,7 @@ CMenuScreen::CMenuScreen(const JsonNode & configNode)
 	menuNameToEntry.push_back("credits");
 
 	tabs = std::make_shared<CTabbedInt>(std::bind(&CMenuScreen::createTab, this, _1));
-	if(!config["video"].isNull())
-	{
-		Point videoPosition(config["video"]["x"].Integer(), config["video"]["y"].Integer());
-		videoPlayer = std::make_shared<VideoWidget>(videoPosition, VideoPath::fromJson(config["video"]["name"]), false);
-	}
-	else
+	if(config["video"].isNull())
 		tabs->setRedrawParent(true);
 
 }

+ 101 - 90
client/windows/CCastleInterface.cpp

@@ -145,7 +145,7 @@ void CBuildingRect::clickPressed(const Point & cursorPosition)
 	if(getBuilding() && area && (parent->selectedBuilding==this))
 	{
 		auto building = getBuilding();
-		parent->buildingClicked(building->bid, building->subId, building->upgrade);
+		parent->buildingClicked(building->bid);
 	}
 }
 
@@ -681,18 +681,76 @@ const CGHeroInstance * CCastleBuildings::getHero()
 		return town->garrisonHero;
 }
 
-void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades)
+void CCastleBuildings::buildingClicked(BuildingID building)
 {
-	logGlobal->trace("You've clicked on %d", (int)building.toEnum());
-	const CBuilding *b = town->town->buildings.find(building)->second;
+	BuildingID buildingToEnter = building;
+	for(;;)
+	{
+		const CBuilding *b = town->town->buildings.find(buildingToEnter)->second;
+
+		if (buildingTryActivateCustomUI(buildingToEnter, building))
+			return;
+
+		if (!b->upgrade.hasValue())
+		{
+			enterBuilding(building);
+			return;
+		}
+
+		buildingToEnter = b->upgrade;
+	}
+}
 
-	if (building >= BuildingID::DWELL_FIRST)
+bool CCastleBuildings::buildingTryActivateCustomUI(BuildingID buildingToTest, BuildingID buildingTarget)
+{
+	logGlobal->trace("You've clicked on %d", (int)buildingToTest.toEnum());
+	const CBuilding *b = town->town->buildings.at(buildingToTest);
+
+	if (town->getWarMachineInBuilding(buildingToTest).hasValue())
 	{
-		enterDwelling((BuildingID::getLevelFromDwelling(building)));
+		enterBlacksmith(buildingTarget, town->getWarMachineInBuilding(buildingToTest));
+		return true;
+	}
+
+	if (!b->marketModes.empty())
+	{
+		switch (*b->marketModes.begin())
+		{
+			case EMarketMode::CREATURE_UNDEAD:
+				GH.windows().createAndPushWindow<CTransformerWindow>(town, getHero(), nullptr);
+				return true;
+
+			case EMarketMode::RESOURCE_SKILL:
+				if (getHero())
+					GH.windows().createAndPushWindow<CUniversityWindow>(getHero(), buildingTarget, town, nullptr);
+				return true;
+
+			case EMarketMode::RESOURCE_RESOURCE:
+				// can't use allied marketplace
+				if (town->getOwner() == LOCPLINT->playerID)
+				{
+					GH.windows().createAndPushWindow<CMarketWindow>(town, getHero(), nullptr, *b->marketModes.begin());
+					return true;
+				}
+				else
+					return false;
+			default:
+				if(getHero())
+					GH.windows().createAndPushWindow<CMarketWindow>(town, getHero(), nullptr, *b->marketModes.begin());
+				else
+					LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % b->getNameTranslated())); //Only visiting heroes may use the %s.
+				return true;
+		}
+	}
+
+	if (buildingToTest >= BuildingID::DWELL_FIRST)
+	{
+		enterDwelling((BuildingID::getLevelFromDwelling(buildingToTest)));
+		return true;
 	}
 	else
 	{
-		switch(building)
+		switch(buildingToTest)
 		{
 		case BuildingID::MAGES_GUILD_1:
 		case BuildingID::MAGES_GUILD_2:
@@ -700,139 +758,91 @@ void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuil
 		case BuildingID::MAGES_GUILD_4:
 		case BuildingID::MAGES_GUILD_5:
 				enterMagesGuild();
-				break;
+				return true;
 
 		case BuildingID::TAVERN:
 				LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE);
-				break;
+				return true;
 
 		case BuildingID::SHIPYARD:
 				if(town->shipyardStatus() == IBoatGenerator::GOOD)
+				{
 					LOCPLINT->showShipyardDialog(town);
+					return true;
+				}
 				else if(town->shipyardStatus() == IBoatGenerator::BOAT_ALREADY_BUILT)
+				{
 					LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[51]);
-				break;
+					return true;
+				}
+				return false;
 
 		case BuildingID::FORT:
 		case BuildingID::CITADEL:
 		case BuildingID::CASTLE:
 				GH.windows().createAndPushWindow<CFortScreen>(town);
-				break;
+				return true;
 
 		case BuildingID::VILLAGE_HALL:
 		case BuildingID::CITY_HALL:
 		case BuildingID::TOWN_HALL:
 		case BuildingID::CAPITOL:
 				enterTownHall();
-				break;
-
-		case BuildingID::MARKETPLACE:
-				// can't use allied marketplace
-				if (town->getOwner() == LOCPLINT->playerID)
-					GH.windows().createAndPushWindow<CMarketWindow>(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_RESOURCE);
-				else
-					enterBuilding(building);
-				break;
-
-		case BuildingID::BLACKSMITH:
-				enterBlacksmith(town->town->warMachine);
-				break;
+				return true;
 
 		case BuildingID::SHIP:
 			LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[51]); //Cannot build another boat
-			break;
+			return true;
 
 		case BuildingID::SPECIAL_1:
 		case BuildingID::SPECIAL_2:
 		case BuildingID::SPECIAL_3:
 		case BuildingID::SPECIAL_4:
-				switch (subID)
+				switch (b->subId)
 				{
-				case BuildingSubID::NONE:
-						enterBuilding(building);
-						break;
-
 				case BuildingSubID::MYSTIC_POND:
-						enterFountain(building, subID, upgrades);
-						break;
-
-				case BuildingSubID::ARTIFACT_MERCHANT:
-						if(town->visitingHero)
-							GH.windows().createAndPushWindow<CMarketWindow>(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_ARTIFACT);
-						else
-							LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % b->getNameTranslated())); //Only visiting heroes may use the %s.
-						break;
-
-				case BuildingSubID::FOUNTAIN_OF_FORTUNE:
-						enterFountain(building, subID, upgrades);
-					break;
-
-				case BuildingSubID::FREELANCERS_GUILD:
-						if(getHero())
-							GH.windows().createAndPushWindow<CMarketWindow>(town, getHero(), nullptr, EMarketMode::CREATURE_RESOURCE);
-						else
-							LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % b->getNameTranslated())); //Only visiting heroes may use the %s.
-						break;
-
-				case BuildingSubID::MAGIC_UNIVERSITY:
-						if (getHero())
-							GH.windows().createAndPushWindow<CUniversityWindow>(getHero(), town, nullptr);
-						else
-							enterBuilding(building);
-						break;
+						enterFountain(buildingToTest, b->subId, buildingTarget);
+						return true;
 
 				case BuildingSubID::CASTLE_GATE:
 						if (LOCPLINT->makingTurn)
+						{
 							enterCastleGate();
-						else
-							enterBuilding(building);
-						break;
-
-				case BuildingSubID::CREATURE_TRANSFORMER: //Skeleton Transformer
-						GH.windows().createAndPushWindow<CTransformerWindow>(town, getHero(), nullptr);
-						break;
+							return true;
+						}
+						return false;
 
 				case BuildingSubID::PORTAL_OF_SUMMONING:
 						if (town->creatures[town->town->creatures.size()].second.empty())//No creatures
 							LOCPLINT->showInfoDialog(CGI->generaltexth->tcommands[30]);
 						else
 							enterDwelling(town->town->creatures.size());
-						break;
-
-				case BuildingSubID::BALLISTA_YARD:
-						enterBlacksmith(ArtifactID::BALLISTA);
-						break;
-
-				case BuildingSubID::THIEVES_GUILD:
-						enterAnyThievesGuild();
-						break;
+						return true;
 
 				case BuildingSubID::BANK:
 						enterBank();
-						break;
-
-				default:
-					if(upgrades == BuildingID::TAVERN)
-						LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE);
-					else
-						enterBuilding(building);
-					break;
+						return true;
 				}
-				break;
+		}
+	}
 
-		default:
-				enterBuilding(building);
-				break;
+	for (auto const & bonus : b->buildingBonuses)
+	{
+		if (bonus->type == BonusType::THIEVES_GUILD_ACCESS)
+		{
+			enterAnyThievesGuild();
+			return true;
 		}
 	}
+	return false;
 }
 
-void CCastleBuildings::enterBlacksmith(ArtifactID artifactID)
+void CCastleBuildings::enterBlacksmith(BuildingID building, ArtifactID artifactID)
 {
 	const CGHeroInstance *hero = town->visitingHero;
 	if(!hero)
 	{
-		LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % town->town->buildings.find(BuildingID::BLACKSMITH)->second->getNameTranslated()));
+		LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % town->town->buildings.find(building)->second->getNameTranslated()));
 		return;
 	}
 	auto art = artifactID.toArtifact();
@@ -843,7 +853,7 @@ void CCastleBuildings::enterBlacksmith(ArtifactID artifactID)
 	{
 		for(auto slot : art->getPossibleSlots().at(ArtBearer::HERO))
 		{
-			if(hero->getArt(slot) == nullptr)
+			if(hero->getArt(slot) == nullptr || hero->getArt(slot)->getTypeId() != artifactID)
 			{
 				possible = true;
 				break;
@@ -854,8 +864,9 @@ void CCastleBuildings::enterBlacksmith(ArtifactID artifactID)
 			}
 		}
 	}
-	CreatureID cre = art->getWarMachine();
-	GH.windows().createAndPushWindow<CBlacksmithDialog>(possible, cre, artifactID, hero->id);
+
+	CreatureID creatureID = artifactID.toArtifact()->getWarMachine();
+	GH.windows().createAndPushWindow<CBlacksmithDialog>(possible, creatureID, artifactID, hero->id);
 }
 
 void CCastleBuildings::enterBuilding(BuildingID building)

+ 3 - 2
client/windows/CCastleInterface.h

@@ -150,7 +150,7 @@ class CCastleBuildings : public CIntObject
 
 	const CGHeroInstance* getHero();//Select hero for buildings usage
 
-	void enterBlacksmith(ArtifactID artifactID);//support for blacksmith + ballista yard
+	void enterBlacksmith(BuildingID building, ArtifactID artifactID);//support for blacksmith + ballista yard
 	void enterBuilding(BuildingID building);//for buildings with simple description + pic left-click messages
 	void enterCastleGate();
 	void enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades);//Rampart's fountains
@@ -173,7 +173,8 @@ public:
 	void enterBank();
 	void enterToTheQuickRecruitmentWindow();
 
-	void buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID = BuildingSubID::NONE, BuildingID upgrades = BuildingID::NONE);
+	bool buildingTryActivateCustomUI(BuildingID buildingToTest, BuildingID buildingTarget);
+	void buildingClicked(BuildingID building);
 	void addBuilding(BuildingID building);
 	void removeBuilding(BuildingID building);//FIXME: not tested!!!
 };

+ 2 - 4
client/windows/CKingdomInterface.cpp

@@ -585,9 +585,7 @@ void CKingdomInterface::generateMinesList(const std::vector<const CGObjectInstan
 			const CGMine * mine = dynamic_cast<const CGMine *>(object);
 			assert(mine);
 			minesCount[mine->producedResource]++;
-
-			if (mine->producedResource == EGameResID::GOLD)
-				totalIncome += mine->getProducedQuantity();
+			totalIncome += mine->dailyIncome()[EGameResID::GOLD];
 		}
 	}
 
@@ -596,7 +594,7 @@ void CKingdomInterface::generateMinesList(const std::vector<const CGObjectInstan
 	auto * playerSettings = LOCPLINT->cb->getPlayerSettings(LOCPLINT->playerID);
 	for(auto & hero : heroes)
 	{
-		totalIncome += hero->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(EGameResID::GOLD)))) * playerSettings->handicap.percentIncome / 100;
+		totalIncome += hero->dailyIncome()[EGameResID::GOLD];
 	}
 
 	//Add town income of all towns

+ 2 - 3
client/windows/GUIClasses.cpp

@@ -946,7 +946,7 @@ void CUniversityWindow::CItem::hover(bool on)
 		GH.statusbar()->clear();
 }
 
-CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, const IMarket * _market, const std::function<void()> & onWindowClosed)
+CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, BuildingID building, const IMarket * _market, const std::function<void()> & onWindowClosed)
 	: CWindowObject(PLAYER_COLORED, ImagePath::builtin("UNIVERS1")),
 	hero(_hero),
 	onWindowClosed(onWindowClosed),
@@ -961,8 +961,7 @@ CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, const IMarket
 	if(auto town = dynamic_cast<const CGTownInstance *>(_market))
 	{
 		auto faction = town->town->faction->getId();
-		auto bid = town->town->getSpecialBuilding(BuildingSubID::MAGIC_UNIVERSITY)->bid;
-		titlePic = std::make_shared<CAnimImage>((*CGI->townh)[faction]->town->clientInfo.buildingsIcons, bid);
+		titlePic = std::make_shared<CAnimImage>((*CGI->townh)[faction]->town->clientInfo.buildingsIcons, building);
 	}
 	else if(auto uni = dynamic_cast<const CGUniversity *>(_market); uni->appearance)
 	{

+ 1 - 1
client/windows/GUIClasses.h

@@ -391,7 +391,7 @@ class CUniversityWindow final : public CStatusbarWindow, public IMarketHolder
 	std::function<void()> onWindowClosed;
 
 public:
-	CUniversityWindow(const CGHeroInstance * _hero, const IMarket * _market, const std::function<void()> & onWindowClosed);
+	CUniversityWindow(const CGHeroInstance * _hero, BuildingID building, const IMarket * _market, const std::function<void()> & onWindowClosed);
 
 	void makeDeal(SecondarySkill skill);
 	void close() override;

+ 43 - 1
config/buildingsLibrary.json

@@ -10,6 +10,11 @@
 			{
 				"type": "MORALE",
 				"val": 1
+			},
+			{
+				"propagator": "PLAYER_PROPAGATOR",
+				"type": "THIEVES_GUILD_ACCESS",
+				"val": 1
 			}
 		]
 	},
@@ -43,7 +48,10 @@
 		"produce": { "gold": 4000 }
 	},
 
-	"marketplace":    { "id" : 14 },
+	"marketplace":    { 
+		"id" : 14,
+		"marketModes" : ["resource-resource", "resource-player"]
+	},
 	"resourceSilo":   { "id" : 15, "requires" : [ "marketplace" ] },
 	"blacksmith":     { "id" : 16 },
 
@@ -198,5 +206,39 @@
 				}
 			]
 		}
+	},
+	
+	// Section 3 - markets
+	"artifactMerchant" : {
+		"requires" : [ "marketplace" ],
+		"marketModes" : ["resource-artifact", "artifact-resource"]
+	},
+	
+	"freelancersGuild" : {
+		"requires" : [ "marketplace" ],
+		"marketModes" : ["creature-resource"]
+	},
+	
+	"magicUniversity" : {
+		"marketModes" : ["resource-skill"]
+	},
+	
+	"creatureTransformer" : {
+		"marketModes" : ["creature-undead"]
+	},
+	
+	// Section 4 - buildings that now have dedicated mechanics
+	"ballistaYard": {
+		"blacksmith" : "ballista"
+	},
+	
+	"thievesGuild" : {
+		"bonuses": [
+			{
+				"propagator": "PLAYER_PROPAGATOR",
+				"type": "THIEVES_GUILD_ACCESS",
+				"val": 2
+			}
+		]
 	}
 }

+ 2 - 3
config/factions/castle.json

@@ -147,7 +147,6 @@
 			],
 			"horde" : [ 2, -1 ],
 			"mageGuild" : 4,
-			"warMachine" : "ballista",
 			"moatAbility" : "castleMoat",
 			// primaryResource not specified so town get both Wood and Ore for resource bonus
 
@@ -166,9 +165,9 @@
 				"townHall":       { },
 				"cityHall":       { },
 				"capitol":        { },
-				"marketplace":    { "marketModes" : ["resource-resource", "resource-player"] },
+				"marketplace":    { },
 				"resourceSilo":   { "produce": { "ore": 1, "wood": 1 } },
-				"blacksmith":     { },
+				"blacksmith":     { "warMachine" : "ballista" },
 
 				"special1":       { 
 					"bonuses": [

+ 4 - 5
config/factions/conflux.json

@@ -152,7 +152,6 @@
 			"horde" : [ 0, -1 ],
 			"mageGuild" : 5,
 			"primaryResource" : "mercury",
-			"warMachine" : "ballista",
 			"moatAbility" : "castleMoat",
 
 			"buildings" :
@@ -171,15 +170,15 @@
 				"townHall":       { },
 				"cityHall":       { },
 				"capitol":        { },
-				"marketplace":    { "marketModes" : ["resource-resource", "resource-player"] },
+				"marketplace":    { },
 				"resourceSilo":   { "produce": { "mercury": 1 } },
-				"blacksmith":     { },
+				"blacksmith":     { "warMachine" : "ballista" },
 
-				"special1":       { "type" : "artifactMerchant", "requires" : [ "marketplace" ], "marketModes" : ["resource-artifact", "artifact-resource"] },
+				"special1":       { "requires" : [ "marketplace" ], "marketModes" : ["resource-artifact", "artifact-resource"] },
 				"horde1":         { "id" : 18, "upgrades" : "dwellingLvl1" },
 				"horde1Upgr":     { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" },
 				"ship":           { "id" : 20, "upgrades" : "shipyard" },
-				"special2":       { "type" : "magicUniversity", "requires" : [ "mageGuild1" ] },
+				"special2":       { "requires" : [ "mageGuild1" ], "marketModes" : ["resource-skill"] },
 				"grail":          { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }},
 				"extraTownHall":  { "id" : 27, "requires" : [ "townHall" ], "mode" : "auto" },
 				"extraCityHall":  { "id" : 28, "requires" : [ "cityHall" ], "mode" : "auto" },

+ 3 - 5
config/factions/dungeon.json

@@ -148,10 +148,8 @@
 			"horde" : [ 0, -1 ],
 			"mageGuild" : 5,
 			"primaryResource" : "sulfur",
-			"warMachine" : "ballista",
 			"moatAbility" : "dungeonMoat",
 
-
 			"buildings" :
 			{
 				"mageGuild1":     { },
@@ -167,11 +165,11 @@
 				"townHall":       { },
 				"cityHall":       { },
 				"capitol":        { },
-				"marketplace":    { "marketModes" : ["resource-resource", "resource-player"] },
+				"marketplace":    { },
 				"resourceSilo":   { "produce": { "sulfur": 1 } },
-				"blacksmith":     { },
+				"blacksmith":     { "warMachine" : "ballista" },
 
-				"special1":       { "type" : "artifactMerchant", "requires" : [ "marketplace" ], "marketModes" : ["resource-artifact", "artifact-resource"] },
+				"special1":       { "requires" : [ "marketplace" ], "marketModes" : ["resource-artifact", "artifact-resource"] },
 				"horde1":         { "id" : 18, "upgrades" : "dwellingLvl1" },
 				"horde1Upgr":     { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" },
 				"special2":       {

+ 2 - 3
config/factions/fortress.json

@@ -147,7 +147,6 @@
 			],
 			"horde" : [ 0, -1 ],
 			"mageGuild" : 3,
-			"warMachine" : "firstAidTent",
 			"moatAbility" : "fortressMoat",
 			// primaryResource not specified so town get both Wood and Ore for resource bonus
 
@@ -165,9 +164,9 @@
 				"townHall":       { },
 				"cityHall":       { },
 				"capitol":        { },
-				"marketplace":    { "marketModes" : ["resource-resource", "resource-player"] },
+				"marketplace":    { },
 				"resourceSilo":   { "produce": { "wood": 1, "ore": 1 } },
-				"blacksmith":     { },
+				"blacksmith":     { "warMachine" : "firstAidTent" },
 
 				"special1":       {
 					"requires" : [ "allOf", [ "townHall" ], [ "special2" ] ],

+ 2 - 3
config/factions/inferno.json

@@ -149,7 +149,6 @@
 			"horde" : [ 0, 2 ],
 			"mageGuild" : 5,
 			"primaryResource" : "mercury",
-			"warMachine" : "ammoCart",
 			"moatAbility" : "infernoMoat",
 
 			"buildings" :
@@ -167,9 +166,9 @@
 				"townHall":       { },
 				"cityHall":       { },
 				"capitol":        { },
-				"marketplace":    { "marketModes" : ["resource-resource", "resource-player"] },
+				"marketplace":    { },
 				"resourceSilo":   { "produce": { "mercury": 1 } },
-				"blacksmith":     { },
+				"blacksmith":     { "warMachine" : "ammoCart" },
 
 				"horde1":         { "id" : 18, "upgrades" : "dwellingLvl1" },
 				"horde1Upgr":     { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" },

+ 3 - 4
config/factions/necropolis.json

@@ -152,7 +152,6 @@
 			],
 			"horde" : [ 0, -1 ],
 			"mageGuild" : 5,
-			"warMachine" : "firstAidTent",
 			"moatAbility" : "necropolisMoat",
 			// primaryResource not specified so town get both Wood and Ore for resource bonus
 
@@ -172,9 +171,9 @@
 				"townHall":       { },
 				"cityHall":       { },
 				"capitol":        { },
-				"marketplace":    { "marketModes" : ["resource-resource", "resource-player"] },
+				"marketplace":    { },
 				"resourceSilo":   { "produce": { "ore": 1, "wood": 1 } },
-				"blacksmith":     { },
+				"blacksmith":     { "warMachine" : "firstAidTent" },
 
 				"special1":       { "requires" : [ "fort" ], "bonuses": [ { "type": "DARKNESS", "val": 20  } ] },
 				"horde1":         { "id" : 18, "upgrades" : "dwellingLvl1", "requires" : [ "special3" ] },
@@ -182,7 +181,7 @@
 				"ship":           { "id" : 20, "upgrades" : "shipyard" },
 				"special2":       { "requires" : [ "mageGuild1" ],
 					"bonuses": [ { "type": "UNDEAD_RAISE_PERCENTAGE", "val": 10, "propagator": "PLAYER_PROPAGATOR" } ] },
-				"special3":       { "type" : "creatureTransformer", "requires" : [ "dwellingLvl1" ], "marketModes" : ["creature-undead"] },
+				"special3":       { "requires" : [ "dwellingLvl1" ], "marketModes" : ["creature-undead"] },
 				"grail":          { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 },
 					"bonuses": [ { "type": "UNDEAD_RAISE_PERCENTAGE", "val": 20, "propagator": "PLAYER_PROPAGATOR" } ] },
 

+ 2 - 3
config/factions/rampart.json

@@ -152,7 +152,6 @@
 			"horde" : [ 1, 4 ],
 			"mageGuild" : 5,
 			"primaryResource" : "crystal",
-			"warMachine" : "firstAidTent",
 			"moatAbility" : "rampartMoat",
 
 			"buildings" :
@@ -170,9 +169,9 @@
 				"townHall":       { },
 				"cityHall":       { },
 				"capitol":        { },
-				"marketplace":    { "marketModes" : ["resource-resource", "resource-player"] },
+				"marketplace":    { },
 				"resourceSilo":   { "produce": { "crystal": 1 } },
-				"blacksmith":     { },
+				"blacksmith":     { "warMachine" : "firstAidTent" },
 
 				"special1":       { "type" : "mysticPond" },
 				"horde1":         { "id" : 18, "upgrades" : "dwellingLvl2" },

+ 4 - 5
config/factions/stronghold.json

@@ -145,7 +145,6 @@
 			],
 			"horde" : [ 0, -1 ],
 			"mageGuild" : 3,
-			"warMachine" : "ammoCart",
 			"moatAbility" : "strongholdMoat",
 			// primaryResource not specified so town get both Wood and Ore for resource bonus
 
@@ -162,15 +161,15 @@
 				"townHall":       { },
 				"cityHall":       { },
 				"capitol":        { },
-				"marketplace":    { "marketModes" : ["resource-resource", "resource-player"] },
+				"marketplace":    { },
 				"resourceSilo":   { "produce": { "ore": 1, "wood": 1 } },
-				"blacksmith":     { },
+				"blacksmith":     { "warMachine" : "ammoCart" },
 
 				"special1":       { "type" : "escapeTunnel", "requires" : [ "fort" ] },
 				"horde1":         { "id" : 18, "upgrades" : "dwellingLvl1" },
 				"horde1Upgr":     { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" },
-				"special2":       { "type" : "freelancersGuild", "requires" : [ "marketplace" ], "marketModes" : ["creature-resource"] },
-				"special3":       { "type" : "ballistaYard", "requires" : [ "blacksmith" ] },
+				"special2":       { "requires" : [ "marketplace" ], "marketModes" : ["creature-resource"] },
+				"special3":       { "warMachine" : "ballista", "requires" : [ "blacksmith" ] },
 				"special4":       { 
 					"requires" : [ "fort" ],
 					"configuration" : {

+ 3 - 4
config/factions/tower.json

@@ -147,7 +147,6 @@
 			"horde" : [ 1, -1 ],
 			"primaryResource" : "gems",
 			"mageGuild" : 5,
-			"warMachine" : "ammoCart",
 			"moatAbility" : "towerMoat",
 
 			"buildings" :
@@ -165,11 +164,11 @@
 				"townHall":       { },
 				"cityHall":       { },
 				"capitol":        { },
-				"marketplace":    { "marketModes" : ["resource-resource", "resource-player"] },
+				"marketplace":    { },
 				"resourceSilo":   { "produce" : { "gems": 1 } },
-				"blacksmith":     { },
+				"blacksmith":     { "warMachine" : "ammoCart" },
 
-				"special1":       { "type" : "artifactMerchant", "requires" : [ "marketplace" ], "marketModes" : ["resource-artifact", "artifact-resource"] },
+				"special1":       { "requires" : [ "marketplace" ], "marketModes" : ["resource-artifact", "artifact-resource"] },
 				"horde1":         { "id" : 18, "upgrades" : "dwellingLvl2" },
 				"horde1Upgr":     { "id" : 19, "upgrades" : "dwellingUpLvl2", "requires" : [ "horde1" ], "mode" : "auto" },
 				"special2":       { "height" : "high", "requires" : [ "fort" ] },

+ 1 - 5
config/schemas/faction.json

@@ -89,7 +89,7 @@
 			"additionalProperties" : false,
 			"required" : [
 				"mapObject", "buildingsIcons", "buildings", "creatures", "guildWindow", "names",
-				"hallBackground", "hallSlots", "horde", "mageGuild", "moatAbility", "defaultTavern", "tavernVideo", "guildBackground", "musicTheme", "siege", "structures", "townBackground", "warMachine"
+				"hallBackground", "hallSlots", "horde", "mageGuild", "moatAbility", "defaultTavern", "tavernVideo", "guildBackground", "musicTheme", "siege", "structures", "townBackground"
 			],
 			"description" : "town",
 			"properties" : {
@@ -133,10 +133,6 @@
 					"type" : "string",
 					"description" : "Primary resource for this town. Produced by Silo and offered as starting bonus"
 				},
-				"warMachine" : {
-					"type" : "string",
-					"description" : "Identifier of war machine produced by blacksmith in town"
-				},
 				"horde" : {
 					"type" : "array",
 					"maxItems" : 2,

+ 9 - 2
config/schemas/townBuilding.json

@@ -36,7 +36,7 @@
 		},
 		"type" : {
 			"type" : "string",
-			"enum" : [ "mysticPond", "artifactMerchant", "freelancersGuild", "magicUniversity", "castleGate", "creatureTransformer", "portalOfSummoning", "ballistaYard", "library", "escapeTunnel", "treasury", "thievesGuild", "bank" ],
+			"enum" : [ "mysticPond", "castleGate", "portalOfSummoning", "library", "escapeTunnel", "treasury", "bank" ],
 			"description" : "Subtype for some special buildings"
 		},
 		"mode" : {
@@ -93,6 +93,10 @@
 				"gems" :    { "type" : "number"}
 			}
 		},
+		"warMachine" : {
+			"type" : "string",
+			"description" : "Artifact ID of a war machine that can be purchased in this building, if any"
+		},
 		"bonuses" : {
 			"type" : "array",
 			"description" : "Bonuses that are provided by this building in any town where this building has been built. Only affects town itself (including siege), to propagate effect to player or team please use bonus propagators",
@@ -100,7 +104,10 @@
 		},
 		"marketModes" : {
 			"type" : "array",
-			"enum" : [ "resource-resource", "resource-player", "creature-resource", "resource-artifact", "artifact-resource", "artifact-experience", "creature-experience", "creature-undead", "resource-skill"],
+			"items" : {
+				"type" : "string",
+				"enum" : [ "resource-resource", "resource-player", "creature-resource", "resource-artifact", "artifact-resource", "artifact-experience", "creature-experience", "creature-undead", "resource-skill"],
+			},
 			"description" : "List of modes available in this market"
 		}
 	}

+ 4 - 2
docs/modders/Bonus/Bonus_Types.md

@@ -1010,9 +1010,11 @@ Dummy bonus that acts as marker for Dendroid's Bind ability
 
 Dummy skill for alternative upgrades mod
 
-### TOWN_MAGIC_WELL
+### THIEVES_GUILD_ACCESS
 
-Internal bonus, do not use
+Increases amount of information available in affected thieves guild (in town or in adventure map tavern). Does not affects adventure map object "Den of Thieves". You may want to use PLAYER_PROPAGATOR with this bonus to make its effect player wide.
+
+- val: additional number of 'levels' of information to grant access to
 
 ### LEVEL_COUNTER
 

+ 0 - 3
docs/modders/Entities_Format/Faction_Format.md

@@ -247,9 +247,6 @@ Each town requires a set of buildings (Around 30-45 buildings)
 	// maximum level of mage guild
 	"mageGuild" : 4,
 
-	// war machine produced in town
-	"warMachine" : "ballista"
-	
 	// Identifier of spell that will create effects for town moat during siege
 	"moatAbility" : "castleMoat"
 }

+ 3 - 2
docs/modders/Entities_Format/Town_Building_Format.md

@@ -136,6 +136,9 @@ These are just a couple of examples of what can be done in VCMI. See vcmi config
 		"gold" : 10000
 	}, 
 	
+	// Artifact ID of a war machine produced in this town building, if any
+	"warMachine" : "ballista",
+	
 	// Allows to define additional functionality of this building, usually using logic of one of original H3 town building
 	// Generally only needs to be specified for "special" buildings
 	// See 'List of unique town buildings' section below for detailed description of this field
@@ -207,14 +210,12 @@ Following Heroes III buildings can be used as unique buildings for a town. Their
 - `castleGate`
 - `creatureTransformer`
 - `portalOfSummoning`
-- `ballistaYard`
 - `library`
 - `escapeTunnel`
 - `treasury`
 
 #### Buildings from other Heroes III mods
 Following HotA buildings can be used as unique building for a town. Functionality should match corresponding HotA building:
-- `thievesGuild`
 - `bank`
 
 #### Custom buildings

+ 1 - 1
lib/CArtHandler.cpp

@@ -460,7 +460,7 @@ std::shared_ptr<CArtifact> CArtHandler::loadFromJson(const std::string & scope,
 	}
 
 	const JsonNode & warMachine = node["warMachine"];
-	if(warMachine.getType() == JsonNode::JsonType::DATA_STRING && !warMachine.String().empty())
+	if(!warMachine.isNull())
 	{
 		VLC->identifiers()->requestIdentifier("creature", warMachine, [=](si32 id)
 		{

+ 15 - 45
lib/CGameInfoCallback.cpp

@@ -236,15 +236,7 @@ void CGameInfoCallback::getThievesGuildInfo(SThievesGuildInfo & thi, const CGObj
 
 	if(obj->ID == Obj::TOWN || obj->ID == Obj::TAVERN)
 	{
-		int taverns = 0;
-		for(auto town : gs->players[*getPlayerID()].towns)
-		{
-			if(town->hasBuilt(BuildingID::TAVERN))
-				taverns++;
-			
-			if(town->hasBuilt(BuildingSubID::THIEVES_GUILD))
-				taverns += 2;
-		}
+		int taverns = gs->players[*getPlayerID()].valOfBonuses(BonusType::THIEVES_GUILD_ACCESS);
 		gs->obtainPlayersStats(thi, taverns);
 	}
 	else if(obj->ID == Obj::DEN_OF_THIEVES)
@@ -256,7 +248,7 @@ void CGameInfoCallback::getThievesGuildInfo(SThievesGuildInfo & thi, const CGObj
 int CGameInfoCallback::howManyTowns(PlayerColor Player) const
 {
 	ERROR_RET_VAL_IF(!hasAccess(Player), "Access forbidden!", -1);
-	return static_cast<int>(gs->players[Player].towns.size());
+	return static_cast<int>(gs->players[Player].getTowns().size());
 }
 
 bool CGameInfoCallback::getTownInfo(const CGObjectInstance * town, InfoAboutTown & dest, const CGObjectInstance * selectedObject) const
@@ -609,7 +601,7 @@ EBuildingState CGameInfoCallback::canBuildStructure( const CGTownInstance *t, Bu
 		const PlayerState *ps = getPlayerState(t->tempOwner, false);
 		if(ps)
 		{
-			for(const CGTownInstance *town : ps->towns)
+			for(const CGTownInstance *town : ps->getTowns())
 			{
 				if(town->hasBuilt(BuildingID::CAPITOL))
 				{
@@ -711,9 +703,9 @@ int CGameInfoCallback::getHeroCount( PlayerColor player, bool includeGarrisoned
 	ERROR_RET_VAL_IF(!p, "No such player!", -1);
 
 	if(includeGarrisoned)
-		return static_cast<int>(p->heroes.size());
+		return static_cast<int>(p->getHeroes().size());
 	else
-		for(const auto & elem : p->heroes)
+		for(const auto & elem : p->getHeroes())
 			if(!elem->inTownGarrison)
 				ret++;
 	return ret;
@@ -757,7 +749,7 @@ std::vector < const CGTownInstance *> CPlayerSpecificInfoCallback::getTownsInfo(
 	auto ret = std::vector < const CGTownInstance *>();
 	for(const auto & i : gs->players)
 	{
-		for(const auto & town : i.second.towns)
+		for(const auto & town : i.second.getTowns())
 		{
 			if(i.first == getPlayerID() || (!onlyOur && isVisible(town, getPlayerID())))
 			{
@@ -789,7 +781,7 @@ int CPlayerSpecificInfoCallback::getHeroSerial(const CGHeroInstance * hero, bool
 		return -1;
 
 	size_t index = 0;
-	auto & heroes = gs->players[*getPlayerID()].heroes;
+	const auto & heroes = gs->players[*getPlayerID()].getHeroes();
 
 	for (auto & possibleHero : heroes)
 	{
@@ -822,34 +814,12 @@ int3 CPlayerSpecificInfoCallback::getGrailPos( double *outKnownRatio )
 
 std::vector < const CGObjectInstance * > CPlayerSpecificInfoCallback::getMyObjects() const
 {
-	std::vector < const CGObjectInstance * > ret;
-	for(const CGObjectInstance * obj : gs->map->objects)
-	{
-		if(obj && obj->tempOwner == getPlayerID())
-			ret.push_back(obj);
-	}
-	return ret;
-}
-
-std::vector < const CGDwelling * > CPlayerSpecificInfoCallback::getMyDwellings() const
-{
-	ASSERT_IF_CALLED_WITH_PLAYER
-	std::vector < const CGDwelling * > ret;
-	for(CGDwelling * dw : gs->getPlayerState(*getPlayerID())->dwellings)
-	{
-		ret.push_back(dw);
-	}
-	return ret;
+	return gs->getPlayerState(*getPlayerID())->getOwnedObjects();
 }
 
 std::vector <QuestInfo> CPlayerSpecificInfoCallback::getMyQuests() const
 {
-	std::vector <QuestInfo> ret;
-	for(const auto & quest : gs->getPlayerState(*getPlayerID())->quests)
-	{
-		ret.push_back (quest);
-	}
-	return ret;
+	return gs->getPlayerState(*getPlayerID())->quests;
 }
 
 int CPlayerSpecificInfoCallback::howManyHeroes(bool includeGarrisoned) const
@@ -867,12 +837,12 @@ const CGHeroInstance* CPlayerSpecificInfoCallback::getHeroBySerial(int serialId,
 
 	if (!includeGarrisoned)
 	{
-		for(ui32 i = 0; i < p->heroes.size() && static_cast<int>(i) <= serialId; i++)
-			if(p->heroes[i]->inTownGarrison)
+		for(ui32 i = 0; i < p->getHeroes().size() && static_cast<int>(i) <= serialId; i++)
+			if(p->getHeroes()[i]->inTownGarrison)
 				serialId++;
 	}
-	ERROR_RET_VAL_IF(serialId < 0 || serialId >= p->heroes.size(), "No player info", nullptr);
-	return p->heroes[serialId];
+	ERROR_RET_VAL_IF(serialId < 0 || serialId >= p->getHeroes().size(), "No player info", nullptr);
+	return p->getHeroes()[serialId];
 }
 
 const CGTownInstance* CPlayerSpecificInfoCallback::getTownBySerial(int serialId) const
@@ -880,8 +850,8 @@ const CGTownInstance* CPlayerSpecificInfoCallback::getTownBySerial(int serialId)
 	ASSERT_IF_CALLED_WITH_PLAYER
 	const PlayerState *p = getPlayerState(*getPlayerID());
 	ERROR_RET_VAL_IF(!p, "No player info", nullptr);
-	ERROR_RET_VAL_IF(serialId < 0 || serialId >= p->towns.size(), "No player info", nullptr);
-	return p->towns[serialId];
+	ERROR_RET_VAL_IF(serialId < 0 || serialId >= p->getTowns().size(), "No player info", nullptr);
+	return p->getTowns()[serialId];
 }
 
 int CPlayerSpecificInfoCallback::getResourceAmount(GameResID type) const

+ 1 - 2
lib/CGameInfoCallback.h

@@ -23,7 +23,7 @@ struct InfoWindow;
 struct PlayerSettings;
 struct CPackForClient;
 struct TerrainTile;
-struct PlayerState;
+class PlayerState;
 class CTown;
 struct StartInfo;
 struct CPathsInfo;
@@ -247,7 +247,6 @@ public:
 	virtual const CGTownInstance* getTownBySerial(int serialId) const; // serial id is [0, number of towns)
 	virtual const CGHeroInstance* getHeroBySerial(int serialId, bool includeGarrisoned=true) const; // serial id is [0, number of heroes)
 	virtual std::vector <const CGHeroInstance *> getHeroesInfo(bool onlyOur = true) const; //true -> only owned; false -> all visible
-	virtual std::vector <const CGDwelling *> getMyDwellings() const; //returns all dwellings that belong to player
 	virtual std::vector <const CGObjectInstance * > getMyObjects() const; //returns all objects flagged by belonging player
 	virtual std::vector <QuestInfo> getMyQuests() const;
 

+ 1 - 0
lib/CMakeLists.txt

@@ -504,6 +504,7 @@ set(lib_MAIN_HEADERS
 	mapObjects/CRewardableObject.h
 	mapObjects/IMarket.h
 	mapObjects/IObjectInterface.h
+	mapObjects/IOwnableObject.h
 	mapObjects/MapObjects.h
 	mapObjects/MiscObjects.h
 	mapObjects/ObjectTemplate.h

+ 53 - 0
lib/CPlayerState.cpp

@@ -10,6 +10,9 @@
 #include "StdInc.h"
 
 #include "CPlayerState.h"
+#include "mapObjects/CGDwelling.h"
+#include "mapObjects/CGTownInstance.h"
+#include "mapObjects/CGHeroInstance.h"
 #include "gameState/QuestInfo.h"
 #include "texts/CGeneralTextHandler.h"
 #include "VCMI_Lib.h"
@@ -90,4 +93,54 @@ int PlayerState::getResourceAmount(int type) const
 	return vstd::atOrDefault(resources, static_cast<size_t>(type), 0);
 }
 
+template<typename T>
+std::vector<T> PlayerState::getObjectsOfType() const
+{
+	std::vector<T> result;
+	for (auto const & object : ownedObjects)
+	{
+		auto casted = dynamic_cast<T>(object);
+		if (casted)
+			result.push_back(casted);
+	}
+	return result;
+}
+
+std::vector<const CGHeroInstance *> PlayerState::getHeroes() const
+{
+	return getObjectsOfType<const CGHeroInstance *>();
+}
+
+std::vector<const CGTownInstance *> PlayerState::getTowns() const
+{
+	return getObjectsOfType<const CGTownInstance *>();
+}
+
+std::vector<CGHeroInstance *> PlayerState::getHeroes()
+{
+	return getObjectsOfType<CGHeroInstance *>();
+}
+
+std::vector<CGTownInstance *> PlayerState::getTowns()
+{
+	return getObjectsOfType<CGTownInstance *>();
+}
+
+std::vector<const CGObjectInstance *> PlayerState::getOwnedObjects() const
+{
+	return {ownedObjects.begin(), ownedObjects.end()};
+}
+
+void PlayerState::addOwnedObject(CGObjectInstance * object)
+{
+	assert(object->asOwnable() != nullptr);
+	ownedObjects.push_back(object);
+}
+
+void PlayerState::removeOwnedObject(CGObjectInstance * object)
+{
+	vstd::erase(ownedObjects, object);
+}
+
+
 VCMI_LIB_NAMESPACE_END

+ 33 - 9
lib/CPlayerState.h

@@ -20,12 +20,13 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
+class CGObjectInstance;
 class CGHeroInstance;
 class CGTownInstance;
 class CGDwelling;
 struct QuestInfo;
 
-struct DLL_LINKAGE PlayerState : public CBonusSystemNode, public Player
+class DLL_LINKAGE PlayerState : public CBonusSystemNode, public Player
 {
 	struct VisitedObjectGlobal
 	{
@@ -47,6 +48,11 @@ struct DLL_LINKAGE PlayerState : public CBonusSystemNode, public Player
 		}
 	};
 
+	std::vector<CGObjectInstance*> ownedObjects;
+
+	template<typename T>
+	std::vector<T> getObjectsOfType() const;
+
 public:
 	PlayerColor color;
 	bool human; //true if human controlled player, false for AI
@@ -55,12 +61,8 @@ public:
 
 	/// list of objects that were "destroyed" by player, either via simple pick-up (e.g. resources) or defeated heroes or wandering monsters
 	std::set<ObjectInstanceID> destroyedObjects;
-
 	std::set<ObjectInstanceID> visitedObjects; // as a std::set, since most accesses here will be from visited status checks
 	std::set<VisitedObjectGlobal> visitedObjectsGlobal;
-	std::vector<ConstTransitivePtr<CGHeroInstance> > heroes;
-	std::vector<ConstTransitivePtr<CGTownInstance> > towns;
-	std::vector<ConstTransitivePtr<CGDwelling> > dwellings; //used for town growth
 	std::vector<QuestInfo> quests; //store info about all received quests
 	std::vector<Bonus> battleBonuses; //additional bonuses to be added during battle with neutrals
 	std::map<uint32_t, std::map<ArtifactPosition, ArtifactID>> costumesArtifacts;
@@ -90,9 +92,19 @@ public:
 	std::string getNameTextID() const override;
 	void registerIcons(const IconRegistar & cb) const override;
 
+	std::vector<const CGHeroInstance* > getHeroes() const;
+	std::vector<const CGTownInstance* > getTowns() const;
+	std::vector<CGHeroInstance* > getHeroes();
+	std::vector<CGTownInstance* > getTowns();
+
+	std::vector<const CGObjectInstance* > getOwnedObjects() const;
+
+	void addOwnedObject(CGObjectInstance * object);
+	void removeOwnedObject(CGObjectInstance * object);
+
 	bool checkVanquished() const
 	{
-		return heroes.empty() && towns.empty();
+		return ownedObjects.empty();
 	}
 
 	template <typename Handler> void serialize(Handler &h)
@@ -103,9 +115,21 @@ public:
 		h & resources;
 		h & status;
 		h & turnTimer;
-		h & heroes;
-		h & towns;
-		h & dwellings;
+
+		if (h.version >= Handler::Version::PLAYER_STATE_OWNED_OBJECTS)
+		{
+			h & ownedObjects;
+		}
+		else
+		{
+			std::vector<const CGObjectInstance* > heroes;
+			std::vector<const CGObjectInstance* > towns;
+			std::vector<const CGObjectInstance* > dwellings;
+
+			h & heroes;
+			h & towns;
+			h & dwellings;
+		}
 		h & quests;
 		h & visitedObjects;
 		h & visitedObjectsGlobal;

+ 1 - 1
lib/IGameCallback.h

@@ -140,7 +140,7 @@ public:
 	virtual void sendAndApply(CPackForClient * pack) = 0;
 	virtual void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2)=0; //when two heroes meet on adventure map
 	virtual void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) = 0;
-	virtual void changeFogOfWar(std::unordered_set<int3> &tiles, PlayerColor player, ETileVisibility mode) = 0;
+	virtual void changeFogOfWar(const std::unordered_set<int3> &tiles, PlayerColor player, ETileVisibility mode) = 0;
 	
 	virtual void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) = 0;
 

+ 6 - 0
lib/ResourceSet.cpp

@@ -64,6 +64,12 @@ void ResourceSet::positive()
 		vstd::amax(elem, 0);
 }
 
+void ResourceSet::applyHandicap(int percentage)
+{
+	for(auto & elem : *this)
+		elem = vstd::divideAndCeil(elem * percentage, 100);
+}
+
 static bool canAfford(const ResourceSet &res, const ResourceSet &price)
 {
 	assert(res.size() == price.size() && price.size() == GameConstants::RESOURCE_QUANTITY);

+ 1 - 0
lib/ResourceSet.h

@@ -190,6 +190,7 @@ public:
 	DLL_LINKAGE void amax(const TResourceCap &val); //performs vstd::amax on each element
 	DLL_LINKAGE void amin(const TResourceCap &val); //performs vstd::amin on each element
 	DLL_LINKAGE void positive(); //values below 0 are set to 0 - upgrade cost can't be negative, for example
+	DLL_LINKAGE void applyHandicap(int percentage);
 	DLL_LINKAGE bool nonZero() const; //returns true if at least one value is non-zero;
 	DLL_LINKAGE bool canAfford(const ResourceSet &price) const;
 	DLL_LINKAGE bool canBeAfforded(const ResourceSet &res) const;

+ 1 - 1
lib/bonuses/BonusEnum.h

@@ -150,7 +150,7 @@ class JsonNode;
 	BONUS_NAME(GARGOYLE) /* gargoyle is special than NON_LIVING, cannot be rised or healed */ \
 	BONUS_NAME(SPECIAL_ADD_VALUE_ENCHANT) /*specialty spell like Aenin has, increased effect of spell, additionalInfo = value to add*/\
 	BONUS_NAME(SPECIAL_FIXED_VALUE_ENCHANT) /*specialty spell like Melody has, constant spell effect (i.e. 3 luck), additionalInfo = value to fix.*/\
-	BONUS_NAME(TOWN_MAGIC_WELL) /*one-time pseudo-bonus to implement Magic Well in the town*/\
+	BONUS_NAME(THIEVES_GUILD_ACCESS) \
 	BONUS_NAME(LIMITED_SHOOTING_RANGE) /*limits range of shooting creatures, doesn't adjust any other mechanics (half vs full damage etc). val - range in hexes, additional info - optional new range for broken arrow mechanic */\
 	BONUS_NAME(LEARN_BATTLE_SPELL_CHANCE) /*skill-agnostic eagle eye chance. subtype = 0 - from enemy, 1 - TODO: from entire battlefield*/\
 	BONUS_NAME(LEARN_BATTLE_SPELL_LEVEL_LIMIT) /*skill-agnostic eagle eye limit, subtype - school (-1 for all), others TODO*/\

+ 10 - 7
lib/constants/Enumerations.h

@@ -26,18 +26,11 @@ namespace BuildingSubID
 		DEFAULT = -50,
 		NONE = -1,
 		CASTLE_GATE,
-		CREATURE_TRANSFORMER,
 		MYSTIC_POND,
-		FOUNTAIN_OF_FORTUNE,
-		ARTIFACT_MERCHANT,
 		LIBRARY,
 		PORTAL_OF_SUMMONING,
 		ESCAPE_TUNNEL,
-		FREELANCERS_GUILD,
-		BALLISTA_YARD,
-		MAGIC_UNIVERSITY,
 		TREASURY,
-		THIEVES_GUILD,
 		BANK
 	};
 }
@@ -256,4 +249,14 @@ enum class EMapLevel : int8_t
 	UNDERGROUND = 1
 };
 
+enum class EWeekType : int8_t
+{
+	FIRST_WEEK,
+	NORMAL,
+	DOUBLE_GROWTH,
+	BONUS_GROWTH,
+	DEITYOFFIRE,
+	PLAGUE
+};
+
 VCMI_LIB_NAMESPACE_END

+ 0 - 7
lib/constants/StringConstants.h

@@ -178,18 +178,11 @@ namespace MappedKeys
 	static const std::map<std::string, BuildingSubID::EBuildingSubID> SPECIAL_BUILDINGS =
 	{
 		{ "mysticPond", BuildingSubID::MYSTIC_POND },
-		{ "artifactMerchant", BuildingSubID::ARTIFACT_MERCHANT },
-		{ "freelancersGuild", BuildingSubID::FREELANCERS_GUILD },
-		{ "magicUniversity", BuildingSubID::MAGIC_UNIVERSITY },
 		{ "castleGate", BuildingSubID::CASTLE_GATE },
-		{ "creatureTransformer", BuildingSubID::CREATURE_TRANSFORMER },//only skeleton transformer yet
 		{ "portalOfSummoning", BuildingSubID::PORTAL_OF_SUMMONING },
-		{ "ballistaYard", BuildingSubID::BALLISTA_YARD },
 		{ "library", BuildingSubID::LIBRARY },
-		{ "fountainOfFortune", BuildingSubID::FOUNTAIN_OF_FORTUNE },//luck garrison bonus
 		{ "escapeTunnel", BuildingSubID::ESCAPE_TUNNEL },
 		{ "treasury", BuildingSubID::TREASURY },
-		{ "thievesGuild", BuildingSubID::THIEVES_GUILD },
 		{ "bank", BuildingSubID::BANK }
 	};
 

+ 1 - 0
lib/entities/building/CBuilding.h

@@ -34,6 +34,7 @@ public:
 	TResources resources;
 	TResources produce;
 	TRequired requirements;
+	ArtifactID warMachine;
 	std::set<EMarketMode> marketModes;
 
 	BuildingID bid; //structure ID

+ 1 - 1
lib/entities/faction/CTown.h

@@ -69,7 +69,7 @@ public:
 	std::map<int,int> hordeLvl; //[0] - first horde building creature level; [1] - second horde building (-1 if not present)
 	ui32 mageLevel; //max available mage guild level
 	GameResID primaryRes;
-	ArtifactID warMachine;
+	CreatureID warMachineDeprecated;
 	SpellID moatAbility;
 
 	// default chance for hero of specific class to appear in tavern, if field "tavern" was not set

+ 15 - 23
lib/entities/faction/CTownHandler.cpp

@@ -324,6 +324,14 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons
 	}
 	loadBuildingRequirements(ret, source["requires"], requirementsToLoad);
 
+	if (!source["warMachine"].isNull())
+	{
+		VLC->identifiers()->requestIdentifier("artifact", source["warMachine"], [=](si32 identifier)
+		{
+			ret->warMachine = ArtifactID(identifier);
+		});
+	}
+
 	if (!source["upgrades"].isNull())
 	{
 		// building id and upgrades can't be the same
@@ -552,7 +560,13 @@ void CTownHandler::loadTown(CTown * town, const JsonNode & source)
 	else
 		town->primaryRes = GameResID(resIter - std::begin(GameConstants::RESOURCE_NAMES));
 
-	warMachinesToLoad[town] = source["warMachine"];
+	if (!source["warMachine"].isNull())
+	{
+		VLC->identifiers()->requestIdentifier( "creature", source["warMachine"], [=](si32 creatureID)
+		{
+			town->warMachineDeprecated = creatureID;
+		});
+	}
 
 	town->mageLevel = static_cast<ui32>(source["mageGuild"].Float());
 
@@ -848,7 +862,6 @@ void CTownHandler::beforeValidate(JsonNode & object)
 void CTownHandler::afterLoadFinalization()
 {
 	initializeRequirements();
-	initializeWarMachines();
 }
 
 void CTownHandler::initializeRequirements()
@@ -878,27 +891,6 @@ void CTownHandler::initializeRequirements()
 	requirementsToLoad.clear();
 }
 
-void CTownHandler::initializeWarMachines()
-{
-	// must be done separately after all objects are loaded
-	for(auto & p : warMachinesToLoad)
-	{
-		CTown * t = p.first;
-		JsonNode creatureKey = p.second;
-
-		auto ret = VLC->identifiers()->getIdentifier("creature", creatureKey, false);
-
-		if(ret)
-		{
-			const CCreature * creature = CreatureID(*ret).toCreature();
-
-			t->warMachine = creature->warMachine;
-		}
-	}
-
-	warMachinesToLoad.clear();
-}
-
 std::set<FactionID> CTownHandler::getDefaultAllowed() const
 {
 	std::set<FactionID> allowedFactions;

+ 0 - 2
lib/entities/faction/CTownHandler.h

@@ -34,14 +34,12 @@ class DLL_LINKAGE CTownHandler : public CHandlerBase<FactionID, Faction, CFactio
 		CTown * town;
 	};
 
-	std::map<CTown *, JsonNode> warMachinesToLoad;
 	std::vector<BuildingRequirementsHelper> requirementsToLoad;
 	std::vector<BuildingRequirementsHelper> overriddenBidsToLoad; //list of buildings, which bonuses should be overridden.
 
 	static const TPropagatorPtr & emptyPropagator();
 
 	void initializeRequirements();
-	void initializeWarMachines();
 
 	/// loads CBuilding's into town
 	void loadBuildingRequirements(CBuilding * building, const JsonNode & source, std::vector<BuildingRequirementsHelper> & bidsToLoad) const;

+ 30 - 90
lib/gameState/CGameState.cpp

@@ -194,6 +194,7 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, Load::Prog
 	initRandomFactionsForPlayers();
 	randomizeMapObjects();
 	placeStartingHeroes();
+	initOwnedObjects();
 	initDifficulty();
 	initHeroes();
 	initStartingBonus();
@@ -344,6 +345,15 @@ void CGameState::initCampaign()
 	map = campaign->getCurrentMap().release();
 }
 
+void CGameState::generateOwnedObjectsAfterDeserialize()
+{
+	for (auto & object : map->objects)
+	{
+		if (object && object->asOwnable() && object->getOwner().isValidPlayer())
+			players.at(object->getOwner()).addOwnedObject(object.get());
+	}
+}
+
 void CGameState::initGlobalBonuses()
 {
 	const JsonNode & baseBonuses = VLC->settings()->getValue(EGameSettings::BONUSES_GLOBAL);
@@ -486,6 +496,15 @@ void CGameState::randomizeMapObjects()
 	}
 }
 
+void CGameState::initOwnedObjects()
+{
+	for(CGObjectInstance *object : map->objects)
+	{
+		if (object && object->getOwner().isValidPlayer())
+			getPlayerState(object->getOwner())->addOwnedObject(object);
+	}
+}
+
 void CGameState::initPlayerStates()
 {
 	logGlobal->debug("\tCreating player entries in gs");
@@ -574,7 +593,6 @@ void CGameState::initHeroes()
 		}
 
 		hero->initHero(getRandomGenerator());
-		getPlayerState(hero->getOwner())->heroes.push_back(hero);
 		map->allHeroes[hero->getHeroType().getNum()] = hero;
 	}
 
@@ -699,14 +717,14 @@ void CGameState::initStartingBonus()
 			}
 		case PlayerStartingBonus::ARTIFACT:
 			{
-				if(elem.second.heroes.empty())
+				if(elem.second.getHeroes().empty())
 				{
 					logGlobal->error("Cannot give starting artifact - no heroes!");
 					break;
 				}
 				const Artifact * toGive = pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE).toEntity(VLC);
 
-				CGHeroInstance *hero = elem.second.heroes[0];
+				CGHeroInstance *hero = elem.second.getHeroes()[0];
 				if(!giveHeroArtifact(hero, toGive->getId()))
 					logGlobal->error("Cannot give starting artifact - no free slots!");
 			}
@@ -893,8 +911,6 @@ void CGameState::initTowns()
 			vti->possibleSpells -= s->id;
 		}
 		vti->possibleSpells.clear();
-		if(vti->getOwner() != PlayerColor::NEUTRAL)
-			getPlayerState(vti->getOwner())->towns.emplace_back(vti);
 	}
 }
 
@@ -937,9 +953,9 @@ void CGameState::placeHeroesInTowns()
 		if(player.first == PlayerColor::NEUTRAL)
 			continue;
 
-		for(CGHeroInstance * h : player.second.heroes)
+		for(CGHeroInstance * h : player.second.getHeroes())
 		{
-			for(CGTownInstance * t : player.second.towns)
+			for(CGTownInstance * t : player.second.getTowns())
 			{
 				if(h->visitablePos().z != t->visitablePos().z)
 					continue;
@@ -971,9 +987,9 @@ void CGameState::initVisitingAndGarrisonedHeroes()
 			continue;
 
 		//init visiting and garrisoned heroes
-		for(CGHeroInstance * h : player.second.heroes)
+		for(CGHeroInstance * h : player.second.getHeroes())
 		{
-			for(CGTownInstance * t : player.second.towns)
+			for(CGTownInstance * t : player.second.getTowns())
 			{
 				if(h->visitablePos().z != t->visitablePos().z)
 					continue;
@@ -1197,82 +1213,6 @@ int3 CGameState::guardingCreaturePosition (int3 pos) const
 	return gs->map->guardingCreaturePositions[pos.z][pos.x][pos.y];
 }
 
-RumorState CGameState::pickNewRumor()
-{
-	RumorState newRumor;
-
-	static const std::vector<RumorState::ERumorType> rumorTypes = {RumorState::TYPE_MAP, RumorState::TYPE_SPECIAL, RumorState::TYPE_RAND, RumorState::TYPE_RAND};
-	std::vector<RumorState::ERumorTypeSpecial> sRumorTypes = {
-		RumorState::RUMOR_OBELISKS, RumorState::RUMOR_ARTIFACTS, RumorState::RUMOR_ARMY, RumorState::RUMOR_INCOME};
-	if(map->grailPos.valid()) // Grail should always be on map, but I had related crash I didn't manage to reproduce
-		sRumorTypes.push_back(RumorState::RUMOR_GRAIL);
-
-	int rumorId = -1;
-	int rumorExtra = -1;
-	auto & rand = getRandomGenerator();
-	newRumor.type = *RandomGeneratorUtil::nextItem(rumorTypes, rand);
-
-	do
-	{
-		switch(newRumor.type)
-		{
-		case RumorState::TYPE_SPECIAL:
-		{
-			SThievesGuildInfo tgi;
-			obtainPlayersStats(tgi, 20);
-			rumorId = *RandomGeneratorUtil::nextItem(sRumorTypes, rand);
-			if(rumorId == RumorState::RUMOR_GRAIL)
-			{
-				rumorExtra = getTile(map->grailPos)->terType->getIndex();
-				break;
-			}
-
-			std::vector<PlayerColor> players = {};
-			switch(rumorId)
-			{
-			case RumorState::RUMOR_OBELISKS:
-				players = tgi.obelisks[0];
-				break;
-
-			case RumorState::RUMOR_ARTIFACTS:
-				players = tgi.artifacts[0];
-				break;
-
-			case RumorState::RUMOR_ARMY:
-				players = tgi.army[0];
-				break;
-
-			case RumorState::RUMOR_INCOME:
-				players = tgi.income[0];
-				break;
-			}
-			rumorExtra = RandomGeneratorUtil::nextItem(players, rand)->getNum();
-
-			break;
-		}
-		case RumorState::TYPE_MAP:
-			// Makes sure that map rumors only used if there enough rumors too choose from
-			if(!map->rumors.empty() && (map->rumors.size() > 1 || !currentRumor.last.count(RumorState::TYPE_MAP)))
-			{
-				rumorId = rand.nextInt((int)map->rumors.size() - 1);
-				break;
-			}
-			else
-				newRumor.type = RumorState::TYPE_RAND;
-			[[fallthrough]];
-
-		case RumorState::TYPE_RAND:
-			auto vector = VLC->generaltexth->findStringsWithPrefix("core.randtvrn");
-			rumorId = rand.nextInt((int)vector.size() - 1);
-
-			break;
-		}
-	}
-	while(!newRumor.update(rumorId, rumorExtra));
-
-	return newRumor;
-}
-
 bool CGameState::isVisible(int3 pos, const std::optional<PlayerColor> & player) const
 {
 	if (!map->isInTheMap(pos))
@@ -1371,7 +1311,7 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio
 		}
 		case EventCondition::HAVE_ARTIFACT: //check if any hero has winning artifact
 		{
-			for(const auto & elem : p->heroes)
+			for(const auto & elem : p->getHeroes())
 				if(elem->hasArt(condition.objectType.as<ArtifactID>()))
 					return true;
 			return false;
@@ -1405,7 +1345,7 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio
 			}
 			else // any town
 			{
-				for (const CGTownInstance * t : p->towns)
+				for (const CGTownInstance * t : p->getTowns())
 				{
 					if (t->hasBuilt(condition.objectType.as<BuildingID>()))
 						return true;
@@ -1550,9 +1490,9 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level)
 	if(level >= 0) //num of towns & num of heroes
 	{
 		//num of towns
-		FILL_FIELD(numOfTowns, g->second.towns.size())
+		FILL_FIELD(numOfTowns, g->second.getTowns().size())
 		//num of heroes
-		FILL_FIELD(numOfHeroes, g->second.heroes.size())
+		FILL_FIELD(numOfHeroes, g->second.getHeroes().size())
 	}
 	if(level >= 1) //best hero's portrait
 	{
@@ -1624,7 +1564,7 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level)
 			if(playerInactive(player.second.color)) //do nothing for neutral player
 				continue;
 			CreatureID bestCre; //best creature's ID
-			for(const auto & elem : player.second.heroes)
+			for(const auto & elem : player.second.getHeroes())
 			{
 				for(const auto & it : elem->Slots())
 				{

+ 5 - 1
lib/gameState/CGameState.h

@@ -108,7 +108,6 @@ public:
 	void calculatePaths(const std::shared_ptr<PathfinderConfig> & config) override;
 	int3 guardingCreaturePosition (int3 pos) const override;
 	std::vector<CGObjectInstance*> guardingCreatures (int3 pos) const;
-	RumorState pickNewRumor();
 
 	/// Gets a artifact ID randomly and removes the selected artifact from this handler.
 	ArtifactID pickRandomArtifact(vstd::RNG & rand, int flags);
@@ -157,6 +156,8 @@ public:
 		h & day;
 		h & map;
 		h & players;
+		if (h.version < Handler::Version::PLAYER_STATE_OWNED_OBJECTS)
+			generateOwnedObjectsAfterDeserialize();
 		h & teams;
 		h & heroesPool;
 		h & globalEffects;
@@ -180,6 +181,7 @@ private:
 	void initGlobalBonuses();
 	void initGrailPosition();
 	void initRandomFactionsForPlayers();
+	void initOwnedObjects();
 	void randomizeMapObjects();
 	void initPlayerStates();
 	void placeStartingHeroes();
@@ -196,6 +198,8 @@ private:
 	void initVisitingAndGarrisonedHeroes();
 	void initCampaign();
 
+	void generateOwnedObjectsAfterDeserialize();
+
 	// ----- bonus system handling -----
 
 	void buildBonusSystemTree();

+ 1 - 1
lib/gameState/CGameStateCampaign.cpp

@@ -536,7 +536,7 @@ void CGameStateCampaign::initHeroes()
 		}
 		assert(humanPlayer != PlayerColor::NEUTRAL);
 
-		std::vector<ConstTransitivePtr<CGHeroInstance> > & heroes = gameState->players[humanPlayer].heroes;
+		const auto & heroes = gameState->players[humanPlayer].getHeroes();
 
 		if (chosenBonus->info1 == 0xFFFD) //most powerful
 		{

+ 21 - 13
lib/gameState/GameStatistics.cpp

@@ -50,10 +50,10 @@ StatisticDataSetEntry StatisticDataSet::createEntry(const PlayerState * ps, cons
 	data.isHuman = ps->isHuman();
 	data.status = ps->status;
 	data.resources = ps->resources;
-	data.numberHeroes = ps->heroes.size();
+	data.numberHeroes = ps->getHeroes().size();
 	data.numberTowns = gs->howManyTowns(ps->color);
 	data.numberArtifacts = Statistic::getNumberOfArts(ps);
-	data.numberDwellings = gs->getPlayerState(ps->color)->dwellings.size();
+	data.numberDwellings = Statistic::getNumberOfDwellings(ps);
 	data.armyStrength = Statistic::getArmyStrength(ps, true);
 	data.totalExperience = Statistic::getTotalExperience(ps);
 	data.income = Statistic::getIncome(gs, ps);
@@ -221,19 +221,29 @@ std::vector<const CGMine *> Statistic::getMines(const CGameState * gs, const Pla
 int Statistic::getNumberOfArts(const PlayerState * ps)
 {
 	int ret = 0;
-	for(auto h : ps->heroes)
+	for(auto h : ps->getHeroes())
 	{
 		ret += h->artifactsInBackpack.size() + h->artifactsWorn.size();
 	}
 	return ret;
 }
 
+int Statistic::getNumberOfDwellings(const PlayerState * ps)
+{
+	int ret = 0;
+	for(const auto * obj : ps->getOwnedObjects())
+		if (!obj->asOwnable()->providedCreatures().empty())
+			ret	+= 1;
+
+	return ret;
+}
+
 // get total strength of player army
 si64 Statistic::getArmyStrength(const PlayerState * ps, bool withTownGarrison)
 {
 	si64 str = 0;
 
-	for(auto h : ps->heroes)
+	for(auto h : ps->getHeroes())
 	{
 		if(!h->inTownGarrison || withTownGarrison)		//original h3 behavior
 			str += h->getArmyStrength();
@@ -246,7 +256,7 @@ si64 Statistic::getTotalExperience(const PlayerState * ps)
 {
 	si64 tmp = 0;
 
-	for(auto h : ps->heroes)
+	for(auto h : ps->getHeroes())
 		tmp += h->exp;
 	
 	return tmp;
@@ -255,20 +265,18 @@ si64 Statistic::getTotalExperience(const PlayerState * ps)
 // get total gold income
 int Statistic::getIncome(const CGameState * gs, const PlayerState * ps)
 {
-	int percentIncome = gs->getStartInfo()->getIthPlayersSettings(ps->color).handicap.percentIncome;
 	int totalIncome = 0;
 
 	//Heroes can produce gold as well - skill, specialty or arts
-	for(const auto & h : ps->heroes)
-		totalIncome += h->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(GameResID::GOLD)))) * percentIncome / 100;
+	for(const auto & h : ps->getHeroes())
+		totalIncome += h->dailyIncome()[EGameResID::GOLD];
 
 	//Add town income of all towns
-	for(const auto & t : ps->towns)
+	for(const auto & t : ps->getTowns())
 		totalIncome += t->dailyIncome()[EGameResID::GOLD];
 
 	for(const CGMine * mine : getMines(gs, ps))
-		if(mine->producedResource == EGameResID::GOLD)
-			totalIncome += mine->getProducedQuantity();
+			totalIncome += mine->dailyIncome()[EGameResID::GOLD];
 
 	return totalIncome;
 }
@@ -297,7 +305,7 @@ float Statistic::getMapExploredRatio(const CGameState * gs, PlayerColor player)
 
 const CGHeroInstance * Statistic::findBestHero(const CGameState * gs, const PlayerColor & color)
 {
-	auto &h = gs->players.at(color).heroes;
+	const auto &h = gs->players.at(color).getHeroes();
 	if(h.empty())
 		return nullptr;
 	//best hero will be that with highest exp
@@ -370,7 +378,7 @@ float Statistic::getTownBuiltRatio(const PlayerState * ps)
 	float built = 0.0;
 	float total = 0.0;
 
-	for(const auto & t : ps->towns)
+	for(const auto & t : ps->getTowns())
 	{
 		built += t->getBuildings().size();
 		for(const auto & b : t->town->buildings)

+ 2 - 1
lib/gameState/GameStatistics.h

@@ -14,7 +14,7 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-struct PlayerState;
+class PlayerState;
 class CGameState;
 class CGHeroInstance;
 class CGMine;
@@ -158,6 +158,7 @@ class DLL_LINKAGE Statistic
 	static std::vector<const CGMine *> getMines(const CGameState * gs, const PlayerState * ps);
 public:
 	static int getNumberOfArts(const PlayerState * ps);
+	static int getNumberOfDwellings(const PlayerState * ps);
 	static si64 getArmyStrength(const PlayerState * ps, bool withTownGarrison = false);
 	static si64 getTotalExperience(const PlayerState * ps);
 	static int getIncome(const CGameState * gs, const PlayerState * ps);

+ 2 - 2
lib/gameState/HighScore.cpp

@@ -29,10 +29,10 @@ HighScoreParameter HighScore::prepareHighScores(const CGameState * gs, PlayerCol
 	param.townAmount = gs->howManyTowns(player);
 	param.usedCheat = gs->getPlayerState(player)->cheated;
 	param.hasGrail = false;
-	for(const CGHeroInstance * h : playerState->heroes)
+	for(const CGHeroInstance * h : playerState->getHeroes())
 		if(h->hasArt(ArtifactID::GRAIL))
 			param.hasGrail = true;
-	for(const CGTownInstance * t : playerState->towns)
+	for(const CGTownInstance * t : playerState->getTowns())
 		if(t->hasBuilt(BuildingID::GRAIL))
 			param.hasGrail = true;
 	param.allEnemiesDefeated = true;

+ 22 - 17
lib/mapObjects/CGDwelling.cpp

@@ -182,10 +182,6 @@ void CGDwelling::initObj(vstd::RNG & rand)
 	case Obj::CREATURE_GENERATOR4:
 		{
 			getObjectHandler()->configureObject(this, rand);
-
-			if (getOwner() != PlayerColor::NEUTRAL)
-				cb->gameState()->players[getOwner()].dwellings.emplace_back(this);
-
 			assert(!creatures.empty());
 			assert(!creatures[0].second.empty());
 			break;
@@ -211,19 +207,6 @@ void CGDwelling::setPropertyDer(ObjProperty what, ObjPropertyID identifier)
 {
 	switch (what)
 	{
-		case ObjProperty::OWNER: //change owner
-			if (ID == Obj::CREATURE_GENERATOR1 || ID == Obj::CREATURE_GENERATOR2
-				|| ID == Obj::CREATURE_GENERATOR3 || ID == Obj::CREATURE_GENERATOR4)
-			{
-				if (tempOwner != PlayerColor::NEUTRAL)
-				{
-					std::vector<ConstTransitivePtr<CGDwelling> >* dwellings = &cb->gameState()->players[tempOwner].dwellings;
-					dwellings->erase (std::find(dwellings->begin(), dwellings->end(), this));
-				}
-				if (identifier.as<PlayerColor>().isValidPlayer())
-					cb->gameState()->players[identifier.as<PlayerColor>()].dwellings.emplace_back(this);
-			}
-			break;
 		case ObjProperty::AVAILABLE_CREATURE:
 			creatures.resize(1);
 			creatures[0].second.resize(1);
@@ -551,4 +534,26 @@ void CGDwelling::serializeJsonOptions(JsonSerializeFormat & handler)
 	}
 }
 
+const IOwnableObject * CGDwelling::asOwnable() const
+{
+	return this;
+}
+
+ResourceSet CGDwelling::dailyIncome() const
+{
+	return {};
+}
+
+std::vector<CreatureID> CGDwelling::providedCreatures() const
+{
+	if (ID == Obj::WAR_MACHINE_FACTORY || ID == Obj::REFUGEE_CAMP)
+		return {};
+
+	std::vector<CreatureID> result;
+	for (const auto & level : creatures)
+		result.insert(result.end(), level.second.begin(), level.second.end());
+
+	return result;
+}
+
 VCMI_LIB_NAMESPACE_END

+ 6 - 1
lib/mapObjects/CGDwelling.h

@@ -11,6 +11,7 @@
 #pragma once
 
 #include "CArmedInstance.h"
+#include "IOwnableObject.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -30,7 +31,7 @@ public:
 	void serializeJson(JsonSerializeFormat & handler);
 };
 
-class DLL_LINKAGE CGDwelling : public CArmedInstance
+class DLL_LINKAGE CGDwelling : public CArmedInstance, public IOwnableObject
 {
 public:
 	using TCreaturesSet = std::vector<std::pair<ui32, std::vector<CreatureID> > >;
@@ -41,6 +42,10 @@ public:
 	CGDwelling(IGameCallback *cb);
 	~CGDwelling() override;
 
+	const IOwnableObject * asOwnable() const final;
+	ResourceSet dailyIncome() const override;
+	std::vector<CreatureID> providedCreatures() const override;
+
 protected:
 	void serializeJsonOptions(JsonSerializeFormat & handler) override;
 

+ 23 - 0
lib/mapObjects/CGHeroInstance.cpp

@@ -1822,4 +1822,27 @@ bool CGHeroInstance::isCampaignGem() const
 	return true;
 }
 
+ResourceSet CGHeroInstance::dailyIncome() const
+{
+	ResourceSet income;
+
+	for (GameResID k : GameResID::ALL_RESOURCES())
+		income[k] += valOfBonuses(BonusType::GENERATE_RESOURCE, BonusSubtypeID(k));
+
+	const auto & playerSettings = cb->getPlayerSettings(getOwner());
+	income.applyHandicap(playerSettings->handicap.percentIncome);
+	return income;
+}
+
+std::vector<CreatureID> CGHeroInstance::providedCreatures() const
+{
+	return {};
+}
+
+const IOwnableObject * CGHeroInstance::asOwnable() const
+{
+	return this;
+}
+
+
 VCMI_LIB_NAMESPACE_END

+ 6 - 1
lib/mapObjects/CGHeroInstance.h

@@ -12,6 +12,7 @@
 #include <vcmi/spells/Caster.h>
 
 #include "CArmedInstance.h"
+#include "IOwnableObject.h"
 
 #include "../CArtHandler.h" // For CArtifactSet
 
@@ -48,7 +49,7 @@ protected:
 };
 
 
-class DLL_LINKAGE CGHeroInstance : public CArmedInstance, public IBoatGenerator, public CArtifactSet, public spells::Caster, public AFactionMember, public ICreatureUpgrader
+class DLL_LINKAGE CGHeroInstance : public CArmedInstance, public IBoatGenerator, public CArtifactSet, public spells::Caster, public AFactionMember, public ICreatureUpgrader, public IOwnableObject
 {
 	// We serialize heroes into JSON for crossover
 	friend class CampaignState;
@@ -165,6 +166,10 @@ public:
 	EAlignment getAlignment() const;
 	bool needsLastStack()const override;
 
+	ResourceSet dailyIncome() const override;
+	std::vector<CreatureID> providedCreatures() const override;
+	const IOwnableObject * asOwnable() const final;
+
 	//INativeTerrainProvider
 	FactionID getFaction() const override;
 	TerrainId getNativeTerrain() const override;

+ 5 - 0
lib/mapObjects/CGObjectInstance.cpp

@@ -395,4 +395,9 @@ BattleField CGObjectInstance::getBattlefield() const
 	return VLC->objtypeh->getHandlerFor(ID, subID)->getBattlefield();
 }
 
+const IOwnableObject * CGObjectInstance::asOwnable() const
+{
+	return nullptr;
+}
+
 VCMI_LIB_NAMESPACE_END

+ 2 - 0
lib/mapObjects/CGObjectInstance.h

@@ -126,6 +126,8 @@ public:
 	virtual std::vector<Component> getPopupComponents(PlayerColor player) const;
 	virtual std::vector<Component> getPopupComponents(const CGHeroInstance * hero) const;
 
+	const IOwnableObject * asOwnable() const override;
+
 	/** OVERRIDES OF IObjectInterface **/
 
 	void initObj(vstd::RNG & rand) override;

+ 47 - 33
lib/mapObjects/CGTownInstance.cpp

@@ -44,13 +44,10 @@ int CGTownInstance::getSightRadius() const //returns sight distance
 
 	for(const auto & bid : builtBuildings)
 	{
-		if(bid.IsSpecialOrGrail())
-		{
-			auto height = town->buildings.at(bid)->height;
-			if(ret < height)
-				ret = height;
+		auto height = town->buildings.at(bid)->height;
+		if(ret < height)
+			ret = height;
 	}
-}
 	return ret;
 }
 
@@ -181,7 +178,7 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const
 	int dwellingBonus = 0;
 	if(const PlayerState *p = cb->getPlayerState(tempOwner, false))
 	{
-		dwellingBonus = getDwellingBonus(creatures[level].second, p->dwellings);
+		dwellingBonus = getDwellingBonus(creatures[level].second, p->getOwnedObjects());
 	}
 	if(dwellingBonus)
 		ret.entries.emplace_back(VLC->generaltexth->allTexts[591], dwellingBonus); // \nExternal dwellings %+d
@@ -192,15 +189,18 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const
 	return ret;
 }
 
-int CGTownInstance::getDwellingBonus(const std::vector<CreatureID>& creatureIds, const std::vector<ConstTransitivePtr<CGDwelling> >& dwellings) const
+int CGTownInstance::getDwellingBonus(const std::vector<CreatureID>& creatureIds, const std::vector<const CGObjectInstance * >& dwellings) const
 {
 	int totalBonus = 0;
 	for (const auto& dwelling : dwellings)
 	{
-		for (const auto& creature : dwelling->creatures)
-		{
-			totalBonus += vstd::contains(creatureIds, creature.second[0]) ? 1 : 0;
-		}
+		const auto & dwellingCreatures = dwelling->asOwnable()->providedCreatures();
+		bool hasMatch = false;
+		for (const auto& creature : dwellingCreatures)
+			hasMatch = vstd::contains(creatureIds, creature);
+
+		if (hasMatch)
+			totalBonus += 1;
 	}
 	return totalBonus;
 }
@@ -225,13 +225,16 @@ TResources CGTownInstance::dailyIncome() const
 		}
 	}
 
-	auto playerSettings = cb->gameState()->scenarioOps->getIthPlayersSettings(getOwner());
-	for(TResources::nziterator it(ret); it.valid(); it++)
-		// always round up income - we don't want to always produce zero if handicap in use
-		ret[it->resType] = vstd::divideAndCeil(ret[it->resType] * playerSettings.handicap.percentIncome, 100);
+	const auto & playerSettings = cb->getPlayerSettings(getOwner());
+	ret.applyHandicap(playerSettings->handicap.percentIncome);
 	return ret;
 }
 
+std::vector<CreatureID> CGTownInstance::providedCreatures() const
+{
+	return {};
+}
+
 bool CGTownInstance::hasFort() const
 {
 	return hasBuilt(BuildingID::FORT);
@@ -478,20 +481,6 @@ void CGTownInstance::newTurn(vstd::RNG & rand) const
 {
 	if (cb->getDate(Date::DAY_OF_WEEK) == 1) //reset on new week
 	{
-		//give resources if there's a Mystic Pond
-		if (hasBuilt(BuildingSubID::MYSTIC_POND)
-			&& cb->getDate(Date::DAY) != 1
-			&& (tempOwner.isValidPlayer())
-			)
-		{
-			int resID = rand.nextInt(2, 5); //bonus to random rare resource
-			resID = (resID==2)?1:resID;
-			int resVal = rand.nextInt(1, 4);//with size 1..4
-			cb->giveResource(tempOwner, static_cast<EGameResID>(resID), resVal);
-			cb->setObjPropertyValue(id, ObjProperty::BONUS_VALUE_FIRST, resID);
-			cb->setObjPropertyValue(id, ObjProperty::BONUS_VALUE_SECOND, resVal);
-		}
-
 		if (tempOwner == PlayerColor::NEUTRAL) //garrison growth for neutral towns
 		{
 			std::vector<SlotID> nativeCrits; //slots
@@ -646,9 +635,9 @@ void CGTownInstance::removeCapitols(const PlayerColor & owner) const
 	if (hasCapitol()) // search if there's an older capitol
 	{
 		PlayerState* state = cb->gameState()->getPlayerState(owner); //get all towns owned by player
-		for (auto i = state->towns.cbegin(); i < state->towns.cend(); ++i)
+		for (const auto & town : state->getTowns())
 		{
-			if (*i != this && (*i)->hasCapitol())
+			if (town != this && town->hasCapitol())
 			{
 				RazeStructures rs;
 				rs.tid = id;
@@ -683,7 +672,7 @@ int CGTownInstance::getMarketEfficiency() const
 	assert(p);
 
 	int marketCount = 0;
-	for(const CGTownInstance *t : p->towns)
+	for(const CGTownInstance *t : p->getTowns())
 		if(t->hasBuiltSomeTradeBuilding())
 			marketCount++;
 
@@ -1184,6 +1173,31 @@ TerrainId CGTownInstance::getNativeTerrain() const
 	return town->faction->getNativeTerrain();
 }
 
+ArtifactID CGTownInstance::getWarMachineInBuilding(BuildingID building) const
+{
+	if (builtBuildings.count(building) == 0)
+		return ArtifactID::NONE;
+
+	if (building == BuildingID::BLACKSMITH && town->warMachineDeprecated.hasValue())
+		return town->warMachineDeprecated.toCreature()->warMachine;
+
+	return town->buildings.at(building)->warMachine;
+}
+
+bool CGTownInstance::isWarMachineAvailable(ArtifactID warMachine) const
+{
+	for (auto const & buildingID : builtBuildings)
+		if (town->buildings.at(buildingID)->warMachine == warMachine)
+			return true;
+
+	if (builtBuildings.count(BuildingID::BLACKSMITH) &&
+	   town->warMachineDeprecated.hasValue() &&
+	   town->warMachineDeprecated.toCreature()->warMachine == warMachine)
+		return true;
+
+	return false;
+}
+
 GrowthInfo::Entry::Entry(const std::string &format, int _count)
 	: count(_count)
 {

+ 9 - 2
lib/mapObjects/CGTownInstance.h

@@ -181,7 +181,9 @@ public:
 	std::set<BuildingID> getBuildings() const;
 
 	TResources getBuildingCost(const BuildingID & buildingID) const;
-	TResources dailyIncome() const; //calculates daily income of this town
+	ResourceSet dailyIncome() const override;
+	std::vector<CreatureID> providedCreatures() const override;
+
 	int spellsAtLevel(int level, bool checkGuild) const; //levels are counted from 1 (1 - 5)
 	bool armedGarrison() const; //true if town has creatures in garrison or garrisoned hero
 	int getTownLevel() const;
@@ -206,6 +208,11 @@ public:
 	FactionID getFaction() const override;
 	TerrainId getNativeTerrain() const override;
 
+	/// Returns ID of war machine that is produced by specified building or NONE if this is not built or if building does not produce war machines
+	ArtifactID getWarMachineInBuilding(BuildingID) const;
+	/// Returns true if provided war machine is available in any of built buildings of this town
+	bool isWarMachineAvailable(ArtifactID) const;
+
 	CGTownInstance(IGameCallback *cb);
 	virtual ~CGTownInstance();
 
@@ -236,7 +243,7 @@ private:
 	FactionID randomizeFaction(vstd::RNG & rand);
 	void setOwner(const PlayerColor & owner) const;
 	void onTownCaptured(const PlayerColor & winner) const;
-	int getDwellingBonus(const std::vector<CreatureID>& creatureIds, const std::vector<ConstTransitivePtr<CGDwelling> >& dwellings) const;
+	int getDwellingBonus(const std::vector<CreatureID>& creatureIds, const std::vector<const CGObjectInstance* >& dwellings) const;
 	bool townEnvisagesBuilding(BuildingSubID::EBuildingSubID bid) const;
 	void initializeConfigurableBuildings(vstd::RNG & rand);
 };

+ 3 - 0
lib/mapObjects/IObjectInterface.h

@@ -33,6 +33,7 @@ class ResourceSet;
 class int3;
 class MetaString;
 class PlayerColor;
+class IOwnableObject;
 
 class DLL_LINKAGE IObjectInterface : public GameCallbackHolder, public virtual Serializeable
 {
@@ -68,6 +69,8 @@ public:
 	//unified helper to show info dialog for object owner
 	virtual void showInfoDialog(const ui32 txtID, const ui16 soundID = 0, EInfoWindowMode mode = EInfoWindowMode::AUTO) const;
 
+	virtual const IOwnableObject * asOwnable() const = 0;
+
 	//unified interface, AI helpers
 	virtual bool wasVisited (PlayerColor player) const;
 	virtual bool wasVisited (const CGHeroInstance * h) const;

+ 31 - 0
lib/mapObjects/IOwnableObject.h

@@ -0,0 +1,31 @@
+/*
+* IOwnableObject.h, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+#pragma once
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class ResourceSet;
+class CreatureID;
+
+class DLL_LINKAGE IOwnableObject
+{
+public:
+	/// Fixed daily income of this object
+	/// May not include random or periodical (e.g. weekly) income sources
+	virtual ResourceSet dailyIncome() const = 0;
+
+	/// List of creatures that are provided by this building
+	/// For use in town dwellings growth bonus and for portal of summoning
+	virtual std::vector<CreatureID> providedCreatures() const = 0;
+
+	virtual ~IOwnableObject() = default;
+};
+
+VCMI_LIB_NAMESPACE_END

+ 43 - 12
lib/mapObjects/MiscObjects.cpp

@@ -93,18 +93,6 @@ void CGMine::onHeroVisit( const CGHeroInstance * h ) const
 	}
 
 	flagMine(h->tempOwner);
-
-}
-
-void CGMine::newTurn(vstd::RNG & rand) const
-{
-	if(cb->getDate() == 1)
-		return;
-
-	if (tempOwner == PlayerColor::NEUTRAL)
-		return;
-
-	cb->giveResource(tempOwner, producedResource, getProducedQuantity());
 }
 
 void CGMine::initObj(vstd::RNG & rand)
@@ -139,11 +127,24 @@ bool CGMine::isAbandoned() const
 	return subID.getNum() >= 7;
 }
 
+const IOwnableObject * CGMine::asOwnable() const
+{
+	return this;
+}
+
+std::vector<CreatureID> CGMine::providedCreatures() const
+{
+	return {};
+}
+
 ResourceSet CGMine::dailyIncome() const
 {
 	ResourceSet result;
 	result[producedResource] += defaultResProduction();
 
+	const auto & playerSettings = cb->getPlayerSettings(getOwner());
+	result.applyHandicap(playerSettings->handicap.percentIncome);
+
 	return result;
 }
 
@@ -978,6 +979,21 @@ void CGSignBottle::serializeJsonOptions(JsonSerializeFormat& handler)
 	handler.serializeStruct("text", message);
 }
 
+const IOwnableObject * CGGarrison::asOwnable() const
+{
+	return this;
+}
+
+ResourceSet CGGarrison::dailyIncome() const
+{
+	return {};
+}
+
+std::vector<CreatureID> CGGarrison::providedCreatures() const
+{
+	return {};
+}
+
 void CGGarrison::onHeroVisit (const CGHeroInstance *h) const
 {
 	auto relations = cb->gameState()->getPlayerRelations(h->tempOwner, tempOwner);
@@ -1281,6 +1297,21 @@ void CGObelisk::setPropertyDer(ObjProperty what, ObjPropertyID identifier)
 	}
 }
 
+const IOwnableObject * CGLighthouse::asOwnable() const
+{
+	return this;
+}
+
+ResourceSet CGLighthouse::dailyIncome() const
+{
+	return {};
+}
+
+std::vector<CreatureID> CGLighthouse::providedCreatures() const
+{
+	return {};
+}
+
 void CGLighthouse::onHeroVisit( const CGHeroInstance * h ) const
 {
 	if(h->tempOwner != tempOwner)

+ 17 - 7
lib/mapObjects/MiscObjects.h

@@ -10,6 +10,7 @@
 #pragma once
 
 #include "CArmedInstance.h"
+#include "IOwnableObject.h"
 #include "../texts/MetaString.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -59,7 +60,7 @@ protected:
 	void serializeJsonOptions(JsonSerializeFormat & handler) override;
 };
 
-class DLL_LINKAGE CGGarrison : public CArmedInstance
+class DLL_LINKAGE CGGarrison : public CArmedInstance, public IOwnableObject
 {
 public:
 	using CArmedInstance::CArmedInstance;
@@ -71,6 +72,10 @@ public:
 	void onHeroVisit(const CGHeroInstance * h) const override;
 	void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
 
+	const IOwnableObject * asOwnable() const final;
+	ResourceSet dailyIncome() const override;
+	std::vector<CreatureID> providedCreatures() const override;
+
 	template <typename Handler> void serialize(Handler &h)
 	{
 		h & static_cast<CArmedInstance&>(*this);
@@ -79,6 +84,7 @@ public:
 protected:
 	void serializeJsonOptions(JsonSerializeFormat & handler) override;
 	void addAntimagicGarrisonBonus();
+
 };
 
 class DLL_LINKAGE CGArtifact : public CArmedInstance
@@ -148,16 +154,13 @@ protected:
 	void serializeJsonOptions(JsonSerializeFormat & handler) override;
 };
 
-class DLL_LINKAGE CGMine : public CArmedInstance
+class DLL_LINKAGE CGMine : public CArmedInstance, public IOwnableObject
 {
 public:
 	GameResID producedResource;
 	ui32 producedQuantity;
 	std::set<GameResID> abandonedMineResources;
-	
 	bool isAbandoned() const;
-	ResourceSet dailyIncome() const;
-
 private:
 	using CArmedInstance::CArmedInstance;
 
@@ -166,7 +169,6 @@ private:
 	void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
 
 	void flagMine(const PlayerColor & player) const;
-	void newTurn(vstd::RNG & rand) const override;
 	void initObj(vstd::RNG & rand) override;
 
 	std::string getObjectName() const override;
@@ -183,6 +185,10 @@ public:
 	ui32 defaultResProduction() const;
 	ui32 getProducedQuantity() const;
 
+	const IOwnableObject * asOwnable() const final;
+	ResourceSet dailyIncome() const override;
+	std::vector<CreatureID> providedCreatures() const override;
+
 protected:
 	void serializeJsonOptions(JsonSerializeFormat & handler) override;
 };
@@ -403,7 +409,7 @@ protected:
 	void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override;
 };
 
-class DLL_LINKAGE CGLighthouse : public CGObjectInstance
+class DLL_LINKAGE CGLighthouse : public CGObjectInstance, public IOwnableObject
 {
 public:
 	using CGObjectInstance::CGObjectInstance;
@@ -411,6 +417,10 @@ public:
 	void onHeroVisit(const CGHeroInstance * h) const override;
 	void initObj(vstd::RNG & rand) override;
 
+	const IOwnableObject * asOwnable() const final;
+	ResourceSet dailyIncome() const override;
+	std::vector<CreatureID> providedCreatures() const override;
+
 	template <typename Handler> void serialize(Handler &h)
 	{
 		h & static_cast<CGObjectInstance&>(*this);

+ 5 - 0
lib/mapObjects/TownBuildingInstance.cpp

@@ -50,6 +50,11 @@ MapObjectSubID TownBuildingInstance::getObjTypeIndex() const
 	return 0;
 }
 
+const IOwnableObject * TownBuildingInstance::asOwnable() const
+{
+	return nullptr;
+}
+
 int3 TownBuildingInstance::visitablePos() const
 {
 	return town->visitablePos();

+ 1 - 0
lib/mapObjects/TownBuildingInstance.h

@@ -35,6 +35,7 @@ public:
 	PlayerColor getOwner() const override;
 	MapObjectID getObjGroupIndex() const override;
 	MapObjectSubID getObjTypeIndex() const override;
+	const IOwnableObject * asOwnable() const override;
 
 	int3 visitablePos() const override;
 	int3 getPosition() const override;

+ 26 - 26
lib/networkPacks/NetPacksLib.cpp

@@ -1171,7 +1171,7 @@ void RemoveObject::applyGs(CGameState *gs)
 		assert(beatenHero);
 		PlayerState * p = gs->getPlayerState(beatenHero->tempOwner);
 		gs->map->heroesOnMap -= beatenHero;
-		p->heroes -= beatenHero;
+		p->removeOwnedObject(beatenHero);
 
 
 		auto * siegeNode = beatenHero->whereShouldBeAttachedOnSiege(gs);
@@ -1417,7 +1417,7 @@ void HeroRecruited::applyGs(CGameState *gs)
 		gs->map->objects[h->id.getNum()] = h;
 
 	gs->map->heroesOnMap.emplace_back(h);
-	p->heroes.emplace_back(h);
+	p->addOwnedObject(h);
 	h->attachTo(*p);
 	gs->map->addBlockVisTiles(h);
 
@@ -1452,7 +1452,7 @@ void GiveHero::applyGs(CGameState *gs)
 	h->setMovementPoints(h->movementPointsLimit(true));
 	h->pos = h->convertFromVisitablePos(oldVisitablePos);
 	gs->map->heroesOnMap.emplace_back(h);
-	gs->getPlayerState(h->getOwner())->heroes.emplace_back(h);
+	gs->getPlayerState(h->getOwner())->addOwnedObject(h);
 
 	gs->map->addBlockVisTiles(h);
 	h->inTownGarrison = false;
@@ -1901,30 +1901,22 @@ void NewTurn::applyGs(CGameState *gs)
 	gs->globalEffects.reduceBonusDurations(Bonus::OneWeek);
 	//TODO not really a single root hierarchy, what about bonuses placed elsewhere? [not an issue with H3 mechanics but in the future...]
 
-	for(const NewTurn::Hero & h : heroes) //give mana/movement point
-	{
-		CGHeroInstance *hero = gs->getHero(h.id);
-		if(!hero)
-		{
-			logGlobal->error("Hero %d not found in NewTurn::applyGs", h.id.getNum());
-			continue;
-		}
+	for(auto & manaPack : heroesMana)
+		manaPack.applyGs(gs);
 
-		hero->setMovementPoints(h.move);
-		hero->mana = h.mana;
-	}
+	for(auto & movePack : heroesMovement)
+		movePack.applyGs(gs);
 
 	gs->heroesPool->onNewDay();
 
-	for(const auto & re : res)
+	for(auto & entry : playerIncome)
 	{
-		assert(re.first.isValidPlayer());
-		gs->getPlayerState(re.first)->resources = re.second;
-		gs->getPlayerState(re.first)->resources.amin(GameConstants::PLAYER_RESOURCES_CAP);
+		gs->getPlayerState(entry.first)->resources += entry.second;
+		gs->getPlayerState(entry.first)->resources.amin(GameConstants::PLAYER_RESOURCES_CAP);
 	}
 
-	for(auto & creatureSet : cres) //set available creatures in towns
-		creatureSet.second.applyGs(gs);
+	for(auto & creatureSet : availableCreatures) //set available creatures in towns
+		creatureSet.applyGs(gs);
 
 	for(CGTownInstance* t : gs->map->towns)
 		t->built = 0;
@@ -1943,6 +1935,18 @@ void SetObjectProperty::applyGs(CGameState *gs)
 	}
 
 	auto * cai = dynamic_cast<CArmedInstance *>(obj);
+
+	if(what == ObjProperty::OWNER && obj->asOwnable())
+	{
+		PlayerColor oldOwner = obj->getOwner();
+		PlayerColor newOwner = identifier.as<PlayerColor>();
+		if(oldOwner.isValidPlayer())
+			gs->getPlayerState(oldOwner)->removeOwnedObject(obj);;
+
+		if(newOwner.isValidPlayer())
+			gs->getPlayerState(newOwner)->addOwnedObject(obj);;
+	}
+
 	if(what == ObjProperty::OWNER && cai)
 	{
 		if(obj->ID == Obj::TOWN)
@@ -1954,17 +1958,13 @@ void SetObjectProperty::applyGs(CGameState *gs)
 			if(oldOwner.isValidPlayer())
 			{
 				auto * state = gs->getPlayerState(oldOwner);
-				state->towns -= t;
-
-				if(state->towns.empty())
+				if(state->getTowns().empty())
 					state->daysWithoutCastle = 0;
 			}
 			if(identifier.as<PlayerColor>().isValidPlayer())
 			{
-				PlayerState * p = gs->getPlayerState(identifier.as<PlayerColor>());
-				p->towns.emplace_back(t);
-
 				//reset counter before NewTurn to avoid no town message if game loaded at turn when one already captured
+				PlayerState * p = gs->getPlayerState(identifier.as<PlayerColor>());
 				if(p->daysWithoutCastle)
 					p->daysWithoutCastle = std::nullopt;
 			}

+ 47 - 44
lib/networkPacks/PacksForClient.h

@@ -294,6 +294,13 @@ struct DLL_LINKAGE SetMana : public CPackForClient
 
 	void visitTyped(ICPackVisitor & visitor) override;
 
+	SetMana() = default;
+	SetMana(ObjectInstanceID hid, si32 val, bool absolute)
+		: hid(hid)
+		, val(val)
+		, absolute(absolute)
+	{}
+
 	ObjectInstanceID hid;
 	si32 val = 0;
 	bool absolute = true;
@@ -310,6 +317,13 @@ struct DLL_LINKAGE SetMovePoints : public CPackForClient
 {
 	void applyGs(CGameState * gs) override;
 
+	SetMovePoints() = default;
+	SetMovePoints(ObjectInstanceID hid, si32 val, bool absolute)
+		: hid(hid)
+		, val(val)
+		, absolute(absolute)
+	{}
+
 	ObjectInstanceID hid;
 	si32 val = 0;
 	bool absolute = true;
@@ -1113,50 +1127,6 @@ struct DLL_LINKAGE HeroVisit : public CPackForClient
 	}
 };
 
-struct DLL_LINKAGE NewTurn : public CPackForClient
-{
-	enum weekType { NORMAL, DOUBLE_GROWTH, BONUS_GROWTH, DEITYOFFIRE, PLAGUE, NO_ACTION };
-
-	void applyGs(CGameState * gs) override;
-
-	void visitTyped(ICPackVisitor & visitor) override;
-
-	struct Hero
-	{
-		ObjectInstanceID id; //id is a general serial id
-		ui32 move;
-		ui32 mana;
-		template <typename Handler> void serialize(Handler & h)
-		{
-			h & id;
-			h & move;
-			h & mana;
-		}
-		bool operator<(const Hero & h)const { return id < h.id; }
-	};
-
-	std::set<Hero> heroes; //updates movement and mana points
-	std::map<PlayerColor, ResourceSet> res; //player ID => resource value[res_id]
-	std::map<ObjectInstanceID, SetAvailableCreatures> cres;//creatures to be placed in towns
-	ui32 day = 0;
-	ui8 specialWeek = 0; //weekType
-	CreatureID creatureid; //for creature weeks
-	std::optional<RumorState> newRumor; // only on new weeks
-
-	NewTurn() = default;
-
-	template <typename Handler> void serialize(Handler & h)
-	{
-		h & heroes;
-		h & cres;
-		h & res;
-		h & day;
-		h & specialWeek;
-		h & creatureid;
-		h & newRumor;
-	}
-};
-
 struct DLL_LINKAGE InfoWindow : public CPackForClient //103  - displays simple info window
 {
 	EInfoWindowMode type = EInfoWindowMode::MODAL;
@@ -1179,6 +1149,39 @@ struct DLL_LINKAGE InfoWindow : public CPackForClient //103  - displays simple i
 	InfoWindow() = default;
 };
 
+struct DLL_LINKAGE NewTurn : public CPackForClient
+{
+	void applyGs(CGameState * gs) override;
+
+	void visitTyped(ICPackVisitor & visitor) override;
+
+	ui32 day = 0;
+	CreatureID creatureid; //for creature weeks
+	EWeekType specialWeek = EWeekType::NORMAL;
+
+	std::vector<SetMovePoints> heroesMovement;
+	std::vector<SetMana> heroesMana;
+	std::vector<SetAvailableCreatures> availableCreatures;
+	std::map<PlayerColor, ResourceSet> playerIncome;
+	std::optional<RumorState> newRumor; // only on new weeks
+	std::optional<InfoWindow> newWeekNotification; // only on new week
+
+	NewTurn() = default;
+
+	template <typename Handler> void serialize(Handler & h)
+	{
+		h & day;
+		h & creatureid;
+		h & specialWeek;
+		h & heroesMovement;
+		h & heroesMana;
+		h & availableCreatures;
+		h & playerIncome;
+		h & newRumor;
+		h & newWeekNotification;
+	}
+};
+
 struct DLL_LINKAGE SetObjectProperty : public CPackForClient
 {
 	void applyGs(CGameState * gs) override;

+ 1 - 2
lib/pathfinder/CPathfinder.cpp

@@ -264,8 +264,7 @@ TeleporterTilesVector CPathfinderHelper::getCastleGates(const PathNodeInfo & sou
 {
 	TeleporterTilesVector allowedExits;
 
-	auto towns = getPlayerState(hero->tempOwner)->towns;
-	for(const auto & town : towns)
+	for(const auto & town : getPlayerState(hero->tempOwner)->getTowns())
 	{
 		if(town->id != source.nodeObject->id && town->visitingHero == nullptr
 			&& town->hasBuilt(BuildingID::CASTLE_GATE, ETownType::INFERNO))

+ 2 - 1
lib/serializer/ESerializationVersion.h

@@ -66,6 +66,7 @@ enum class ESerializationVersion : int32_t
 	NEW_TOWN_BUILDINGS, // 855 - old bonusing buildings have been removed
 	STATISTICS_SCREEN, // 856 - extent statistic functions
 	NEW_MARKETS, // 857 - reworked market classes
+	PLAYER_STATE_OWNED_OBJECTS, // 858 - player state stores all owned objects in a single list
 
-	CURRENT = NEW_MARKETS
+	CURRENT = PLAYER_STATE_OWNED_OBJECTS
 };

+ 2 - 2
lib/spells/AdventureSpellMechanics.cpp

@@ -692,9 +692,9 @@ std::vector <const CGTownInstance*> TownPortalMechanics::getPossibleTowns(SpellC
 
 	for(const auto & color : team->players)
 	{
-		for(auto currTown : env->getCb()->getPlayerState(color)->towns)
+		for(auto currTown : env->getCb()->getPlayerState(color)->getTowns())
 		{
-			ret.push_back(currTown.get());
+			ret.push_back(currTown);
 		}
 	}
 	return ret;

+ 60 - 443
server/CGameHandler.cpp

@@ -16,6 +16,7 @@
 #include "ServerSpellCastEnvironment.h"
 #include "battles/BattleProcessor.h"
 #include "processors/HeroPoolProcessor.h"
+#include "processors/NewTurnProcessor.h"
 #include "processors/PlayerMessageProcessor.h"
 #include "processors/TurnOrderProcessor.h"
 #include "queries/QueriesProcessor.h"
@@ -489,6 +490,7 @@ CGameHandler::CGameHandler(CVCMIServer * lobby)
 	, complainNotEnoughCreatures("Cannot split that stack, not enough creatures!")
 	, complainInvalidSlot("Invalid slot accessed!")
 	, turnTimerHandler(std::make_unique<TurnTimerHandler>(*this))
+	, newTurnProcessor(std::make_unique<NewTurnProcessor>(this))
 {
 	QID = 1;
 
@@ -553,25 +555,25 @@ void CGameHandler::setPortalDwelling(const CGTownInstance * town, bool forced=fa
 			ssi.creatures = town->creatures;
 			ssi.creatures[town->town->creatures.size()].second.clear();//remove old one
 
-			const std::vector<ConstTransitivePtr<CGDwelling> > &dwellings = p->dwellings;
-			if (dwellings.empty())//no dwellings - just remove
+			std::set<CreatureID> availableCreatures;
+			for (const auto & dwelling : p->getOwnedObjects())
 			{
-				sendAndApply(&ssi);
-				return;
+				const auto & dwellingCreatures = dwelling->asOwnable()->providedCreatures();
+				availableCreatures.insert(dwellingCreatures.begin(), dwellingCreatures.end());
 			}
 
-			auto dwelling = *RandomGeneratorUtil::nextItem(dwellings, getRandomGenerator());
+			if (availableCreatures.empty())
+				return;
 
-			// for multi-creature dwellings like Golem Factory
-			auto creatureId = RandomGeneratorUtil::nextItem(dwelling->creatures, getRandomGenerator())->second[0];
+			CreatureID creatureId = *RandomGeneratorUtil::nextItem(availableCreatures, getRandomGenerator());
 
 			if (clear)
 			{
-				ssi.creatures[town->town->creatures.size()].first = std::max(1, (VLC->creh->objects.at(creatureId)->getGrowth())/2);
+				ssi.creatures[town->town->creatures.size()].first = std::max(1, (creatureId.toEntity(VLC)->getGrowth())/2);
 			}
 			else
 			{
-				ssi.creatures[town->town->creatures.size()].first = VLC->creh->objects.at(creatureId)->getGrowth();
+				ssi.creatures[town->town->creatures.size()].first = creatureId.toEntity(VLC)->getGrowth();
 			}
 			ssi.creatures[town->town->creatures.size()].second.push_back(creatureId);
 			sendAndApply(&ssi);
@@ -582,54 +584,12 @@ void CGameHandler::onPlayerTurnStarted(PlayerColor which)
 {
 	events::PlayerGotTurn::defaultExecute(serverEventBus.get(), which);
 	turnTimerHandler->onPlayerGetTurn(which);
-
-	const auto * playerState = gs->getPlayerState(which);
-
-	handleTimeEvents(which);
-	for (auto t : playerState->towns)
-		handleTownEvents(t);
-
-	for (auto t : playerState->towns)
-	{
-		//garrison hero first - consistent with original H3 Mana Vortex and Battle Scholar Academy levelup windows order
-		if (t->garrisonHero != nullptr)
-			objectVisited(t, t->garrisonHero);
-
-		if (t->visitingHero != nullptr)
-			objectVisited(t, t->visitingHero);
-	}
+	newTurnProcessor->onPlayerTurnStarted(which);
 }
 
 void CGameHandler::onPlayerTurnEnded(PlayerColor which)
 {
-	const auto * playerState = gs->getPlayerState(which);
-	assert(playerState->status == EPlayerStatus::INGAME);
-
-	if (playerState->towns.empty())
-	{
-		DaysWithoutTown pack;
-		pack.player = which;
-		pack.daysWithoutCastle = playerState->daysWithoutCastle.value_or(0) + 1;
-		sendAndApply(&pack);
-	}
-	else
-	{
-		if (playerState->daysWithoutCastle.has_value())
-		{
-			DaysWithoutTown pack;
-			pack.player = which;
-			pack.daysWithoutCastle = std::nullopt;
-			sendAndApply(&pack);
-		}
-	}
-
-	// check for 7 days without castle
-	checkVictoryLossConditionsForPlayer(which);
-
-	bool newWeek = getDate(Date::DAY_OF_WEEK) == 7; // end of 7th day
-
-	if (newWeek) //new heroes in tavern
-		heroPool->onNewWeek(which);
+	newTurnProcessor->onPlayerTurnEnded(which);
 }
 
 void CGameHandler::addStatistics(StatisticDataSet &stat) const
@@ -648,17 +608,10 @@ void CGameHandler::addStatistics(StatisticDataSet &stat) const
 void CGameHandler::onNewTurn()
 {
 	logGlobal->trace("Turn %d", gs->day+1);
-	NewTurn n;
-	n.specialWeek = NewTurn::NO_ACTION;
-	n.creatureid = CreatureID::NONE;
-	n.day = gs->day + 1;
 
 	bool firstTurn = !getDate(Date::DAY);
-	bool newWeek = getDate(Date::DAY_OF_WEEK) == 7; //day numbers are confusing, as day was not yet switched
 	bool newMonth = getDate(Date::DAY_OF_MONTH) == 28;
 
-	std::map<PlayerColor, si32> hadGold;//starting gold - for buildings like dwarven treasury
-
 	if (firstTurn)
 	{
 		for (auto obj : gs->map->objects)
@@ -668,253 +621,43 @@ void CGameHandler::onNewTurn()
 				giveExperience(getHero(obj->id), 0);
 			}
 		}
-	}
-	else
-	{
-		addStatistics(gameState()->statistic); // write at end of turn
-	}
 
-	for (const auto & player : gs->players)
-	{
-		if (player.second.status != EPlayerStatus::INGAME)
-			continue;
+		for (auto & elem : gs->players)
+			heroPool->onNewWeek(elem.first);
 
-		if (player.second.heroes.empty() && player.second.towns.empty())
-			throw std::runtime_error("Invalid player in player state! Player " + std::to_string(player.first.getNum()) + ", map name: " + gs->map->name.toString() + ", map description: " + gs->map->description.toString());
 	}
-
-	if (newWeek && !firstTurn)
+	else
 	{
-		n.specialWeek = NewTurn::NORMAL;
-		bool deityOfFireBuilt = false;
-		for (const CGTownInstance *t : gs->map->towns)
-		{
-			if (t->hasBuilt(BuildingID::GRAIL, ETownType::INFERNO))
-			{
-				deityOfFireBuilt = true;
-				break;
-			}
-		}
-
-		if (deityOfFireBuilt)
-		{
-			n.specialWeek = NewTurn::DEITYOFFIRE;
-			n.creatureid = CreatureID::IMP;
-		}
-		else if(VLC->settings()->getBoolean(EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS))
-		{
-			int monthType = getRandomGenerator().nextInt(99);
-			if (newMonth) //new month
-			{
-				if (monthType < 40) //double growth
-				{
-					n.specialWeek = NewTurn::DOUBLE_GROWTH;
-					if (VLC->settings()->getBoolean(EGameSettings::CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH))
-					{
-						n.creatureid = VLC->creh->pickRandomMonster(getRandomGenerator());
-					}
-					else if (VLC->creh->doubledCreatures.size())
-					{
-						n.creatureid = *RandomGeneratorUtil::nextItem(VLC->creh->doubledCreatures, getRandomGenerator());
-					}
-					else
-					{
-						complain("Cannot find creature that can be spawned!");
-						n.specialWeek = NewTurn::NORMAL;
-					}
-				}
-				else if (monthType < 50)
-					n.specialWeek = NewTurn::PLAGUE;
-			}
-			else //it's a week, but not full month
-			{
-				if (monthType < 25)
-				{
-					n.specialWeek = NewTurn::BONUS_GROWTH; //+5
-					std::pair<int, CreatureID> newMonster(54, CreatureID());
-					do
-					{
-						newMonster.second = VLC->creh->pickRandomMonster(getRandomGenerator());
-					} while (VLC->creh->objects[newMonster.second] &&
-						(*VLC->townh)[VLC->creatures()->getById(newMonster.second)->getFaction()]->town == nullptr); // find first non neutral creature
-					n.creatureid = newMonster.second;
-				}
-			}
-		}
+		addStatistics(gameState()->statistic); // write at end of turn
 	}
 
-	for (auto & elem : gs->players)
-	{
-		if (elem.first == PlayerColor::NEUTRAL)
-			continue;
-
-		assert(elem.first.isValidPlayer());//illegal player number!
-			
-		auto playerSettings = gameState()->scenarioOps->getIthPlayersSettings(elem.first);
-
-		std::pair<PlayerColor, si32> playerGold(elem.first, elem.second.resources[EGameResID::GOLD]);
-		hadGold.insert(playerGold);
-
-		if (firstTurn)
-			heroPool->onNewWeek(elem.first);
-
-		n.res[elem.first] = elem.second.resources;
-
-		if(!firstTurn)
-		{
-			for (GameResID k = GameResID::WOOD; k < GameResID::COUNT; k++)
-			{
-				n.res[elem.first][k] += elem.second.valOfBonuses(BonusType::RESOURCES_CONSTANT_BOOST, BonusSubtypeID(k)) * playerSettings.handicap.percentIncome / 100;
-				n.res[elem.first][k] += elem.second.valOfBonuses(BonusType::RESOURCES_TOWN_MULTIPLYING_BOOST, BonusSubtypeID(k)) * elem.second.towns.size() * playerSettings.handicap.percentIncome / 100;
-			}
-
-			if(newWeek) //weekly crystal generation if 1 or more crystal dragons in any hero army or town garrison
-			{
-				bool hasCrystalGenCreature = false;
-				for(CGHeroInstance * hero : elem.second.heroes)
-				{
-					for(auto stack : hero->stacks)
-					{
-						if(stack.second->hasBonusOfType(BonusType::SPECIAL_CRYSTAL_GENERATION))
-						{
-							hasCrystalGenCreature = true;
-							break;
-						}
-					}
-				}
-				if(!hasCrystalGenCreature) //not found in armies, check towns
-				{
-					for(CGTownInstance * town : elem.second.towns)
-					{
-						for(auto stack : town->stacks)
-						{
-							if(stack.second->hasBonusOfType(BonusType::SPECIAL_CRYSTAL_GENERATION))
-							{
-								hasCrystalGenCreature = true;
-								break;
-							}
-						}
-					}
-				}
-				if(hasCrystalGenCreature)
-					n.res[elem.first][EGameResID::CRYSTAL] += 3 * playerSettings.handicap.percentIncome / 100;
-			}
-		}
-
-		for (CGHeroInstance *h : (elem).second.heroes)
-		{
-			if (h->visitedTown)
-				giveSpells(h->visitedTown, h);
-
-			NewTurn::Hero hth;
-			hth.id = h->id;
-			auto ti = std::make_unique<TurnInfo>(h, 1);
-			// TODO: this code executed when bonuses of previous day not yet updated (this happen in NewTurn::applyGs). See issue 2356
-			hth.move = h->movementPointsLimitCached(gs->map->getTile(h->visitablePos()).terType->isLand(), ti.get());
-			hth.mana = h->getManaNewTurn();
-
-			n.heroes.insert(hth);
-
-			if (!firstTurn) //not first day
-			{
-				for (GameResID k = GameResID::WOOD; k < GameResID::COUNT; k++)
-				{
-					n.res[elem.first][k] += h->valOfBonuses(BonusType::GENERATE_RESOURCE, BonusSubtypeID(k)) * playerSettings.handicap.percentIncome / 100;
-				}
-			}
-		}
-	}
 	for (CGTownInstance *t : gs->map->towns)
 	{
 		PlayerColor player = t->tempOwner;
-		if (newWeek) //first day of week
-		{
-			if (t->hasBuilt(BuildingSubID::PORTAL_OF_SUMMONING))
-				setPortalDwelling(t, true, (n.specialWeek == NewTurn::PLAGUE ? true : false)); //set creatures for Portal of Summoning
-
-			if (!firstTurn)
-				if (t->hasBuilt(BuildingSubID::TREASURY) && player.isValidPlayer())
-						n.res[player][EGameResID::GOLD] += hadGold.at(player)/10; //give 10% of starting gold
-
-			if (!vstd::contains(n.cres, t->id))
-			{
-				n.cres[t->id].tid = t->id;
-				n.cres[t->id].creatures = t->creatures;
-			}
-			auto & sac = n.cres.at(t->id);
 
-			for (int k=0; k < t->town->creatures.size(); k++) //creature growths
-			{
-				if (!t->creatures.at(k).second.empty()) // there are creatures at this level
-				{
-					ui32 &availableCount = sac.creatures.at(k).first;
-					const CCreature *cre = t->creatures.at(k).second.back().toCreature();
-
-					if (n.specialWeek == NewTurn::PLAGUE)
-						availableCount = t->creatures.at(k).first / 2; //halve their number, no growth
-					else
-					{
-						if (firstTurn) //first day of game: use only basic growths
-							availableCount = cre->getGrowth();
-						else
-							availableCount += t->creatureGrowth(k);
-
-						//Deity of fire week - upgrade both imps and upgrades
-						if (n.specialWeek == NewTurn::DEITYOFFIRE && vstd::contains(t->creatures.at(k).second, n.creatureid))
-							availableCount += 15;
-
-						if (cre->getId() == n.creatureid) //bonus week, effect applies only to identical creatures
-						{
-							if (n.specialWeek == NewTurn::DOUBLE_GROWTH)
-								availableCount *= 2;
-							else if (n.specialWeek == NewTurn::BONUS_GROWTH)
-								availableCount += 5;
-						}
-					}
-				}
-			}
-		}
-		if (!firstTurn  &&  player.isValidPlayer())//not the first day and town not neutral
-		{
-			n.res[player] = n.res[player] + t->dailyIncome();
-		}
 		if(t->hasBuilt(BuildingID::GRAIL)
 			&& t->town->buildings.at(BuildingID::GRAIL)->height == CBuilding::HEIGHT_SKYSHIP)
 		{
 			// Skyship, probably easier to handle same as Veil of darkness
-			//do it every new day after veils apply
-			if (player != PlayerColor::NEUTRAL) //do not reveal fow for neutral player
-			{
-				FoWChange fw;
-				fw.mode = ETileVisibility::REVEALED;
-				fw.player = player;
-				// find all hidden tiles
-				const auto & fow = getPlayerTeam(player)->fogOfWarMap;
-
-				auto shape = fow.shape();
-				for(size_t z = 0; z < shape[0]; z++)
-					for(size_t x = 0; x < shape[1]; x++)
-						for(size_t y = 0; y < shape[2]; y++)
-							if (!fow[z][x][y])
-								fw.tiles.insert(int3(x, y, z));
-
-				sendAndApply (&fw);
-			}
+			// do it every new day before veils
+			if (player.isValidPlayer())
+				changeFogOfWar(t->getSightCenter(), t->getSightRadius(), player, ETileVisibility::REVEALED);
 		}
+	}
+
+	for (CGTownInstance *t : gs->map->towns)
+	{
 		if (t->hasBonusOfType (BonusType::DARKNESS))
 		{
 			for (auto & player : gs->players)
 			{
 				if (getPlayerStatus(player.first) == EPlayerStatus::INGAME &&
 					getPlayerRelations(player.first, t->tempOwner) == PlayerRelations::ENEMIES)
-					changeFogOfWar(t->getSightCenter(), t->getFirstBonus(Selector::type()(BonusType::DARKNESS))->val, player.first, ETileVisibility::HIDDEN);
+					changeFogOfWar(t->getSightCenter(), t->valOfBonuses(BonusType::DARKNESS), player.first, ETileVisibility::HIDDEN);
 			}
 		}
 	}
 
-	if (newWeek)
-		n.newRumor = gameState()->pickNewRumor();
-
 	if (newMonth)
 	{
 		SetAvailableArtifacts saa;
@@ -922,67 +665,12 @@ void CGameHandler::onNewTurn()
 		pickAllowedArtsSet(saa.arts, getRandomGenerator());
 		sendAndApply(&saa);
 	}
-	sendAndApply(&n);
 
-	if (newWeek)
-	{
-		//spawn wandering monsters
-		if (newMonth && (n.specialWeek == NewTurn::DOUBLE_GROWTH || n.specialWeek == NewTurn::DEITYOFFIRE))
-		{
-			spawnWanderingMonsters(n.creatureid);
-		}
-
-		//new week info popup
-		if (!firstTurn)
-		{
-			InfoWindow iw;
-			switch (n.specialWeek)
-			{
-				case NewTurn::DOUBLE_GROWTH:
-					iw.text.appendLocalString(EMetaText::ARRAY_TXT, 131);
-					iw.text.replaceNameSingular(n.creatureid);
-					iw.text.replaceNameSingular(n.creatureid);
-					break;
-				case NewTurn::PLAGUE:
-					iw.text.appendLocalString(EMetaText::ARRAY_TXT, 132);
-					break;
-				case NewTurn::BONUS_GROWTH:
-					iw.text.appendLocalString(EMetaText::ARRAY_TXT, 134);
-					iw.text.replaceNameSingular(n.creatureid);
-					iw.text.replaceNameSingular(n.creatureid);
-					break;
-				case NewTurn::DEITYOFFIRE:
-					iw.text.appendLocalString(EMetaText::ARRAY_TXT, 135);
-					iw.text.replaceNameSingular(CreatureID::IMP); //%s imp
-					iw.text.replaceNameSingular(CreatureID::IMP); //%s imp
-					iw.text.replacePositiveNumber(15);//%+d 15
-					iw.text.replaceNameSingular(CreatureID::FAMILIAR); //%s familiar
-					iw.text.replacePositiveNumber(15);//%+d 15
-					break;
-				default:
-					if (newMonth)
-					{
-						iw.text.appendLocalString(EMetaText::ARRAY_TXT, (130));
-						iw.text.replaceLocalString(EMetaText::ARRAY_TXT, getRandomGenerator().nextInt(32, 41));
-					}
-					else
-					{
-						iw.text.appendLocalString(EMetaText::ARRAY_TXT, (133));
-						iw.text.replaceLocalString(EMetaText::ARRAY_TXT, getRandomGenerator().nextInt(43, 57));
-					}
-			}
-			for (auto & elem : gs->players)
-			{
-				iw.player = elem.first;
-				sendAndApply(&iw);
-			}
-		}
-	}
+	newTurnProcessor->onNewTurn();
 
 	if (!firstTurn)
 		checkVictoryLossConditionsForAll(); // check for map turn limit
 
-	logGlobal->trace("Info about turn %d has been sent!", n.day);
 	//call objects
 	for (auto & elem : gs->map->objects)
 	{
@@ -1376,11 +1064,9 @@ void CGameHandler::setOwner(const CGObjectInstance * obj, const PlayerColor owne
 		}
 	}
 
-	const PlayerState * p = getPlayerState(owner);
-
-	if ((obj->ID == Obj::CREATURE_GENERATOR1 || obj->ID == Obj::CREATURE_GENERATOR4) && p && p->dwellings.size()==1)//first dwelling captured
+	if ((obj->ID == Obj::CREATURE_GENERATOR1 || obj->ID == Obj::CREATURE_GENERATOR4))
 	{
-		for (const CGTownInstance * t : getPlayerState(owner)->towns)
+		for (const CGTownInstance * t : getPlayerState(owner)->getTowns())
 		{
 			if (t->hasBuilt(BuildingSubID::PORTAL_OF_SUMMONING))
 				setPortalDwelling(t);//set initial creatures for all portals of summoning
@@ -3112,9 +2798,19 @@ bool CGameHandler::buyArtifact(ObjectInstanceID hid, ArtifactID aid)
 		const int price = art->getPrice();
 		COMPLAIN_RET_FALSE_IF(getPlayerState(hero->getOwner())->resources[EGameResID::GOLD] < price, "Not enough gold!");
 
-		if ((town->hasBuilt(BuildingID::BLACKSMITH) && town->town->warMachine == aid)
-		 || (town->hasBuilt(BuildingSubID::BALLISTA_YARD) && aid == ArtifactID::BALLISTA))
+		if(town->isWarMachineAvailable(aid))
 		{
+			bool hasFreeSlot = false;
+			for(auto slot : art->getPossibleSlots().at(ArtBearer::HERO))
+				if (hero->getArt(slot) == nullptr)
+					hasFreeSlot = true;
+
+			if (!hasFreeSlot)
+			{
+				auto slot = art->getPossibleSlots().at(ArtBearer::HERO).front();
+				removeArtifact(ArtifactLocation(hero->id, slot));
+			}
+
 			giveResource(hero->getOwner(),EGameResID::GOLD,-price);
 			return giveHeroNewArtifact(hero, art);
 		}
@@ -3363,89 +3059,6 @@ bool CGameHandler::queryReply(QueryID qid, std::optional<int32_t> answer, Player
 	return true;
 }
 
-void CGameHandler::handleTimeEvents(PlayerColor color)
-{
-	for (auto const & event : gs->map->events)
-	{
-		if (!event.occursToday(gs->day))
-			continue;
-
-		if (!event.affectsPlayer(color, getPlayerState(color)->isHuman()))
-			continue;
-
-		InfoWindow iw;
-		iw.player = color;
-		iw.text = event.message;
-
-		//give resources
-		if (!event.resources.empty())
-		{
-			giveResources(color, event.resources);
-			for (GameResID i : GameResID::ALL_RESOURCES())
-				if (event.resources[i])
-					iw.components.emplace_back(ComponentType::RESOURCE, i, event.resources[i]);
-		}
-		sendAndApply(&iw); //show dialog
-	}
-}
-
-void CGameHandler::handleTownEvents(CGTownInstance * town)
-{
-	for (auto const & event : town->events)
-	{
-		if (!event.occursToday(gs->day))
-			continue;
-
-		PlayerColor player = town->getOwner();
-		if (!event.affectsPlayer(player, getPlayerState(player)->isHuman()))
-			continue;
-
-		// dialog
-		InfoWindow iw;
-		iw.player = player;
-		iw.text = event.message;
-
-		if (event.resources.nonZero())
-		{
-			giveResources(player, event.resources);
-
-			for (GameResID i : GameResID::ALL_RESOURCES())
-				if (event.resources[i])
-					iw.components.emplace_back(ComponentType::RESOURCE, i, event.resources[i]);
-		}
-
-		for (auto & i : event.buildings)
-		{
-			// Only perform action if:
-			// 1. Building exists in town (don't attempt to build Lvl 5 guild in Fortress
-			// 2. Building was not built yet
-			// othervice, silently ignore / skip it
-			if (town->town->buildings.count(i) && !town->hasBuilt(i))
-			{
-				buildStructure(town->id, i, true);
-				iw.components.emplace_back(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFaction(), i));
-			}
-		}
-
-		if (!event.creatures.empty())
-		{
-			SetAvailableCreatures sac;
-			sac.tid = town->id;
-			sac.creatures = town->creatures;
-
-			for (si32 i=0;i<event.creatures.size();i++) //creature growths
-			{
-				if (!town->creatures.at(i).second.empty() && event.creatures.at(i) > 0)//there is dwelling
-				{
-					sac.creatures[i].first += event.creatures.at(i);
-					iw.components.emplace_back(ComponentType::CREATURE, town->creatures.at(i).second.back(), event.creatures.at(i));
-				}
-			}
-		}
-		sendAndApply(&iw); //show dialog
-	}
-}
-
 bool CGameHandler::complain(const std::string &problem)
 {
 #ifndef ENABLE_GOLDMASTER
@@ -3723,10 +3336,10 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player)
 		else
 		{
 			//copy heroes vector to avoid iterator invalidation as removal change PlayerState
-			auto hlp = p->heroes;
+			auto hlp = p->getHeroes();
 			for (auto h : hlp) //eliminate heroes
 			{
-				if (h.get())
+				if (h)
 					removeObject(h, player);
 			}
 
@@ -4248,19 +3861,6 @@ void CGameHandler::changeFogOfWar(int3 center, ui32 radius, PlayerColor player,
 	if (mode == ETileVisibility::HIDDEN)
 	{
 		getTilesInRange(tiles, center, radius, ETileVisibility::REVEALED, player);
-
-		std::unordered_set<int3> observedTiles; //do not hide tiles observed by heroes. May lead to disastrous AI problems
-		auto p = getPlayerState(player);
-		for (auto h : p->heroes)
-		{
-			getTilesInRange(observedTiles, h->getSightCenter(), h->getSightRadius(), ETileVisibility::REVEALED, h->tempOwner);
-		}
-		for (auto t : p->towns)
-		{
-			getTilesInRange(observedTiles, t->getSightCenter(), t->getSightRadius(), ETileVisibility::REVEALED, t->tempOwner);
-		}
-		for (auto tile : observedTiles)
-			vstd::erase_if_present (tiles, tile);
 	}
 	else
 	{
@@ -4269,7 +3869,7 @@ void CGameHandler::changeFogOfWar(int3 center, ui32 radius, PlayerColor player,
 	changeFogOfWar(tiles, player, mode);
 }
 
-void CGameHandler::changeFogOfWar(std::unordered_set<int3> &tiles, PlayerColor player, ETileVisibility mode)
+void CGameHandler::changeFogOfWar(const std::unordered_set<int3> &tiles, PlayerColor player, ETileVisibility mode)
 {
 	if (tiles.empty())
 		return;
@@ -4278,6 +3878,23 @@ void CGameHandler::changeFogOfWar(std::unordered_set<int3> &tiles, PlayerColor p
 	fow.tiles = tiles;
 	fow.player = player;
 	fow.mode = mode;
+
+	if (mode == ETileVisibility::HIDDEN)
+	{
+		// do not hide tiles observed by owned objects. May lead to disastrous AI problems
+		// FIXME: this leads to a bug - shroud of darkness from Necropolis does can not override Skyship from Tower
+		std::unordered_set<int3> observedTiles;
+		auto p = getPlayerState(player);
+		for (auto obj : p->getOwnedObjects())
+			getTilesInRange(observedTiles, obj->getSightCenter(), obj->getSightRadius(), ETileVisibility::REVEALED, obj->getOwner());
+
+		for (auto tile : observedTiles)
+			vstd::erase_if_present (fow.tiles, tile);
+
+		if (fow.tiles.empty())
+			return;
+	}
+
 	sendAndApply(&fow);
 }
 

+ 3 - 3
server/CGameHandler.h

@@ -51,6 +51,7 @@ class TurnOrderProcessor;
 class TurnTimerHandler;
 class QueriesProcessor;
 class CObjectVisitQuery;
+class NewTurnProcessor;
 
 class CGameHandler : public IGameCallback, public Environment
 {
@@ -62,6 +63,7 @@ public:
 	std::unique_ptr<QueriesProcessor> queries;
 	std::unique_ptr<TurnOrderProcessor> turnOrder;
 	std::unique_ptr<TurnTimerHandler> turnTimerHandler;
+	std::unique_ptr<NewTurnProcessor> newTurnProcessor;
 	std::unique_ptr<CRandomGenerator> randomNumberGenerator;
 
 	//use enums as parameters, because doMove(sth, true, false, true) is not readable
@@ -157,7 +159,7 @@ public:
 	void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override;
 
 	void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override;
-	void changeFogOfWar(std::unordered_set<int3> &tiles, PlayerColor player,ETileVisibility mode) override;
+	void changeFogOfWar(const std::unordered_set<int3> &tiles, PlayerColor player,ETileVisibility mode) override;
 	
 	void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override;
 
@@ -227,8 +229,6 @@ public:
 	void onNewTurn();
 	void addStatistics(StatisticDataSet &stat) const;
 
-	void handleTimeEvents(PlayerColor player);
-	void handleTownEvents(CGTownInstance *town);
 	bool complain(const std::string &problem); //sends message to all clients, prints on the logs and return true
 	void objectVisited( const CGObjectInstance * obj, const CGHeroInstance * h );
 	void objectVisitEnded(const CObjectVisitQuery &query);

+ 2 - 0
server/CMakeLists.txt

@@ -12,6 +12,7 @@ set(vcmiservercommon_SRCS
 		queries/QueriesProcessor.cpp
 
 		processors/HeroPoolProcessor.cpp
+		processors/NewTurnProcessor.cpp
 		processors/PlayerMessageProcessor.cpp
 		processors/TurnOrderProcessor.cpp
 
@@ -38,6 +39,7 @@ set(vcmiservercommon_HEADERS
 		queries/QueriesProcessor.h
 
 		processors/HeroPoolProcessor.h
+		processors/NewTurnProcessor.h
 		processors/PlayerMessageProcessor.h
 		processors/TurnOrderProcessor.h
 

+ 1 - 1
server/battles/BattleResultProcessor.cpp

@@ -484,7 +484,7 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle)
 		if(!finishingBattle->isDraw())
 		{
 			ConstTransitivePtr<CGHeroInstance> strongestHero = nullptr;
-			for(auto & hero : gameHandler->gameState()->getPlayerState(finishingBattle->loser)->heroes)
+			for(auto & hero : gameHandler->gameState()->getPlayerState(finishingBattle->loser)->getHeroes())
 				if(!strongestHero || hero->exp > strongestHero->exp)
 					strongestHero = hero;
 			if(strongestHero->id == finishingBattle->loserHero->id && strongestHero->level > 5)

+ 570 - 0
server/processors/NewTurnProcessor.cpp

@@ -0,0 +1,570 @@
+/*
+ * NewTurnProcessor.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 "NewTurnProcessor.h"
+
+#include "HeroPoolProcessor.h"
+
+#include "../CGameHandler.h"
+
+#include "../../lib/CPlayerState.h"
+#include "../../lib/GameSettings.h"
+#include "../../lib/StartInfo.h"
+#include "../../lib/TerrainHandler.h"
+#include "../../lib/entities/building/CBuilding.h"
+#include "../../lib/entities/faction/CTownHandler.h"
+#include "../../lib/gameState/CGameState.h"
+#include "../../lib/gameState/SThievesGuildInfo.h"
+#include "../../lib/mapObjects/CGHeroInstance.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
+#include "../../lib/mapObjects/IOwnableObject.h"
+#include "../../lib/mapping/CMap.h"
+#include "../../lib/networkPacks/PacksForClient.h"
+#include "../../lib/pathfinder/TurnInfo.h"
+#include "../../lib/texts/CGeneralTextHandler.h"
+
+#include <vstd/RNG.h>
+
+NewTurnProcessor::NewTurnProcessor(CGameHandler * gameHandler)
+	:gameHandler(gameHandler)
+{
+}
+
+void NewTurnProcessor::handleTimeEvents(PlayerColor color)
+{
+	for (auto const & event : gameHandler->gameState()->map->events)
+	{
+		if (!event.occursToday(gameHandler->gameState()->day))
+			continue;
+
+		if (!event.affectsPlayer(color, gameHandler->getPlayerState(color)->isHuman()))
+			continue;
+
+		InfoWindow iw;
+		iw.player = color;
+		iw.text = event.message;
+
+		//give resources
+		if (!event.resources.empty())
+		{
+			gameHandler->giveResources(color, event.resources);
+			for (GameResID i : GameResID::ALL_RESOURCES())
+				if (event.resources[i])
+					iw.components.emplace_back(ComponentType::RESOURCE, i, event.resources[i]);
+		}
+		gameHandler->sendAndApply(&iw); //show dialog
+	}
+}
+
+void NewTurnProcessor::handleTownEvents(const CGTownInstance * town)
+{
+	for (auto const & event : town->events)
+	{
+		if (!event.occursToday(gameHandler->gameState()->day))
+			continue;
+
+		PlayerColor player = town->getOwner();
+		if (!event.affectsPlayer(player, gameHandler->getPlayerState(player)->isHuman()))
+			continue;
+
+		// dialog
+		InfoWindow iw;
+		iw.player = player;
+		iw.text = event.message;
+
+		if (event.resources.nonZero())
+		{
+			gameHandler->giveResources(player, event.resources);
+
+			for (GameResID i : GameResID::ALL_RESOURCES())
+				if (event.resources[i])
+					iw.components.emplace_back(ComponentType::RESOURCE, i, event.resources[i]);
+		}
+
+		for (auto & i : event.buildings)
+		{
+			// Only perform action if:
+			// 1. Building exists in town (don't attempt to build Lvl 5 guild in Fortress
+			// 2. Building was not built yet
+			// othervice, silently ignore / skip it
+			if (town->town->buildings.count(i) && !town->hasBuilt(i))
+			{
+				gameHandler->buildStructure(town->id, i, true);
+				iw.components.emplace_back(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFaction(), i));
+			}
+		}
+
+		if (!event.creatures.empty())
+		{
+			SetAvailableCreatures sac;
+			sac.tid = town->id;
+			sac.creatures = town->creatures;
+
+			for (si32 i=0;i<event.creatures.size();i++) //creature growths
+			{
+				if (!town->creatures.at(i).second.empty() && event.creatures.at(i) > 0)//there is dwelling
+				{
+					sac.creatures[i].first += event.creatures.at(i);
+					iw.components.emplace_back(ComponentType::CREATURE, town->creatures.at(i).second.back(), event.creatures.at(i));
+				}
+			}
+		}
+		gameHandler->sendAndApply(&iw); //show dialog
+	}
+}
+
+void NewTurnProcessor::onPlayerTurnStarted(PlayerColor which)
+{
+	const auto * playerState = gameHandler->gameState()->getPlayerState(which);
+
+	handleTimeEvents(which);
+	for (const auto * t : playerState->getTowns())
+		handleTownEvents(t);
+
+	for (const auto * t : playerState->getTowns())
+	{
+		//garrison hero first - consistent with original H3 Mana Vortex and Battle Scholar Academy levelup windows order
+		if (t->garrisonHero != nullptr)
+			gameHandler->objectVisited(t, t->garrisonHero);
+
+		if (t->visitingHero != nullptr)
+			gameHandler->objectVisited(t, t->visitingHero);
+	}
+}
+
+void NewTurnProcessor::onPlayerTurnEnded(PlayerColor which)
+{
+	const auto * playerState = gameHandler->gameState()->getPlayerState(which);
+	assert(playerState->status == EPlayerStatus::INGAME);
+
+	if (playerState->getTowns().empty())
+	{
+		DaysWithoutTown pack;
+		pack.player = which;
+		pack.daysWithoutCastle = playerState->daysWithoutCastle.value_or(0) + 1;
+		gameHandler->sendAndApply(&pack);
+	}
+	else
+	{
+		if (playerState->daysWithoutCastle.has_value())
+		{
+			DaysWithoutTown pack;
+			pack.player = which;
+			pack.daysWithoutCastle = std::nullopt;
+			gameHandler->sendAndApply(&pack);
+		}
+	}
+
+	// check for 7 days without castle
+	gameHandler->checkVictoryLossConditionsForPlayer(which);
+
+	bool newWeek = gameHandler->getDate(Date::DAY_OF_WEEK) == 7; // end of 7th day
+
+	if (newWeek) //new heroes in tavern
+		gameHandler->heroPool->onNewWeek(which);
+}
+
+ResourceSet NewTurnProcessor::generatePlayerIncome(PlayerColor playerID, bool newWeek)
+{
+	const auto & playerSettings = gameHandler->getPlayerSettings(playerID);
+	const PlayerState & state = gameHandler->gameState()->players.at(playerID);
+	ResourceSet income;
+
+	for (const auto & town : state.getTowns())
+	{
+		if (newWeek && town->hasBuilt(BuildingSubID::TREASURY))
+		{
+			//give 10% of starting gold
+			income[EGameResID::GOLD] += state.resources[EGameResID::GOLD] / 10;
+		}
+
+		//give resources if there's a Mystic Pond
+		if (newWeek && town->hasBuilt(BuildingSubID::MYSTIC_POND))
+		{
+			static constexpr std::array rareResources = {
+				GameResID::MERCURY,
+				GameResID::SULFUR,
+				GameResID::CRYSTAL,
+				GameResID::GEMS
+			};
+
+			auto resID = *RandomGeneratorUtil::nextItem(rareResources, gameHandler->getRandomGenerator());
+			int resVal = gameHandler->getRandomGenerator().nextInt(1, 4);
+
+			income[resID] += resVal;
+
+			gameHandler->setObjPropertyValue(town->id, ObjProperty::BONUS_VALUE_FIRST, resID);
+			gameHandler->setObjPropertyValue(town->id, ObjProperty::BONUS_VALUE_SECOND, resVal);
+		}
+	}
+
+	for (GameResID k = GameResID::WOOD; k < GameResID::COUNT; k++)
+	{
+		income += state.valOfBonuses(BonusType::RESOURCES_CONSTANT_BOOST, BonusSubtypeID(k));
+		income += state.valOfBonuses(BonusType::RESOURCES_TOWN_MULTIPLYING_BOOST, BonusSubtypeID(k)) * state.getTowns().size();
+	}
+
+	if(newWeek) //weekly crystal generation if 1 or more crystal dragons in any hero army or town garrison
+	{
+		bool hasCrystalGenCreature = false;
+		for (const auto & hero : state.getHeroes())
+			for(auto stack : hero->stacks)
+				if(stack.second->hasBonusOfType(BonusType::SPECIAL_CRYSTAL_GENERATION))
+					hasCrystalGenCreature = true;
+
+		for(const auto & town : state.getTowns())
+			for(auto stack : town->stacks)
+				if(stack.second->hasBonusOfType(BonusType::SPECIAL_CRYSTAL_GENERATION))
+					hasCrystalGenCreature = true;
+
+		if(hasCrystalGenCreature)
+			income[EGameResID::CRYSTAL] += 3;
+	}
+
+	TResources incomeHandicapped = income;
+	incomeHandicapped.applyHandicap(playerSettings->handicap.percentIncome);
+
+	for (auto obj :	state.getOwnedObjects())
+		incomeHandicapped += obj->asOwnable()->dailyIncome();
+
+	return incomeHandicapped;
+}
+
+SetAvailableCreatures NewTurnProcessor::generateTownGrowth(const CGTownInstance * t, EWeekType weekType, CreatureID creatureWeek, bool firstDay)
+{
+	SetAvailableCreatures sac;
+	PlayerColor player = t->tempOwner;
+
+	sac.tid = t->id;
+	sac.creatures = t->creatures;
+
+	for (int k=0; k < t->town->creatures.size(); k++)
+	{
+		if (t->creatures.at(k).second.empty())
+			continue;
+
+		uint32_t creaturesBefore = t->creatures.at(k).first;
+		uint32_t creatureGrowth = 0;
+		const CCreature *cre = t->creatures.at(k).second.back().toCreature();
+
+		if (firstDay)
+		{
+			creatureGrowth = cre->getGrowth();
+		}
+		else
+		{
+			creatureGrowth = t->creatureGrowth(k);
+
+			//Deity of fire week - upgrade both imps and upgrades
+			if (weekType == EWeekType::DEITYOFFIRE && vstd::contains(t->creatures.at(k).second, creatureWeek))
+				creatureGrowth += 15;
+
+			//bonus week, effect applies only to identical creatures
+			if (weekType == EWeekType::BONUS_GROWTH && cre->getId() == creatureWeek)
+				creatureGrowth += 5;
+		}
+
+		// Neutral towns have halved creature growth
+		if (!player.isValidPlayer())
+			creatureGrowth /= 2;
+
+		uint32_t resultingCreatures = 0;
+
+		if (weekType == EWeekType::PLAGUE)
+			resultingCreatures = creaturesBefore / 2;
+		else if (weekType == EWeekType::DOUBLE_GROWTH)
+			resultingCreatures = (creaturesBefore + creatureGrowth) * 2;
+		else
+			resultingCreatures = creaturesBefore + creatureGrowth;
+
+		sac.creatures.at(k).first = resultingCreatures;
+	}
+
+	return sac;
+}
+
+RumorState NewTurnProcessor::pickNewRumor()
+{
+	RumorState newRumor;
+
+	static const std::vector<RumorState::ERumorType> rumorTypes = {RumorState::TYPE_MAP, RumorState::TYPE_SPECIAL, RumorState::TYPE_RAND, RumorState::TYPE_RAND};
+	std::vector<RumorState::ERumorTypeSpecial> sRumorTypes = {
+															  RumorState::RUMOR_OBELISKS, RumorState::RUMOR_ARTIFACTS, RumorState::RUMOR_ARMY, RumorState::RUMOR_INCOME};
+	if(gameHandler->gameState()->map->grailPos.valid()) // Grail should always be on map, but I had related crash I didn't manage to reproduce
+		sRumorTypes.push_back(RumorState::RUMOR_GRAIL);
+
+	int rumorId = -1;
+	int rumorExtra = -1;
+	auto & rand = gameHandler->getRandomGenerator();
+	newRumor.type = *RandomGeneratorUtil::nextItem(rumorTypes, rand);
+
+	do
+	{
+		switch(newRumor.type)
+		{
+			case RumorState::TYPE_SPECIAL:
+			{
+				SThievesGuildInfo tgi;
+				gameHandler->gameState()->obtainPlayersStats(tgi, 20);
+				rumorId = *RandomGeneratorUtil::nextItem(sRumorTypes, rand);
+				if(rumorId == RumorState::RUMOR_GRAIL)
+				{
+					rumorExtra = gameHandler->gameState()->getTile(gameHandler->gameState()->map->grailPos)->terType->getIndex();
+					break;
+				}
+
+				std::vector<PlayerColor> players = {};
+				switch(rumorId)
+				{
+					case RumorState::RUMOR_OBELISKS:
+						players = tgi.obelisks[0];
+						break;
+
+					case RumorState::RUMOR_ARTIFACTS:
+						players = tgi.artifacts[0];
+						break;
+
+					case RumorState::RUMOR_ARMY:
+						players = tgi.army[0];
+						break;
+
+					case RumorState::RUMOR_INCOME:
+						players = tgi.income[0];
+						break;
+				}
+				rumorExtra = RandomGeneratorUtil::nextItem(players, rand)->getNum();
+
+				break;
+			}
+			case RumorState::TYPE_MAP:
+				// Makes sure that map rumors only used if there enough rumors too choose from
+				if(!gameHandler->gameState()->map->rumors.empty() && (gameHandler->gameState()->map->rumors.size() > 1 || !gameHandler->gameState()->currentRumor.last.count(RumorState::TYPE_MAP)))
+				{
+					rumorId = rand.nextInt(gameHandler->gameState()->map->rumors.size() - 1);
+					break;
+				}
+				else
+					newRumor.type = RumorState::TYPE_RAND;
+				[[fallthrough]];
+
+			case RumorState::TYPE_RAND:
+				auto vector = VLC->generaltexth->findStringsWithPrefix("core.randtvrn");
+				rumorId = rand.nextInt((int)vector.size() - 1);
+
+				break;
+		}
+	}
+	while(!newRumor.update(rumorId, rumorExtra));
+
+	return newRumor;
+}
+
+std::tuple<EWeekType, CreatureID> NewTurnProcessor::pickWeekType(bool newMonth)
+{
+	for (const CGTownInstance *t : gameHandler->gameState()->map->towns)
+	{
+		if (t->hasBuilt(BuildingID::GRAIL, ETownType::INFERNO))
+			return { EWeekType::DEITYOFFIRE, CreatureID::IMP };
+	}
+
+	if(!VLC->settings()->getBoolean(EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS))
+		return { EWeekType::NORMAL, CreatureID::NONE};
+
+	int monthType = gameHandler->getRandomGenerator().nextInt(99);
+	if (newMonth) //new month
+	{
+		if (monthType < 40) //double growth
+		{
+			if (VLC->settings()->getBoolean(EGameSettings::CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH))
+			{
+				CreatureID creatureID = VLC->creh->pickRandomMonster(gameHandler->getRandomGenerator());
+				return { EWeekType::DOUBLE_GROWTH, creatureID};
+			}
+			else if (VLC->creh->doubledCreatures.size())
+			{
+				CreatureID creatureID = *RandomGeneratorUtil::nextItem(VLC->creh->doubledCreatures, gameHandler->getRandomGenerator());
+				return { EWeekType::DOUBLE_GROWTH, creatureID};
+			}
+			else
+			{
+				gameHandler->complain("Cannot find creature that can be spawned!");
+				return { EWeekType::NORMAL, CreatureID::NONE};
+			}
+		}
+
+		if (monthType < 50)
+			return { EWeekType::PLAGUE, CreatureID::NONE};
+
+		return { EWeekType::NORMAL, CreatureID::NONE};
+	}
+	else //it's a week, but not full month
+	{
+		if (monthType < 25)
+		{
+			std::pair<int, CreatureID> newMonster(54, CreatureID());
+			do
+			{
+				newMonster.second = VLC->creh->pickRandomMonster(gameHandler->getRandomGenerator());
+			} while (VLC->creh->objects[newMonster.second] &&
+					(*VLC->townh)[VLC->creatures()->getById(newMonster.second)->getFaction()]->town == nullptr); // find first non neutral creature
+
+			return { EWeekType::BONUS_GROWTH, newMonster.second};
+		}
+		return { EWeekType::NORMAL, CreatureID::NONE};
+	}
+}
+
+std::vector<SetMana> NewTurnProcessor::updateHeroesManaPoints()
+{
+	std::vector<SetMana> result;
+
+	for (auto & elem : gameHandler->gameState()->players)
+	{
+		for (CGHeroInstance *h : elem.second.getHeroes())
+		{
+			int32_t newMana = h->getManaNewTurn();
+
+			if (newMana != h->mana)
+				result.emplace_back(h->id, newMana, true);
+		}
+	}
+	return result;
+}
+
+std::vector<SetMovePoints> NewTurnProcessor::updateHeroesMovementPoints()
+{
+	std::vector<SetMovePoints> result;
+
+	for (auto & elem : gameHandler->gameState()->players)
+	{
+		for (CGHeroInstance *h : elem.second.getHeroes())
+		{
+			auto ti = std::make_unique<TurnInfo>(h, 1);
+			// NOTE: this code executed when bonuses of previous day not yet updated (this happen in NewTurn::applyGs). See issue 2356
+			int32_t newMovementPoints = h->movementPointsLimitCached(gameHandler->gameState()->map->getTile(h->visitablePos()).terType->isLand(), ti.get());
+
+			if (newMovementPoints != h->movementPointsRemaining())
+				result.emplace_back(h->id, newMovementPoints, true);
+		}
+	}
+	return result;
+}
+
+InfoWindow NewTurnProcessor::createInfoWindow(EWeekType weekType, CreatureID creatureWeek, bool newMonth)
+{
+	InfoWindow iw;
+	switch (weekType)
+	{
+		case EWeekType::DOUBLE_GROWTH:
+			iw.text.appendLocalString(EMetaText::ARRAY_TXT, 131);
+			iw.text.replaceNameSingular(creatureWeek);
+			iw.text.replaceNameSingular(creatureWeek);
+			break;
+		case EWeekType::PLAGUE:
+			iw.text.appendLocalString(EMetaText::ARRAY_TXT, 132);
+			break;
+		case EWeekType::BONUS_GROWTH:
+			iw.text.appendLocalString(EMetaText::ARRAY_TXT, 134);
+			iw.text.replaceNameSingular(creatureWeek);
+			iw.text.replaceNameSingular(creatureWeek);
+			break;
+		case EWeekType::DEITYOFFIRE:
+			iw.text.appendLocalString(EMetaText::ARRAY_TXT, 135);
+			iw.text.replaceNameSingular(CreatureID::IMP); //%s imp
+			iw.text.replaceNameSingular(CreatureID::IMP); //%s imp
+			iw.text.replacePositiveNumber(15);//%+d 15
+			iw.text.replaceNameSingular(CreatureID::FAMILIAR); //%s familiar
+			iw.text.replacePositiveNumber(15);//%+d 15
+			break;
+		default:
+			if (newMonth)
+			{
+				iw.text.appendLocalString(EMetaText::ARRAY_TXT, (130));
+				iw.text.replaceLocalString(EMetaText::ARRAY_TXT, gameHandler->getRandomGenerator().nextInt(32, 41));
+			}
+			else
+			{
+				iw.text.appendLocalString(EMetaText::ARRAY_TXT, (133));
+				iw.text.replaceLocalString(EMetaText::ARRAY_TXT, gameHandler->getRandomGenerator().nextInt(43, 57));
+			}
+	}
+	return iw;
+}
+
+NewTurn NewTurnProcessor::generateNewTurnPack()
+{
+	NewTurn n;
+	n.specialWeek = EWeekType::FIRST_WEEK;
+	n.creatureid = CreatureID::NONE;
+	n.day = gameHandler->gameState()->day + 1;
+
+	bool firstTurn = !gameHandler->getDate(Date::DAY);
+	bool newWeek = gameHandler->getDate(Date::DAY_OF_WEEK) == 7; //day numbers are confusing, as day was not yet switched
+	bool newMonth = gameHandler->getDate(Date::DAY_OF_MONTH) == 28;
+
+	if (!firstTurn)
+	{
+		for (const auto & player : gameHandler->gameState()->players)
+			n.playerIncome[player.first] = generatePlayerIncome(player.first, newWeek);
+	}
+
+	if (newWeek && !firstTurn)
+	{
+		auto [specialWeek, creatureID] = pickWeekType(newMonth);
+		n.specialWeek = specialWeek;
+		n.creatureid = creatureID;
+	}
+
+	n.heroesMana = updateHeroesManaPoints();
+	n.heroesMovement = updateHeroesMovementPoints();
+
+	if (newWeek)
+	{
+		for (CGTownInstance *t : gameHandler->gameState()->map->towns)
+			n.availableCreatures.push_back(generateTownGrowth(t, n.specialWeek, n.creatureid, firstTurn));
+	}
+
+	if (newWeek)
+		n.newRumor = pickNewRumor();
+
+	if (newWeek)
+	{
+		//new week info popup
+		if (n.specialWeek != EWeekType::FIRST_WEEK)
+			n.newWeekNotification = createInfoWindow(n.specialWeek, n.creatureid, newMonth);
+	}
+
+	return n;
+}
+
+void NewTurnProcessor::onNewTurn()
+{
+	NewTurn n = generateNewTurnPack();
+
+	bool newWeek = gameHandler->getDate(Date::DAY_OF_WEEK) == 7; //day numbers are confusing, as day was not yet switched
+	bool newMonth = gameHandler->getDate(Date::DAY_OF_MONTH) == 28;
+
+	gameHandler->sendAndApply(&n);
+
+	if (newWeek)
+	{
+		for (CGTownInstance *t : gameHandler->gameState()->map->towns)
+			if (t->hasBuilt(BuildingSubID::PORTAL_OF_SUMMONING))
+				gameHandler->setPortalDwelling(t, true, (n.specialWeek == EWeekType::PLAGUE ? true : false)); //set creatures for Portal of Summoning
+	}
+
+	//spawn wandering monsters
+	if (newMonth && (n.specialWeek == EWeekType::DOUBLE_GROWTH || n.specialWeek == EWeekType::DEITYOFFIRE))
+	{
+		gameHandler->spawnWanderingMonsters(n.creatureid);
+	}
+
+	logGlobal->trace("Info about turn %d has been sent!", n.day);
+}

+ 51 - 0
server/processors/NewTurnProcessor.h

@@ -0,0 +1,51 @@
+/*
+ * NewTurnProcessor.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/constants/EntityIdentifiers.h"
+#include "../../lib/constants/Enumerations.h"
+#include "../../lib/gameState/RumorState.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+class CGTownInstance;
+class ResourceSet;
+struct SetAvailableCreatures;
+struct SetMovePoints;
+struct SetMana;
+struct InfoWindow;
+struct NewTurn;
+VCMI_LIB_NAMESPACE_END
+
+class CGameHandler;
+
+class NewTurnProcessor : boost::noncopyable
+{
+	CGameHandler * gameHandler;
+
+	std::vector<SetMana> updateHeroesManaPoints();
+	std::vector<SetMovePoints> updateHeroesMovementPoints();
+
+	ResourceSet generatePlayerIncome(PlayerColor playerID, bool newWeek);
+	SetAvailableCreatures generateTownGrowth(const CGTownInstance * town, EWeekType weekType, CreatureID creatureWeek, bool firstDay);
+	RumorState pickNewRumor();
+	InfoWindow createInfoWindow(EWeekType weekType, CreatureID creatureWeek, bool newMonth);
+	std::tuple<EWeekType, CreatureID> pickWeekType(bool newMonth);
+
+	NewTurn generateNewTurnPack();
+	void handleTimeEvents(PlayerColor player);
+	void handleTownEvents(const CGTownInstance *town);
+
+public:
+	NewTurnProcessor(CGameHandler * gameHandler);
+
+	void onNewTurn();
+	void onPlayerTurnStarted(PlayerColor color);
+	void onPlayerTurnEnded(PlayerColor color);
+};

+ 2 - 2
server/processors/PlayerMessageProcessor.cpp

@@ -710,11 +710,11 @@ bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerCo
 			executeCheatCode(cheatName, i.first, ObjectInstanceID::NONE, parameters);
 
 		if (vstd::contains(townTargetedCheats, cheatName))
-			for (const auto & t : i.second.towns)
+			for (const auto & t : i.second.getTowns())
 				executeCheatCode(cheatName, i.first, t->id, parameters);
 
 		if (vstd::contains(heroTargetedCheats, cheatName))
-			for (const auto & h : i.second.heroes)
+			for (const auto & h : i.second.getHeroes())
 				executeCheatCode(cheatName, i.first, h->id, parameters);
 	}
 

+ 2 - 2
server/processors/TurnOrderProcessor.cpp

@@ -121,7 +121,7 @@ bool TurnOrderProcessor::playersInContact(PlayerColor left, PlayerColor right) c
 		}
 	}
 
-	for(const auto & hero : leftInfo->heroes)
+	for(const auto & hero : leftInfo->getHeroes())
 	{
 		CPathsInfo out(mapSize, hero);
 		auto config = std::make_shared<SingleHeroPathfinderConfig>(out, gameHandler->gameState(), hero);
@@ -137,7 +137,7 @@ bool TurnOrderProcessor::playersInContact(PlayerColor left, PlayerColor right) c
 						leftReachability[z][x][y] = true;
 	}
 
-	for(const auto & hero : rightInfo->heroes)
+	for(const auto & hero : rightInfo->getHeroes())
 	{
 		CPathsInfo out(mapSize, hero);
 		auto config = std::make_shared<SingleHeroPathfinderConfig>(out, gameHandler->gameState(), hero);

+ 1 - 1
test/mock/mock_IGameCallback.h

@@ -91,7 +91,7 @@ public:
 	void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {}
 	void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {} //when two heroes meet on adventure map
 	void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override {}
-	void changeFogOfWar(std::unordered_set<int3> &tiles, PlayerColor player, ETileVisibility mode) override {}
+	void changeFogOfWar(const std::unordered_set<int3> &tiles, PlayerColor player, ETileVisibility mode) override {}
 	void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {}
 
 	///useful callback methods