浏览代码

Merge pull request #4518 from IvanSavenko/new_turn_refactor

Refactor new turn processing on server
Ivan Savenko 1 年之前
父节点
当前提交
175f6716d2
共有 47 个文件被更改,包括 1131 次插入757 次删除
  1. 1 3
      AI/Nullkiller/Analyzers/BuildAnalyzer.cpp
  2. 1 1
      client/Client.h
  3. 8 0
      client/NetPacksClient.cpp
  4. 2 4
      client/windows/CKingdomInterface.cpp
  5. 15 37
      lib/CGameInfoCallback.cpp
  6. 1 2
      lib/CGameInfoCallback.h
  7. 1 0
      lib/CMakeLists.txt
  8. 53 0
      lib/CPlayerState.cpp
  9. 33 9
      lib/CPlayerState.h
  10. 1 1
      lib/IGameCallback.h
  11. 6 0
      lib/ResourceSet.cpp
  12. 1 0
      lib/ResourceSet.h
  13. 10 0
      lib/constants/Enumerations.h
  14. 23 90
      lib/gameState/CGameState.cpp
  15. 4 1
      lib/gameState/CGameState.h
  16. 1 1
      lib/gameState/CGameStateCampaign.cpp
  17. 21 13
      lib/gameState/GameStatistics.cpp
  18. 2 1
      lib/gameState/GameStatistics.h
  19. 2 2
      lib/gameState/HighScore.cpp
  20. 22 17
      lib/mapObjects/CGDwelling.cpp
  21. 6 1
      lib/mapObjects/CGDwelling.h
  22. 23 0
      lib/mapObjects/CGHeroInstance.cpp
  23. 6 1
      lib/mapObjects/CGHeroInstance.h
  24. 5 0
      lib/mapObjects/CGObjectInstance.cpp
  25. 2 0
      lib/mapObjects/CGObjectInstance.h
  26. 19 27
      lib/mapObjects/CGTownInstance.cpp
  27. 4 2
      lib/mapObjects/CGTownInstance.h
  28. 3 0
      lib/mapObjects/IObjectInterface.h
  29. 31 0
      lib/mapObjects/IOwnableObject.h
  30. 43 12
      lib/mapObjects/MiscObjects.cpp
  31. 17 7
      lib/mapObjects/MiscObjects.h
  32. 5 0
      lib/mapObjects/TownBuildingInstance.cpp
  33. 1 0
      lib/mapObjects/TownBuildingInstance.h
  34. 26 26
      lib/networkPacks/NetPacksLib.cpp
  35. 47 44
      lib/networkPacks/PacksForClient.h
  36. 1 2
      lib/pathfinder/CPathfinder.cpp
  37. 2 1
      lib/serializer/ESerializationVersion.h
  38. 2 2
      lib/spells/AdventureSpellMechanics.cpp
  39. 48 441
      server/CGameHandler.cpp
  40. 3 3
      server/CGameHandler.h
  41. 2 0
      server/CMakeLists.txt
  42. 1 1
      server/battles/BattleResultProcessor.cpp
  43. 570 0
      server/processors/NewTurnProcessor.cpp
  44. 51 0
      server/processors/NewTurnProcessor.h
  45. 2 2
      server/processors/PlayerMessageProcessor.cpp
  46. 2 2
      server/processors/TurnOrderProcessor.cpp
  47. 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/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)

+ 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

+ 15 - 37
lib/CGameInfoCallback.cpp

@@ -237,7 +237,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)
+		for(auto town : gs->players[*getPlayerID()].getTowns())
 		{
 			if(town->hasBuilt(BuildingID::TAVERN))
 				taverns++;
@@ -256,7 +256,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 +609,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 +711,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 +757,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 +789,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 +822,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 +845,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 +858,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;

+ 10 - 0
lib/constants/Enumerations.h

@@ -256,4 +256,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

+ 23 - 90
lib/gameState/CGameState.cpp

@@ -344,6 +344,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);
@@ -483,6 +492,9 @@ void CGameState::randomizeMapObjects()
 				}
 			}
 		}
+
+		if (object->getOwner().isValidPlayer())
+			getPlayerState(object->getOwner())->addOwnedObject(object);
 	}
 }
 
@@ -574,7 +586,6 @@ void CGameState::initHeroes()
 		}
 
 		hero->initHero(getRandomGenerator());
-		getPlayerState(hero->getOwner())->heroes.push_back(hero);
 		map->allHeroes[hero->getHeroType().getNum()] = hero;
 	}
 
@@ -699,14 +710,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 +904,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 +946,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 +980,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 +1206,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 +1304,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 +1338,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 +1483,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 +1557,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())
 				{

+ 4 - 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;
@@ -196,6 +197,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;

+ 19 - 27
lib/mapObjects/CGTownInstance.cpp

@@ -181,7 +181,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 +192,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 +228,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 +484,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 +638,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 +675,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++;
 

+ 4 - 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;
@@ -236,7 +238,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;

+ 48 - 441
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
@@ -3363,89 +3049,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 +3326,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 +3851,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 +3859,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 +3868,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