diff --git a/CREDITS.md b/CREDITS.md index 4d68805694..a61efc8822 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -678,7 +678,9 @@ This page lists all the individual contributions to the project by their author. - **solar-III (凤九歌)** - Target scanning delay customization (documentation) - Skip target scanning function calling for unarmed technos (documentation) -- **Flactine** - add target filtering options to attacheffect system +- **Flactine** + - Add target filtering options to attacheffect system + - Add veterancy-based target filtering for weapons and warheads - **tyuah8**: - Drive/Jumpjet/Ship/Teleport locomotor did not power on when it is un-piggybacked bugfix - Destroyed unit leaves sensors bugfix diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md index 952ca5f730..ea0cfad43c 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -2348,6 +2348,7 @@ Conventional.IgnoreUnits=false ; boolean ### Customizable Warhead trigger conditions - `AffectsBelowPercent` and `AffectsAbovePercent` can be used to set the health percentage thresholds that target needs to be below/equal and/or above of for the Warhead to detonate. If target has zero health left this check is bypassed. +- `AffectsVeterancy` sets the veterancy levels that allow the Warhead to detonate on the target. - If set to `false`, `AffectsNeutral` makes the warhead can't damage or affect target that belongs to neutral house. - If set to `false`, `EffectsRequireVerses` makes the Phobos-introduced warhead effects trigger even if it can't damage the target because of it's current ArmorType (e.g. 0% in `Verses`). @@ -2356,6 +2357,7 @@ In `rulesmd.ini`: [SOMEWARHEAD] ; WarheadType AffectsBelowPercent=1.0 ; floating point value, percents or absolute AffectsAbovePercent=0.0 ; floating point value, percents or absolute +AffectsVeterancy=all ; List of Affected Veterancy Enumeration (none|rookie|veteran|elite|all) AffectsNeutral=true ; boolean EffectsRequireVerses=false ; boolean ``` diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 7822c67fc9..fb36d81046 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -2992,11 +2992,12 @@ This function is only used as an additional scattering visual display, which is In `rulesmd.ini`: ```ini -[SOMEWEAPON] ; WeaponType -CanTarget=all ; List of Affected Target Enumeration (none|land|water|empty|infantry|units|buildings|all) -CanTargetHouses=all ; List of Affected House Enumeration (none|owner/self|allies/ally|team|enemies/enemy|all) -CanTarget.MaxHealth=1.0 ; floating point value, percents or absolute -CanTarget.MinHealth=0.0 ; floating point value, percents or absolute +[SOMEWEAPON] ; WeaponType +CanTarget=all ; List of Affected Target Enumeration (none|land|water|empty|infantry|units|buildings|all) +CanTargetHouses=all ; List of Affected House Enumeration (none|owner/self|allies/ally|team|enemies/enemy|all) +CanTarget.MaxHealth=1.0 ; floating point value, percents or absolute +CanTarget.MinHealth=0.0 ; floating point value, percents or absolute +CanTargetVeterancy=all ; List of Affected Veterancy Enumeration (none|rookie|veteran|elite|all) ``` ```{note} diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 0d9a54c964..d1aefbfe8e 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -518,6 +518,8 @@ New: - [Attack non-threatening structures extensions](New-or-Enhanced-Logics.md#attack-non-threatening-structures-techno) (by FlyStar) - [Customize size for mind controlled unit](New-or-Enhanced-Logics.md#mind-control-enhancement) (by NetsuNegi) - [Deploy priority filtering](New-or-Enhanced-Logics.md#low-priority-for-deploy) (by Starkku) +- [Weapon target filtering by target veterancy](New-or-Enhanced-Logics.md#weapon-targeting-filter) (by Flactine) +- [Warhead effect filtering by target veterancy](Fixed-or-Improved-Logics.md#customizable-warhead-trigger-conditions) (by Flactine) Vanilla fixes: - Fixed sidebar not updating queued unit numbers when adding or removing units when the production is on hold (by CrimRecya) diff --git a/src/Ext/Bullet/Hooks.DetonateLogics.cpp b/src/Ext/Bullet/Hooks.DetonateLogics.cpp index 1788de4815..81a00ca2cc 100644 --- a/src/Ext/Bullet/Hooks.DetonateLogics.cpp +++ b/src/Ext/Bullet/Hooks.DetonateLogics.cpp @@ -19,7 +19,7 @@ DEFINE_HOOK(0x4690D4, BulletClass_Logics_NewChecks, 0x6) if (auto const pTarget = abstract_cast(pBullet->Target)) { // Check if the WH should affect the techno target or skip it - if (!pExt->IsHealthInThreshold(pTarget) || (!pExt->AffectsNeutral && pTarget->Owner->IsNeutral())) + if (!pExt->IsHealthInThreshold(pTarget) || !pExt->IsVeterancyInThreshold(pTarget) || (!pExt->AffectsNeutral && pTarget->Owner->IsNeutral())) return GoToExtras; } @@ -382,7 +382,7 @@ DEFINE_HOOK(0x469AA4, BulletClass_Logics_Extras, 0x5) auto const pWHExt = WarheadTypeExt::ExtMap.Find(pWH); auto const pTarget = abstract_cast(pThis->Target); - if (pTarget && !pWHExt->IsHealthInThreshold(pTarget)) + if (pTarget && !pWHExt->IsHealthInThreshold(pTarget) && !pWHExt->IsVeterancyInThreshold(pTarget)) continue; int damage = defaultDamage; @@ -556,7 +556,8 @@ static bool IsAllowedSplitsTarget(TechnoClass* pSource, HouseClass* pOwner, Weap if (!EnumFunctions::CanTargetHouse(pWeaponExt->CanTargetHouses, pOwner, pTarget->Owner) || !EnumFunctions::IsCellEligible(pTarget->GetCell(), pWeaponExt->CanTarget, true, true) || !EnumFunctions::IsTechnoEligible(pTarget, pWeaponExt->CanTarget) - || !pWeaponExt->IsHealthInThreshold(pTarget)) + || !pWeaponExt->IsHealthInThreshold(pTarget) + || !pWeaponExt->IsVeterancyInThreshold(pTarget)) { return false; } diff --git a/src/Ext/Bullet/Hooks.cpp b/src/Ext/Bullet/Hooks.cpp index df78ad4eb6..b15f71d53e 100644 --- a/src/Ext/Bullet/Hooks.cpp +++ b/src/Ext/Bullet/Hooks.cpp @@ -274,7 +274,7 @@ DEFINE_HOOK(0x46A4FB, BulletClass_Shrapnel_Targeting, 0x6) if (!pWeaponExt->SkipWeaponPicking) { if (!EnumFunctions::CanTargetHouse(pWeaponExt->CanTargetHouses, pOwner, pTechno->Owner) || !EnumFunctions::IsTechnoEligible(pTechno, pWeaponExt->CanTarget) - || !pWeaponExt->IsHealthInThreshold(pTechno) || !pWeaponExt->HasRequiredAttachedEffects(pTechno, pSource)) + || !pWeaponExt->IsHealthInThreshold(pTechno) || !pWeaponExt->IsVeterancyInThreshold(pTechno) || !pWeaponExt->HasRequiredAttachedEffects(pTechno, pSource)) { return SkipObject; } diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 8f6b540c09..41b6eb42c2 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -286,6 +286,7 @@ class TechnoExt static void CreateDelayedFireAnim(TechnoClass* pThis, AnimTypeClass* pAnimType, int weaponIndex, bool attach, bool center, bool removeOnNoDelay, bool onTurret, CoordStruct firingCoords); static bool HandleDelayedFireWithPauseSequence(TechnoClass* pThis, WeaponTypeClass* pWeapon, int weaponIndex, int frame, int firingFrame); static bool IsHealthInThreshold(TechnoClass* pObject, double min, double max); + static bool IsVeterancyInThreshold(TechnoClass* pObject, double min, double max); static UnitTypeClass* GetUnitTypeExtra(UnitClass* pUnit, TechnoTypeExt::ExtData* pData); static AircraftTypeClass* GetAircraftTypeExtra(AircraftClass* pAircraft); static bool CannotMove(UnitClass* pThis); diff --git a/src/Ext/Techno/Hooks.Firing.cpp b/src/Ext/Techno/Hooks.Firing.cpp index c56c9f5b6f..ae3667f16b 100644 --- a/src/Ext/Techno/Hooks.Firing.cpp +++ b/src/Ext/Techno/Hooks.Firing.cpp @@ -361,6 +361,7 @@ DEFINE_HOOK(0x6FC339, TechnoClass_CanFire, 0x6) if (!EnumFunctions::IsTechnoEligible(pTargetTechno, pWeaponExt->CanTarget) || !EnumFunctions::CanTargetHouse(pWeaponExt->CanTargetHouses, pThis->Owner, pTargetTechno->Owner) || !pWeaponExt->IsHealthInThreshold(pTargetTechno) + || !pWeaponExt->IsVeterancyInThreshold(pTargetTechno) || !pWeaponExt->HasRequiredAttachedEffects(pTargetTechno, pThis)) { return CannotFire; diff --git a/src/Ext/Techno/Hooks.ReceiveDamage.cpp b/src/Ext/Techno/Hooks.ReceiveDamage.cpp index 4a5a495207..2da6c1a33e 100644 --- a/src/Ext/Techno/Hooks.ReceiveDamage.cpp +++ b/src/Ext/Techno/Hooks.ReceiveDamage.cpp @@ -21,7 +21,7 @@ DEFINE_HOOK(0x701900, TechnoClass_ReceiveDamage_Shield, 0x6) // AffectsAbove/BelowPercent & AffectsNeutral can ignore IgnoreDefenses like AffectsAllies/Enmies/Owner // They should be checked here to cover all cases that directly use ReceiveDamage to deal damage - if (!pWHExt->IsHealthInThreshold(pThis) || (!pWHExt->AffectsNeutral && pThis->Owner->IsNeutral())) + if (!pWHExt->IsHealthInThreshold(pThis) || !pWHExt->IsVeterancyInThreshold(pThis) || (!pWHExt->AffectsNeutral && pThis->Owner->IsNeutral())) { damage = 0; return 0; diff --git a/src/Ext/Techno/WeaponHelpers.cpp b/src/Ext/Techno/WeaponHelpers.cpp index 8441be3b4a..64ea5740ae 100644 --- a/src/Ext/Techno/WeaponHelpers.cpp +++ b/src/Ext/Techno/WeaponHelpers.cpp @@ -40,6 +40,7 @@ int TechnoExt::PickWeaponIndex(TechnoClass* pThis, TechnoClass* pTargetTechno, A if (!EnumFunctions::IsTechnoEligible(pTargetTechno, pSecondExt->CanTarget) || !EnumFunctions::CanTargetHouse(pSecondExt->CanTargetHouses, pThis->Owner, pTargetTechno->Owner) || !pSecondExt->IsHealthInThreshold(pTargetTechno) + || !pSecondExt->IsVeterancyInThreshold(pTargetTechno) || !pSecondExt->HasRequiredAttachedEffects(pTargetTechno, pThis)) { return weaponIndexOne; @@ -70,6 +71,7 @@ int TechnoExt::PickWeaponIndex(TechnoClass* pThis, TechnoClass* pTargetTechno, A if (!EnumFunctions::IsTechnoEligible(pTargetTechno, pFirstExt->CanTarget) || !EnumFunctions::CanTargetHouse(pFirstExt->CanTargetHouses, pThis->Owner, pTargetTechno->Owner) || !pFirstExt->IsHealthInThreshold(pTargetTechno) + || !pFirstExt->IsVeterancyInThreshold(pTargetTechno) || !firstAllowedAE) { return weaponIndexTwo; diff --git a/src/Ext/WarheadType/Body.cpp b/src/Ext/WarheadType/Body.cpp index c95a3aae44..541d47ab48 100644 --- a/src/Ext/WarheadType/Body.cpp +++ b/src/Ext/WarheadType/Body.cpp @@ -37,6 +37,9 @@ bool WarheadTypeExt::ExtData::CanAffectTarget(TechnoClass* pTarget) const if (!IsHealthInThreshold(pTarget)) return false; + if (!IsVeterancyInThreshold(pTarget)) + return false; + if (!this->EffectsRequireVerses) return true; @@ -51,6 +54,14 @@ bool WarheadTypeExt::ExtData::IsHealthInThreshold(TechnoClass* pTarget) const return TechnoExt::IsHealthInThreshold(pTarget, this->AffectsAbovePercent, this->AffectsBelowPercent); } +bool WarheadTypeExt::ExtData::IsVeterancyInThreshold(TechnoClass* pTarget) const +{ + if (!this->VeterancyCheck) + return true; + + return EnumFunctions::CanTargetVeterancy(this->AffectsVeterancy, pTarget); +} + // Checks if Warhead can affect target that might or might be currently invulnerable. bool WarheadTypeExt::ExtData::CanAffectInvulnerable(TechnoClass* pTarget) const { @@ -283,11 +294,13 @@ void WarheadTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->AffectsBelowPercent.Read(exINI, pSection, "AffectsBelowPercent"); this->AffectsAbovePercent.Read(exINI, pSection, "AffectsAbovePercent"); + this->AffectsVeterancy.Read(exINI, pSection, "AffectsVeterancy"); this->AffectsNeutral.Read(exINI, pSection, "AffectsNeutral"); this->AffectsGround.Read(exINI, pSection, "AffectsGround"); this->AffectsAir.Read(exINI, pSection, "AffectsAir"); this->CellSpread_Cylinder.Read(exINI, pSection, "CellSpread.Cylinder"); this->HealthCheck = this->AffectsAbovePercent > 0.0 || this->AffectsBelowPercent < 1.0; + this->VeterancyCheck = this->AffectsVeterancy != AffectedVeterancy::All; if (this->AffectsAbovePercent > this->AffectsBelowPercent) Debug::Log("[Developer warning][%s] AffectsAbovePercent is bigger than AffectsBelowPercent, the warhead will never activate!\n", pSection); @@ -537,11 +550,13 @@ void WarheadTypeExt::ExtData::Serialize(T& Stm) .Process(this->AffectsBelowPercent) .Process(this->AffectsAbovePercent) + .Process(this->AffectsVeterancy) .Process(this->AffectsNeutral) .Process(this->AffectsGround) .Process(this->AffectsAir) .Process(this->CellSpread_Cylinder) .Process(this->HealthCheck) + .Process(this->VeterancyCheck) .Process(this->InflictLocomotor) .Process(this->RemoveInflictedLocomotor) diff --git a/src/Ext/WarheadType/Body.h b/src/Ext/WarheadType/Body.h index 159e6508ea..97b0450132 100644 --- a/src/Ext/WarheadType/Body.h +++ b/src/Ext/WarheadType/Body.h @@ -191,6 +191,8 @@ class WarheadTypeExt Valueable AffectsBelowPercent; Valueable AffectsAbovePercent; + Valueable AffectsVeterancy; + Valueable AffectsNeutral; Valueable AffectsGround; Valueable AffectsAir; @@ -231,6 +233,7 @@ class WarheadTypeExt int RemainingAnimCreationInterval; bool PossibleCellSpreadDetonate; bool HealthCheck; + bool VeterancyCheck; TechnoClass* DamageAreaTarget; private: @@ -405,6 +408,7 @@ class WarheadTypeExt , AffectsBelowPercent { 1.0 } , AffectsAbovePercent { 0.0 } + , AffectsVeterancy { AffectedVeterancy::All } , AffectsNeutral { true } , AffectsGround { true } , AffectsAir { true } @@ -426,6 +430,7 @@ class WarheadTypeExt , RemainingAnimCreationInterval { 0 } , PossibleCellSpreadDetonate { false } , HealthCheck { false } + , VeterancyCheck { false } , DamageAreaTarget {} , CanKill { true } @@ -462,6 +467,7 @@ class WarheadTypeExt bool CanAffectInvulnerable(TechnoClass* pTarget) const; bool EligibleForFullMapDetonation(TechnoClass* pTechno, TechnoTypeClass* pType, HouseClass* pOwner) const; bool IsHealthInThreshold(TechnoClass* pTarget) const; + bool IsVeterancyInThreshold(TechnoClass* pTarget) const; virtual ~ExtData() = default; virtual void LoadFromINIFile(CCINIClass* pINI) override; diff --git a/src/Ext/WeaponType/Body.cpp b/src/Ext/WeaponType/Body.cpp index 5e06ff4722..f86aab9fa7 100644 --- a/src/Ext/WeaponType/Body.cpp +++ b/src/Ext/WeaponType/Body.cpp @@ -45,6 +45,11 @@ bool WeaponTypeExt::ExtData::IsHealthInThreshold(TechnoClass* pTarget) const return TechnoExt::IsHealthInThreshold(pTarget, this->CanTarget_MinHealth, this->CanTarget_MaxHealth); } +bool WeaponTypeExt::ExtData::IsVeterancyInThreshold(TechnoClass* pTarget) const +{ + return EnumFunctions::CanTargetVeterancy(this->CanTargetVeterancy, pTarget); +} + void WeaponTypeExt::ExtData::Initialize() { this->RadType = RadTypeClass::FindOrAllocate(GameStrings::Radiation); @@ -103,6 +108,7 @@ void WeaponTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->CanTargetHouses.Read(exINI, pSection, "CanTargetHouses"); this->CanTarget_MaxHealth.Read(exINI, pSection, "CanTarget.MaxHealth"); this->CanTarget_MinHealth.Read(exINI, pSection, "CanTarget.MinHealth"); + this->CanTargetVeterancy.Read(exINI, pSection, "CanTargetVeterancy"); this->Burst_Delays.Read(exINI, pSection, "Burst.Delays"); this->Burst_FireWithinSequence.Read(exINI, pSection, "Burst.FireWithinSequence"); this->Burst_NoDelay.Read(exINI, pSection, "Burst.NoDelay"); @@ -163,6 +169,7 @@ void WeaponTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) // handle SkipWeaponPicking if (this->CanTarget != AffectedTarget::All || this->CanTargetHouses != AffectedHouse::All || this->CanTarget_MaxHealth < 1.0 || this->CanTarget_MinHealth > 0.0 + || this->CanTargetVeterancy != AffectedVeterancy::All || this->AttachEffect_RequiredTypes.size() || this->AttachEffect_RequiredGroups.size() || this->AttachEffect_DisallowedTypes.size() || this->AttachEffect_DisallowedGroups.size()) { @@ -192,6 +199,7 @@ void WeaponTypeExt::ExtData::Serialize(T& Stm) .Process(this->CanTargetHouses) .Process(this->CanTarget_MaxHealth) .Process(this->CanTarget_MinHealth) + .Process(this->CanTargetVeterancy) .Process(this->RadType) .Process(this->Burst_Delays) .Process(this->Burst_FireWithinSequence) diff --git a/src/Ext/WeaponType/Body.h b/src/Ext/WeaponType/Body.h index f784b74d22..fe72e04118 100644 --- a/src/Ext/WeaponType/Body.h +++ b/src/Ext/WeaponType/Body.h @@ -36,6 +36,7 @@ class WeaponTypeExt Valueable CanTargetHouses; Valueable CanTarget_MaxHealth; Valueable CanTarget_MinHealth; + Valueable CanTargetVeterancy; ValueableVector Burst_Delays; Valueable Burst_FireWithinSequence; Valueable Burst_NoDelay; @@ -115,6 +116,7 @@ class WeaponTypeExt , CanTargetHouses { AffectedHouse::All } , CanTarget_MaxHealth { 1.0 } , CanTarget_MinHealth { 0.0 } + , CanTargetVeterancy { AffectedVeterancy::All } , Burst_Delays {} , Burst_FireWithinSequence { false } , Burst_NoDelay { false } @@ -177,6 +179,7 @@ class WeaponTypeExt int GetBurstDelay(int burstIndex) const; bool HasRequiredAttachedEffects(TechnoClass* pTechno, TechnoClass* pFirer) const; bool IsHealthInThreshold(TechnoClass* pTarget) const; + bool IsVeterancyInThreshold(TechnoClass* pTarget) const; virtual ~ExtData() = default; diff --git a/src/Utilities/Enum.h b/src/Utilities/Enum.h index a93f54b31d..2109fdf943 100644 --- a/src/Utilities/Enum.h +++ b/src/Utilities/Enum.h @@ -111,6 +111,18 @@ enum class AffectedTarget : unsigned char MAKE_ENUM_FLAGS(AffectedTarget); +enum class AffectedVeterancy : unsigned char +{ + None = 0x0, + Rookie = 0x1, + Veteran = 0x2, + Elite = 0x4, + + All = Rookie | Veteran | Elite +}; + +MAKE_ENUM_FLAGS(AffectedVeterancy); + enum class AffectedHouse : unsigned char { None = 0x0, diff --git a/src/Utilities/EnumFunctions.cpp b/src/Utilities/EnumFunctions.cpp index 6d2ed85f3d..2fc159e8c7 100644 --- a/src/Utilities/EnumFunctions.cpp +++ b/src/Utilities/EnumFunctions.cpp @@ -11,6 +11,23 @@ bool EnumFunctions::CanTargetHouse(AffectedHouse flags, HouseClass* ownerHouse, return (flags & AffectedHouse::Enemies) != AffectedHouse::None; } +bool EnumFunctions::CanTargetVeterancy(AffectedVeterancy flags, TechnoClass* pTechno) +{ + if (flags == AffectedVeterancy::All) + return true; + + switch (pTechno->Veterancy.GetRemainingLevel()) + { + case Rank::Elite: + return (flags & AffectedVeterancy::Elite) != AffectedVeterancy::None; + case Rank::Veteran: + return (flags & AffectedVeterancy::Veteran) != AffectedVeterancy::None; + default: + return (flags & AffectedVeterancy::Rookie) != AffectedVeterancy::None; + } +} + + bool EnumFunctions::IsCellEligible(CellClass* const pCell, AffectedTarget allowed, bool explicitEmptyCells, bool considerBridgesLand) { if (allowed == AffectedTarget::All) diff --git a/src/Utilities/EnumFunctions.h b/src/Utilities/EnumFunctions.h index 3a9a53269f..a06c5c273a 100644 --- a/src/Utilities/EnumFunctions.h +++ b/src/Utilities/EnumFunctions.h @@ -9,6 +9,7 @@ class EnumFunctions { public: static bool CanTargetHouse(AffectedHouse flags, HouseClass* ownerHouse, HouseClass* targetHouse); + static bool CanTargetVeterancy(AffectedVeterancy flags, TechnoClass* pTechno); static bool IsCellEligible(CellClass* const pCell, AffectedTarget allowed, bool explicitEmptyCells = false, bool considerBridgesLand = false); static bool IsTechnoEligible(TechnoClass* const pTechno, AffectedTarget allowed, bool considerAircraftSeparately = false); static bool AreCellAndObjectsEligible(CellClass* const pCell, AffectedTarget allowed, AffectedHouse allowedHouses, HouseClass* owner, bool explicitEmptyCells = false, bool considerAircraftSeparately = false, bool allowBridges = false); diff --git a/src/Utilities/TemplateDef.h b/src/Utilities/TemplateDef.h index 089a27499b..462b1d456e 100644 --- a/src/Utilities/TemplateDef.h +++ b/src/Utilities/TemplateDef.h @@ -787,6 +787,52 @@ namespace detail return false; } + template <> + inline bool read(AffectedVeterancy& value, INI_EX& parser, const char* pSection, const char* pKey) + { + if (parser.ReadString(pSection, pKey)) + { + static const std::pair Names[] = + { + {"rookie", AffectedVeterancy::Rookie}, + {"veteran", AffectedVeterancy::Veteran}, + {"elite", AffectedVeterancy::Elite}, + {"all", AffectedVeterancy::All}, + {"none", AffectedVeterancy::None}, + }; + + + auto parsed = AffectedVeterancy::None; + for (auto&& part : std::string_view { parser.value() } | std::views::split(',')) + { + std::string_view&& cur { part.begin(),part.end() }; + *const_cast(cur.data() + cur.find_last_not_of(" \t\r") + 1) = 0; + auto pCur = cur.data() + cur.find_first_not_of(" \t\r"); + bool matched = false; + for (auto const& [name, val] : Names) + { + if (_strcmpi(pCur, name) == 0) + { + parsed |= val; + matched = true; + break; + } + } + if (!matched) + { + Debug::INIParseFailed(pSection, pKey, pCur, "Expected an affected veterancy"); + return false; + } + } + + value = parsed; + return true; + } + + return false; + } + + template <> inline bool read(AttachedAnimFlag& value, INI_EX& parser, const char* pSection, const char* pKey) {