1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-11-23 22:37:55 +02:00
Files
vcmi/lib/modding/IdentifierStorage.cpp
Ivan Savenko f58d08e563 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.
2025-07-14 00:18:11 +03:00

519 lines
19 KiB
C++

/*
* IdentifierStorage.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 "IdentifierStorage.h"
#include "CModHandler.h"
#include "ModScope.h"
#include "../GameLibrary.h"
#include "../constants/StringConstants.h"
#include "../spells/CSpellHandler.h"
#include <vstd/StringUtils.h>
VCMI_LIB_NAMESPACE_BEGIN
CIdentifierStorage::CIdentifierStorage()
{
registerObject(ModScope::scopeBuiltin(), "spellSchool", "any", SpellSchool::ANY.getNum());
for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i)
registerObject(ModScope::scopeBuiltin(), "resource", GameConstants::RESOURCE_NAMES[i], i);
for (int i = 0; i < std::size(GameConstants::PLAYER_COLOR_NAMES); ++i)
registerObject(ModScope::scopeBuiltin(), "playerColor", GameConstants::PLAYER_COLOR_NAMES[i], i);
for(int i=0; i<GameConstants::PRIMARY_SKILLS; ++i)
{
registerObject(ModScope::scopeBuiltin(), "primSkill", NPrimarySkill::names[i], i);
registerObject(ModScope::scopeBuiltin(), "primarySkill", NPrimarySkill::names[i], i);
}
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureDamageBoth", 0);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureDamageMin", 1);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureDamageMax", 2);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "damageTypeAll", -1);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "damageTypeMelee", 0);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "damageTypeRanged", 1);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "heroMovementLand", 1);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "heroMovementSea", 0);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareGorgon", 0);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareCommander", 1);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareNoRangePenalty", 2);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareRangePenalty", 3);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareObstaclePenalty", 4);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareRangeObstaclePenalty", 5);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "rebirthRegular", 0);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "rebirthSpecial", 1);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "visionsMonsters", 0);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "visionsHeroes", 1);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "visionsTowns", 2);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "immunityBattleWide", 0);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "immunityEnemyHero", 1);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "transmutationPerHealth", 0);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "transmutationPerUnit", 1);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "destructionKillPercentage", 0);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "destructionKillAmount", 1);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "soulStealPermanent", 0);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "soulStealBattle", 1);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "movementFlying", 0);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "movementTeleporting", 1);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "spellLevel1", 1);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "spellLevel2", 2);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "spellLevel3", 3);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "spellLevel4", 4);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "spellLevel5", 5);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel1", 1);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel2", 2);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel3", 3);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel4", 4);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel5", 5);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel6", 6);
registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel7", 7);
registerObject(ModScope::scopeBuiltin(), "spell", "preset", SpellID::PRESET);
registerObject(ModScope::scopeBuiltin(), "spell", "spellbook_preset", SpellID::SPELLBOOK_PRESET);
}
void CIdentifierStorage::checkIdentifier(const std::string & ID)
{
if (boost::algorithm::ends_with(ID, "."))
logMod->error("BIG WARNING: identifier %s seems to be broken!", ID);
}
void CIdentifierStorage::requestIdentifier(const ObjectCallback & callback) const
{
checkIdentifier(callback.type);
checkIdentifier(callback.name);
assert(!callback.localScope.empty());
if (state != ELoadingState::FINISHED) // enqueue request if loading is still in progress
scheduledRequests.push_back(callback);
else // execute immediately for "late" requests
resolveIdentifier(callback);
}
CIdentifierStorage::ObjectCallback CIdentifierStorage::ObjectCallback::fromNameWithType(const std::string & scope, const std::string & fullName, const std::function<void(si32)> & callback, bool optional)
{
assert(!scope.empty());
auto scopeAndFullName = vstd::splitStringToPair(fullName, ':');
auto typeAndName = vstd::splitStringToPair(scopeAndFullName.second, '.');
if (scope == scopeAndFullName.first)
logMod->debug("Target scope for identifier '%s' is redundant! Identifier already defined in mod '%s'", fullName, scope);
ObjectCallback result;
result.localScope = scope;
result.remoteScope = scopeAndFullName.first;
result.type = typeAndName.first;
result.name = typeAndName.second;
result.callback = callback;
result.optional = optional;
result.dynamicType = true;
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, bool bypassDependenciesCheck)
{
assert(!scope.empty());
auto scopeAndFullName = vstd::splitStringToPair(fullName, ':');
auto typeAndName = vstd::splitStringToPair(scopeAndFullName.second, '.');
if(!typeAndName.first.empty())
{
if (typeAndName.first != type)
logMod->warn("Identifier '%s' from mod '%s' requested with different type! Type '%s' expected!", fullName, scope, type);
else
logMod->debug("Target type for identifier '%s' defined in mod '%s' is redundant!", fullName, scope);
}
if (scope == scopeAndFullName.first)
logMod->debug("Target scope for identifier '%s' is redundant! Identifier already defined in mod '%s'", fullName, scope);
ObjectCallback result;
result.localScope = scope;
result.remoteScope = scopeAndFullName.first;
result.type = type;
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, false));
}
void CIdentifierStorage::requestIdentifier(const std::string & scope, const std::string & fullName, const std::function<void(si32)> & callback) const
{
requestIdentifier(ObjectCallback::fromNameWithType(scope, fullName, callback, false));
}
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, false));
}
void CIdentifierStorage::requestIdentifier(const JsonNode & name, const std::function<void(si32)> & callback) const
{
requestIdentifier(ObjectCallback::fromNameWithType(name.getModScope(), name.String(), callback, false));
}
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, 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, 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, false);
return getIdentifierImpl(options, silent);
}
std::optional<si32> CIdentifierStorage::getIdentifier(const std::string & type, const JsonNode & name, bool silent) const
{
assert(state != ELoadingState::LOADING);
auto options = ObjectCallback::fromNameAndType(name.getModScope(), type, name.String(), std::function<void(si32)>(), silent, false);
return getIdentifierImpl(options, silent);
}
std::optional<si32> CIdentifierStorage::getIdentifier(const JsonNode & name, bool silent) const
{
assert(state != ELoadingState::LOADING);
auto options = ObjectCallback::fromNameWithType(name.getModScope(), name.String(), std::function<void(si32)>(), silent);
return getIdentifierImpl(options, silent);
}
std::optional<si32> CIdentifierStorage::getIdentifier(const std::string & scope, const std::string & fullName, bool silent) const
{
assert(state != ELoadingState::LOADING);
auto options = ObjectCallback::fromNameWithType(scope, fullName, std::function<void(si32)>(), silent);
return getIdentifierImpl(options, silent);
}
std::optional<si32> CIdentifierStorage::getIdentifierImpl(const ObjectCallback & options, bool silent) const
{
auto idList = getPossibleIdentifiers(options);
if (idList.size() == 1)
return idList.front().id;
if (!silent)
showIdentifierResolutionErrorDetails(options);
return std::optional<si32>();
}
void CIdentifierStorage::showIdentifierResolutionErrorDetails(const ObjectCallback & options) const
{
auto idList = getPossibleIdentifiers(options);
logMod->error("Failed to resolve identifier '%s' of type '%s' from mod '%s'", options.name, options.type, options.localScope);
if (options.dynamicType && options.type.empty())
{
bool suggestionFound = false;
for (auto const & entry : registeredObjects)
{
if (!boost::algorithm::ends_with(entry.first, options.name))
continue;
suggestionFound = true;
logMod->error("Perhaps you wanted to use identifier '%s' from mod '%s' instead?", entry.first, entry.second.scope);
}
if (suggestionFound)
return;
}
if (idList.empty())
{
// check whether identifier is unavailable due to a missing dependency on a mod
ObjectCallback testOptions = options;
testOptions.localScope = ModScope::scopeGame();
testOptions.remoteScope = {};
auto testList = getPossibleIdentifiers(testOptions);
if (testList.empty())
{
logMod->error("Identifier '%s' of type '%s' does not exists in any loaded mod!", options.name, options.type);
}
else
{
// 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
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;
}
// attempt to access identifier in form 'modName:object', but identifier is only present in different mod
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;
}
}
}
}
else
{
logMod->error("Multiple possible candidates:");
for (auto const & testOption : idList)
{
logMod->error("Identifier %s exists in mod %s", options.name, testOption.scope);
if (options.dynamicType)
logMod->error("Please use '%s:%s.%s' to access this identifier", testOption.scope, options.type, options.name);
else
logMod->error("Please use '%s:%s' to access this identifier", testOption.scope, options.name);
}
}
}
void CIdentifierStorage::registerObject(const std::string & scope, const std::string & type, const std::string & name, si32 identifier)
{
assert(state != ELoadingState::FINISHED);
ObjectData data;
data.scope = scope;
data.id = identifier;
std::string fullID = type + '.' + name;
checkIdentifier(fullID);
std::pair<const std::string, ObjectData> mapping = std::make_pair(fullID, data);
if(!vstd::containsMapping(registeredObjects, mapping))
{
logMod->trace("registered '%s' as %s:%s", fullID, scope, identifier);
registeredObjects.insert(mapping);
}
else
{
logMod->trace("Duplicate object '%s' found!", fullID);
}
}
std::vector<CIdentifierStorage::ObjectData> CIdentifierStorage::getPossibleIdentifiers(const ObjectCallback & request) const
{
std::set<std::string> allowedScopes;
bool isValidScope = true;
// called have not specified destination mod explicitly
if (request.remoteScope.empty())
{
// special scope that should have access to all in-game objects
if (request.localScope == ModScope::scopeGame())
{
for(const auto & modName : LIBRARY->modh->getActiveMods())
allowedScopes.insert(modName);
}
// normally ID's from all required mods, own mod and virtual built-in mod are allowed
else if(request.localScope != ModScope::scopeBuiltin() && !request.localScope.empty())
{
allowedScopes = LIBRARY->modh->getModDependencies(request.localScope, isValidScope);
if(!isValidScope)
return std::vector<ObjectData>();
allowedScopes.insert(request.localScope);
}
// all mods can access built-in mod
allowedScopes.insert(ModScope::scopeBuiltin());
}
else
{
//if destination mod was specified explicitly, restrict lookup to this mod
if(request.remoteScope == ModScope::scopeBuiltin() )
{
//built-in mod is an implicit dependency for all mods, allow access into it
allowedScopes.insert(request.remoteScope);
}
else if ( request.localScope == ModScope::scopeGame() )
{
// allow access, this is special scope that should have access to all in-game objects
allowedScopes.insert(request.remoteScope);
}
else if(request.remoteScope == request.localScope )
{
// 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
auto myDeps = LIBRARY->modh->getModDependencies(request.localScope, isValidScope);
if(!isValidScope)
return std::vector<ObjectData>();
if(myDeps.count(request.remoteScope))
allowedScopes.insert(request.remoteScope);
}
}
std::string fullID = request.type + '.' + request.name;
auto entries = registeredObjects.equal_range(fullID);
if (entries.first != entries.second)
{
std::vector<ObjectData> locatedIDs;
for (auto it = entries.first; it != entries.second; it++)
{
if (vstd::contains(allowedScopes, it->second.scope))
{
locatedIDs.push_back(it->second);
}
}
return locatedIDs;
}
return std::vector<ObjectData>();
}
bool CIdentifierStorage::resolveIdentifier(const ObjectCallback & request) const
{
auto identifiers = getPossibleIdentifiers(request);
if (identifiers.size() == 1) // normally resolved ID
{
request.callback(identifiers.front().id);
return true;
}
if (request.optional && identifiers.empty()) // failed to resolve optional ID
{
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);
return false;
}
void CIdentifierStorage::finalize()
{
assert(state == ELoadingState::LOADING);
state = ELoadingState::FINALIZING;
while ( !scheduledRequests.empty() )
{
// Use local copy since new requests may appear during resolving, invalidating any iterators
auto request = scheduledRequests.back();
scheduledRequests.pop_back();
resolveIdentifier(request);
}
state = ELoadingState::FINISHED;
}
void CIdentifierStorage::debugDumpIdentifiers()
{
logMod->trace("List of all registered objects:");
std::map<std::string, std::vector<std::string>> objectList;
for(const auto & object : registeredObjects)
{
size_t categoryLength = object.first.find('.');
assert(categoryLength != std::string::npos);
std::string objectCategory = object.first.substr(0, categoryLength);
std::string objectName = object.first.substr(categoryLength + 1);
objectList[objectCategory].push_back("[" + object.second.scope + "] " + objectName);
}
for(auto & category : objectList)
boost::range::sort(category.second);
for(const auto & category : objectList)
{
logMod->trace("");
logMod->trace("### %s", category.first);
logMod->trace("");
for(const auto & entry : category.second)
logMod->trace("- " + entry);
}
}
std::vector<std::string> CIdentifierStorage::getModsWithFailedRequests() const
{
std::vector<std::string> result;
for (const auto & request : failedRequests)
if (!vstd::contains(result, request.localScope) && !ModScope::isScopeReserved(request.localScope))
result.push_back(request.localScope);
return result;
}
VCMI_LIB_NAMESPACE_END