瀏覽代碼

Merge pull request #4483 from vcmi/custom_objects_per_zone

Customizable objects in RMG zones
DjWarmonger 1 年之前
父節點
當前提交
ffed9480e0

+ 1 - 0
AI/VCAI/MapObjectsEvaluator.cpp

@@ -13,6 +13,7 @@
 #include "../../lib/VCMI_Lib.h"
 #include "../../lib/CCreatureHandler.h"
 #include "../../lib/CHeroHandler.h"
+#include "../../lib/mapObjects/CompoundMapObjectID.h"
 #include "../../lib/mapObjectConstructors/AObjectTypeHandler.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/mapObjects/CGTownInstance.h"

+ 46 - 0
config/schemas/template.json

@@ -22,6 +22,7 @@
 				"minesLikeZone" : { "type" : "number" },
 				"terrainTypeLikeZone" : { "type" : "number" },
 				"treasureLikeZone" : { "type" : "number" },
+				"customObjectsLikeZone" : { "type" : "number" },
 				
 				"terrainTypes": {"$ref" : "#/definitions/stringArray"},
 				"bannedTerrains": {"$ref" : "#/definitions/stringArray"},
@@ -49,6 +50,51 @@
 						},
 						"additionalProperties" : false
 					}
+				},
+				"customObjects" : {
+					"type" : "object",
+					"properties": {
+						"bannedCategories": {
+							"type": "array",
+							"items": {
+								"type": "string",
+								"enum": ["all", "dwelling", "creatureBank", "randomArtifact", "bonus", "resource", "resourceGenerator", "spellScroll", "pandorasBox", "questArtifact", "seerHut"]
+							}
+						},
+						"bannedObjects": {
+							"type": "array",
+							"items": {
+								"type": "string"
+							}
+						},
+						"commonObjects": {
+							"type": "array",
+							"items": {
+								"type": "object",
+								"properties": {
+									"id": {
+										"type": "string"
+									},
+									"rmg": {
+										"type": "object",
+										"properties": {
+											"value": {
+												"type": "integer"
+											},
+											"rarity": {
+												"type": "integer"
+											},
+											"zoneLimit": {
+												"type": "integer"
+											}
+										},
+										"required": ["value", "rarity"]
+									}
+								},
+								"required": ["id", "rmg"]
+							}
+						}
+					}
 				}
 			}
 		},

+ 28 - 3
docs/modders/Random_Map_Template.md

@@ -99,10 +99,13 @@
 	"minesLikeZone" : 1,
 	
 	// Treasures will have same configuration as in linked zone
-	"treasureLikeZone" : 1
+	"treasureLikeZone" : 1,
 	
 	// Terrain type will have same configuration as in linked zone
-	"terrainTypeLikeZone" : 3
+	"terrainTypeLikeZone" : 3,
+
+	// Custom objects will have same configuration as in linked zone
+	"customObjectsLikeZone" : 1,
 
 	// factions of monsters allowed on this zone
 	"allowedMonsters" : ["inferno", "necropolis"] 
@@ -130,6 +133,28 @@
 			"density" : 5
 		}
 		  ...
