فهرست منبع

Support for banned game entities in random map templates

The following entities can now be banned in a random map template
definition:
- Hero
- Artifact
- Spell
- Secondary skill

The ban follows the same rules as banning via the map settings in the
map editor.

It is also now possible to bypass dependencies and access identifiers
from mods that are not dependencies when defining:
- Banned entities in random map templates
- the chance of a hero class appearing in a tavern of a specific faction
- the chance of a spell appearing in a mage guild of a specific faction
- the chance of a hero class receiving a secondary skill

For this to work, the identifier must be specified in full, e.g.
`modName:objectName`. If the specified mod is not active, the game will
silently ignore this entry.

This behaviour is not affected by mod load order. It is possible to use
this format to access a mod that has not yet been loaded.
Ivan Savenko 3 ماه پیش
والد
کامیت
f58d08e563

+ 9 - 1
client/lobby/RandomMapTab.cpp

@@ -182,6 +182,9 @@ void RandomMapTab::updateMapInfoByHost()
 	mapInfo->mapHeader->name.appendLocalString(EMetaText::GENERAL_TXT, 740);
 	mapInfo->mapHeader->description.appendLocalString(EMetaText::GENERAL_TXT, 741);
 
+	if(mapGenOptions->getWaterContent() != EWaterContent::RANDOM)
+		mapInfo->mapHeader->banWaterHeroes(mapGenOptions->getWaterContent() != EWaterContent::NONE);
+
 	const auto * temp = mapGenOptions->getMapTemplate();
 	if (temp)
 	{
@@ -191,6 +194,9 @@ void RandomMapTab::updateMapInfoByHost()
 			auto description = std::string("\n\n") + randomTemplateDescription;
 			mapInfo->mapHeader->description.appendRawString(description);
 		}
+
+		for (const auto & hero : temp->getBannedHeroes())
+			mapInfo->mapHeader->allowedHeroes.erase(hero);
 	}
 
 	mapInfo->mapHeader->difficulty = EMapDifficulty::NORMAL;
