From f58d08e56337d4de55748226e02b758c4523b0b1 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 14 Jul 2025 00:18:11 +0300 Subject: [PATCH 1/2] 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. --- client/lobby/RandomMapTab.cpp | 10 +++- config/schemas/template.json | 21 +++++++ .../modders/Entities_Format/Faction_Format.md | 11 +++- .../Entities_Format/Hero_Class_Format.md | 10 +++- .../Entities_Format/Secondary_Skill_Format.md | 3 + docs/modders/Entities_Format/Spell_Format.md | 5 +- docs/modders/Random_Map_Template.md | 32 +++++++++++ lib/CSkillHandler.cpp | 24 ++++++++ lib/entities/faction/CTownHandler.cpp | 12 ++-- lib/entities/hero/CHeroClassHandler.cpp | 48 +++++++--------- .../CommonConstructors.cpp | 2 +- lib/mapping/CMap.cpp | 8 +-- lib/mapping/CMap.h | 1 - lib/mapping/CMapHeader.h | 2 + lib/modding/IdentifierStorage.cpp | 57 ++++++++++++++----- lib/modding/IdentifierStorage.h | 7 ++- lib/rmg/CMapGenerator.cpp | 12 ++++ lib/rmg/CRmgTemplate.cpp | 5 ++ lib/rmg/CRmgTemplate.h | 12 +++- lib/serializer/JsonSerializeFormat.h | 20 +++---- lib/spells/CSpellHandler.cpp | 2 +- 21 files changed, 226 insertions(+), 78 deletions(-) diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index 7914ac217..5d6a52332 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/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); diff --git a/config/schemas/template.json b/config/schemas/template.json index d139f257b..d17fa48b6 100644 --- a/config/schemas/template.json +++ b/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" diff --git a/docs/modders/Entities_Format/Faction_Format.md b/docs/modders/Entities_Format/Faction_Format.md index 78e80ab3d..255342d95 100644 --- a/docs/modders/Entities_Format/Faction_Format.md +++ b/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) diff --git a/docs/modders/Entities_Format/Hero_Class_Format.md b/docs/modders/Entities_Format/Hero_Class_Format.md index 1a25cc01c..b1cb0e939 100644 --- a/docs/modders/Entities_Format/Hero_Class_Format.md +++ b/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 } } ``` diff --git a/docs/modders/Entities_Format/Secondary_Skill_Format.md b/docs/modders/Entities_Format/Secondary_Skill_Format.md index eb9dc38db..ab04bfd51 100644 --- a/docs/modders/Entities_Format/Secondary_Skill_Format.md +++ b/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 diff --git a/docs/modders/Entities_Format/Spell_Format.md b/docs/modders/Entities_Format/Spell_Format.md index f66b332ed..dbb1aeb39 100644 --- a/docs/modders/Entities_Format/Spell_Format.md +++ b/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":{}, diff --git a/docs/modders/Random_Map_Template.md b/docs/modders/Random_Map_Template.md index 1750e636b..a4166ed63 100644 --- a/docs/modders/Random_Map_Template.md +++ b/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" : diff --git a/lib/CSkillHandler.cpp b/lib/CSkillHandler.cpp index 4eddd9b3e..543c547bd 100644 --- a/lib/CSkillHandler.cpp +++ b/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 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(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: diff --git a/lib/entities/faction/CTownHandler.cpp b/lib/entities/faction/CTownHandler.cpp index 20a99cb3b..17c81dc47 100644 --- a/lib/entities/faction/CTownHandler.cpp +++ b/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(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(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; }); diff --git a/lib/entities/hero/CHeroClassHandler.cpp b/lib/entities/hero/CHeroClassHandler.cpp index 05be20e0b..9a8f7a6ab 100644 --- a/lib/entities/hero/CHeroClassHandler.cpp +++ b/lib/entities/hero/CHeroClassHandler.cpp @@ -97,45 +97,37 @@ std::shared_ptr CHeroClassHandler::loadFromJson(const std::string & for(auto skillPair : node["secondarySkills"].Struct()) { int probability = static_cast(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(node["defaultTavern"].Float()); for(const auto & tavern : node["tavern"].Struct()) { int value = static_cast(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("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(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; } diff --git a/lib/mapObjectConstructors/CommonConstructors.cpp b/lib/mapObjectConstructors/CommonConstructors.cpp index 6d33d85c4..f055f654a 100644 --- a/lib/mapObjectConstructors/CommonConstructors.cpp +++ b/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); }); diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index 145f17d4d..8f9a9def4 100644 --- a/lib/mapping/CMap.cpp +++ b/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; }); } diff --git a/lib/mapping/CMap.h b/lib/mapping/CMap.h index 8544c82cb..dc8b5bf05 100644 --- a/lib/mapping/CMap.h +++ b/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(); diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index 8bd828eb9..5391877b0 100644 --- a/lib/mapping/CMapHeader.h +++ b/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 diff --git a/lib/modding/IdentifierStorage.cpp b/lib/modding/IdentifierStorage.cpp index 894221bef..d71afbe98 100644 --- a/lib/modding/IdentifierStorage.cpp +++ b/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 & callback, bool optional) +CIdentifierStorage::ObjectCallback CIdentifierStorage::ObjectCallback::fromNameAndType(const std::string & scope, const std::string & type, const std::string & fullName, const std::function & 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 & 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 & 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 & 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 & 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 & callback) const +void CIdentifierStorage::requestIdentifierIfNotNull(const std::string & type, const JsonNode & name, const std::function & callback) const { if (!name.isNull()) requestIdentifier(type, name, callback); } +void CIdentifierStorage::requestIdentifierIfFound(const std::string & type, const JsonNode & name, const std::function & 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 & 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 & 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 & 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 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(), silent); + auto options = ObjectCallback::fromNameAndType(scope, type, name, std::function(), silent, false); return getIdentifierImpl(options, silent); } @@ -200,7 +211,7 @@ std::optional CIdentifierStorage::getIdentifier(const std::string & type, { assert(state != ELoadingState::LOADING); - auto options = ObjectCallback::fromNameAndType(name.getModScope(), type, name.String(), std::function(), silent); + auto options = ObjectCallback::fromNameAndType(name.getModScope(), type, name.String(), std::function(), 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::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); diff --git a/lib/modding/IdentifierStorage.h b/lib/modding/IdentifierStorage.h index 76b67bc76..51ab94c1f 100644 --- a/lib/modding/IdentifierStorage.h +++ b/lib/modding/IdentifierStorage.h @@ -32,13 +32,14 @@ class DLL_LINKAGE CIdentifierStorage std::string name; /// string ID std::function 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 & 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 & callback, bool optional); + static ObjectCallback fromNameAndType(const std::string & scope, const std::string & type, const std::string & fullName, const std::function & 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 & callback) const; void requestIdentifier(const JsonNode & name, const std::function & callback) const; - void requestIdentifierOptional(const std::string & type, const JsonNode & name, const std::function & callback) const; + void requestIdentifierIfNotNull(const std::string & type, const JsonNode & name, const std::function & callback) const; + void requestIdentifierIfFound(const std::string & type, const JsonNode & name, const std::function & callback) const; + void requestIdentifierIfFound(const std::string & scope, const std::string & type, const std::string & name, const std::function & 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 & callback) const; diff --git a/lib/rmg/CMapGenerator.cpp b/lib/rmg/CMapGenerator.cpp index 86c2e53c1..7c1fcb1b7 100644 --- a/lib/rmg/CMapGenerator.cpp +++ b/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() diff --git a/lib/rmg/CRmgTemplate.cpp b/lib/rmg/CRmgTemplate.cpp index 26860fd22..3ecc2a35e 100644 --- a/lib/rmg/CRmgTemplate.cpp +++ b/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"]; { diff --git a/lib/rmg/CRmgTemplate.h b/lib/rmg/CRmgTemplate.h index fc9fb8ac0..16cc0a5ef 100644 --- a/lib/rmg/CRmgTemplate.h +++ b/lib/rmg/CRmgTemplate.h @@ -338,6 +338,11 @@ public: const JsonNode & getMapSettings() const; const std::vector & getConnectedZoneIds() const; + const std::set & getBannedSpells() const { return bannedSpells; } + const std::set & getBannedArtifacts() const { return bannedArtifacts; } + const std::set & getBannedSkills() const { return bannedSkills; } + const std::set & 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 allowedWaterContent; std::unique_ptr mapSettings; + std::set bannedSpells; + std::set bannedArtifacts; + std::set bannedSkills; + std::set bannedHeroes; + std::set inheritTerrainType(std::shared_ptr zone, uint32_t iteration = 0); std::map inheritMineTypes(std::shared_ptr zone, uint32_t iteration = 0); std::vector inheritTreasureInfo(std::shared_ptr zone, uint32_t iteration = 0); @@ -375,4 +385,4 @@ private: }; -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/JsonSerializeFormat.h b/lib/serializer/JsonSerializeFormat.h index b8267fe6d..3ec30f37c 100644 --- a/lib/serializer/JsonSerializeFormat.h +++ b/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 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 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)); }); } diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index 7f55193d2..1ce14fcae 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -767,7 +767,7 @@ std::shared_ptr CSpellHandler::loadFromJson(const std::string & scope, c { const int chance = static_cast(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; }); From 51408cccd82ef2386b908820c20c442b89b474a1 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 14 Jul 2025 11:26:41 +0300 Subject: [PATCH 2/2] Update docs --- docs/modders/Random_Map_Template.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/modders/Random_Map_Template.md b/docs/modders/Random_Map_Template.md index a4166ed63..2dfa225c8 100644 --- a/docs/modders/Random_Map_Template.md +++ b/docs/modders/Random_Map_Template.md @@ -124,6 +124,8 @@ "monsters" : "normal", //possible terrain types. All terrains will be available if not specified + /// 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 "terrainTypes" : [ "sand" ], // List of type hints for every town, in the order of placement. First present hint if used for each town @@ -143,6 +145,8 @@ ], //optional, list of explicitly banned terrain types + /// 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 "bannedTerrains" : ["lava", "asphalt"] // if true, terrain for this zone will match native terrain of player faction. Used only in owned zones @@ -161,15 +165,27 @@ "customObjectsLikeZone" : 1, // factions of monsters allowed on this zone + /// 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 + "allowedMonsters" : ["inferno", "necropolis"] // These monsers will never appear in the zone + /// 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 + "bannedMonsters" : ["fortress", "stronghold", "conflux"] // towns allowed on this terrain + /// 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 + "allowedTowns" : ["castle", "tower", "rampart"] // towns will never spawn on this terrain + /// 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 + "bannedTowns" : ["necropolis"] // List of mines that will be added to this zone