-	]
+	],
+
+	// Objects with different configuration than default / set by mods
+	"customObjects" :
+	{
+		// All of objects of this kind will be removed from zone
+		// Possible values: "all", "none", "creatureBank", "bonus", "dwelling", "resource", "resourceGenerator", "spellScroll", "randomArtifact", "pandorasBox", "questArtifact", "seerHut", "other
+		"bannedCategories" : ["all", "dwelling", "creatureBank", "other"],
+		// Specify object types and subtypes
+		"bannedObjects" :["core:object.randomArtifactRelic"],
+		// Configure individual common objects - overrides banned objects
+		"commonObjects":
+		[
+			{
+				"id" : "core:object.creatureBank.dragonFlyHive",
+				"rmg" : {
+					"value"		: 9000,
+					"rarity"	: 500,
+					"zoneLimit" : 2
+				}
+			}
+		]
+	}
 }
 ```

+ 5 - 0
lib/CMakeLists.txt

@@ -184,6 +184,8 @@ set(lib_MAIN_SRCS
 	rmg/TileInfo.cpp
 	rmg/Zone.cpp
 	rmg/Functions.cpp
+	rmg/ObjectInfo.cpp
+	rmg/ObjectConfig.cpp
 	rmg/RmgMap.cpp
 	rmg/PenroseTiling.cpp
 	rmg/modificators/Modificator.cpp
@@ -510,6 +512,7 @@ set(lib_MAIN_HEADERS
 	mapObjects/IOwnableObject.h
 	mapObjects/MapObjects.h
 	mapObjects/MiscObjects.h
+	mapObjects/CompoundMapObjectID.h
 	mapObjects/ObjectTemplate.h
 	mapObjects/ObstacleSetHandler.h
 
@@ -587,6 +590,8 @@ set(lib_MAIN_HEADERS
 	rmg/RmgMap.h
 	rmg/float3.h
 	rmg/Functions.h
+	rmg/ObjectInfo.h
+	rmg/ObjectConfig.h
 	rmg/PenroseTiling.h
 	rmg/modificators/Modificator.h
 	rmg/modificators/ObjectManager.h

+ 70 - 0
lib/mapObjectConstructors/CObjectClassesHandler.cpp

@@ -40,6 +40,8 @@
 #include "../texts/CGeneralTextHandler.h"
 #include "../texts/CLegacyConfigParser.h"
 
+#include <vstd/StringUtils.h>
+
 VCMI_LIB_NAMESPACE_BEGIN
 
 CObjectClassesHandler::CObjectClassesHandler()
@@ -390,6 +392,62 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(CompoundMapObjectID comp
 	return getHandlerFor(compoundIdentifier.primaryID, compoundIdentifier.secondaryID);
 }
 
+CompoundMapObjectID CObjectClassesHandler::getCompoundIdentifier(const std::string & scope, const std::string & type, const std::string & subtype) const
+{
+	std::optional<si32> id;
+	if (scope.empty())
+	{
+		id = VLC->identifiers()->getIdentifier("object", type);
+	}
+	else
+	{
+		id = VLC->identifiers()->getIdentifier(scope, "object", type);
+	}
+
+	if(id)
+	{
+		if (subtype.empty())
+			return CompoundMapObjectID(id.value(), 0);
+
+		const auto & object = mapObjectTypes.at(id.value());
+		std::optional<si32> subID = VLC->identifiers()->getIdentifier(scope, object->getJsonKey(), subtype);
+
+		if (subID)
+			return CompoundMapObjectID(id.value(), subID.value());
+	}
+
+	std::string errorString = "Failed to get id for object of type " + type + "." + subtype;
+	logGlobal->error(errorString);
+	throw std::runtime_error(errorString);
+}
+
+CompoundMapObjectID CObjectClassesHandler::getCompoundIdentifier(const std::string & objectName) const
+{
+	std::string subtype = "object"; //Default for objects with no subIds
+	std::string type;
+
+	auto scopeAndFullName = vstd::splitStringToPair(objectName, ':');
+	logGlobal->debug("scopeAndFullName: %s, %s", scopeAndFullName.first, scopeAndFullName.second);
+	
+	auto typeAndName = vstd::splitStringToPair(scopeAndFullName.second, '.');
+	logGlobal->debug("typeAndName: %s, %s", typeAndName.first, typeAndName.second);
+	
+	auto nameAndSubtype = vstd::splitStringToPair(typeAndName.second, '.');
+	logGlobal->debug("nameAndSubtype: %s, %s", nameAndSubtype.first, nameAndSubtype.second);
+
+	if (!nameAndSubtype.first.empty())
+	{
+		type = nameAndSubtype.first;
+		subtype = nameAndSubtype.second;
+	}
+	else
+	{
+		type = typeAndName.second;
+	}
+	
+	return getCompoundIdentifier(boost::to_lower_copy(scopeAndFullName.first), type, subtype);
+}
+
 std::set<MapObjectID> CObjectClassesHandler::knownObjects() const
 {
 	std::set<MapObjectID> ret;
@@ -459,6 +517,18 @@ void CObjectClassesHandler::afterLoadFinalization()
 				logGlobal->warn("No templates found for %s:%s", entry->getJsonKey(), obj->getJsonKey());
 		}
 	}
+
+	for(auto & entry : objectIdHandlers)
+	{
+		// Call function for each object id
+		entry.second(entry.first);
+	}
+}
+
+void CObjectClassesHandler::resolveObjectCompoundId(const std::string & id, std::function<void(CompoundMapObjectID)> callback)
+{
+	auto compoundId = getCompoundIdentifier(id);
+	objectIdHandlers.push_back(std::make_pair(compoundId, callback));
 }
 
 void CObjectClassesHandler::generateExtraMonolithsForRMG(ObjectClass * container)

+ 8 - 23
lib/mapObjectConstructors/CObjectClassesHandler.h

@@ -9,7 +9,7 @@
  */
 #pragma once
 
-#include "../constants/EntityIdentifiers.h"
+#include "../mapObjects/CompoundMapObjectID.h"
 #include "../IHandlerBase.h"
 #include "../json/JsonNode.h"
 
@@ -19,27 +19,6 @@ class AObjectTypeHandler;
 class ObjectTemplate;
 struct SObjectSounds;
 
-struct DLL_LINKAGE CompoundMapObjectID
-{
-	si32 primaryID;
-	si32 secondaryID;
-
-	CompoundMapObjectID(si32 primID, si32 secID) : primaryID(primID), secondaryID(secID) {};
-
-	bool operator<(const CompoundMapObjectID& other) const
-	{
-		if(this->primaryID != other.primaryID)
-			return this->primaryID < other.primaryID;
-		else
-			return this->secondaryID < other.secondaryID;
-	}
-
-	bool operator==(const CompoundMapObjectID& other) const
-	{
-		return (this->primaryID == other.primaryID) && (this->secondaryID == other.secondaryID);
-	}
-};
-
 class CGObjectInstance;
 
 using TObjectTypeHandler = std::shared_ptr<AObjectTypeHandler>;
@@ -74,6 +53,8 @@ class DLL_LINKAGE CObjectClassesHandler : public IHandlerBase, boost::noncopyabl
 	/// map that is filled during construction with all known handlers. Not serializeable due to usage of std::function
 	std::map<std::string, std::function<TObjectTypeHandler()> > handlerConstructors;
 
+	std::vector<std::pair<CompoundMapObjectID, std::function<void(CompoundMapObjectID)>>> objectIdHandlers;
+
 	/// container with H3 templates, used only during loading, no need to serialize it
 	using TTemplatesContainer = std::multimap<std::pair<MapObjectID, MapObjectSubID>, std::shared_ptr<const ObjectTemplate>>;
 	TTemplatesContainer legacyTemplates;
@@ -110,15 +91,19 @@ public:
 	TObjectTypeHandler getHandlerFor(MapObjectID type, MapObjectSubID subtype) const;
 	TObjectTypeHandler getHandlerFor(const std::string & scope, const std::string & type, const std::string & subtype) const;
 	TObjectTypeHandler getHandlerFor(CompoundMapObjectID compoundIdentifier) const;
+	CompoundMapObjectID getCompoundIdentifier(const std::string & scope, const std::string & type, const std::string & subtype) const;
+	CompoundMapObjectID getCompoundIdentifier(const std::string & objectName) const;
 
 	std::string getObjectName(MapObjectID type, MapObjectSubID subtype) const;
 
 	SObjectSounds getObjectSounds(MapObjectID type, MapObjectSubID subtype) const;
 
+	void resolveObjectCompoundId(const std::string & id, std::function<void(CompoundMapObjectID)> callback);
+
 	/// Returns handler string describing the handler (for use in client)
 	std::string getObjectHandlerName(MapObjectID type) const;
 
 	std::string getJsonKey(MapObjectID type) const;
 };
 
-VCMI_LIB_NAMESPACE_END
+VCMI_LIB_NAMESPACE_END

+ 3 - 0
lib/mapObjectConstructors/IObjectInfo.h

@@ -49,7 +49,10 @@ public:
 
 	virtual bool givesBonuses() const { return false; }
 
+	virtual bool hasGuards() const { return false; }
+
 	virtual ~IObjectInfo() = default;
+
 };
 
 VCMI_LIB_NAMESPACE_END

+ 37 - 0
lib/mapObjects/CompoundMapObjectID.h

@@ -0,0 +1,37 @@
+/*
+ * CompoundMapObjectID.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 "../constants/EntityIdentifiers.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct DLL_LINKAGE CompoundMapObjectID
+{
+	si32 primaryID;
+	si32 secondaryID;
+
+	CompoundMapObjectID(si32 primID, si32 secID) : primaryID(primID), secondaryID(secID) {};
+
+	bool operator<(const CompoundMapObjectID& other) const
+	{
+		if(this->primaryID != other.primaryID)
+			return this->primaryID < other.primaryID;
+		else
+			return this->secondaryID < other.secondaryID;
+	}
+
+	bool operator==(const CompoundMapObjectID& other) const
+	{
+		return (this->primaryID == other.primaryID) && (this->secondaryID == other.secondaryID);
+	}
+};
+
+VCMI_LIB_NAMESPACE_END

+ 5 - 0
lib/mapObjects/ObjectTemplate.cpp

@@ -508,6 +508,11 @@ bool ObjectTemplate::canBePlacedAt(TerrainId terrainID) const
 	return vstd::contains(allowedTerrains, terrainID);
 }
 
+CompoundMapObjectID ObjectTemplate::getCompoundID() const
+{
+	return CompoundMapObjectID(id, subid);
+}
+
 void ObjectTemplate::recalculate()
 {
 	calculateWidth();

+ 4 - 0
lib/mapObjects/ObjectTemplate.h

@@ -13,6 +13,7 @@
 #include "../int3.h"
 #include "../filesystem/ResourcePath.h"
 #include "../serializer/Serializeable.h"
+#include "../mapObjects/CompoundMapObjectID.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -46,6 +47,7 @@ public:
 	/// H3 ID/subID of this object
 	MapObjectID id;
 	MapObjectSubID subid;
+
 	/// print priority, objects with higher priority will be print first, below everything else
 	si32 printPriority;
 	/// animation file that should be used to display object
@@ -122,6 +124,8 @@ public:
 	// Checks if object can be placed on specific terrain
 	bool canBePlacedAt(TerrainId terrain) const;
 
+	CompoundMapObjectID getCompoundID() const;
+
 	ObjectTemplate();
 
 	void readTxt(CLegacyConfigParser & parser);

+ 5 - 0
lib/rewardable/Info.cpp

@@ -526,6 +526,11 @@ bool Rewardable::Info::givesBonuses() const
 	return testForKey(parameters, "bonuses");
 }
 
+bool Rewardable::Info::hasGuards() const
+{
+	return testForKey(parameters, "guards");
+}
+
 const JsonNode & Rewardable::Info::getParameters() const
 {
 	return parameters;

+ 2 - 0
lib/rewardable/Info.h

@@ -68,6 +68,8 @@ public:
 
 	bool givesBonuses() const override;
 
+	bool hasGuards() const override;
+
 	void configureObject(Rewardable::Configuration & object, vstd::RNG & rng, IGameCallback * cb) const;
 
 	void init(const JsonNode & objectConfig, const std::string & objectTextID);

+ 81 - 51
lib/rmg/CRmgTemplate.cpp

@@ -102,6 +102,7 @@ void ZoneOptions::CTownInfo::serializeJson(JsonSerializeFormat & handler)
 	handler.serializeInt("castles", castleCount, 0);
 	handler.serializeInt("townDensity", townDensity, 0);
 	handler.serializeInt("castleDensity", castleDensity, 0);
+	handler.serializeInt("sourceZone", sourceZone, NO_ZONE);
 }
 
 ZoneOptions::ZoneOptions():
@@ -156,7 +157,7 @@ std::optional<int> ZoneOptions::getOwner() const
 	return owner;
 }
 
-const std::set<TerrainId> ZoneOptions::getTerrainTypes() const
+std::set<TerrainId> ZoneOptions::getTerrainTypes() const
 {
 	if (terrainTypes.empty())
 	{
@@ -191,7 +192,7 @@ std::set<FactionID> ZoneOptions::getDefaultTownTypes() const
 	return VLC->townh->getDefaultAllowed();
 }
 
-const std::set<FactionID> ZoneOptions::getTownTypes() const
+std::set<FactionID> ZoneOptions::getTownTypes() const
 {
 	if (townTypes.empty())
 	{
@@ -214,7 +215,7 @@ void ZoneOptions::setMonsterTypes(const std::set<FactionID> & value)
 	monsterTypes = value;
 }
 
-const std::set<FactionID> ZoneOptions::getMonsterTypes() const
+std::set<FactionID> ZoneOptions::getMonsterTypes() const
 {
 	return vstd::difference(monsterTypes, bannedMonsters);
 }
@@ -250,7 +251,7 @@ void ZoneOptions::addTreasureInfo(const CTreasureInfo & value)
 	vstd::amax(maxTreasureValue, value.max);
 }
 
-const std::vector<CTreasureInfo> & ZoneOptions::getTreasureInfo() const
+std::vector<CTreasureInfo> ZoneOptions::getTreasureInfo() const
 {
 	return treasureInfo;
 }
@@ -272,7 +273,22 @@ TRmgTemplateZoneId ZoneOptions::getTerrainTypeLikeZone() const
 
 TRmgTemplateZoneId ZoneOptions::getTreasureLikeZone() const
 {
-    return treasureLikeZone;
+	return treasureLikeZone;
+}
+
+ObjectConfig ZoneOptions::getCustomObjects() const
+{
+	return objectConfig;
+}
+
+void ZoneOptions::setCustomObjects(const ObjectConfig & value)
+{
+	objectConfig = value;
+}
+
+TRmgTemplateZoneId ZoneOptions::getCustomObjectsLikeZone() const
+{
+	return customObjectsLikeZone;
 }
 
 void ZoneOptions::addConnection(const ZoneConnection & connection)
@@ -334,6 +350,7 @@ void ZoneOptions::serializeJson(JsonSerializeFormat & handler)
 	SERIALIZE_ZONE_LINK(minesLikeZone);
 	SERIALIZE_ZONE_LINK(terrainTypeLikeZone);
 	SERIALIZE_ZONE_LINK(treasureLikeZone);
+	SERIALIZE_ZONE_LINK(customObjectsLikeZone);
 
 	#undef SERIALIZE_ZONE_LINK
 
@@ -398,6 +415,8 @@ void ZoneOptions::serializeJson(JsonSerializeFormat & handler)
 			handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], mines[idx], 0);
 		}
 	}
+
+	handler.serializeStruct("customObjects", objectConfig);
 }
 
 ZoneConnection::ZoneConnection():
@@ -759,53 +778,29 @@ const JsonNode & CRmgTemplate::getMapSettings() const
 	return *mapSettings;
 }
 
-std::set<TerrainId> CRmgTemplate::inheritTerrainType(std::shared_ptr<ZoneOptions> zone, uint32_t iteration /* = 0 */)
+template<typename T>
+T CRmgTemplate::inheritZoneProperty(std::shared_ptr<rmg::ZoneOptions> zone, 
+									T (rmg::ZoneOptions::*getter)() const,
+									void (rmg::ZoneOptions::*setter)(const T&),
+									TRmgTemplateZoneId (rmg::ZoneOptions::*inheritFrom)() const,
+									const std::string& propertyString,
+									uint32_t iteration)
 {
 	if (iteration >= 50)
 	{
-		logGlobal->error("Infinite recursion for terrain types detected in template %s", name);
-		return std::set<TerrainId>();
+		logGlobal->error("Infinite recursion for %s detected in template %s", propertyString, name);
+		return T();
 	}
-	if (zone->getTerrainTypeLikeZone() != ZoneOptions::NO_ZONE)
-	{
-		iteration++;
-		const auto otherZone = zones.at(zone->getTerrainTypeLikeZone());
-		zone->setTerrainTypes(inheritTerrainType(otherZone, iteration));
-	}
-	//This implicitly excludes banned terrains
-	return zone->getTerrainTypes();
-}
-
-std::map<TResource, ui16> CRmgTemplate::inheritMineTypes(std::shared_ptr<ZoneOptions> zone, uint32_t iteration /* = 0 */)
-{
-	if (iteration >= 50)
-	{
-		logGlobal->error("Infinite recursion for mine types detected in template %s", name);
-		return std::map<TResource, ui16>();
-	}
-	if (zone->getMinesLikeZone() != ZoneOptions::NO_ZONE)
-	{
-		iteration++;
-		const auto otherZone = zones.at(zone->getMinesLikeZone());
-		zone->setMinesInfo(inheritMineTypes(otherZone, iteration));
-	}
-	return zone->getMinesInfo();
-}
-
-std::vector<CTreasureInfo> CRmgTemplate::inheritTreasureInfo(std::shared_ptr<ZoneOptions> zone, uint32_t iteration /* = 0 */)
-{
-	if (iteration >= 50)
-	{
-		logGlobal->error("Infinite recursion for treasures detected in template %s", name);
-		return std::vector<CTreasureInfo>();
-	}
-	if (zone->getTreasureLikeZone() != ZoneOptions::NO_ZONE)
+	
+	if (((*zone).*inheritFrom)() != rmg::ZoneOptions::NO_ZONE)
 	{
 		iteration++;
-		const auto otherZone = zones.at(zone->getTreasureLikeZone());
-		zone->setTreasureInfo(inheritTreasureInfo(otherZone, iteration));
+		const auto otherZone = zones.at(((*zone).*inheritFrom)());
+		T inheritedValue = inheritZoneProperty(otherZone, getter, setter, inheritFrom, propertyString, iteration);
+		((*zone).*setter)(inheritedValue);
 	}
-	return zone->getTreasureInfo();
+	
+	return ((*zone).*getter)();
 }
 
 void CRmgTemplate::afterLoad()
@@ -814,12 +809,32 @@ void CRmgTemplate::afterLoad()
 	{
 		auto zone = idAndZone.second;
 
-		//Inherit properties recursively.
-		inheritTerrainType(zone);
-		inheritMineTypes(zone);
-		inheritTreasureInfo(zone);
-
-		//TODO: Inherit monster types as well
+		// Inherit properties recursively
+		inheritZoneProperty(zone, 
+							&rmg::ZoneOptions::getTerrainTypes, 
+							&rmg::ZoneOptions::setTerrainTypes, 
+							&rmg::ZoneOptions::getTerrainTypeLikeZone,
+							"terrain types");
+		
+		inheritZoneProperty(zone, 
+							&rmg::ZoneOptions::getMinesInfo, 
+							&rmg::ZoneOptions::setMinesInfo, 
+							&rmg::ZoneOptions::getMinesLikeZone,
+							"mine types");
+		
+		inheritZoneProperty(zone, 
+							&rmg::ZoneOptions::getTreasureInfo, 
+							&rmg::ZoneOptions::setTreasureInfo, 
+							&rmg::ZoneOptions::getTreasureLikeZone,
+							"treasure info");
+
+		inheritZoneProperty(zone, 
+							&rmg::ZoneOptions::getCustomObjects, 
+							&rmg::ZoneOptions::setCustomObjects, 
+							&rmg::ZoneOptions::getCustomObjectsLikeZone,
+							"custom objects");
+
+				//TODO: Inherit monster types as well
 		auto monsterTypes = zone->getMonsterTypes();
 		if (monsterTypes.empty())
 		{
@@ -848,6 +863,7 @@ void CRmgTemplate::afterLoad()
 	allowedWaterContent.erase(EWaterContent::RANDOM);
 }
 
+// TODO: Allow any integer size which does not match enum, as well
 void CRmgTemplate::serializeSize(JsonSerializeFormat & handler, int3 & value, const std::string & fieldName)
 {
 	static const std::map<std::string, int3> sizeMapping =
@@ -916,5 +932,19 @@ void CRmgTemplate::serializePlayers(JsonSerializeFormat & handler, CPlayerCountR
 		value.fromString(encodedValue);
 }
 
+const std::vector<CompoundMapObjectID> & ZoneOptions::getBannedObjects() const
+{
+	return objectConfig.getBannedObjects();
+}
+
+const std::vector<ObjectConfig::EObjectCategory> & ZoneOptions::getBannedObjectCategories() const
+{
+	return objectConfig.getBannedObjectCategories();
+}
+
+const std::vector<ObjectInfo> & ZoneOptions::getConfiguredObjects() const
+{
+	return objectConfig.getConfiguredObjects();
+}
 
 VCMI_LIB_NAMESPACE_END

+ 38 - 5
lib/rmg/CRmgTemplate.h

@@ -13,10 +13,14 @@
 #include "../int3.h"
 #include "../GameConstants.h"
 #include "../ResourceSet.h"
+#include "ObjectInfo.h"
+#include "ObjectConfig.h"
+#include "../mapObjectConstructors/CObjectClassesHandler.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
 class JsonSerializeFormat;
+struct CompoundMapObjectID;
 
 enum class ETemplateZoneType
 {
@@ -132,6 +136,9 @@ public:
 		int castleCount;
 		int townDensity;
 		int castleDensity;
+
+		// TODO: Copy from another zone once its randomized
+		TRmgTemplateZoneId sourceZone = NO_ZONE;
 	};
 
 	ZoneOptions();
@@ -146,15 +153,15 @@ public:
 	void setSize(int value);
 	std::optional<int> getOwner() const;
 
-	const std::set<TerrainId> getTerrainTypes() const;
+	std::set<TerrainId> getTerrainTypes() const;
 	void setTerrainTypes(const std::set<TerrainId> & value);
 	std::set<TerrainId> getDefaultTerrainTypes() const;
 
 	const CTownInfo & getPlayerTowns() const;
 	const CTownInfo & getNeutralTowns() const;
 	std::set<FactionID> getDefaultTownTypes() const;
-	const std::set<FactionID> getTownTypes() const;
-	const std::set<FactionID> getMonsterTypes() const;
+	std::set<FactionID> getTownTypes() const;
+	std::set<FactionID> getMonsterTypes() const;
 
 	void setTownTypes(const std::set<FactionID> & value);
 	void setMonsterTypes(const std::set<FactionID> & value);
@@ -164,7 +171,7 @@ public:
 
 	void setTreasureInfo(const std::vector<CTreasureInfo> & value);
 	void addTreasureInfo(const CTreasureInfo & value);
-	const std::vector<CTreasureInfo> & getTreasureInfo() const;
+	std::vector<CTreasureInfo> getTreasureInfo() const;
 	ui32 getMaxTreasureValue() const;
 	void recalculateMaxTreasureValue();
 
@@ -183,12 +190,24 @@ public:
 	bool areTownsSameType() const;
 	bool isMatchTerrainToTown() const;
 
+	// Get a group of configured objects
+	const std::vector<CompoundMapObjectID> & getBannedObjects() const;
+	const std::vector<ObjectConfig::EObjectCategory> & getBannedObjectCategories() const;
+	const std::vector<ObjectInfo> & getConfiguredObjects() const;
+
+	// Copy whole custom object config from another zone
+	ObjectConfig getCustomObjects() const;
+	void setCustomObjects(const ObjectConfig & value);
+	TRmgTemplateZoneId	getCustomObjectsLikeZone() const;
+
 protected:
 	TRmgTemplateZoneId id;
 	ETemplateZoneType type;
 	int size;
 	ui32 maxTreasureValue;
 	std::optional<int> owner;
+
+	ObjectConfig objectConfig;
 	CTownInfo playerTowns;
 	CTownInfo neutralTowns;
 	bool matchTerrainToTown;
@@ -211,6 +230,7 @@ protected:
 	TRmgTemplateZoneId minesLikeZone;
 	TRmgTemplateZoneId terrainTypeLikeZone;
 	TRmgTemplateZoneId treasureLikeZone;
+	TRmgTemplateZoneId customObjectsLikeZone;
 };
 
 }
@@ -280,8 +300,21 @@ private:
 	std::set<TerrainId> inheritTerrainType(std::shared_ptr<rmg::ZoneOptions> zone, uint32_t iteration = 0);
 	std::map<TResource, ui16> inheritMineTypes(std::shared_ptr<rmg::ZoneOptions> zone, uint32_t iteration = 0);
 	std::vector<CTreasureInfo> inheritTreasureInfo(std::shared_ptr<rmg::ZoneOptions> zone, uint32_t iteration = 0);
+
+	// TODO: Copy custom object settings
+	// TODO: Copy town type after source town is actually randomized
+
 	void serializeSize(JsonSerializeFormat & handler, int3 & value, const std::string & fieldName);
 	void serializePlayers(JsonSerializeFormat & handler, CPlayerCountRange & value, const std::string & fieldName);
+
+	template<typename T>
+	T inheritZoneProperty(std::shared_ptr<rmg::ZoneOptions> zone, 
+						  T (rmg::ZoneOptions::*getter)() const,
+						  void (rmg::ZoneOptions::*setter)(const T&),
+						  TRmgTemplateZoneId (rmg::ZoneOptions::*inheritFrom)() const,
+						  const std::string& propertyString,
+						  uint32_t iteration = 0);
+
 };
 
-VCMI_LIB_NAMESPACE_END
+VCMI_LIB_NAMESPACE_END

+ 189 - 0
lib/rmg/ObjectConfig.cpp

@@ -0,0 +1,189 @@
+/*
+ * ObjectConfig.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 <boost/bimap.hpp>
+#include <boost/assign.hpp>
+#include "ObjectInfo.h"
+#include "ObjectConfig.h"
+
+#include "../VCMI_Lib.h"
+#include "../mapObjectConstructors/CObjectClassesHandler.h"
+#include "../mapObjectConstructors/AObjectTypeHandler.h"
+#include "../serializer/JsonSerializeFormat.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+void ObjectConfig::addBannedObject(const CompoundMapObjectID & objid)
+{
+	// FIXME: We do not need to store the object info, just the id
+
+	bannedObjects.push_back(objid);
+
+	logGlobal->info("Banned object of type %d.%d", objid.primaryID, objid.secondaryID);
+}
+
+void ObjectConfig::addCustomObject(const ObjectInfo & object, const CompoundMapObjectID & objid)
+{
+	customObjects.push_back(object);
+	auto & lastObject = customObjects.back();
+	lastObject.setAllTemplates(objid.primaryID, objid.secondaryID);
+
+	assert(lastObject.templates.size() > 0);
+	logGlobal->info("Added custom object of type %d.%d", objid.primaryID, objid.secondaryID);
+}
+
+void ObjectConfig::serializeJson(JsonSerializeFormat & handler)
+{
+	// TODO: We need serializer utility for list of enum values
+
+	static const boost::bimap<EObjectCategory, std::string> OBJECT_CATEGORY_STRINGS = boost::assign::list_of<boost::bimap<EObjectCategory, std::string>::relation>
+		(EObjectCategory::OTHER, "other")
+		(EObjectCategory::ALL, "all")
+		(EObjectCategory::NONE, "none")
+		(EObjectCategory::CREATURE_BANK, "creatureBank")
+		(EObjectCategory::BONUS, "bonus")
+		(EObjectCategory::DWELLING, "dwelling")
+		(EObjectCategory::RESOURCE, "resource")
+		(EObjectCategory::RESOURCE_GENERATOR, "resourceGenerator")
+		(EObjectCategory::SPELL_SCROLL, "spellScroll")
+		(EObjectCategory::RANDOM_ARTIFACT, "randomArtifact")
+		(EObjectCategory::PANDORAS_BOX, "pandorasBox")
+		(EObjectCategory::QUEST_ARTIFACT, "questArtifact")
+		(EObjectCategory::SEER_HUT, "seerHut");
+
+
+	// TODO: Separate into individual methods to enforce RAII destruction?
+	{
+		auto categories = handler.enterArray("bannedCategories");
+		if (handler.saving)
+		{
+			for (const auto& category : bannedObjectCategories)
+			{
+				auto str = OBJECT_CATEGORY_STRINGS.left.at(category);
+				categories.serializeString(categories.size(), str);
+			}
+		}
+		else
+		{
+			std::vector<std::string> categoryNames;
+			categories.serializeArray(categoryNames);
+
+			for (const auto & categoryName : categoryNames)
+			{
+				auto it = OBJECT_CATEGORY_STRINGS.right.find(categoryName);
+				if (it != OBJECT_CATEGORY_STRINGS.right.end())
+				{
+					bannedObjectCategories.push_back(it->second);
+				}
+			}
+		}
+	}
+
+	// FIXME: Doesn't seem to use this field at all
+	
+	{
+		auto bannedObjectData = handler.enterArray("bannedObjects");	
+		if (handler.saving)
+		{
+
+			// FIXME: Do we even need to serialize / store banned objects?
+			/*
+			for (const auto & object : bannedObjects)
+			{
+				// TODO: Translate id back to string?
+
+
+				JsonNode node;
+				node.String() = VLC->objtypeh->getHandlerFor(object.primaryID, object.secondaryID);
+				// TODO: Check if AI-generated code is right
+
+
+			}
+			// handler.serializeRaw("bannedObjects", node, std::nullopt);
+
+			*/
+		}
+		else
+		{
+			std::vector<std::string> objectNames;
+			bannedObjectData.serializeArray(objectNames);
+
+			for (const auto & objectName : objectNames)
+			{
+				VLC->objtypeh->resolveObjectCompoundId(objectName,
+					[this](CompoundMapObjectID objid)
+					{
+						addBannedObject(objid);
+					}
+				);
+				
+			}
+		}
+	}
+
+	auto commonObjectData = handler.getCurrent()["commonObjects"].Vector();	
+	if (handler.saving)
+	{
+
+		//TODO?
+	}
+	else
+	{
+		for (const auto & objectConfig : commonObjectData)
+		{
+			auto objectName = objectConfig["id"].String();
+			auto rmg = objectConfig["rmg"].Struct();
+
+			// TODO: Use common code with default rmg config
+			auto objectValue = rmg["value"].Integer();
+			auto objectProbability = rmg["rarity"].Integer();
+
+			auto objectMaxPerZone = rmg["zoneLimit"].Integer();
+			if (objectMaxPerZone == 0)
+			{
+				objectMaxPerZone = std::numeric_limits<int>::max();
+			}
+
+			VLC->objtypeh->resolveObjectCompoundId(objectName,
+
+				[this, objectValue, objectProbability, objectMaxPerZone](CompoundMapObjectID objid)
+				{
+					ObjectInfo object(objid.primaryID, objid.secondaryID);
+					
+					// TODO: Configure basic generateObject function
+
+					object.value = objectValue;
+					object.probability = objectProbability;
+					object.maxPerZone = objectMaxPerZone;
+					addCustomObject(object, objid);
+				}
+			);
+			
+		}
+	}
+}
+
+const std::vector<ObjectInfo> & ObjectConfig::getConfiguredObjects() const
+{
+	return customObjects;
+}
+
+const std::vector<CompoundMapObjectID> & ObjectConfig::getBannedObjects() const
+{
+	return bannedObjects;
+}
+
+const std::vector<ObjectConfig::EObjectCategory> & ObjectConfig::getBannedObjectCategories() const
+{
+	return bannedObjectCategories;
+}
+
+VCMI_LIB_NAMESPACE_END

+ 57 - 0
lib/rmg/ObjectConfig.h

@@ -0,0 +1,57 @@
+/*
+ * ObjectInfo.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 "../mapObjects/CompoundMapObjectID.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class DLL_LINKAGE ObjectConfig
+{
+public:
+
+	enum class EObjectCategory
+	{
+		OTHER = -2,
+		ALL = -1,
+		NONE = 0,
+		CREATURE_BANK = 1,
+		BONUS,
+		DWELLING,
+		RESOURCE,
+		RESOURCE_GENERATOR,
+		SPELL_SCROLL,
+		RANDOM_ARTIFACT,
+		PANDORAS_BOX,
+		QUEST_ARTIFACT,
+		SEER_HUT
+	};
+
+	void addBannedObject(const CompoundMapObjectID & objid);
+	void addCustomObject(const ObjectInfo & object, const CompoundMapObjectID & objid);
+	void clearBannedObjects();
+	void clearCustomObjects();
+	const std::vector<CompoundMapObjectID> & getBannedObjects() const;
+	const std::vector<EObjectCategory> & getBannedObjectCategories() const;
+	const std::vector<ObjectInfo> & getConfiguredObjects() const;
+
+	void serializeJson(JsonSerializeFormat & handler);
+private:
+	// TODO: Add convenience method for banning objects by name
+	std::vector<CompoundMapObjectID> bannedObjects;
+	std::vector<EObjectCategory> bannedObjectCategories;
+
+	// TODO: In what format should I store custom objects?
+	// Need to convert map serialization format to ObjectInfo
+	std::vector<ObjectInfo> customObjects;
+};
+
+VCMI_LIB_NAMESPACE_END

+ 85 - 0
lib/rmg/ObjectInfo.cpp

@@ -0,0 +1,85 @@
+/*
+ * ObjectInfo.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 "ObjectInfo.h"
+
+#include "../VCMI_Lib.h"
+#include "../mapObjectConstructors/CObjectClassesHandler.h"
+#include "../mapObjectConstructors/AObjectTypeHandler.h"
+#include "../serializer/JsonSerializeFormat.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+ObjectInfo::ObjectInfo(si32 ID, si32 subID):
+	primaryID(ID),
+	secondaryID(subID),
+	destroyObject([](CGObjectInstance * obj){}),
+	maxPerZone(std::numeric_limits<ui32>::max())
+{
+}
+
+ObjectInfo::ObjectInfo(CompoundMapObjectID id):
+	ObjectInfo(id.primaryID, id.secondaryID)
+{
+}
+
+ObjectInfo::ObjectInfo(const ObjectInfo & other)
+{
+	templates = other.templates;
+	primaryID = other.primaryID;
+	secondaryID = other.secondaryID;
+	value = other.value;
+	probability = other.probability;
+	maxPerZone = other.maxPerZone;
+	generateObject = other.generateObject;
+	destroyObject = other.destroyObject;
+}
+
+ObjectInfo & ObjectInfo::operator=(const ObjectInfo & other)
+{
+	if (this == &other)
+		return *this;
+
+	templates = other.templates;
+	primaryID = other.primaryID;
+	secondaryID = other.secondaryID;
+	value = other.value;
+	probability = other.probability;
+	maxPerZone = other.maxPerZone;
+	generateObject = other.generateObject;
+	destroyObject = other.destroyObject;
+	return *this;
+}
+
+void ObjectInfo::setAllTemplates(MapObjectID type, MapObjectSubID subtype)
+{
+	auto templHandler = VLC->objtypeh->getHandlerFor(type, subtype);
+	if(!templHandler)
+		return;
+	
+	templates = templHandler->getTemplates();
+}
+
+void ObjectInfo::setTemplates(MapObjectID type, MapObjectSubID subtype, TerrainId terrainType)
+{
+	auto templHandler = VLC->objtypeh->getHandlerFor(type, subtype);
+	if(!templHandler)
+		return;
+	
+	templates = templHandler->getTemplates(terrainType);
+}
+
+CompoundMapObjectID ObjectInfo::getCompoundID() const
+{
+	return CompoundMapObjectID(primaryID, secondaryID);
+}
+
+VCMI_LIB_NAMESPACE_END

+ 45 - 0
lib/rmg/ObjectInfo.h

@@ -0,0 +1,45 @@
+/*
+ * ObjectInfo.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 "../mapObjects/ObjectTemplate.h"
+#include "../mapObjects/CompoundMapObjectID.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct CompoundMapObjectID;
+class CGObjectInstance;
+
+struct DLL_LINKAGE ObjectInfo
+{
+	ObjectInfo(si32 ID, si32 subID);
+	ObjectInfo(CompoundMapObjectID id);
+	ObjectInfo(const ObjectInfo & other);
+	ObjectInfo & operator=(const ObjectInfo & other);
+
+	std::vector<std::shared_ptr<const ObjectTemplate>> templates;
+	si32 primaryID;
+	si32 secondaryID;
+	ui32 value = 0;
+	ui16 probability = 0;
+	ui32 maxPerZone = 1;
+	//ui32 maxPerMap; //unused
+	std::function<CGObjectInstance *()> generateObject;
+	std::function<void(CGObjectInstance *)> destroyObject;
+	
+	void setAllTemplates(MapObjectID type, MapObjectSubID subtype);
+	void setTemplates(MapObjectID type, MapObjectSubID subtype, TerrainId terrain);
+
+	CompoundMapObjectID getCompoundID() const;
+	//bool matchesId(const CompoundMapObjectID & id) const;
+};
+
+VCMI_LIB_NAMESPACE_END

+ 2 - 1
lib/rmg/modificators/ObjectDistributor.cpp

@@ -45,7 +45,6 @@ void ObjectDistributor::init()
 
 void ObjectDistributor::distributeLimitedObjects()
 {
-	ObjectInfo oi;
 	auto zones = map.getZones();
 
 	for (auto primaryID : VLC->objtypeh->knownObjects())
@@ -81,6 +80,8 @@ void ObjectDistributor::distributeLimitedObjects()
 					RandomGeneratorUtil::randomShuffle(matchingZones, zone.getRand());
 					for (auto& zone : matchingZones)
 					{
+						ObjectInfo oi(primaryID, secondaryID);
+						
 						oi.generateObject = [cb=map.mapInstance->cb, primaryID, secondaryID]() -> CGObjectInstance *
 						{
 							return VLC->objtypeh->getHandlerFor(primaryID, secondaryID)->create(cb, nullptr);

+ 389 - 98
lib/rmg/modificators/TreasurePlacer.cpp

@@ -10,6 +10,7 @@
 
 #include "StdInc.h"
 #include "TreasurePlacer.h"
+#include "../CRmgTemplate.h"
 #include "../CMapGenerator.h"
 #include "../Functions.h"
 #include "ObjectManager.h"
@@ -24,6 +25,7 @@
 #include "../../mapObjectConstructors/AObjectTypeHandler.h"
 #include "../../mapObjectConstructors/CObjectClassesHandler.h"
 #include "../../mapObjectConstructors/DwellingInstanceConstructor.h"
+#include "../../rewardable/Info.h"
 #include "../../mapObjects/CGHeroInstance.h"
 #include "../../mapObjects/CGPandoraBox.h"
 #include "../../mapObjects/CQuest.h"
@@ -37,15 +39,29 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-ObjectInfo::ObjectInfo():
-	destroyObject([](CGObjectInstance * obj){})
+void TreasurePlacer::process()
 {
+	if (zone.getMaxTreasureValue() == 0)
+	{
+		//No treasures at all
+		return;
+	}
 
-}
+	tierValues = generator.getConfig().pandoraCreatureValues;
+	// Add all native creatures
+	for(auto const & cre : VLC->creh->objects)
+	{
+		if(!cre->special && cre->getFaction() == zone.getTownType())
+		{
+			creatures.push_back(cre.get());
+		}
+	}
 
-void TreasurePlacer::process()
-{
+	// Get default objects
 	addAllPossibleObjects();
+	// Override with custom objects
+	objects.patchWithZoneConfig(zone, this);
+
 	auto * m = zone.getModificator<ObjectManager>();
 	if(m)
 		createTreasures(*m);
@@ -62,14 +78,37 @@ void TreasurePlacer::init()
 
 void TreasurePlacer::addObjectToRandomPool(const ObjectInfo& oi)
 {
+	if (oi.templates.empty())
+	{
+		logGlobal->error("Attempt to add ObjectInfo with no templates! Value: %d", oi.value);
+		return;
+	}
+	if (!oi.generateObject)
+	{
+		logGlobal->error("Attempt to add ObjectInfo with no generateObject function! Value: %d", oi.value);
+		return;
+	}
+	if (!oi.maxPerZone)
+	{
+		logGlobal->warn("Attempt to add ObjectInfo with 0 maxPerZone! Value: %d", oi.value);
+		return;
+	}
 	RecursiveLock lock(externalAccessMutex);
-	possibleObjects.push_back(oi);
+	objects.addObject(oi);
 }
 
 void TreasurePlacer::addAllPossibleObjects()
 {
-	ObjectInfo oi;
-	
+	addCommonObjects();
+	addDwellings();
+	addPandoraBoxes();
+	addSeerHuts();
+	addPrisons();
+	addScrolls();
+}
+
+void TreasurePlacer::addCommonObjects()
+{
 	for(auto primaryID : VLC->objtypeh->knownObjects())
 	{
 		for(auto secondaryID : VLC->objtypeh->knownSubObjects(primaryID))
@@ -83,21 +122,31 @@ void TreasurePlacer::addAllPossibleObjects()
 					//Skip objects with per-map limit here
 					continue;
 				}
+				ObjectInfo oi(primaryID, secondaryID);
+				setBasicProperties(oi, CompoundMapObjectID(primaryID, secondaryID));
 
-				oi.generateObject = [this, primaryID, secondaryID]() -> CGObjectInstance *
-				{
-					return VLC->objtypeh->getHandlerFor(primaryID, secondaryID)->create(map.mapInstance->cb, nullptr);
-				};
 				oi.value = rmgInfo.value;
 				oi.probability = rmgInfo.rarity;
-				oi.setTemplates(primaryID, secondaryID, zone.getTerrainType());
 				oi.maxPerZone = rmgInfo.zoneLimit;
+
 				if(!oi.templates.empty())
 					addObjectToRandomPool(oi);
 			}
 		}
 	}
+}
+
+void TreasurePlacer::setBasicProperties(ObjectInfo & oi, CompoundMapObjectID objid) const
+{
+	oi.generateObject = [this, objid]() -> CGObjectInstance *
+	{
+		return VLC->objtypeh->getHandlerFor(objid)->create(map.mapInstance->cb, nullptr);
+	};
+	oi.setTemplates(objid.primaryID, objid.secondaryID, zone.getTerrainType());
+}
 
+void TreasurePlacer::addPrisons()
+{
 	//Generate Prison on water only if it has a template
 	auto prisonTemplates = VLC->objtypeh->getHandlerFor(Obj::PRISON, 0)->getTemplates(zone.getTerrainType());
 	if (!prisonTemplates.empty())
@@ -119,7 +168,7 @@ void TreasurePlacer::addAllPossibleObjects()
 		size_t prisonsLeft = getMaxPrisons();
 		for (int i = prisonsLevels - 1; i >= 0; i--)
 		{
-			ObjectInfo oi; // Create new instance which will hold destructor operation
+			ObjectInfo oi(Obj::PRISON, 0); // Create new instance which will hold destructor operation
 
 			oi.value = generator.getConfig().prisonValues[i];
 			if (oi.value > zone.getMaxTreasureValue())
@@ -157,22 +206,13 @@ void TreasurePlacer::addAllPossibleObjects()
 				addObjectToRandomPool(oi);
 		}
 	}
+}
 
+void TreasurePlacer::addDwellings()
+{
 	if(zone.getType() == ETemplateZoneType::WATER)
 		return;
-	
-	//all following objects are unlimited
-	oi.maxPerZone = std::numeric_limits<ui32>::max();
 
-	std::vector<const CCreature *> creatures; //native creatures for this zone
-	for(auto const & cre : VLC->creh->objects)
-	{
-		if(!cre->special && cre->getFaction() == zone.getTownType())
-		{
-			creatures.push_back(cre.get());
-		}
-	}
-	
 	//dwellings
 	auto dwellingTypes = {Obj::CREATURE_GENERATOR1, Obj::CREATURE_GENERATOR4};
 	
@@ -199,6 +239,9 @@ void TreasurePlacer::addAllPossibleObjects()
 			if(cre->getFaction() == zone.getTownType())
 			{
 				auto nativeZonesCount = static_cast<float>(map.getZoneCount(cre->getFaction()));
+				ObjectInfo oi(dwellingType, secondaryID);
+				setBasicProperties(oi, CompoundMapObjectID(dwellingType, secondaryID));
+
 				oi.value = static_cast<ui32>(cre->getAIValue() * cre->getGrowth() * (1 + (nativeZonesCount / map.getTotalZoneCount()) + (nativeZonesCount / 2)));
 				oi.probability = 40;
 				
@@ -208,13 +251,20 @@ void TreasurePlacer::addAllPossibleObjects()
 					obj->tempOwner = PlayerColor::NEUTRAL;
 					return obj;
 				};
-				oi.setTemplates(dwellingType, secondaryID, zone.getTerrainType());
 				if(!oi.templates.empty())
 					addObjectToRandomPool(oi);
 			}
 		}
 	}
-	
+}
+
+void TreasurePlacer::addScrolls()
+{
+	if(zone.getType() == ETemplateZoneType::WATER)
+		return;
+
+	ObjectInfo oi(Obj::SPELL_SCROLL, 0);
+
 	for(int i = 0; i < generator.getConfig().scrollValues.size(); i++)
 	{
 		oi.generateObject = [i, this]() -> CGObjectInstance *
@@ -239,7 +289,22 @@ void TreasurePlacer::addAllPossibleObjects()
 			addObjectToRandomPool(oi);
 	}
 	
-	//pandora box with gold
+}
+
+void TreasurePlacer::addPandoraBoxes()
+{
+	if(zone.getType() == ETemplateZoneType::WATER)
+		return;
+
+	addPandoraBoxesWithGold();
+	addPandoraBoxesWithExperience();
+	addPandoraBoxesWithCreatures();
+	addPandoraBoxesWithSpells();
+}
+
+void TreasurePlacer::addPandoraBoxesWithGold()
+{
+	ObjectInfo oi(Obj::PANDORAS_BOX, 0);
 	for(int i = 1; i < 5; i++)
 	{
 		oi.generateObject = [this, i]() -> CGObjectInstance *
@@ -260,8 +325,11 @@ void TreasurePlacer::addAllPossibleObjects()
 		if(!oi.templates.empty())
 			addObjectToRandomPool(oi);
 	}
-	
-	//pandora box with experience
+}
+
+void TreasurePlacer::addPandoraBoxesWithExperience()
+{
+	ObjectInfo oi(Obj::PANDORAS_BOX, 0);
 	for(int i = 1; i < 5; i++)
 	{
 		oi.generateObject = [this, i]() -> CGObjectInstance *
@@ -282,49 +350,17 @@ void TreasurePlacer::addAllPossibleObjects()
 		if(!oi.templates.empty())
 			addObjectToRandomPool(oi);
 	}
-	
-	//pandora box with creatures
-	const std::vector<int> & tierValues = generator.getConfig().pandoraCreatureValues;
-	
-	auto creatureToCount = [tierValues](const CCreature * creature) -> int
-	{
-		if(!creature->getAIValue() || tierValues.empty()) //bug #2681
-			return 0; //this box won't be generated
-		
-		//Follow the rules from https://heroes.thelazy.net/index.php/Pandora%27s_Box
-
-		int actualTier = creature->getLevel() > tierValues.size() ?
-			tierValues.size() - 1 :
-			creature->getLevel() - 1;
-		float creaturesAmount = std::floor((static_cast<float>(tierValues[actualTier])) / creature->getAIValue());
-		if (creaturesAmount < 1)
-		{
-			return 0;
-		}
-		else if(creaturesAmount <= 5)
-		{
-			//No change
-		}
-		else if(creaturesAmount <= 12)
-		{
-			creaturesAmount = std::ceil(creaturesAmount / 2) * 2;
-		}
-		else if(creaturesAmount <= 50)
-		{
-			creaturesAmount = std::round(creaturesAmount / 5) * 5;
-		}
-		else
-		{
-			creaturesAmount = std::round(creaturesAmount / 10) * 10;
-		}
-		return static_cast<int>(creaturesAmount);
-	};
+}
 
+void TreasurePlacer::addPandoraBoxesWithCreatures()
+{
 	for(auto * creature : creatures)
 	{
 		int creaturesAmount = creatureToCount(creature);
 		if(!creaturesAmount)
 			continue;
+
+		ObjectInfo oi(Obj::PANDORAS_BOX, 0);
 		
 		oi.generateObject = [this, creature, creaturesAmount]() -> CGObjectInstance *
 		{
@@ -339,12 +375,16 @@ void TreasurePlacer::addAllPossibleObjects()
 			return obj;
 		};
 		oi.setTemplates(Obj::PANDORAS_BOX, 0, zone.getTerrainType());
-		oi.value = static_cast<ui32>((2 * (creature->getAIValue()) * creaturesAmount * (1 + static_cast<float>(map.getZoneCount(creature->getFaction())) / map.getTotalZoneCount())) / 3);
+		oi.value = static_cast<ui32>(creature->getAIValue() * creaturesAmount * (1 + static_cast<float>(map.getZoneCount(creature->getFaction())) / map.getTotalZoneCount()));
 		oi.probability = 3;
 		if(!oi.templates.empty())
 			addObjectToRandomPool(oi);
 	}
-	
+}
+
+void TreasurePlacer::addPandoraBoxesWithSpells()
+{
+	ObjectInfo oi(Obj::PANDORAS_BOX, 0);
 	//Pandora with 12 spells of certain level
 	for(int i = 1; i <= GameConstants::SPELL_LEVELS; i++)
 	{
@@ -441,9 +481,14 @@ void TreasurePlacer::addAllPossibleObjects()
 	oi.probability = 2;
 	if(!oi.templates.empty())
 		addObjectToRandomPool(oi);
-	
+}
+
+void TreasurePlacer::addSeerHuts()
+{
 	//Seer huts with creatures or generic rewards
 
+	ObjectInfo oi(Obj::SEER_HUT, 0);
+
 	if(zone.getConnectedZoneIds().size()) //Unlikely, but...
 	{
 		auto * qap = zone.getModificator<QuestArtifactPlacer>();
@@ -588,12 +633,6 @@ void TreasurePlacer::addAllPossibleObjects()
 	}
 }
 
-size_t TreasurePlacer::getPossibleObjectsSize() const
-{
-	RecursiveLock lock(externalAccessMutex);
-	return possibleObjects.size();
-}
-
 void TreasurePlacer::setMaxPrisons(size_t count)
 {
 	RecursiveLock lock(externalAccessMutex);
@@ -606,6 +645,40 @@ size_t TreasurePlacer::getMaxPrisons() const
 	return maxPrisons;
 }
 
+int TreasurePlacer::creatureToCount(const CCreature * creature) const
+{
+	if(!creature->getAIValue() || tierValues.empty()) //bug #2681
+		return 0; //this box won't be generated
+	
+	//Follow the rules from https://heroes.thelazy.net/index.php/Pandora%27s_Box
+
+	int actualTier = creature->getLevel() > tierValues.size() ?
+		tierValues.size() - 1 :
+		creature->getLevel() - 1;
+	float creaturesAmount = std::floor((static_cast<float>(tierValues[actualTier])) / creature->getAIValue());
+	if (creaturesAmount < 1)
+	{
+		return 0;
+	}
+	else if(creaturesAmount <= 5)
+	{
+		//No change
+	}
+	else if(creaturesAmount <= 12)
+	{
+		creaturesAmount = std::ceil(creaturesAmount / 2) * 2;
+	}
+	else if(creaturesAmount <= 50)
+	{
+		creaturesAmount = std::round(creaturesAmount / 5) * 5;
+	}
+	else
+	{
+		creaturesAmount = std::round(creaturesAmount / 10) * 10;
+	}
+	return static_cast<int>(creaturesAmount);
+};
+
 bool TreasurePlacer::isGuardNeededForTreasure(int value)
 {// no guard in a zone with "monsters: none" and for small treasures; water zones cen get monster strength ZONE_NONE elsewhere if needed
 	return zone.monsterStrength != EMonsterStrength::ZONE_NONE && value > minGuardedValue;
@@ -623,6 +696,7 @@ std::vector<ObjectInfo*> TreasurePlacer::prepareTreasurePile(const CTreasureInfo
 	bool hasLargeObject = false;
 	while(currentValue <= static_cast<int>(desiredValue) - 100) //no objects with value below 100 are available
 	{
+		// FIXME: Pointer might be invalidated after this
 		auto * oi = getRandomObject(desiredValue, currentValue, !hasLargeObject);
 		if(!oi) //fail
 			break;
@@ -674,12 +748,21 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector<ObjectInfo*>
 			accessibleArea.add(int3());
 		}
 		
-		auto * object = oi->generateObject();
-		if(oi->templates.empty())
+		CGObjectInstance * object = nullptr;
+		if (oi->generateObject)
 		{
-			logGlobal->warn("Deleting randomized object with no templates: %s", object->getObjectName());
-			oi->destroyObject(object);
-			delete object;
+			object = oi->generateObject();
+			if(oi->templates.empty())
+			{
+				logGlobal->warn("Deleting randomized object with no templates: %s", object->getObjectName());
+				oi->destroyObject(object);
+				delete object;
+				continue;
+			}
+		}
+		else
+		{
+			logGlobal->error("ObjectInfo has no generateObject function! Templates: %d", oi->templates.size());
 			continue;
 		}
 		
@@ -785,7 +868,7 @@ ObjectInfo * TreasurePlacer::getRandomObject(ui32 desiredValue, ui32 currentValu
 	ui32 maxVal = desiredValue - currentValue;
 	ui32 minValue = static_cast<ui32>(0.25f * (desiredValue - currentValue));
 	
-	for(ObjectInfo & oi : possibleObjects) //copy constructor turned out to be costly
+	for(ObjectInfo & oi : objects.getPossibleObjects()) //copy constructor turned out to be costly
 	{
 		if(oi.value > maxVal)
 			break; //this assumes values are sorted in ascending order
@@ -859,24 +942,19 @@ void TreasurePlacer::createTreasures(ObjectManager& manager)
 	boost::sort(treasureInfo, valueComparator);
 
 	//sort treasures by ascending value so we can stop checking treasures with too high value
-	boost::sort(possibleObjects, [](const ObjectInfo& oi1, const ObjectInfo& oi2) -> bool
-	{
-		return oi1.value < oi2.value;
-	});
+	objects.sortPossibleObjects();
 
 	const size_t size = zone.area()->getTilesVector().size();
 
 	int totalDensity = 0;
 
+	// FIXME: No need to use iterator here
 	for (auto t  = treasureInfo.begin(); t != treasureInfo.end(); t++)
 	{
 		std::vector<rmg::Object> treasures;
 
 		//discard objects with too high value to be ever placed
-		vstd::erase_if(possibleObjects, [t](const ObjectInfo& oi) -> bool
-		{
-			return oi.value > t->max;
-		});
+		objects.discardObjectsAboveValue(t->max);
 
 		totalDensity += t->density;
 
@@ -895,7 +973,11 @@ void TreasurePlacer::createTreasures(ObjectManager& manager)
 				continue;
 			}
 
-			int value = std::accumulate(treasurePileInfos.begin(), treasurePileInfos.end(), 0, [](int v, const ObjectInfo* oi) {return v + oi->value; });
+			int value = std::accumulate(treasurePileInfos.begin(), treasurePileInfos.end(), 0,
+				[](int v, const ObjectInfo* oi)
+			{
+				return v + oi->value;
+			});
 
 			const ui32 maxPileGenerationAttempts = 2;
 			for (ui32 attempt = 0; attempt < maxPileGenerationAttempts; attempt++)
@@ -1016,13 +1098,222 @@ char TreasurePlacer::dump(const int3 & t)
 	return Modificator::dump(t);
 }
 
-void ObjectInfo::setTemplates(MapObjectID type, MapObjectSubID subtype, TerrainId terrainType)
+void TreasurePlacer::ObjectPool::addObject(const ObjectInfo & info)
 {
-	auto templHandler = VLC->objtypeh->getHandlerFor(type, subtype);
-	if(!templHandler)
-		return;
-	
-	templates = templHandler->getTemplates(terrainType);
+	possibleObjects.push_back(info);
+}
+
+void TreasurePlacer::ObjectPool::updateObject(MapObjectID id, MapObjectSubID subid, ObjectInfo info)
+{
+	/*
+	Handle separately:
+		- Dwellings
+		- Prisons
+		- Seer huts (quests)
+		- Pandora Boxes
+	*/
+	// FIXME: This will drop all templates
+	customObjects.insert(std::make_pair(CompoundMapObjectID(id, subid), info));
+}
+
+void TreasurePlacer::ObjectPool::patchWithZoneConfig(const Zone & zone, TreasurePlacer * tp)
+{
+	// FIXME: Wycina wszystkie obiekty poza pandorami i dwellami :?
+
+	// Copy standard objects if they are not already modified
+	/*
+	for (const auto & object : possibleObjects)
+	{
+		for (const auto & templ : object.templates)
+		{
+			// FIXME: Objects with same temmplates (Pandora boxes) are not added
+			CompoundMapObjectID key(templ->id, templ->subid);
+			if (!vstd::contains(customObjects, key))
+			{
+				customObjects[key] = object;
+			}
+		}
+	}
+	*/
+	auto bannedObjectCategories = zone.getBannedObjectCategories();
+	auto categoriesSet = std::unordered_set<ObjectConfig::EObjectCategory>(bannedObjectCategories.begin(), bannedObjectCategories.end());
+
+	if (categoriesSet.count(ObjectConfig::EObjectCategory::ALL))
+	{
+		possibleObjects.clear();
+	}
+	else
+	{
+		vstd::erase_if(possibleObjects, [this, &categoriesSet](const ObjectInfo & oi) -> bool
+
+		{
+			auto category = getObjectCategory(oi.getCompoundID());
+			if (categoriesSet.count(category))
+			{
+				logGlobal->info("Removing object %s from possible objects", oi.templates.front()->stringID);
+				return true;
+			}
+			return false;
+		});
+
+		auto bannedObjects = zone.getBannedObjects();
+		auto bannedObjectsSet = std::set<CompoundMapObjectID>(bannedObjects.begin(), bannedObjects.end());
+		vstd::erase_if(possibleObjects, [&bannedObjectsSet](const ObjectInfo & object)
+		{
+			for (const auto & templ : object.templates)
+			{
+				CompoundMapObjectID key = object.getCompoundID();
+				if (bannedObjectsSet.count(key))
+				{
+					// FIXME: Stopped working, nothing is banned
+					logGlobal->info("Banning object %s from possible objects", templ->stringID);
+					return true;
+				}
+			}
+			return false;
+		});
+	}
+
+	auto configuredObjects = zone.getConfiguredObjects();
+
+	// FIXME: Access TreasurePlacer from ObjectPool
+	for (auto & object : configuredObjects)
+	{
+		tp->setBasicProperties(object, object.getCompoundID());
+		addObject(object);
+		logGlobal->info("Added custom object of type %d.%d", object.primaryID, object.secondaryID);
+	}
+	// TODO: Overwrite or add to possibleObjects
+
+	// FIXME: Protect with mutex as well?
+	/*
+	for (const auto & customObject : customObjects)
+	{
+		addObject(customObject.second);
+	}
+	*/
+	// TODO: Consider adding custom Pandora boxes with arbitrary content
+}
+
+std::vector<ObjectInfo> & TreasurePlacer::ObjectPool::getPossibleObjects()
+{
+	return possibleObjects;
+}
+
+void TreasurePlacer::ObjectPool::sortPossibleObjects()
+{
+	boost::sort(possibleObjects, [](const ObjectInfo& oi1, const ObjectInfo& oi2) -> bool
+	{
+		return oi1.value < oi2.value;
+	});
+}
+
+void TreasurePlacer::ObjectPool::discardObjectsAboveValue(ui32 value)
+{
+	vstd::erase_if(possibleObjects, [value](const ObjectInfo& oi) -> bool
+	{
+		return oi.value > value;
+	});
+}
+
+ObjectConfig::EObjectCategory TreasurePlacer::ObjectPool::getObjectCategory(CompoundMapObjectID id)
+{
+	auto name = VLC->objtypeh->getObjectHandlerName(id.primaryID);
+
+	if (name == "configurable")
+	{
+		auto handler = VLC->objtypeh->getHandlerFor(id.primaryID, id.secondaryID);
+		if (!handler)
+		{
+			return ObjectConfig::EObjectCategory::NONE;
+		}
+
+		auto temp = handler->getTemplates().front();
+		auto info = handler->getObjectInfo(temp);
+
+		if (info->hasGuards())
+		{
+			return ObjectConfig::EObjectCategory::CREATURE_BANK;
+		}
+		else if (info->givesResources())
+		{
+			return ObjectConfig::EObjectCategory::RESOURCE;
+		}
+		else if (info->givesArtifacts())
+		{
+			return ObjectConfig::EObjectCategory::RANDOM_ARTIFACT;
+		}
+		else if (info->givesBonuses())
+		{
+			return ObjectConfig::EObjectCategory::BONUS;
+		}
+
+		return ObjectConfig::EObjectCategory::OTHER;
+	}
+	else if (name == "dwelling" || name == "randomDwelling")
+	{
+		// TODO: Special handling for different tiers
+		return ObjectConfig::EObjectCategory::DWELLING;
+	}
+	else if (name == "bank")
+		return ObjectConfig::EObjectCategory::CREATURE_BANK;
+	else if (name == "market")
+		return ObjectConfig::EObjectCategory::OTHER;
+	else if (name == "hillFort")
+		return ObjectConfig::EObjectCategory::OTHER;
+	else if (name == "resource" || name == "randomResource")
+		return ObjectConfig::EObjectCategory::RESOURCE;
+	else if (name == "randomArtifact") //"artifact"
+		return ObjectConfig::EObjectCategory::RANDOM_ARTIFACT;
+	else if (name == "artifact")
+	{
+		if (id.primaryID == Obj::SPELL_SCROLL ) // randomArtifactTreasure
+		{
+			return ObjectConfig::EObjectCategory::SPELL_SCROLL;
+		}
+		else
+		{
+			return ObjectConfig::EObjectCategory::QUEST_ARTIFACT;
+		}
+	}
+	else if (name == "denOfThieves")
+		return ObjectConfig::EObjectCategory::OTHER;
+	else if (name == "lighthouse")
+	{
+		return ObjectConfig::EObjectCategory::BONUS;
+	}
+	else if (name == "magi")
+	{
+		// TODO: By default, both eye and hut are banned in every zone
+		return ObjectConfig::EObjectCategory::OTHER;
+	}
+	else if (name == "mine")
+		return ObjectConfig::EObjectCategory::RESOURCE_GENERATOR;
+	else if (name == "pandora")
+		return ObjectConfig::EObjectCategory::PANDORAS_BOX;
+	else if (name == "prison")
+	{
+		// TODO: Prisons should be configurable
+		return ObjectConfig::EObjectCategory::OTHER;
+	}
+	else if (name == "questArtifact")
+	{
+		// TODO: There are no dedicated quest artifacts, needs extra logic
+		return ObjectConfig::EObjectCategory::QUEST_ARTIFACT;
+	}
+	else if (name == "seerHut")
+	{
+		return ObjectConfig::EObjectCategory::SEER_HUT;
+	}
+	else if (name == "siren")
+		return ObjectConfig::EObjectCategory::BONUS;
+	else if (name == "obelisk")
+		return ObjectConfig::EObjectCategory::OTHER;
+
+	// TODO: ObjectConfig::EObjectCategory::SPELL_SCROLL
+
+	// Not interesting for us
+	return ObjectConfig::EObjectCategory::NONE;
 }
 
 VCMI_LIB_NAMESPACE_END

+ 43 - 17
lib/rmg/modificators/TreasurePlacer.h

@@ -9,6 +9,8 @@
  */
 
 #pragma once
+
+#include "../ObjectInfo.h"
 #include "../Zone.h"
 #include "../../mapObjects/ObjectTemplate.h"
 
@@ -18,21 +20,7 @@ class CGObjectInstance;
 class ObjectManager;
 class RmgMap;
 class CMapGenerator;
-
-struct ObjectInfo
-{
-	ObjectInfo();
-
-	std::vector<std::shared_ptr<const ObjectTemplate>> templates;
-	ui32 value = 0;
-	ui16 probability = 0;
-	ui32 maxPerZone = 1;
-	//ui32 maxPerMap; //unused
-	std::function<CGObjectInstance *()> generateObject;
-	std::function<void(CGObjectInstance *)> destroyObject;
-	
-	void setTemplates(MapObjectID type, MapObjectSubID subtype, TerrainId terrain);
-};
+class ObjectConfig;
 
 class TreasurePlacer: public Modificator
 {
@@ -45,11 +33,27 @@ public:
 	
 	void createTreasures(ObjectManager & manager);
 	void addObjectToRandomPool(const ObjectInfo& oi);
+	void setBasicProperties(ObjectInfo & oi, CompoundMapObjectID objid) const;
+
+	// TODO: Can be defaulted to addAllPossibleObjects, but then each object will need to be configured
+	void addCommonObjects();
+	void addDwellings();
+	void addPandoraBoxes();
+	void addPandoraBoxesWithGold();
+	void addPandoraBoxesWithExperience();
+	void addPandoraBoxesWithCreatures();
+	void addPandoraBoxesWithSpells();
+	void addSeerHuts();
+	void addPrisons();
+	void addScrolls();
 	void addAllPossibleObjects(); //add objects, including zone-specific, to possibleObjects
+	// TODO: Read custom object config from zone file
+
+	/// Get all objects for this terrain
 
-	size_t getPossibleObjectsSize() const;
 	void setMaxPrisons(size_t count);
 	size_t getMaxPrisons() const;
+	int creatureToCount(const CCreature * creature) const;
 	
 protected:
 	bool isGuardNeededForTreasure(int value);
@@ -59,7 +63,26 @@ protected:
 	rmg::Object constructTreasurePile(const std::vector<ObjectInfo*> & treasureInfos, bool densePlacement = false);
 
 protected:
-	std::vector<ObjectInfo> possibleObjects;
+	class ObjectPool
+	{
+	public:
+		void addObject(const ObjectInfo & info);
+		void updateObject(MapObjectID id, MapObjectSubID subid, ObjectInfo info);
+		std::vector<ObjectInfo> & getPossibleObjects();
+		void patchWithZoneConfig(const Zone & zone, TreasurePlacer * tp);
+		void sortPossibleObjects();
+		void discardObjectsAboveValue(ui32 value);
+
+		ObjectConfig::EObjectCategory getObjectCategory(CompoundMapObjectID id);
+
+	private:
+
+		std::vector<ObjectInfo> possibleObjects;
+		std::map<CompoundMapObjectID, ObjectInfo> customObjects;
+
+	} objects;
+	// TODO: Need to nagivate and update these
+
 	int minGuardedValue = 0;
 	
 	rmg::Area treasureArea;
@@ -67,6 +90,9 @@ protected:
 	rmg::Area guards;
 
 	size_t maxPrisons;
+
+	std::vector<const CCreature *> creatures; //native creatures for this zone
+	std::vector<int> tierValues;
 };
 
 VCMI_LIB_NAMESPACE_END