@@ -590,9 +596,11 @@ void RandomMapTab::saveOptions(const CMapGenOptions & options)
 
 void RandomMapTab::loadOptions()
 {
-	auto rmgSettings = persistentStorage["rmg"]["rmg"];
+	JsonNode rmgSettings = persistentStorage["rmg"]["rmg"];
+
 	if (!rmgSettings.Struct().empty())
 	{
+		rmgSettings.setModScope(ModScope::scopeGame());
 		mapGenOptions.reset(new CMapGenOptions());
 		JsonDeserializer handler(nullptr, rmgSettings);
 		handler.serializeStruct("lastSettings", *mapGenOptions);

+ 21 - 0
config/schemas/template.json

@@ -206,6 +206,27 @@
 			"type" : "object",
 			"$ref" : "gameSettings.json"
 		},
+		
+		"bannedSpells": {
+			"description" : "List of spells that are banned on this template",
+			"$ref" : "#/definitions/stringArray"
+		},
+		
+		"bannedArtifacts": {
+			"description" : "List of artifacts that are banned on this template",
+			"$ref" : "#/definitions/stringArray"
+		},
+		
+		"bannedSkills": {
+			"description" : "List of skills that are banned on this template",
+			"$ref" : "#/definitions/stringArray"
+		},
+		
+		"bannedHeroes": {
+			"description" : "List of heroes that are banned on this template",
+			"$ref" : "#/definitions/stringArray"
+		},
+		
 		"name" : {
 			"description" : "Optional name - useful to have several template variations with same name",
 			"type": "string"

+ 9 - 2
docs/modders/Entities_Format/Faction_Format.md

@@ -228,19 +228,26 @@ Each town requires a set of buildings (Around 30-45 buildings)
 
 	// Chance of specific hero class to appear in this town
 	// Mirrored version of field "tavern" from hero class format
+	/// Identifier without modID specifier MUST exist in base game or in one of dependencies
+	/// Identifier with explicit modID specifier will be silently skipped if corresponding mod is not loaded
 	"tavern" :
 	{
 		"knight" : 5,
-		"druid"  : 6
+		"druid"  : 6,
+		"modID:classFromMod" : 4
 	},
 
 	// Chance of specific spell to appear in mages guild of this town
 	// If spell is missing or set to 0 it will not appear unless set as "always present" in editor
 	// Spells from unavailable levels are not required to be in this list
+	/// Identifier without modID specifier MUST exist in base game or in one of dependencies
+	/// Identifier with explicit modID specifier will be silently skipped if corresponding mod is not loaded
+
 	"guildSpells" :
 	{
 		"magicArrow" : 30,
-		"bless"  : 10
+		"bless"  : 10,
+		"modID:spellFromMod" : 20
 	},
 
 	// Which tiers in this town have creature hordes. Set to -1 to disable horde(s)

+ 8 - 2
docs/modders/Entities_Format/Hero_Class_Format.md

@@ -81,13 +81,16 @@ In order to make functional hero class you also need:
 
 	// Chance to get specific secondary skill on level-up
 	// All missing skills are considered to be banned, including universities
+	/// Identifier without modID specifier MUST exist in base game or in one of dependencies
+	/// Identifier with explicit modID specifier will be silently skipped if corresponding mod is not loaded
 	"secondarySkills" :
 	{
 		"pathfinding"  : 3.
 		"archery"      : 6.
 		 ...
 		"resistance"   : 5,
-		"firstAid"     : 4
+		"firstAid"     : 4,
+		"modName:skillName" : 9
 	},
 
 	// Chance for a this hero class to appear in a town, creates pair with same field in town format 
@@ -99,11 +102,14 @@ In order to make functional hero class you also need:
 	// Reversed version of field "tavern" from town format
 	// If faction-class pair is not listed in any of them
 	// chance set to 0 and the class won't appear in tavern of this town
+	/// Identifier without modID specifier MUST exist in base game or in one of dependencies
+	/// Identifier with explicit modID specifier will be silently skipped if corresponding mod is not loaded
 	"tavern" :
 	{
 		"castle"     : 4,
 		 ...
-		"conflux"    : 6
+		"conflux"    : 6,
+		"modID:factionFromMod" : 5
 	}
 }
 ```

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

@@ -32,6 +32,8 @@
 		],
 		
 		// Chance for the skill to be offered on level-up (heroClass may override)
+		/// Identifier without modID specifier MUST exist in base game or in one of dependencies
+		/// Identifier with explicit modID specifier will be silently skipped if corresponding mod is not loaded
 		"gainChance" : {
 			// Chance for hero classes with might affinity
 			"might" : 4,
@@ -41,6 +43,7 @@
 			"knight" : 2,
 			"cleric" : 8,
 			...
+			"modName:heroClassName" : 5
 		},
 		
 		// This skill is major obligatory (like H3 Wisdom) and is guaranteed to be offered once per specific number of levels

+ 4 - 1
docs/modders/Entities_Format/Spell_Format.md

@@ -39,9 +39,12 @@
 	
 		// Chance for this spell to appear in Mage Guild of a specific faction
 		// Symmetric property of "guildSpells" property in towns
+		/// Identifier without modID specifier MUST exist in base game or in one of dependencies
+		/// Identifier with explicit modID specifier will be silently skipped if corresponding mod is not loaded
 		"gainChance":
 		{
-			"factionName" : 3
+			"factionName" : 3,
+			"modID:anotherFactionName" : 5
 		},
 
 		"animation":{<Animation format>},

+ 32 - 0
docs/modders/Random_Map_Template.md

@@ -35,6 +35,38 @@
 			"perPlayerOnMapCap" : 1
 		}
 	},
+	
+	/// List of spells that are banned on this map. 
+	/// Identifier without modID specifier MUST exist in base game or in one of dependencies
+	/// Identifier with explicit modID specifier will be silently skipped if corresponding mod is not loaded
+	"bannedSpells": [
+		"townPortal",
+		"modID:spellFromMod"
+	],
+
+	/// List of artifacts that are banned on this map. 
+	/// Identifier without modID specifier MUST exist in base game or in one of dependencies
+	/// Identifier with explicit modID specifier will be silently skipped if corresponding mod is not loaded
+	"bannedArtifacts": [
+		"armageddonsBlade",
+		"modID:artifactFromMod"
+	],
+
+	/// List of secondary skills that are banned on this map. 
+	/// Identifier without modID specifier MUST exist in base game or in one of dependencies
+	/// Identifier with explicit modID specifier will be silently skipped if corresponding mod is not loaded
+	"bannedSkills": [
+		"diplomacy",
+		"modID:secondarySkillFromMod"
+	],
+
+	/// List of heroes that are banned on this map. 
+	/// Identifier without modID specifier MUST exist in base game or in one of dependencies
+	/// Identifier with explicit modID specifier will be silently skipped if corresponding mod is not loaded
+	"bannedHeroes": [
+		"lordHaart",
+		"modID:heroFromMod"
+	]
 
 	/// List of named zones, see below for format description
 	"zones" :

+ 24 - 0
lib/CSkillHandler.cpp

@@ -16,6 +16,7 @@
 #include "GameLibrary.h"
 #include "bonuses/Updaters.h"
 #include "constants/StringConstants.h"
+#include "entities/hero/CHeroClassHandler.h"
 #include "filesystem/Filesystem.h"
 #include "modding/IdentifierStorage.h"
 #include "texts/CGeneralTextHandler.h"
@@ -221,6 +222,29 @@ std::shared_ptr<CSkill> CSkillHandler::loadFromJson(const std::string & scope, c
 	skill->special = json["special"].Bool();
 
 	LIBRARY->generaltexth->registerString(scope, skill->getNameTextID(), json["name"]);
+
+	for(auto skillPair : json["gainChance"].Struct())
+	{
+		int probability = static_cast<int>(skillPair.second.Integer());
+
+		if (skillPair.first == "might")
+		{
+			skill->gainChance[0] = probability;
+			continue;
+		}
+
+		if (skillPair.first == "magic")
+		{
+			skill->gainChance[1] = probability;
+			continue;
+		}
+
+		LIBRARY->identifiers()->requestIdentifierIfFound(skillPair.second.getModScope(), "heroClass", skillPair.first, [skill, probability](si32 classID)
+		{
+			LIBRARY->heroclassesh->objects[classID]->secSkillProbability[skill->id] = probability;
+		});
+	}
+
 	switch(json["gainChance"].getType())
 	{
 	case JsonNode::JsonType::DATA_INTEGER:

+ 6 - 6
lib/entities/faction/CTownHandler.cpp

@@ -300,17 +300,17 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons
 	const JsonNode & fortifications = source["fortifications"];
 	if (!fortifications.isNull())
 	{
-		LIBRARY->identifiers()->requestIdentifierOptional("creature", fortifications["citadelShooter"], [=](si32 identifier)
+		LIBRARY->identifiers()->requestIdentifierIfNotNull("creature", fortifications["citadelShooter"], [=](si32 identifier)
 		{
 			ret->fortifications.citadelShooter = CreatureID(identifier);
 		});
 
-		LIBRARY->identifiers()->requestIdentifierOptional("creature", fortifications["upperTowerShooter"], [=](si32 identifier)
+		LIBRARY->identifiers()->requestIdentifierIfNotNull("creature", fortifications["upperTowerShooter"], [=](si32 identifier)
 		{
 			ret->fortifications.upperTowerShooter = CreatureID(identifier);
 		});
 
-		LIBRARY->identifiers()->requestIdentifierOptional("creature", fortifications["lowerTowerShooter"], [=](si32 identifier)
+		LIBRARY->identifiers()->requestIdentifierIfNotNull("creature", fortifications["lowerTowerShooter"], [=](si32 identifier)
 		{
 			ret->fortifications.lowerTowerShooter = CreatureID(identifier);
 		});
@@ -326,7 +326,7 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons
 
 	if(!source["mapObjectLikeBonuses"].isNull())
 	{
-		LIBRARY->identifiers()->requestIdentifierOptional("object", source["mapObjectLikeBonuses"], [ret](si32 identifier)
+		LIBRARY->identifiers()->requestIdentifierIfNotNull("object", source["mapObjectLikeBonuses"], [ret](si32 identifier)
 		{
 			ret->mapObjectLikeBonuses = MapObjectID(identifier);
 		});
@@ -684,7 +684,7 @@ void CTownHandler::loadTown(CTown * town, const JsonNode & source)
 	{
 		int chance = static_cast<int>(node.second.Float());
 
-		LIBRARY->identifiers()->requestIdentifier(node.second.getModScope(), "heroClass",node.first, [=](si32 classID)
+		LIBRARY->identifiers()->requestIdentifierIfFound(node.second.getModScope(), "heroClass", node.first, [=](si32 classID)
 		{
 			LIBRARY->heroclassesh->objects[classID]->selectionProbability[town->faction->getId()] = chance;
 		});
@@ -694,7 +694,7 @@ void CTownHandler::loadTown(CTown * town, const JsonNode & source)
 	{
 		int chance = static_cast<int>(node.second.Float());
 
-		LIBRARY->identifiers()->requestIdentifier(node.second.getModScope(), "spell", node.first, [=](si32 spellID)
+		LIBRARY->identifiers()->requestIdentifierIfFound(node.second.getModScope(), "spell", node.first, [=](si32 spellID)
 		{
 			LIBRARY->spellh->objects.at(spellID)->probabilities[town->faction->getId()] = chance;
 		});

+ 21 - 29
lib/entities/hero/CHeroClassHandler.cpp

@@ -97,45 +97,37 @@ std::shared_ptr<CHeroClass> CHeroClassHandler::loadFromJson(const std::string &
 	for(auto skillPair : node["secondarySkills"].Struct())
 	{
 		int probability = static_cast<int>(skillPair.second.Integer());
-		LIBRARY->identifiers()->requestIdentifier(skillPair.second.getModScope(), "skill", skillPair.first, [heroClass, probability](si32 skillID)
-											  {
-												  heroClass->secSkillProbability[skillID] = probability;
-											  });
+		LIBRARY->identifiers()->requestIdentifierIfFound(skillPair.second.getModScope(), "skill", skillPair.first, [heroClass, probability](si32 skillID) {
+			heroClass->secSkillProbability[skillID] = probability;
+		});
 	}
 
-	LIBRARY->identifiers()->requestIdentifier ("creature", node["commander"],
-										  [=](si32 commanderID)
-										  {
-											  heroClass->commander = CreatureID(commanderID);
-										  });
+	LIBRARY->identifiers()->requestIdentifier ("creature", node["commander"], [=](si32 commanderID) {
+			heroClass->commander = CreatureID(commanderID);
+		});
 
 	heroClass->defaultTavernChance = static_cast<ui32>(node["defaultTavern"].Float());
 	for(const auto & tavern : node["tavern"].Struct())
 	{
 		int value = static_cast<int>(tavern.second.Float());
 
-		LIBRARY->identifiers()->requestIdentifier(tavern.second.getModScope(), "faction", tavern.first,
-											  [=](si32 factionID)
-											  {
-												  heroClass->selectionProbability[FactionID(factionID)] = value;
-											  });
+		LIBRARY->identifiers()->requestIdentifierIfFound(tavern.second.getModScope(), "faction", tavern.first, [=](si32 factionID) {
+			heroClass->selectionProbability[FactionID(factionID)] = value;
+		});
 	}
 
-	LIBRARY->identifiers()->requestIdentifier("faction", node["faction"],
-										  [=](si32 factionID)
-										  {
-											  heroClass->faction.setNum(factionID);
-										  });
-
-	LIBRARY->identifiers()->requestIdentifier(scope, "object", "hero", [=](si32 index)
-										  {
-											  JsonNode classConf = node["mapObject"];
-											  classConf["heroClass"].String() = identifier;
-											  if (!node["compatibilityIdentifiers"].isNull())
-												  classConf["compatibilityIdentifiers"] = node["compatibilityIdentifiers"];
-											  classConf.setModScope(scope);
-											  LIBRARY->objtypeh->loadSubObject(identifier, classConf, index, heroClass->getIndex());
-										  });
+	LIBRARY->identifiers()->requestIdentifier("faction", node["faction"], [=](si32 factionID) {
+		heroClass->faction.setNum(factionID);
+	});
+
+	LIBRARY->identifiers()->requestIdentifier(scope, "object", "hero", [=](si32 index) {
+		JsonNode classConf = node["mapObject"];
+		classConf["heroClass"].String() = identifier;
+		if (!node["compatibilityIdentifiers"].isNull())
+			classConf["compatibilityIdentifiers"] = node["compatibilityIdentifiers"];
+		classConf.setModScope(scope);
+		LIBRARY->objtypeh->loadSubObject(identifier, classConf, index, heroClass->getIndex());
+	});
 
 	return heroClass;
 }

+ 1 - 1
lib/mapObjectConstructors/CommonConstructors.cpp

@@ -51,7 +51,7 @@ void ResourceInstanceConstructor::initTypeData(const JsonNode & input)
 	config = input;
 
 	resourceType = GameResID::GOLD; //set up fallback
-	LIBRARY->identifiers()->requestIdentifierOptional("resource", input["resource"], [&](si32 index)
+	LIBRARY->identifiers()->requestIdentifierIfNotNull("resource", input["resource"], [&](si32 index)
 	{
 		resourceType = GameResID(index);
 	});

+ 4 - 4
lib/mapping/CMap.cpp

@@ -687,7 +687,7 @@ bool CMap::calculateWaterContent()
 
 void CMap::banWaterContent()
 {
-	banWaterHeroes();
+	banWaterHeroes(isWaterMap());
 	banWaterArtifacts();
 	banWaterSpells();
 	banWaterSkills();
@@ -717,16 +717,16 @@ void CMap::banWaterSkills()
 	});
 }
 
-void CMap::banWaterHeroes()
+void CMapHeader::banWaterHeroes(bool isWaterMap)
 {
 	vstd::erase_if(allowedHeroes, [&](HeroTypeID hero)
 	{
-		return hero.toHeroType()->onlyOnWaterMap && !isWaterMap();
+		return hero.toHeroType()->onlyOnWaterMap && !isWaterMap;
 	});
 
 	vstd::erase_if(allowedHeroes, [&](HeroTypeID hero)
 	{
-		return hero.toHeroType()->onlyOnMapWithoutWater && isWaterMap();
+		return hero.toHeroType()->onlyOnMapWithoutWater && isWaterMap;
 	});
 }
 

+ 0 - 1
lib/mapping/CMap.h

@@ -222,7 +222,6 @@ public:
 	bool isWaterMap() const;
 	bool calculateWaterContent();
 	void banWaterArtifacts();
-	void banWaterHeroes();
 	void banHero(const HeroTypeID& id);
 	void unbanHero(const HeroTypeID & id);
 	void banWaterSpells();

+ 2 - 0
lib/mapping/CMapHeader.h

@@ -239,6 +239,8 @@ public:
 
 	ui8 levels() const;
 
+	void banWaterHeroes(bool isWaterMap);
+
 	EMapFormat version; /// The default value is EMapFormat::SOD.
 	ModCompatibilityInfo mods; /// set of mods required to play a map
 

+ 42 - 15
lib/modding/IdentifierStorage.cpp

@@ -123,7 +123,7 @@ CIdentifierStorage::ObjectCallback CIdentifierStorage::ObjectCallback::fromNameW
 	return result;
 }
 
-CIdentifierStorage::ObjectCallback CIdentifierStorage::ObjectCallback::fromNameAndType(const std::string & scope, const std::string & type, const std::string & fullName, const std::function<void(si32)> & callback, bool optional)
+CIdentifierStorage::ObjectCallback CIdentifierStorage::ObjectCallback::fromNameAndType(const std::string & scope, const std::string & type, const std::string & fullName, const std::function<void(si32)> & callback, bool optional, bool bypassDependenciesCheck)
 {
 	assert(!scope.empty());
 
@@ -148,13 +148,14 @@ CIdentifierStorage::ObjectCallback CIdentifierStorage::ObjectCallback::fromNameA
 	result.name = typeAndName.second;
 	result.callback = callback;
 	result.optional = optional;
+	result.bypassDependenciesCheck = bypassDependenciesCheck;
 	result.dynamicType = false;
 	return result;
 }
 
 void CIdentifierStorage::requestIdentifier(const std::string & scope, const std::string & type, const std::string & name, const std::function<void(si32)> & callback) const
 {
-	requestIdentifier(ObjectCallback::fromNameAndType(scope, type, name, callback, false));
+	requestIdentifier(ObjectCallback::fromNameAndType(scope, type, name, callback, false, false));
 }
 
 void CIdentifierStorage::requestIdentifier(const std::string & scope, const std::string & fullName, const std::function<void(si32)> & callback) const
@@ -164,7 +165,7 @@ void CIdentifierStorage::requestIdentifier(const std::string & scope, const std:
 
 void CIdentifierStorage::requestIdentifier(const std::string & type, const JsonNode & name, const std::function<void(si32)> & callback) const
 {
-	requestIdentifier(ObjectCallback::fromNameAndType(name.getModScope(), type, name.String(), callback, false));
+	requestIdentifier(ObjectCallback::fromNameAndType(name.getModScope(), type, name.String(), callback, false, false));
 }
 
 void CIdentifierStorage::requestIdentifier(const JsonNode & name, const std::function<void(si32)> & callback) const
@@ -172,27 +173,37 @@ void CIdentifierStorage::requestIdentifier(const JsonNode & name, const std::fun
 	requestIdentifier(ObjectCallback::fromNameWithType(name.getModScope(), name.String(), callback, false));
 }
 
-void CIdentifierStorage::requestIdentifierOptional(const std::string & type, const JsonNode & name, const std::function<void(si32)> & callback) const
+void CIdentifierStorage::requestIdentifierIfNotNull(const std::string & type, const JsonNode & name, const std::function<void(si32)> & callback) const
 {
 	if (!name.isNull())
 		requestIdentifier(type, name, callback);
 }
 
+void CIdentifierStorage::requestIdentifierIfFound(const std::string & type, const JsonNode & name, const std::function<void(si32)> & callback) const
+{
+	requestIdentifier(ObjectCallback::fromNameAndType(name.getModScope(), type, name.String(), callback, false, true));
+}
+
+void CIdentifierStorage::requestIdentifierIfFound(const std::string & scope, const std::string & type, const std::string & name, const std::function<void(si32)> & callback) const
+{
+	requestIdentifier(ObjectCallback::fromNameAndType(scope, type, name, callback, false, true));
+}
+
 void CIdentifierStorage::tryRequestIdentifier(const std::string & scope, const std::string & type, const std::string & name, const std::function<void(si32)> & callback) const
 {
-	requestIdentifier(ObjectCallback::fromNameAndType(scope, type, name, callback, true));
+	requestIdentifier(ObjectCallback::fromNameAndType(scope, type, name, callback, true, false));
 }
 
 void CIdentifierStorage::tryRequestIdentifier(const std::string & type, const JsonNode & name, const std::function<void(si32)> & callback) const
 {
-	requestIdentifier(ObjectCallback::fromNameAndType(name.getModScope(), type, name.String(), callback, true));
+	requestIdentifier(ObjectCallback::fromNameAndType(name.getModScope(), type, name.String(), callback, true, false));
 }
 
 std::optional<si32> CIdentifierStorage::getIdentifier(const std::string & scope, const std::string & type, const std::string & name, bool silent) const
 {
 	//assert(state != ELoadingState::LOADING);
 
-	auto options = ObjectCallback::fromNameAndType(scope, type, name, std::function<void(si32)>(), silent);
+	auto options = ObjectCallback::fromNameAndType(scope, type, name, std::function<void(si32)>(), silent, false);
 	return getIdentifierImpl(options, silent);
 }
 
@@ -200,7 +211,7 @@ std::optional<si32> CIdentifierStorage::getIdentifier(const std::string & type,
 {
 	assert(state != ELoadingState::LOADING);
 
-	auto options = ObjectCallback::fromNameAndType(name.getModScope(), type, name.String(), std::function<void(si32)>(), silent);
+	auto options = ObjectCallback::fromNameAndType(name.getModScope(), type, name.String(), std::function<void(si32)>(), silent, false);
 
 	return getIdentifierImpl(options, silent);
 }
@@ -269,26 +280,28 @@ void CIdentifierStorage::showIdentifierResolutionErrorDetails(const ObjectCallba
 		}
 		else
 		{
-			// such identifiers exists, but were not picked for some reason
-			if (options.remoteScope.empty())
+			// such identifier(s) exists, but were not picked for some reason
+			for (auto const & testOption : testList)
 			{
 				// attempt to access identifier from mods that is not dependency
-				for (auto const & testOption : testList)
+				bool isValidScope = true;
+				const auto & dependencies = LIBRARY->modh->getModDependencies(options.localScope, isValidScope);
+				if (!vstd::contains(dependencies, testOption.scope))
 				{
 					logMod->error("Identifier '%s' exists in mod %s", options.name, testOption.scope);
 					logMod->error("Please add mod '%s' as dependency of mod '%s' to access this identifier", testOption.scope, options.localScope);
+					continue;
 				}
-			}
-			else
-			{
+
 				// attempt to access identifier in form 'modName:object', but identifier is only present in different mod
-				for (auto const & testOption : testList)
+				if (options.remoteScope.empty())
 				{
 					logMod->error("Identifier '%s' exists in mod '%s' but identifier was explicitly requested from mod '%s'!", options.name, testOption.scope, options.remoteScope);
 					if (options.dynamicType)
 						logMod->error("Please use form '%s.%s' or '%s:%s.%s' to access this identifier", options.type, options.name, testOption.scope, options.type, options.name);
 					else
 						logMod->error("Please use form '%s' or '%s:%s' to access this identifier", options.name, testOption.scope, options.name);
+					continue;
 				}
 			}
 		}
@@ -377,6 +390,11 @@ std::vector<CIdentifierStorage::ObjectData> CIdentifierStorage::getPossibleIdent
 			// allow self-access
 			allowedScopes.insert(request.remoteScope);
 		}
+		else if (request.bypassDependenciesCheck)
+		{
+			// this is request for an identifier that bypasses mod dependencies check
+			allowedScopes.insert(request.remoteScope);
+		}
 		else
 		{
 			// allow access only if mod is in our dependencies
@@ -423,6 +441,15 @@ bool CIdentifierStorage::resolveIdentifier(const ObjectCallback & request) const
 		return true;
 	}
 
+	if (request.bypassDependenciesCheck)
+	{
+		if (!vstd::contains(LIBRARY->modh->getActiveMods(), request.remoteScope))
+		{
+			logMod->debug("Mod '%s' requested identifier '%s' from not loaded mod '%s'. Ignoring.", request.localScope, request.remoteScope, request.name);
+			return true; // mod was not loaded - ignore
+		}
+	}
+
 	// error found. Try to generate some debug info
 	failedRequests.push_back(request);
 	showIdentifierResolutionErrorDetails(request);

+ 5 - 2
lib/modding/IdentifierStorage.h

@@ -32,13 +32,14 @@ class DLL_LINKAGE CIdentifierStorage
 		std::string name;        /// string ID
 		std::function<void(si32)> callback;
 		bool optional;
+		bool bypassDependenciesCheck;
 		bool dynamicType;
 
 		/// Builds callback from identifier in form "targetMod:type.name"
 		static ObjectCallback fromNameWithType(const std::string & scope, const std::string & fullName, const std::function<void(si32)> & callback, bool optional);
 
 		/// Builds callback from identifier in form "targetMod:name"
-		static ObjectCallback fromNameAndType(const std::string & scope, const std::string & type, const std::string & fullName, const std::function<void(si32)> & callback, bool optional);
+		static ObjectCallback fromNameAndType(const std::string & scope, const std::string & type, const std::string & fullName, const std::function<void(si32)> & callback, bool optional, bool bypassDependenciesCheck);
 
 	private:
 		ObjectCallback() = default;
@@ -87,7 +88,9 @@ public:
 	void requestIdentifier(const std::string & type, const JsonNode & name, const std::function<void(si32)> & callback) const;
 	void requestIdentifier(const JsonNode & name, const std::function<void(si32)> & callback) const;
 
-	void requestIdentifierOptional(const std::string & type, const JsonNode & name, const std::function<void(si32)> & callback) const;
+	void requestIdentifierIfNotNull(const std::string & type, const JsonNode & name, const std::function<void(si32)> & callback) const;
+	void requestIdentifierIfFound(const std::string & type, const JsonNode & name, const std::function<void(si32)> & callback) const;
+	void requestIdentifierIfFound(const std::string & scope, const std::string & type, const std::string & name, const std::function<void(si32)> & callback) const;
 
 	/// try to request ID. If ID with such name won't be loaded, callback function will not be called
 	void tryRequestIdentifier(const std::string & scope, const std::string & type, const std::string & name, const std::function<void(si32)> & callback) const;

+ 12 - 0
lib/rmg/CMapGenerator.cpp

@@ -466,6 +466,18 @@ void CMapGenerator::addHeaderInfo()
 	m.waterMap = (mapGenOptions.getWaterContent() != EWaterContent::EWaterContent::NONE);
 	m.banWaterContent();
 	m.overrideGameSettings(mapGenOptions.getMapTemplate()->getMapSettings());
+
+	for (const auto & spell : mapGenOptions.getMapTemplate()->getBannedSpells())
+		m.allowedSpells.erase(spell);
+
+	for (const auto & artifact : mapGenOptions.getMapTemplate()->getBannedArtifacts())
+		m.allowedArtifact.erase(artifact);
+
+	for (const auto & skill : mapGenOptions.getMapTemplate()->getBannedSkills())
+		m.allowedAbilities.erase(skill);
+
+	for (const auto & hero : mapGenOptions.getMapTemplate()->getBannedHeroes())
+		m.allowedHeroes.erase(hero);
 }
 
 int CMapGenerator::getNextMonlithIndex()

+ 5 - 0
lib/rmg/CRmgTemplate.cpp

@@ -835,6 +835,11 @@ void CRmgTemplate::serializeJson(JsonSerializeFormat & handler)
 	serializePlayers(handler, players, "players");
 	serializePlayers(handler, humanPlayers, "humans"); // TODO: Rename this parameter
 
+	handler.serializeIdArray("bannedSpells", bannedSpells);
+	handler.serializeIdArray("bannedArtifacts", bannedArtifacts);
+	handler.serializeIdArray("bannedSkills", bannedSkills);
+	handler.serializeIdArray("bannedHeroes", bannedHeroes);
+
 	*mapSettings = handler.getCurrent()["settings"];
 
 	{

+ 11 - 1
lib/rmg/CRmgTemplate.h

@@ -338,6 +338,11 @@ public:
 	const JsonNode & getMapSettings() const;
 	const std::vector<rmg::ZoneConnection> & getConnectedZoneIds() const;
 
+	const std::set<SpellID> & getBannedSpells() const { return bannedSpells; }
+	const std::set<ArtifactID> & getBannedArtifacts() const { return bannedArtifacts; }
+	const std::set<SecondarySkill> & getBannedSkills() const { return bannedSkills; }
+	const std::set<HeroTypeID> & getBannedHeroes() const { return bannedHeroes; }
+
 	void validate() const; /// Tests template on validity and throws exception on failure
 
 	void serializeJson(JsonSerializeFormat & handler);
@@ -356,6 +361,11 @@ private:
 	std::set<EWaterContent::EWaterContent> allowedWaterContent;
 	std::unique_ptr<JsonNode> mapSettings;
 
+	std::set<SpellID> bannedSpells;
+	std::set<ArtifactID> bannedArtifacts;
+	std::set<SecondarySkill> bannedSkills;
+	std::set<HeroTypeID> bannedHeroes;
+
 	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);
@@ -375,4 +385,4 @@ private:
 
 };
 
-VCMI_LIB_NAMESPACE_END
+VCMI_LIB_NAMESPACE_END

+ 7 - 13
lib/serializer/JsonSerializeFormat.h

@@ -316,12 +316,11 @@ public:
 		}
 		else
 		{
-			std::string fieldValue;
-			serializeString(fieldName, fieldValue);
+			const JsonNode & fieldValue = getCurrent()[fieldName];
 
-			if (!fieldValue.empty())
+			if (!fieldValue.String().empty())
 			{
-				LIBRARY->identifiers()->requestIdentifier(ModScope::scopeGame(), IdentifierType::entityType(), fieldValue, [&value](int32_t index){
+				LIBRARY->identifiers()->requestIdentifier(IdentifierType::entityType(), fieldValue, [&value](int32_t index){
 					value = IdentifierType(index);
 				});
 			}
@@ -347,14 +346,12 @@ public:
 		}
 		else
 		{
-			std::vector<std::string> fieldValue;
-			serializeInternal(fieldName, fieldValue);
-
+			const JsonVector & fieldValue = getCurrent()[fieldName].Vector();
 			value.resize(fieldValue.size());
 
 			for(size_t i = 0; i < fieldValue.size(); ++i)
 			{
-				LIBRARY->identifiers()->requestIdentifier(ModScope::scopeGame(), E::entityType(), fieldValue[i], [&value, i](int32_t index){
+				LIBRARY->identifiers()->requestIdentifier(E::entityType(), fieldValue[i], [&value, i](int32_t index){
 					value[i] = T(index);
 				});
 			}
@@ -376,12 +373,9 @@ public:
 		}
 		else
 		{
-			std::vector<std::string> fieldValue;
-			serializeInternal(fieldName, fieldValue);
-
-			for(size_t i = 0; i < fieldValue.size(); ++i)
+			for (const auto & element : getCurrent()[fieldName].Vector())
 			{
-				LIBRARY->identifiers()->requestIdentifier(ModScope::scopeGame(), U::entityType(), fieldValue[i], [&value](int32_t index){
+				LIBRARY->identifiers()->requestIdentifierIfFound(U::entityType(), element, [&value](int32_t index){
 					value.insert(T(index));
 				});
 			}

+ 1 - 1
lib/spells/CSpellHandler.cpp

@@ -767,7 +767,7 @@ std::shared_ptr<CSpell> CSpellHandler::loadFromJson(const std::string & scope, c
 	{
 		const int chance = static_cast<int>(node.second.Integer());
 
-		LIBRARY->identifiers()->requestIdentifier(node.second.getModScope(), "faction", node.first, [=](si32 factionID)
+		LIBRARY->identifiers()->requestIdentifierIfFound(node.second.getModScope(), "faction", node.first, [=](si32 factionID)
 		{
 			spell->probabilities[FactionID(factionID)] = chance;
 		});