2023-07-30 19:12:25 +02:00
/*
* 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 "../VCMI_Lib.h"
2023-08-25 21:36:00 +02:00
# include "../constants/StringConstants.h"
# include "../spells/CSpellHandler.h"
2023-07-30 19:12:25 +02:00
# include <vstd/StringUtils.h>
VCMI_LIB_NAMESPACE_BEGIN
2023-08-25 21:36:00 +02:00
CIdentifierStorage : : CIdentifierStorage ( )
{
//TODO: moddable spell schools
for ( auto i = 0 ; i < GameConstants : : DEFAULT_SCHOOLS ; + + i )
2023-11-08 16:13:08 +02:00
registerObject ( ModScope : : scopeBuiltin ( ) , " spellSchool " , SpellConfig : : SCHOOL [ i ] . jsonName , SpellConfig : : SCHOOL [ i ] . id . getNum ( ) ) ;
2023-08-25 21:36:00 +02:00
2023-11-08 16:13:08 +02:00
registerObject ( ModScope : : scopeBuiltin ( ) , " spellSchool " , " any " , SpellSchool : : ANY . getNum ( ) ) ;
2023-08-25 21:36:00 +02:00
for ( int i = 0 ; i < GameConstants : : RESOURCE_QUANTITY ; + + i )
registerObject ( ModScope : : scopeBuiltin ( ) , " resource " , GameConstants : : RESOURCE_NAMES [ i ] , i ) ;
2023-09-04 21:21:57 +02:00
for ( int i = 0 ; i < std : : size ( GameConstants : : PLAYER_COLOR_NAMES ) ; + + i )
registerObject ( ModScope : : scopeBuiltin ( ) , " playerColor " , GameConstants : : PLAYER_COLOR_NAMES [ i ] , i ) ;
2023-08-25 21:36:00 +02:00
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 ) ;
}
2023-10-15 17:36:04 +02:00
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 ) ;
2024-01-13 15:55:07 +02:00
registerObject ( ModScope : : scopeBuiltin ( ) , " bonusSubtype " , " deathStareNoRangePenalty " , 2 ) ;
registerObject ( ModScope : : scopeBuiltin ( ) , " bonusSubtype " , " deathStareRangePenalty " , 3 ) ;
registerObject ( ModScope : : scopeBuiltin ( ) , " bonusSubtype " , " deathStareObstaclePenalty " , 4 ) ;
registerObject ( ModScope : : scopeBuiltin ( ) , " bonusSubtype " , " deathStareRangeObstaclePenalty " , 5 ) ;
2023-10-15 17:36:04 +02:00
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 ) ;
2023-08-25 21:36:00 +02:00
}
2023-07-30 19:12:25 +02:00
void CIdentifierStorage : : checkIdentifier ( std : : string & ID )
{
if ( boost : : algorithm : : ends_with ( ID , " . " ) )
logMod - > warn ( " BIG WARNING: identifier %s seems to be broken! " , ID ) ;
else
{
size_t pos = 0 ;
do
{
if ( std : : tolower ( ID [ pos ] ) ! = ID [ pos ] ) //Not in camelCase
{
logMod - > warn ( " Warning: identifier %s is not in camelCase! " , ID ) ;
ID [ pos ] = std : : tolower ( ID [ pos ] ) ; // Try to fix the ID
}
pos = ID . find ( ' . ' , pos ) ;
}
while ( pos + + ! = std : : string : : npos ) ;
}
}
void CIdentifierStorage : : requestIdentifier ( ObjectCallback callback ) const
{
checkIdentifier ( callback . type ) ;
checkIdentifier ( callback . name ) ;
assert ( ! callback . localScope . empty ( ) ) ;
2023-08-25 20:42:20 +02:00
if ( state ! = ELoadingState : : FINISHED ) // enqueue request if loading is still in progress
2023-07-30 19:12:25 +02:00
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 ;
2024-01-08 21:32:10 +02:00
result . dynamicType = true ;
2023-07-30 19:12:25 +02:00
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 )
{
assert ( ! scope . empty ( ) ) ;
auto scopeAndFullName = vstd : : splitStringToPair ( fullName , ' : ' ) ;
auto typeAndName = vstd : : splitStringToPair ( scopeAndFullName . second , ' . ' ) ;
if ( ! typeAndName . first . empty ( ) )
{
if ( typeAndName . first ! = type )
2023-10-15 17:36:04 +02:00
logMod - > warn ( " Identifier '%s' from mod '%s' requested with different type! Type '%s' expected! " , fullName , scope , type ) ;
2023-07-30 19:12:25 +02:00
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 ;
2024-01-08 21:32:10 +02:00
result . dynamicType = false ;
2023-07-30 19:12:25 +02:00
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 ) ) ;
}
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 . meta , type , name . String ( ) , callback , false ) ) ;
}
void CIdentifierStorage : : requestIdentifier ( const JsonNode & name , const std : : function < void ( si32 ) > & callback ) const
{
requestIdentifier ( ObjectCallback : : fromNameWithType ( name . meta , name . String ( ) , callback , false ) ) ;
}
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 ) ) ;
}
void CIdentifierStorage : : tryRequestIdentifier ( const std : : string & type , const JsonNode & name , const std : : function < void ( si32 ) > & callback ) const
{
requestIdentifier ( ObjectCallback : : fromNameAndType ( name . meta , type , name . String ( ) , callback , true ) ) ;
}
std : : optional < si32 > CIdentifierStorage : : getIdentifier ( const std : : string & scope , const std : : string & type , const std : : string & name , bool silent ) const
{
2023-09-04 21:21:57 +02:00
assert ( state ! = ELoadingState : : LOADING ) ;
2023-08-25 20:42:20 +02:00
2024-01-07 15:14:07 +02:00
auto options = ObjectCallback : : fromNameAndType ( scope , type , name , std : : function < void ( si32 ) > ( ) , silent ) ;
return getIdentifierImpl ( options , silent ) ;
2023-07-30 19:12:25 +02:00
}
std : : optional < si32 > CIdentifierStorage : : getIdentifier ( const std : : string & type , const JsonNode & name , bool silent ) const
{
2023-08-25 20:42:20 +02:00
assert ( state ! = ELoadingState : : LOADING ) ;
2024-01-07 15:14:07 +02:00
auto options = ObjectCallback : : fromNameAndType ( name . meta , type , name . String ( ) , std : : function < void ( si32 ) > ( ) , silent ) ;
2023-07-30 19:12:25 +02:00
2024-01-07 15:14:07 +02:00
return getIdentifierImpl ( options , silent ) ;
2023-07-30 19:12:25 +02:00
}
std : : optional < si32 > CIdentifierStorage : : getIdentifier ( const JsonNode & name , bool silent ) const
{
2023-08-25 20:42:20 +02:00
assert ( state ! = ELoadingState : : LOADING ) ;
2024-01-07 15:14:07 +02:00
auto options = ObjectCallback : : fromNameWithType ( name . meta , name . String ( ) , std : : function < void ( si32 ) > ( ) , silent ) ;
return getIdentifierImpl ( options , silent ) ;
2023-07-30 19:12:25 +02:00
}
std : : optional < si32 > CIdentifierStorage : : getIdentifier ( const std : : string & scope , const std : : string & fullName , bool silent ) const
{
2023-08-25 20:42:20 +02:00
assert ( state ! = ELoadingState : : LOADING ) ;
2024-01-07 15:14:07 +02:00
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 ) ;
2023-07-30 19:12:25 +02:00
if ( idList . size ( ) = = 1 )
return idList . front ( ) . id ;
if ( ! silent )
2024-01-07 15:14:07 +02:00
showIdentifierResolutionErrorDetails ( options ) ;
2023-07-30 19:12:25 +02:00
return std : : optional < si32 > ( ) ;
}
2024-01-07 15:14:07 +02:00
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 ) ;
2024-01-08 21:32:10 +02:00
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 ;
}
2024-01-07 15:14:07 +02:00
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 identifiers exists, but were not picked for some reason
if ( options . remoteScope . empty ( ) )
{
// attempt to access identifier from mods that is not dependency
for ( auto const & testOption : testList )
{
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 ) ;
}
}
else
{
// attempt to access identifier in form 'modName:object', but identifier is only present in different mod
for ( auto const & testOption : testList )
{
2024-01-08 21:32:10 +02:00
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 ) ;
2024-01-07 15:14:07 +02:00
}
}
}
}
else
{
logMod - > error ( " Multiple possible candidates: " ) ;
for ( auto const & testOption : idList )
{
logMod - > error ( " Identifier %s exists in mod %s " , options . name , testOption . scope ) ;
2024-01-08 21:32:10 +02:00
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 ) ;
2024-01-07 15:14:07 +02:00
}
}
}
2023-07-30 19:12:25 +02:00
void CIdentifierStorage : : registerObject ( const std : : string & scope , const std : : string & type , const std : : string & name , si32 identifier )
{
2023-08-25 20:42:20 +02:00
assert ( state ! = ELoadingState : : FINISHED ) ;
2023-07-30 19:12:25 +02:00
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 ) ;
}
}
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 : VLC - > 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 = VLC - > 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
{
// allow access only if mod is in our dependencies
auto myDeps = VLC - > 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 optinal ID
{
return true ;
}
// error found. Try to generate some debug info
2024-01-07 15:14:07 +02:00
showIdentifierResolutionErrorDetails ( request ) ;
2023-07-30 19:12:25 +02:00
return false ;
}
void CIdentifierStorage : : finalize ( )
{
2023-08-25 20:42:20 +02:00
assert ( state = = ELoadingState : : LOADING ) ;
state = ELoadingState : : FINALIZING ;
2023-07-30 19:12:25 +02:00
while ( ! scheduledRequests . empty ( ) )
{
// Use local copy since new requests may appear during resolving, invalidating any iterators
auto request = scheduledRequests . back ( ) ;
scheduledRequests . pop_back ( ) ;
2024-01-07 15:14:07 +02:00
resolveIdentifier ( request ) ;
2023-07-30 19:12:25 +02:00
}
2023-08-25 20:42:20 +02:00
state = ELoadingState : : FINISHED ;
2023-09-23 18:26:15 +02:00
}
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 ) ;
}
2023-07-30 19:12:25 +02:00
}
VCMI_LIB_NAMESPACE_END