1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-11-23 22:37:55 +02:00
Files
vcmi/lib/json/JsonBonus.cpp

948 lines
29 KiB
C++
Raw Normal View History

2024-02-14 15:48:06 +02:00
/*
* JsonUtils.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 "JsonBonus.h"
#include "JsonValidator.h"
#include "../texts/CGeneralTextHandler.h"
#include "../GameLibrary.h"
2024-02-14 15:48:06 +02:00
#include "../bonuses/Limiters.h"
#include "../bonuses/Propagators.h"
#include "../bonuses/Updaters.h"
#include "../CBonusTypeHandler.h"
2024-02-14 15:48:06 +02:00
#include "../constants/StringConstants.h"
2024-02-13 22:19:24 +02:00
#include "../modding/IdentifierStorage.h"
2024-02-14 15:48:06 +02:00
VCMI_LIB_USING_NAMESPACE
template <typename T>
const T parseByMap(const std::map<std::string, T> & map, const JsonNode * val, const std::string & err)
{
if (!val->isNull())
{
const std::string & type = val->String();
auto it = map.find(type);
if (it == map.end())
{
logMod->error("Error: invalid %s%s.", err, type);
return {};
}
else
{
return it->second;
}
}
else
return {};
}
template <typename T>
const T parseByMapN(const std::map<std::string, T> & map, const JsonNode * val, const std::string & err)
{
if(val->isNumber())
return static_cast<T>(val->Integer());
else
return parseByMap<T>(map, val, err);
}
2024-02-14 15:48:06 +02:00
static void loadBonusSubtype(BonusSubtypeID & subtype, BonusType type, const JsonNode & node)
{
if (node.isNull())
{
subtype = BonusSubtypeID();
return;
}
if (node.isNumber()) // Compatibility code for 1.3 or older
{
logMod->warn("Bonus subtype must be string! (%s)", node.getModScope());
2024-02-14 15:48:06 +02:00
subtype = BonusCustomSubtype(node.Integer());
return;
}
if (!node.isString())
{
logMod->warn("Bonus subtype must be string! (%s)", node.getModScope());
2024-02-14 15:48:06 +02:00
subtype = BonusSubtypeID();
return;
}
switch (type)
{
case BonusType::MAGIC_SCHOOL_SKILL:
case BonusType::SPELL_DAMAGE:
case BonusType::SPELLS_OF_SCHOOL:
case BonusType::SPELL_DAMAGE_REDUCTION:
case BonusType::SPELL_SCHOOL_IMMUNITY:
case BonusType::NEGATIVE_EFFECTS_IMMUNITY:
{
LIBRARY->identifiers()->requestIdentifier( "spellSchool", node, [&subtype](int32_t identifier)
2024-02-14 15:48:06 +02:00
{
subtype = SpellSchool(identifier);
});
break;
}
case BonusType::NO_TERRAIN_PENALTY:
{
LIBRARY->identifiers()->requestIdentifier( "terrain", node, [&subtype](int32_t identifier)
2024-02-14 15:48:06 +02:00
{
subtype = TerrainId(identifier);
});
break;
}
case BonusType::PRIMARY_SKILL:
{
LIBRARY->identifiers()->requestIdentifier( "primarySkill", node, [&subtype](int32_t identifier)
2024-02-14 15:48:06 +02:00
{
subtype = PrimarySkill(identifier);
});
break;
}
case BonusType::IMPROVED_NECROMANCY:
case BonusType::HERO_GRANTS_ATTACKS:
case BonusType::BONUS_DAMAGE_CHANCE:
case BonusType::BONUS_DAMAGE_PERCENTAGE:
case BonusType::SPECIAL_UPGRADE:
case BonusType::HATE:
case BonusType::SUMMON_GUARDIANS:
case BonusType::MANUAL_CONTROL:
2025-06-11 12:57:48 +03:00
case BonusType::SKELETON_TRANSFORMER_TARGET:
2024-02-14 15:48:06 +02:00
{
LIBRARY->identifiers()->requestIdentifier( "creature", node, [&subtype](int32_t identifier)
2024-02-14 15:48:06 +02:00
{
subtype = CreatureID(identifier);
});
break;
}
case BonusType::SPELL_IMMUNITY:
case BonusType::SPELL_DURATION:
case BonusType::SPECIAL_ADD_VALUE_ENCHANT:
case BonusType::SPECIAL_FIXED_VALUE_ENCHANT:
case BonusType::SPECIAL_PECULIAR_ENCHANT:
case BonusType::SPECIAL_SPELL_LEV:
case BonusType::SPECIFIC_SPELL_DAMAGE:
2025-06-25 15:08:11 +02:00
case BonusType::SPECIFIC_SPELL_RANGE:
2024-02-14 15:48:06 +02:00
case BonusType::SPELL:
case BonusType::OPENING_BATTLE_SPELL:
case BonusType::SPELL_LIKE_ATTACK:
case BonusType::CATAPULT:
case BonusType::CATAPULT_EXTRA_SHOTS:
case BonusType::HEALER:
case BonusType::SPELLCASTER:
case BonusType::ENCHANTER:
case BonusType::SPELL_AFTER_ATTACK:
case BonusType::SPELL_BEFORE_ATTACK:
case BonusType::SPECIFIC_SPELL_POWER:
case BonusType::ENCHANTED:
case BonusType::MORE_DAMAGE_FROM_SPELL:
case BonusType::NOT_ACTIVE:
{
LIBRARY->identifiers()->requestIdentifier( "spell", node, [&subtype](int32_t identifier)
2024-02-14 15:48:06 +02:00
{
subtype = SpellID(identifier);
});
break;
}
case BonusType::GENERATE_RESOURCE:
case BonusType::RESOURCES_CONSTANT_BOOST:
case BonusType::RESOURCES_TOWN_MULTIPLYING_BOOST:
2024-02-14 15:48:06 +02:00
{
LIBRARY->identifiers()->requestIdentifier( "resource", node, [&subtype](int32_t identifier)
2024-02-14 15:48:06 +02:00
{
subtype = GameResID(identifier);
});
break;
}
case BonusType::MOVEMENT:
case BonusType::WATER_WALKING:
case BonusType::FLYING_MOVEMENT:
case BonusType::NEGATE_ALL_NATURAL_IMMUNITIES:
case BonusType::CREATURE_DAMAGE:
case BonusType::FLYING:
case BonusType::FIRST_STRIKE:
case BonusType::GENERAL_DAMAGE_REDUCTION:
case BonusType::PERCENTAGE_DAMAGE_BOOST:
case BonusType::SOUL_STEAL:
case BonusType::TRANSMUTATION:
case BonusType::DESTRUCTION:
case BonusType::DEATH_STARE:
case BonusType::REBIRTH:
case BonusType::VISIONS:
case BonusType::SPELLS_OF_LEVEL: // spell level
case BonusType::CREATURE_GROWTH: // creature level
{
LIBRARY->identifiers()->requestIdentifier( "bonusSubtype", node, [&subtype](int32_t identifier)
2024-02-14 15:48:06 +02:00
{
subtype = BonusCustomSubtype(identifier);
});
break;
}
default:
{
logMod->warn("Bonus type %s does not supports subtypes!", LIBRARY->bth->bonusToString(type));
2024-02-14 15:48:06 +02:00
subtype = BonusSubtypeID();
}
2024-02-14 15:48:06 +02:00
}
}
static void loadBonusAddInfo(CAddInfo & var, BonusType type, const JsonNode & value)
2025-05-20 09:42:30 +03:00
{
const auto & getFirstValue = [](const JsonNode & jsonNode) -> const JsonNode &
{
if (jsonNode.isVector())
return jsonNode[0];
else
return jsonNode;
};
if (value.isNull())
return;
switch (type)
{
case BonusType::IMPROVED_NECROMANCY:
case BonusType::SPECIAL_ADD_VALUE_ENCHANT:
case BonusType::SPECIAL_FIXED_VALUE_ENCHANT:
case BonusType::DESTRUCTION:
case BonusType::LIMITED_SHOOTING_RANGE:
case BonusType::ACID_BREATH:
case BonusType::BIND_EFFECT:
2025-05-20 09:42:30 +03:00
case BonusType::SPELLCASTER:
case BonusType::FEROCITY:
case BonusType::PRIMARY_SKILL:
case BonusType::ENCHANTER:
case BonusType::SPECIAL_PECULIAR_ENCHANT:
case BonusType::SPELL_IMMUNITY:
case BonusType::DARKNESS:
case BonusType::FULL_MAP_SCOUTING:
case BonusType::FULL_MAP_DARKNESS:
case BonusType::OPENING_BATTLE_SPELL:
2025-05-20 09:42:30 +03:00
// 1 number
var = getFirstValue(value).Integer();
break;
case BonusType::SPECIAL_UPGRADE:
case BonusType::TRANSMUTATION:
// 1 creature ID
LIBRARY->identifiers()->requestIdentifier("creature", getFirstValue(value), [&](si32 identifier) { var = identifier; });
break;
case BonusType::DEATH_STARE:
// 1 spell ID
LIBRARY->identifiers()->requestIdentifier("spell", getFirstValue(value), [&](si32 identifier) { var = identifier; });
break;
case BonusType::SPELL_BEFORE_ATTACK:
case BonusType::SPELL_AFTER_ATTACK:
// 3 numbers
if (value.isNumber())
{
var = getFirstValue(value).Integer();
}
else
{
var.resize(3);
var[0] = value[0].Integer();
var[1] = value[1].Integer();
var[2] = value[2].Integer();
}
2025-05-20 09:42:30 +03:00
break;
case BonusType::MULTIHEX_UNIT_ATTACK:
case BonusType::MULTIHEX_ENEMY_ATTACK:
case BonusType::MULTIHEX_ANIMATION:
for (const auto & sequence : value.Vector())
{
static const std::map<char, int> charToDirection = {
{ 'f', 1 }, { 'l', 6}, {'r', 2}, {'b', 4}
};
int converted = 0;
for (const auto & ch : boost::adaptors::reverse(sequence.String()))
{
char chLower = std::tolower(ch);
if (charToDirection.count(chLower))
converted = 10 * converted + charToDirection.at(chLower);
}
var.push_back(converted);
}
break;
2025-05-20 09:42:30 +03:00
default:
logMod->warn("Bonus type %s does not supports addInfo!", LIBRARY->bth->bonusToString(type) );
2025-05-20 09:42:30 +03:00
}
}
2024-02-14 15:48:06 +02:00
static void loadBonusSourceInstance(BonusSourceID & sourceInstance, BonusSource sourceType, const JsonNode & node)
{
if (node.isNull())
{
sourceInstance = BonusCustomSource();
return;
}
if (node.isNumber()) // Compatibility code for 1.3 or older
{
logMod->warn("Bonus source must be string!");
sourceInstance = BonusCustomSource(node.Integer());
return;
}
if (!node.isString())
{
logMod->warn("Bonus source must be string!");
sourceInstance = BonusCustomSource();
return;
}
switch (sourceType)
{
case BonusSource::ARTIFACT:
case BonusSource::ARTIFACT_INSTANCE:
{
LIBRARY->identifiers()->requestIdentifier( "artifact", node, [&sourceInstance](int32_t identifier)
2024-02-14 15:48:06 +02:00
{
sourceInstance = ArtifactID(identifier);
});
break;
}
case BonusSource::OBJECT_TYPE:
{
LIBRARY->identifiers()->requestIdentifier( "object", node, [&sourceInstance](int32_t identifier)
2024-02-14 15:48:06 +02:00
{
sourceInstance = Obj(identifier);
});
break;
}
case BonusSource::OBJECT_INSTANCE:
case BonusSource::HERO_BASE_SKILL:
sourceInstance = ObjectInstanceID(ObjectInstanceID::decode(node.String()));
break;
case BonusSource::CREATURE_ABILITY:
{
LIBRARY->identifiers()->requestIdentifier( "creature", node, [&sourceInstance](int32_t identifier)
2024-02-14 15:48:06 +02:00
{
sourceInstance = CreatureID(identifier);
});
break;
}
case BonusSource::TERRAIN_OVERLAY:
{
LIBRARY->identifiers()->requestIdentifier( "spell", node, [&sourceInstance](int32_t identifier)
2024-02-14 15:48:06 +02:00
{
sourceInstance = BattleField(identifier);
});
break;
}
case BonusSource::SPELL_EFFECT:
{
LIBRARY->identifiers()->requestIdentifier( "spell", node, [&sourceInstance](int32_t identifier)
2024-02-14 15:48:06 +02:00
{
sourceInstance = SpellID(identifier);
});
break;
}
case BonusSource::TOWN_STRUCTURE:
assert(0); // TODO
sourceInstance = BuildingTypeUniqueID();
break;
case BonusSource::SECONDARY_SKILL:
{
LIBRARY->identifiers()->requestIdentifier( "secondarySkill", node, [&sourceInstance](int32_t identifier)
2024-02-14 15:48:06 +02:00
{
sourceInstance = SecondarySkill(identifier);
});
break;
}
case BonusSource::HERO_SPECIAL:
{
LIBRARY->identifiers()->requestIdentifier( "hero", node, [&sourceInstance](int32_t identifier)
2024-02-14 15:48:06 +02:00
{
sourceInstance = HeroTypeID(identifier);
});
break;
}
case BonusSource::CAMPAIGN_BONUS:
sourceInstance = CampaignScenarioID(CampaignScenarioID::decode(node.String()));
break;
case BonusSource::ARMY:
case BonusSource::STACK_EXPERIENCE:
case BonusSource::COMMANDER:
case BonusSource::GLOBAL:
case BonusSource::TERRAIN_NATIVE:
case BonusSource::OTHER:
default:
sourceInstance = BonusSourceID();
break;
}
}
static TUpdaterPtr parseUpdater(const JsonNode & updaterJson)
{
const std::map<std::string, std::shared_ptr<IUpdater>> bonusUpdaterMap =
{
{"TIMES_HERO_LEVEL", std::make_shared<TimesHeroLevelUpdater>()},
{"TIMES_HERO_LEVEL_DIVIDE_STACK_LEVEL", std::make_shared<TimesHeroLevelDivideStackLevelUpdater>()},
{"DIVIDE_STACK_LEVEL", std::make_shared<DivideStackLevelUpdater>()},
{"TIMES_STACK_LEVEL", std::make_shared<TimesStackLevelUpdater>()},
{"TIMES_STACK_SIZE", std::make_shared<TimesStackSizeUpdater>()},
{"BONUS_OWNER_UPDATER", std::make_shared<OwnerUpdater>()}
};
switch(updaterJson.getType())
{
case JsonNode::JsonType::DATA_STRING:
{
auto it = bonusUpdaterMap.find(updaterJson.String());
if (it != bonusUpdaterMap.end())
return it->second;
logGlobal->error("Unknown bonus updater type '%s'", updaterJson.String());
return nullptr;
}
case JsonNode::JsonType::DATA_STRUCT:
if(updaterJson["type"].String() == "GROWS_WITH_LEVEL")
{
2025-06-11 14:15:30 +03:00
// MOD COMPATIBILITY - parameters is deprecated in 1.7
const JsonNode & param = updaterJson["parameters"];
2025-06-11 14:15:30 +03:00
int valPer20 = updaterJson["valPer20"].isNull() ? param[0].Integer() : updaterJson["valPer20"].Integer();
int stepSize = updaterJson["stepSize"].isNull() ? param[1].Integer() : updaterJson["stepSize"].Integer();
return std::make_shared<GrowsWithLevelUpdater>(valPer20, std::max(1, stepSize));
}
if(updaterJson["type"].String() == "TIMES_HERO_LEVEL")
{
int stepSize = updaterJson["stepSize"].Integer();
return std::make_shared<TimesHeroLevelUpdater>(std::max(1, stepSize));
}
if(updaterJson["type"].String() == "TIMES_STACK_SIZE")
{
int minimum = updaterJson["minimum"].isNull() ? std::numeric_limits<int>::min() : updaterJson["minimum"].Integer();
int maximum = updaterJson["maximum"].isNull() ? std::numeric_limits<int>::max() : updaterJson["maximum"].Integer();
int stepSize = updaterJson["stepSize"].Integer();
if (minimum > maximum)
{
logMod->warn("TIMES_STACK_SIZE updater: minimum value (%d) is above maximum value(%d)!", minimum, maximum);
return std::make_shared<TimesStackSizeUpdater>(maximum, minimum, std::max(1, stepSize));
}
return std::make_shared<TimesStackSizeUpdater>(minimum, maximum, std::max(1, stepSize));
2025-06-26 15:10:55 +03:00
}
if(updaterJson["type"].String() == "TIMES_ARMY_SIZE")
{
auto result = std::make_shared<TimesArmySizeUpdater>();
result->minimum = updaterJson["minimum"].isNull() ? std::numeric_limits<int>::min() : updaterJson["minimum"].Integer();
result->maximum = updaterJson["maximum"].isNull() ? std::numeric_limits<int>::max() : updaterJson["maximum"].Integer();
result->stepSize = updaterJson["stepSize"].isNull() ? 1 : updaterJson["stepSize"].Integer();
result->filteredLevel = updaterJson["filteredLevel"].isNull() ? -1 : updaterJson["filteredLevel"].Integer();
if (result->minimum > result->maximum)
{
logMod->warn("TIMES_ARMY_SIZE updater: minimum value (%d) is above maximum value(%d)!", result->minimum, result->maximum);
std::swap(result->minimum, result->maximum);
}
if (!updaterJson["filteredFaction"].isNull())
{
LIBRARY->identifiers()->requestIdentifier( "faction", updaterJson["filteredFaction"], [result](int32_t identifier)
{
result->filteredFaction = FactionID(identifier);
});
}
if (!updaterJson["filteredCreature"].isNull())
{
LIBRARY->identifiers()->requestIdentifier( "creature", updaterJson["filteredCreature"], [result](int32_t identifier)
{
result->filteredCreature = CreatureID(identifier);
});
}
return result;
}
else
logMod->warn("Unknown updater type \"%s\"", updaterJson["type"].String());
break;
}
return nullptr;
}
VCMI_LIB_NAMESPACE_BEGIN
2024-02-14 15:48:06 +02:00
std::shared_ptr<Bonus> JsonUtils::parseBonus(const JsonVector & ability_vec)
{
auto b = std::make_shared<Bonus>();
const JsonNode & typeNode = ability_vec[0];
const JsonNode & subtypeNode = ability_vec[2];
LIBRARY->identifiers()->requestIdentifier("bonus", typeNode, [b, subtypeNode](si32 bonusID)
{
2025-06-13 16:36:24 +03:00
b->type = static_cast<BonusType>(bonusID);
loadBonusSubtype(b->subtype, b->type, subtypeNode);
});
2024-02-14 15:48:06 +02:00
b->val = static_cast<si32>(ability_vec[1].Float());
b->additionalInfo = static_cast<si32>(ability_vec[3].Float());
b->duration = BonusDuration::PERMANENT; //TODO: handle flags (as integer)
b->turnsRemain = 0;
return b;
}
static std::shared_ptr<const ILimiter> parseAggregateLimiter(const JsonNode & limiter)
2024-02-14 15:48:06 +02:00
{
const JsonVector & subLimiters = limiter.Vector();
if(subLimiters.empty())
{
logMod->warn("Warning: empty limiter list");
return std::make_shared<AllOfLimiter>();
}
std::shared_ptr<AggregateLimiter> result;
int offset = 1;
// determine limiter type and offset for sub-limiters
if(subLimiters[0].getType() == JsonNode::JsonType::DATA_STRING)
2024-02-14 15:48:06 +02:00
{
const std::string & aggregator = subLimiters[0].String();
if(aggregator == AllOfLimiter::aggregator)
result = std::make_shared<AllOfLimiter>();
else if(aggregator == AnyOfLimiter::aggregator)
result = std::make_shared<AnyOfLimiter>();
else if(aggregator == NoneOfLimiter::aggregator)
result = std::make_shared<NoneOfLimiter>();
}
if(!result)
{
// collapse for single limiter without explicit aggregate operator
if(subLimiters.size() == 1)
return JsonUtils::parseLimiter(subLimiters[0]);
// implicit aggregator must be allOf
result = std::make_shared<AllOfLimiter>();
offset = 0;
}
if(subLimiters.size() == offset)
logMod->warn("Warning: empty sub-limiter list");
for(int sl = offset; sl < subLimiters.size(); ++sl)
result->add(JsonUtils::parseLimiter(subLimiters[sl]));
return result;
}
static std::shared_ptr<const ILimiter> parseCreatureTypeLimiter(const JsonNode & limiter)
{
auto creatureLimiter = std::make_shared<CCreatureTypeLimiter>();
const JsonNode & parameters = limiter["parameters"];
const JsonNode & creatureNode = limiter.Struct().count("creature") ? limiter["creature"] : parameters[0];
const JsonNode & upgradesNode = limiter.Struct().count("includeUpgrades") ? limiter["includeUpgrades"] : parameters[1];
LIBRARY->identifiers()->requestIdentifier( "creature", creatureNode, [creatureLimiter](si32 creature)
{
creatureLimiter->setCreature(CreatureID(creature));
});
if (upgradesNode.isString())
{
logGlobal->warn("CREATURE_TYPE_LIMITER: parameter 'includeUpgrades' is invalid! expected boolean, but string '%s' found!", upgradesNode.String());
if (upgradesNode.String() == "true") // MOD COMPATIBILITY - broken mod, compensating
creatureLimiter->includeUpgrades = true;
}
else
{
creatureLimiter->includeUpgrades = upgradesNode.Bool();
}
return creatureLimiter;
}
static std::shared_ptr<const ILimiter> parseHasAnotherBonusLimiter(const JsonNode & limiter)
{
auto bonusLimiter = std::make_shared<HasAnotherBonusLimiter>();
const JsonNode & parameters = limiter["parameters"];
const JsonNode & jsonType = limiter.Struct().count("bonusType") ? limiter["bonusType"] : parameters[0];
const JsonNode & jsonSubtype = limiter.Struct().count("bonusSubtype") ? limiter["bonusSubtype"] : parameters[1];
const JsonNode & jsonSourceType = limiter.Struct().count("bonusSourceType") ? limiter["bonusSourceType"] : parameters[2]["type"];
const JsonNode & jsonSourceID = limiter.Struct().count("bonusSourceID") ? limiter["bonusSourceID"] : parameters[2]["id"];
if (!jsonType.isNull())
{
LIBRARY->identifiers()->requestIdentifier("bonus", jsonType, [bonusLimiter, jsonSubtype](si32 bonusID)
2024-02-14 15:48:06 +02:00
{
bonusLimiter->type = static_cast<BonusType>(bonusID);
if (!jsonSubtype.isNull())
{
loadBonusSubtype(bonusLimiter->subtype, bonusLimiter->type, jsonSubtype);
bonusLimiter->isSubtypeRelevant = true;
}
});
}
if(!jsonSourceType.isNull())
{
auto sourceIt = bonusSourceMap.find(jsonSourceType.String());
if(sourceIt != bonusSourceMap.end())
{
bonusLimiter->source = sourceIt->second;
bonusLimiter->isSourceRelevant = true;
if(!jsonSourceID.isNull()) {
loadBonusSourceInstance(bonusLimiter->sid, bonusLimiter->source, jsonSourceID);
bonusLimiter->isSourceIDRelevant = true;
2024-02-14 15:48:06 +02:00
}
}
else
logMod->warn("HAS_ANOTHER_BONUS_LIMITER: unknown bonus source type '%s'!", jsonSourceType.String());
}
return bonusLimiter;
}
static std::shared_ptr<const ILimiter> parseCreatureAlignmentLimiter(const JsonNode & limiter)
{
const JsonNode & parameters = limiter["parameters"];
const JsonNode & alignmentNode = limiter.Struct().count("alignment") ? limiter["alignment"] : parameters[0];
int alignment = vstd::find_pos(GameConstants::ALIGNMENT_NAMES, alignmentNode.String());
if(alignment == -1)
{
logMod->error("Error: invalid alignment %s.", alignmentNode.String());
return nullptr;
}
else
return std::make_shared<CreatureAlignmentLimiter>(static_cast<EAlignment>(alignment));
}
static std::shared_ptr<const ILimiter> parseFactionLimiter(const JsonNode & limiter)
{
const JsonNode & parameters = limiter["parameters"];
const JsonNode & factionNode = limiter.Struct().count("faction") ? limiter["faction"] : parameters[0];
auto factionLimiter = std::make_shared<FactionLimiter>();
LIBRARY->identifiers()->requestIdentifier("faction", factionNode, [=](si32 faction)
{
factionLimiter->faction = FactionID(faction);
});
return factionLimiter;
}
static std::shared_ptr<const ILimiter> parseCreatureLevelLimiter(const JsonNode & limiter)
{
const JsonNode & parameters = limiter["parameters"];
const JsonNode & minLevelNode = limiter.Struct().count("minLevel") ? limiter["minLevel"] : parameters[0];
const JsonNode & maxLevelNode = limiter.Struct().count("maxlevel") ? limiter["maxlevel"] : parameters[1];
//If parameters is empty, level limiter works as CREATURES_ONLY limiter
auto levelLimiter = std::make_shared<CreatureLevelLimiter>();
if (!minLevelNode.isNull())
levelLimiter->minLevel = minLevelNode.Integer();
if (!maxLevelNode.isNull())
levelLimiter->maxLevel = maxLevelNode.Integer();
return levelLimiter;
}
static std::shared_ptr<const ILimiter> parseCreatureTerrainLimiter(const JsonNode & limiter)
{
const JsonNode & parameters = limiter["parameters"];
const JsonNode & terrainNode = limiter.Struct().count("terrain") ? limiter["terrain"] : parameters[0];
auto terrainLimiter = std::make_shared<CreatureTerrainLimiter>();
if(!terrainNode.isNull())
{
LIBRARY->identifiers()->requestIdentifier("terrain", terrainNode, [terrainLimiter](si32 terrain)
2024-02-14 15:48:06 +02:00
{
terrainLimiter->terrainType = terrain;
});
}
return terrainLimiter;
}
2024-02-14 15:48:06 +02:00
static std::shared_ptr<const ILimiter> parseUnitOnHexLimiter(const JsonNode & limiter)
{
const JsonNode & parameters = limiter["parameters"];
const JsonNode & hexesNode = limiter.Struct().count("hexes") ? limiter["hexes"] : parameters[0];
2024-02-14 15:48:06 +02:00
auto hexLimiter = std::make_shared<UnitOnHexLimiter>();
Better support for Adela specialty (+new modding functionality for it) Fixes Adela specialty that was apparently broken back in #1518 and replaced with logic that was clearly not tested - it was neither functional, nor it was following H3 behavior. - `HAS_ANOTHER_BONUS_LIMITER` now accepts `null` in place of bonus type, for cases when limiting is needed by bonus source or bonus subtype. This allows Adela Bless specialty to always work, irregardless of which bonuses are provided by Bless. - Implemented `DIVIDE_STACK_LEVEL` updater that functions same as `TIMES_STACK_LEVEL`, but it divides bonus value, instead of multiplying it (to make Adela specialty weaker for high-tier units, as in H3) - Implemented `TIMES_HERO_LEVEL_DIVIDE_STACK_LEVEL` updater that combines two existing updaters, to implement `val * heroLevel / unitLevel` formula needed for Adela specialty - Removed deprecated `ARMY_MOVEMENT` updater. Its functionality has already been removed in 1.6.X releases, and it was remaining only as a placeholder - Updated modding documentation to account for these changes & to remove some TODO's Fixed regression from #777 that could led to either duplicated bonuses or to multiple application of updaters. It introduced double-recursion - node parents were gathered recursively, and then bonuses were also collected recursively within each parent. This created situation where updater could be applied different number of times. For example, hero bonus that is propagated to unit in combat could be selected directly, or via hero->combat unit chain, or via hero->garrison unit->combat unit chains, leading to different calls to updaters if updater handles garrison unit node type
2025-04-11 15:04:30 +03:00
for (const auto & parameter : hexesNode.Vector())
hexLimiter->applicableHexes.insert(BattleHex(parameter.Integer()));
return hexLimiter;
}
Better support for Adela specialty (+new modding functionality for it) Fixes Adela specialty that was apparently broken back in #1518 and replaced with logic that was clearly not tested - it was neither functional, nor it was following H3 behavior. - `HAS_ANOTHER_BONUS_LIMITER` now accepts `null` in place of bonus type, for cases when limiting is needed by bonus source or bonus subtype. This allows Adela Bless specialty to always work, irregardless of which bonuses are provided by Bless. - Implemented `DIVIDE_STACK_LEVEL` updater that functions same as `TIMES_STACK_LEVEL`, but it divides bonus value, instead of multiplying it (to make Adela specialty weaker for high-tier units, as in H3) - Implemented `TIMES_HERO_LEVEL_DIVIDE_STACK_LEVEL` updater that combines two existing updaters, to implement `val * heroLevel / unitLevel` formula needed for Adela specialty - Removed deprecated `ARMY_MOVEMENT` updater. Its functionality has already been removed in 1.6.X releases, and it was remaining only as a placeholder - Updated modding documentation to account for these changes & to remove some TODO's Fixed regression from #777 that could led to either duplicated bonuses or to multiple application of updaters. It introduced double-recursion - node parents were gathered recursively, and then bonuses were also collected recursively within each parent. This created situation where updater could be applied different number of times. For example, hero bonus that is propagated to unit in combat could be selected directly, or via hero->combat unit chain, or via hero->garrison unit->combat unit chains, leading to different calls to updaters if updater handles garrison unit node type
2025-04-11 15:04:30 +03:00
static std::shared_ptr<const ILimiter> parseHasChargesLimiter(const JsonNode & limiter)
{
const JsonNode & parameters = limiter["parameters"];
const JsonNode & costNode = limiter.Struct().count("cost") ? limiter["cost"] : parameters[0];
auto hasChargesLimiter = std::make_shared<HasChargesLimiter>();
if (!costNode.isNull())
hasChargesLimiter->chargeCost = costNode.Integer();
return hasChargesLimiter;
}
std::shared_ptr<const ILimiter> JsonUtils::parseLimiter(const JsonNode & limiter)
{
static const std::map<std::string, std::function<std::shared_ptr<const ILimiter>(const JsonNode & limiter)>> limiterParsers = {
{"CREATURE_TYPE_LIMITER", parseCreatureTypeLimiter },
{"HAS_ANOTHER_BONUS_LIMITER", parseHasAnotherBonusLimiter },
{"CREATURE_ALIGNMENT_LIMITER", parseCreatureAlignmentLimiter},
{"FACTION_LIMITER", parseFactionLimiter },
{"CREATURE_FACTION_LIMITER", parseFactionLimiter },
{"CREATURE_LEVEL_LIMITER", parseCreatureLevelLimiter },
{"CREATURE_TERRAIN_LIMITER", parseCreatureTerrainLimiter },
{"UNIT_ON_HEXES", parseUnitOnHexLimiter },
{"HAS_CHARGES_LIMITER", parseHasChargesLimiter },
};
switch(limiter.getType())
{
case JsonNode::JsonType::DATA_VECTOR:
return parseAggregateLimiter(limiter);
case JsonNode::JsonType::DATA_STRING: //pre-defined limiters
return parseByMap(bonusLimiterMap, &limiter, "limiter type ");
case JsonNode::JsonType::DATA_STRUCT: //customizable limiters
{
std::string limiterType = limiter["type"].String();
if (limiterParsers.count(limiterType))
return limiterParsers.at(limiterType)(limiter);
2024-02-14 15:48:06 +02:00
else
logMod->error("Error: invalid customizable limiter type %s.", limiterType);
}
}
return nullptr;
}
std::shared_ptr<Bonus> JsonUtils::parseBonus(const JsonNode &ability, const TextIdentifier & descriptionID)
2024-02-14 15:48:06 +02:00
{
auto b = std::make_shared<Bonus>();
if (!parseBonus(ability, b.get(), descriptionID))
2024-02-14 15:48:06 +02:00
{
// caller code can not handle this case and presumes that returned bonus is always valid
logGlobal->error("Failed to parse bonus! Json config was %S ", ability.toString());
2024-02-14 15:48:06 +02:00
b->type = BonusType::NONE;
return b;
}
return b;
}
bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b, const TextIdentifier & descriptionID)
2024-02-14 15:48:06 +02:00
{
const JsonNode * value = nullptr;
const JsonNode & subtypeNode = ability["subtype"];
const JsonNode & addinfoNode = ability["addInfo"];
2024-02-14 15:48:06 +02:00
if (ability["type"].isNull())
{
logMod->error("Failed to parse bonus. Description: '%s'. Config: '%s'", descriptionID.get(), ability.toCompactString());
return false;
}
LIBRARY->identifiers()->requestIdentifier("bonus", ability["type"], [b, subtypeNode, addinfoNode](si32 bonusID)
{
2025-06-13 16:36:24 +03:00
b->type = static_cast<BonusType>(bonusID);
loadBonusSubtype(b->subtype, b->type, subtypeNode);
loadBonusAddInfo(b->additionalInfo, b->type, addinfoNode);
});
2024-02-14 15:48:06 +02:00
2025-06-08 16:12:04 +03:00
b->val = static_cast<si32>(ability["val"].Float());
2024-02-14 15:48:06 +02:00
2025-06-08 16:12:04 +03:00
value = &ability["valueType"];
if (!value->isNull())
b->valType = static_cast<BonusValueType>(parseByMapN(bonusValueMap, value, "value type "));
2024-02-14 15:48:06 +02:00
b->stacking = ability["stacking"].String();
b->turnsRemain = static_cast<si32>(ability["turns"].Float());
if(!ability["description"].isNull())
{
if (ability["description"].isString() && !ability["description"].String().empty())
{
if (ability["description"].String()[0] == '@')
b->description.appendTextID(ability["description"].String().substr(1));
else if (!descriptionID.get().empty())
{
LIBRARY->generaltexth->registerString(ability.getModScope(), descriptionID, ability["description"]);
b->description.appendTextID(descriptionID.get());
}
}
2024-02-14 15:48:06 +02:00
if (ability["description"].isNumber())
b->description.appendTextID("core.arraytxt." + std::to_string(ability["description"].Integer()));
2024-02-14 15:48:06 +02:00
}
if(!ability["icon"].isNull())
b->customIconPath = ImagePath::fromJson(ability["icon"]);
2025-07-08 11:59:16 +02:00
b->hidden = !ability["hidden"].isNull() && ability["hidden"].Bool();
2024-02-14 15:48:06 +02:00
value = &ability["effectRange"];
if (!value->isNull())
b->effectRange = static_cast<BonusLimitEffect>(parseByMapN(bonusLimitEffect, value, "effect range "));
value = &ability["duration"];
if (!value->isNull())
{
switch (value->getType())
{
case JsonNode::JsonType::DATA_STRING:
b->duration = parseByMap(bonusDurationMap, value, "duration type ");
break;
case JsonNode::JsonType::DATA_VECTOR:
{
BonusDuration::Type dur = 0;
for (const JsonNode & d : value->Vector())
dur |= parseByMapN(bonusDurationMap, &d, "duration type ");
b->duration = dur;
}
break;
default:
logMod->error("Error! Wrong bonus duration format.");
}
}
value = &ability["sourceType"];
if (!value->isNull())
b->source = static_cast<BonusSource>(parseByMap(bonusSourceMap, value, "source type "));
if (!ability["sourceID"].isNull())
loadBonusSourceInstance(b->sid, b->source, ability["sourceID"]);
value = &ability["targetSourceType"];
if (!value->isNull())
b->targetSourceType = static_cast<BonusSource>(parseByMap(bonusSourceMap, value, "target type "));
value = &ability["limiters"];
if (!value->isNull())
b->limiter = parseLimiter(*value);
value = &ability["propagator"];
if (!value->isNull())
{
//ALL_CREATURES old propagator compatibility
if(value->String() == "ALL_CREATURES")
{
logMod->warn("ALL_CREATURES propagator is deprecated. Use GLOBAL_EFFECT propagator with CREATURES_ONLY limiter");
b->addLimiter(std::make_shared<CreatureLevelLimiter>());
b->propagator = bonusPropagatorMap.at("GLOBAL_EFFECT");
}
else
b->propagator = parseByMap(bonusPropagatorMap, value, "propagator type ");
}
value = &ability["updater"];
if(!value->isNull())
b->addUpdater(parseUpdater(*value));
value = &ability["propagationUpdater"];
if(!value->isNull())
b->propagationUpdater = parseUpdater(*value);
return true;
}
CSelector JsonUtils::parseSelector(const JsonNode & ability)
{
CSelector ret = Selector::all;
// Recursive parsers for anyOf, allOf, noneOf
const auto * value = &ability["allOf"];
if(value->isVector())
{
for(const auto & andN : value->Vector())
ret = ret.And(parseSelector(andN));
}
value = &ability["anyOf"];
if(value->isVector())
{
CSelector base = Selector::none;
for(const auto & andN : value->Vector())
base = base.Or(parseSelector(andN));
ret = ret.And(base);
}
value = &ability["noneOf"];
if(value->isVector())
{
CSelector base = Selector::none;
for(const auto & andN : value->Vector())
base = base.Or(parseSelector(andN));
ret = ret.And(base.Not());
}
BonusType type = BonusType::NONE;
// Actual selector parser
value = &ability["type"];
if(value->isString())
{
ret = ret.And(Selector::type()(static_cast<BonusType>(*LIBRARY->identifiers()->getIdentifier("bonus", value->String()))));
2024-02-14 15:48:06 +02:00
}
value = &ability["subtype"];
if(!value->isNull() && type != BonusType::NONE)
{
BonusSubtypeID subtype;
loadBonusSubtype(subtype, type, ability);
ret = ret.And(Selector::subtype()(subtype));
}
value = &ability["sourceType"];
std::optional<BonusSource> src = std::nullopt; //Fixes for GCC false maybe-uninitialized
std::optional<BonusSourceID> id = std::nullopt;
if(value->isString())
{
auto it = bonusSourceMap.find(value->String());
if(it != bonusSourceMap.end())
src = it->second;
}
value = &ability["sourceID"];
if(!value->isNull() && src.has_value())
{
loadBonusSourceInstance(*id, *src, ability);
}
if(src && id)
ret = ret.And(Selector::source(*src, *id));
else if(src)
ret = ret.And(Selector::sourceTypeSel(*src));
value = &ability["targetSourceType"];
if(value->isString())
{
auto it = bonusSourceMap.find(value->String());
if(it != bonusSourceMap.end())
ret = ret.And(Selector::targetSourceType()(it->second));
}
value = &ability["valueType"];
if(value->isString())
{
auto it = bonusValueMap.find(value->String());
if(it != bonusValueMap.end())
ret = ret.And(Selector::valueType(it->second));
}
CAddInfo info;
value = &ability["addInfo"];
if(!value->isNull())
{
2025-05-20 09:42:30 +03:00
loadBonusAddInfo(info, type, ability["addInfo"]);
2024-02-14 15:48:06 +02:00
ret = ret.And(Selector::info()(info));
}
value = &ability["effectRange"];
if(value->isString())
{
auto it = bonusLimitEffect.find(value->String());
if(it != bonusLimitEffect.end())
ret = ret.And(Selector::effectRange()(it->second));
}
value = &ability["lastsTurns"];
if(value->isNumber())
ret = ret.And(Selector::turns(value->Integer()));
value = &ability["lastsDays"];
if(value->isNumber())
ret = ret.And(Selector::days(value->Integer()));
return ret;
}
VCMI_LIB_NAMESPACE_END