diff --git a/CREDITS.md b/CREDITS.md index f92cc6854e..2b494116d1 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -732,6 +732,7 @@ This page lists all the individual contributions to the project by their author. - Fix the bug where landing direction cannot be correctly converted when set to a value exceeding 256 - Separately define the global default values of TerrainTypes' `IsPassable` and `CanBeBuiltOn` based on `SpawnsTiberium` - Remove the restriction that prohibits InfantryTypes from using the InitialPayload logic + - Add `ammo`, `health`, `mission`, `landtype` and `sequence` conditions to `DiscardOn` - **Ollerus**: - Build limit group enhancement - Customizable rocker amplitude diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 00e3c041e4..b950eb612f 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -23,7 +23,13 @@ This page describes all the engine features that are either new and introduced b - `selling`: Discard when the building to which the effect is attached is sold. - `undeploying`: Discard when the building to which the effect is attached performs undeploy. - `harvesting`: Discard when the object the effect is attached is harvesting ore. This can only be used when `DiscardOn.ConsiderHarvestingAsStationary=false`. - - `invokerdie`: Discard when the invoker of the effect is destroyed. + - `invokerdie`: Discard when the invoker of the effect is destroyed. + - `ammo`: Discard when the ammo of the object the effect is attached to is within the interval `[DiscardOn.Ammo.MinimumAmount, DiscardOn.Ammo.MaximumAmount]` (set to `-1` to ignore either bound). + - `health`: Discard when the health percentage of the object the effect is attached to is within the interval `(DiscardOn.Health.AbovePercent, DiscardOn.Health.BelowPercent]` (set to `-1` to ignore either bound). + - `mission`: Discard when the current mission of the object the effect is attached to matches any one in the `DiscardOn.Missions` list (or `DiscardOn.AIMissions` for AI-controlled objects, if set). + - `landtype`: Discard when the land type of the cell where the object the effect is attached to is currently located matches any land type in the `DiscardOn.LandTypes` list. + - `sequence`: Discard when the infantry to which the effect is attached is playing a sequence that matches any one in the `DiscardOn.Sequences` list. + - `DiscardOn.Sequences.Immediate` defines whether the `sequence` discard condition triggers immediately while the infantry is playing a matching sequence, or only when the infantry starts playing its next sequence after finishing that sequence. - `DiscardOn.MoveBasedOnDestination` defines whether to determine the movement state according to the presence or absence of a destination. It treats Jumpjet units hovering in the air as movement, and units that have no destination but are turning as stationary. - If used for an AE that has `DiscardOn=harvesting`, in order for it to judge correctly, this should be set to `true`. - `DiscardOn.ConsiderHarvestingAsStationary` defines whether to treat `harvesting` as `stationary`. When this flag is set to `false`, `DiscardOn=harvesting` can be used and it will not be considered `stationary` while `harvesting`. @@ -99,6 +105,7 @@ This page describes all the engine features that are either new and introduced b In `rulesmd.ini`: ```ini [General] +DiscardOn.Sequences.Immediate=true ; boolean DiscardOn.MoveBasedOnDestination=false ; boolean DiscardOn.ConsiderHarvestingAsStationary=true ; boolean OpenTopped.UseTransportRangeModifiers=false ; boolean @@ -114,7 +121,16 @@ Duration.ApplyArmorMultOnTarget=false ; boolean Cumulative=false ; boolean Cumulative.MaxCount=-1 ; integer Powered=false ; boolean -DiscardOn=none ; List of discard condition enumeration (none|entry|move|stationary|drain|inrange|outofrange|selling|undeploying|harvesting|invokerdie) +DiscardOn=none ; List of discard condition enumeration (none|entry|move|stationary|drain|inrange|outofrange|selling|undeploying|harvesting|invokerdie|ammo|health|mission|landtype|sequence) +DiscardOn.Ammo.MinimumAmount=-1 ; integer +DiscardOn.Ammo.MaximumAmount=-1 ; integer +DiscardOn.Health.BelowPercent=-1 ; floating point value +DiscardOn.Health.AbovePercent=-1 ; floating point value +DiscardOn.Missions= ; List of MissionTypes +DiscardOn.AIMissions= ; List of MissionTypes, default to [AttachEffectType] -> DiscardOn.Missions +DiscardOn.LandTypes= ; List of LandTypes (none | clear | road | water | rock | wall | tiberium | beach | rough | ice | railroad | tunnel | weeds) +DiscardOn.Sequences= ; List of Sequences (ready | guard | prone | walk | fireup | fireprone | secondaryfire | secondaryprone | down | crawl | up | idle1 | idle2 | die1 | die2 | die3 | die4 | die5 | deploy | deployed | deployedfire | deployedidle | undeploy | paradrop | cheer | panic | shovel | carry | fly | hover | firefly | tumble | airdeathstart | airdeathfalling | airdeathfinish | tread | swim | wetattack | wetidle1 | wetidle2 | wetdie1 | wetdie2) +DiscardOn.Sequences.Immediate= ; boolean, default to [General] -> DiscardOn.Sequences.Immediate DiscardOn.RangeOverride= ; floating point value, distance in cells DiscardOn.MoveBasedOnDestination= ; boolean, default to [General] -> DiscardOn.MoveBasedOnDestination DiscardOn.ConsiderHarvestingAsStationary= ; boolean, default to [General] -> DiscardOn.ConsiderHarvestingAsStationary diff --git a/docs/Whats-New.md b/docs/Whats-New.md index e40e61b614..2a84918e2e 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -654,6 +654,7 @@ HideShakeEffects=false ; boolean - [DeployFire supports buildings](New-or-Enhanced-Logics.md#deployfire-supports) (By FlyStar) - `OmniFire` supports buildings with `Turret=yes` (by FlyStar) - [Automatic conversion based on heallth](New-or-Enhanced-Logics.md#automatic-conversion-based-on-heallth) (by obsidianus) +- Add `ammo`, `health`, `mission`, `landtype` and `sequence` conditions to `DiscardOn` (by Noble_Fish) #### 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/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 5cf6099f5f..716de5412e 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -546,6 +546,7 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) deploySound.Read(exINI, GameStrings::AudioVisual, "DeploySound"); pThis->DeploySound = deploySound; + this->DiscardOn_Sequences_Immediate.Read(exINI, GameStrings::General, "DiscardOn.Sequences.Immediate"); this->DiscardOn_MoveBasedOnDestination.Read(exINI, GameStrings::General, "DiscardOn.MoveBasedOnDestination"); this->DiscardOn_ConsiderHarvestingAsStationary.Read(exINI, GameStrings::General, "DiscardOn.ConsiderHarvestingAsStationary"); @@ -1031,6 +1032,7 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->Vertical_AircraftFix) .Process(this->Temporal_ApplyVersus) .Process(this->Temporal_ApplyMultiplier) + .Process(this->DiscardOn_Sequences_Immediate) .Process(this->DiscardOn_MoveBasedOnDestination) .Process(this->DiscardOn_ConsiderHarvestingAsStationary) .Process(this->RemoveMindControl_Silent) diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index 136cf87f17..21a4d8018b 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -486,6 +486,7 @@ class RulesExt Valueable Temporal_ApplyVersus; Valueable Temporal_ApplyMultiplier; + Valueable DiscardOn_Sequences_Immediate; Valueable DiscardOn_MoveBasedOnDestination; Valueable DiscardOn_ConsiderHarvestingAsStationary; Valueable RemoveMindControl_Silent; @@ -941,6 +942,7 @@ class RulesExt , Vertical_AircraftFix { true } , Temporal_ApplyVersus { false } , Temporal_ApplyMultiplier { false } + , DiscardOn_Sequences_Immediate { true } , DiscardOn_MoveBasedOnDestination { false } , DiscardOn_ConsiderHarvestingAsStationary { true } , RemoveMindControl_Silent { false } diff --git a/src/New/Entity/AttachEffectClass.cpp b/src/New/Entity/AttachEffectClass.cpp index 88d01d870a..de7e785691 100644 --- a/src/New/Entity/AttachEffectClass.cpp +++ b/src/New/Entity/AttachEffectClass.cpp @@ -42,6 +42,7 @@ AttachEffectClass::AttachEffectClass(AttachEffectTypeClass* pType, TechnoClass* , ShouldRecalculateStats { false } , LastDiscardCheckFrame { -1 } , LastDiscardCheckValue { false } + , LastSequenceCheck { Sequence::Nothing } { this->HasInitialized = false; @@ -635,6 +636,93 @@ bool AttachEffectClass::ShouldBeDiscardedNow() return true; } + if ((discardOn & DiscardCondition::Ammo) != DiscardCondition::None) + { + const int min = pType->DiscardOn_Ammo_MinimumAmount; + const int max = pType->DiscardOn_Ammo_MaximumAmount; + const int ammo = pTechno->Ammo; + + if ((min < 0 || ammo >= min) && (max < 0 || ammo <= max)) + { + this->LastDiscardCheckValue = true; + return true; + } + } + + if ((discardOn & DiscardCondition::Health) != DiscardCondition::None) + { + if (auto const pTypeData = pTechno->GetTechnoType()) + { + const double min = pType->DiscardOn_Health_AbovePercent; + const double max = pType->DiscardOn_Health_BelowPercent; + if (TechnoExt::IsHealthInThreshold(pTechno, min, max)) + { + this->LastDiscardCheckValue = true; + return true; + } + } + } + + if ((discardOn & DiscardCondition::LandType) != DiscardCondition::None) + { + if (pType->DiscardOn_LandTypes != LandTypeFlags::None) + { + if (auto const pCell = pTechno->GetCell()) + { + LandTypeFlags landFlags = pType->DiscardOn_LandTypes; + if (IsLandTypeInFlags(landFlags, pCell->LandType)) + { + this->LastDiscardCheckValue = true; + return true; + } + } + } + } + + if ((discardOn & DiscardCondition::Mission) != DiscardCondition::None) + { + auto const& missions = pTechno->Owner->IsControlledByHuman() + ? pType->DiscardOn_Missions + : (pType->DiscardOn_AIMissions.HasValue() + ? static_cast&>(pType->DiscardOn_AIMissions) + : pType->DiscardOn_Missions); + + if (missions.size() > 0 && missions.Contains(pTechno->CurrentMission)) + { + this->LastDiscardCheckValue = true; + return true; + } + } + + if ((discardOn & DiscardCondition::Sequence) != DiscardCondition::None) + { + if (auto const pInf = abstract_cast(pTechno)) + { + if (pType->DiscardOn_Sequences.size() > 0) + { + if (pType->DiscardOn_Sequences_Immediate.Get(RulesExt::Global()->DiscardOn_Sequences_Immediate)) + { + if (pType->DiscardOn_Sequences.Contains(pInf->SequenceAnim)) + { + this->LastDiscardCheckValue = true; + return true; + } + } + else + { + if (this->LastSequenceCheck != pInf->SequenceAnim && pType->DiscardOn_Sequences.Contains(this->LastSequenceCheck)) + { + this->LastDiscardCheckValue = true; + return true; + } + this->LastSequenceCheck = pInf->SequenceAnim; + } + } + } + else + this->LastSequenceCheck = Sequence::Nothing; + } + if (pTechno->Target) { const bool inRange = (discardOn & DiscardCondition::InRange) != DiscardCondition::None; @@ -1184,6 +1272,7 @@ bool AttachEffectClass::Serialize(T& Stm) .Process(this->LastActiveStat) .Process(this->LaserTrail) .Process(this->ShouldRecalculateStats) + .Process(this->LastSequenceCheck) .Success(); } diff --git a/src/New/Entity/AttachEffectClass.h b/src/New/Entity/AttachEffectClass.h index 203100ea96..2923710f39 100644 --- a/src/New/Entity/AttachEffectClass.h +++ b/src/New/Entity/AttachEffectClass.h @@ -98,6 +98,7 @@ class AttachEffectClass bool LastDiscardCheckValue; bool LastActiveStat; LaserTrailClass* LaserTrail; + Sequence LastSequenceCheck; public: bool HasCumulativeAnim; diff --git a/src/New/Type/AttachEffectTypeClass.cpp b/src/New/Type/AttachEffectTypeClass.cpp index 6ed4ce8503..b39a2a95a7 100644 --- a/src/New/Type/AttachEffectTypeClass.cpp +++ b/src/New/Type/AttachEffectTypeClass.cpp @@ -100,6 +100,23 @@ void AttachEffectTypeClass::LoadFromINI(CCINIClass* pINI) this->Cumulative_MaxCount.Read(exINI, pSection, "Cumulative.MaxCount"); this->Powered.Read(exINI, pSection, "Powered"); this->DiscardOn.Read(exINI, pSection, "DiscardOn"); + this->DiscardOn_Ammo_MinimumAmount.Read(exINI, pSection, "DiscardOn.Ammo.MinimumAmount"); + this->DiscardOn_Ammo_MaximumAmount.Read(exINI, pSection, "DiscardOn.Ammo.MaximumAmount"); + + if (this->DiscardOn_Ammo_MinimumAmount > this->DiscardOn_Ammo_MaximumAmount) + Debug::Log("[Developer warning][%s] DiscardOn.Ammo.MinimumAmount is greater than DiscardOn.Ammo.MaximumAmount, the ammo discard condition cannot be established.\n", pSection); + + this->DiscardOn_Health_BelowPercent.Read(exINI, pSection, "DiscardOn.Health.BelowPercent"); + this->DiscardOn_Health_AbovePercent.Read(exINI, pSection, "DiscardOn.Health.AbovePercent"); + + if (this->DiscardOn_Health_AbovePercent > this->DiscardOn_Health_BelowPercent) + Debug::Log("[Developer warning][%s] DiscardOn.Health.AbovePercent is greater than DiscardOn.Health.BelowPercent, the health discard condition cannot be established.\n", pSection); + + this->DiscardOn_Missions.Read(exINI, pSection, "DiscardOn.Missions"); + this->DiscardOn_AIMissions.Read(exINI, pSection, "DiscardOn.AIMissions"); + this->DiscardOn_LandTypes.Read(exINI, pSection, "DiscardOn.LandTypes"); + this->DiscardOn_Sequences.Read(exINI, pSection, "DiscardOn.Sequences"); + this->DiscardOn_Sequences_Immediate.Read(exINI, pSection, "DiscardOn.Sequences.Immediate"); this->DiscardOn_RangeOverride.Read(exINI, pSection, "DiscardOn.RangeOverride"); this->DiscardOn_MoveBasedOnDestination.Read(exINI, pSection, "DiscardOn.MoveBasedOnDestination"); this->DiscardOn_ConsiderHarvestingAsStationary.Read(exINI, pSection, "DiscardOn.ConsiderHarvestingAsStationary"); @@ -218,6 +235,15 @@ void AttachEffectTypeClass::Serialize(T& Stm) .Process(this->Cumulative_MaxCount) .Process(this->Powered) .Process(this->DiscardOn) + .Process(this->DiscardOn_Ammo_MinimumAmount) + .Process(this->DiscardOn_Ammo_MaximumAmount) + .Process(this->DiscardOn_Health_BelowPercent) + .Process(this->DiscardOn_Health_AbovePercent) + .Process(this->DiscardOn_Missions) + .Process(this->DiscardOn_AIMissions) + .Process(this->DiscardOn_LandTypes) + .Process(this->DiscardOn_Sequences) + .Process(this->DiscardOn_Sequences_Immediate) .Process(this->DiscardOn_RangeOverride) .Process(this->DiscardOn_MoveBasedOnDestination) .Process(this->DiscardOn_ConsiderHarvestingAsStationary) @@ -353,6 +379,26 @@ namespace detail { parsed |= DiscardCondition::InvokerDie; } + else if (!_strcmpi(cur, "ammo")) + { + parsed |= DiscardCondition::Ammo; + } + else if (!_strcmpi(cur, "health")) + { + parsed |= DiscardCondition::Health; + } + else if (!_strcmpi(cur, "mission")) + { + parsed |= DiscardCondition::Mission; + } + else if (!_strcmpi(cur, "landtype")) + { + parsed |= DiscardCondition::LandType; + } + else if (!_strcmpi(cur, "sequence")) + { + parsed |= DiscardCondition::Sequence; + } else { Debug::INIParseFailed(pSection, pKey, cur, "Expected a discard condition type"); diff --git a/src/New/Type/AttachEffectTypeClass.h b/src/New/Type/AttachEffectTypeClass.h index 6de5e2d64c..ef02b9d3a4 100644 --- a/src/New/Type/AttachEffectTypeClass.h +++ b/src/New/Type/AttachEffectTypeClass.h @@ -21,7 +21,12 @@ enum class DiscardCondition : unsigned short Selling = 0x80, Undeploying = 0x100, Harvesting = 0x200, - InvokerDie = 0x400 + InvokerDie = 0x400, + Ammo = 0x800, + Health = 0x1000, + Mission = 0x2000, + LandType = 0x4000, + Sequence = 0x8000 }; MAKE_ENUM_FLAGS(DiscardCondition); @@ -52,6 +57,15 @@ class AttachEffectTypeClass final : public Enumerable Valueable Cumulative_MaxCount; Valueable Powered; Valueable DiscardOn; + Valueable DiscardOn_Ammo_MinimumAmount; + Valueable DiscardOn_Ammo_MaximumAmount; + Nullable DiscardOn_Health_BelowPercent; + Nullable DiscardOn_Health_AbovePercent; + ValueableVector DiscardOn_Missions; + NullableVector DiscardOn_AIMissions; + Valueable DiscardOn_LandTypes; + ValueableVector DiscardOn_Sequences; + NullableDiscardOn_Sequences_Immediate; Nullable DiscardOn_RangeOverride; Nullable DiscardOn_MoveBasedOnDestination; Nullable DiscardOn_ConsiderHarvestingAsStationary; @@ -122,6 +136,15 @@ class AttachEffectTypeClass final : public Enumerable , Cumulative_MaxCount { -1 } , Powered { false } , DiscardOn { DiscardCondition::None } + , DiscardOn_Ammo_MinimumAmount { -1 } + , DiscardOn_Ammo_MaximumAmount { -1 } + , DiscardOn_Health_BelowPercent { -1 } + , DiscardOn_Health_AbovePercent { -1 } + , DiscardOn_Missions {} + , DiscardOn_AIMissions {} + , DiscardOn_LandTypes { LandTypeFlags::None } + , DiscardOn_Sequences {} + , DiscardOn_Sequences_Immediate {} , DiscardOn_RangeOverride {} , DiscardOn_MoveBasedOnDestination {} , DiscardOn_ConsiderHarvestingAsStationary {} diff --git a/src/Utilities/TemplateDef.h b/src/Utilities/TemplateDef.h index a98518648d..aae4ce2b37 100644 --- a/src/Utilities/TemplateDef.h +++ b/src/Utilities/TemplateDef.h @@ -560,6 +560,48 @@ namespace detail return false; } + inline bool parse_sequence(const char* str, Sequence& seq) + { + static const auto Sequences = { + "Ready", "Guard", "Prone", "Walk", "FireUp", "Down", "Crawl", "Up", + "FireProne", "Idle1", "Idle2", "Die1", "Die2", "Die3", "Die4", "Die5", + "Tread", "Swim", "WetIdle1", "WetIdle2", "WetDie1", "WetDie2", "WetAttack", + "Hover", "Fly", "Tumble", "FireFly", "Deploy", "Deployed", "DeployedFire", + "DeployedIdle", "Undeploy", "Cheer", "Paradrop", "AirDeathStart", + "AirDeathFalling", "AirDeathFinish", "Panic", "Shovel", "Carry", + "SecondaryFire", "SecondaryProne" + }; + auto it = Sequences.begin(); + for (auto i = 0u; i < Sequences.size(); ++i) + { + if (_strcmpi(str, *it++) == 0) + { + seq = static_cast(i); + return true; + } + } + return false; + } + + template <> + inline bool read(Sequence& value, INI_EX& parser, const char* pSection, const char* pKey) + { + if (parser.ReadString(pSection, pKey)) + { + Sequence seq; + if (detail::parse_sequence(parser.value(), seq)) + { + value = seq; + return true; + } + else if (!parser.empty()) + { + Debug::INIParseFailed(pSection, pKey, parser.value(), "Expected a Sequence animation name"); + } + } + return false; + } + template <> inline bool read(DirType& value, INI_EX& parser, const char* pSection, const char* pKey) { @@ -1867,6 +1909,44 @@ inline void ValueableVector::Read(INI_EX& parser, const char* } } +template <> +inline void ValueableVector::Read(INI_EX& parser, const char* pSection, const char* pKey) +{ + if (parser.ReadString(pSection, pKey)) + { + this->clear(); + char* str = parser.value(); + char* context = nullptr; + for (char* cur = strtok_s(str, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context)) + { + auto mission = MissionControlClass::FindIndex(cur); + if (mission != Mission::None) + this->push_back(mission); + else if (!INIClass::IsBlank(cur)) + Debug::INIParseFailed(pSection, pKey, cur, "Invalid Mission name"); + } + } +} + +template <> +inline void ValueableVector::Read(INI_EX& parser, const char* pSection, const char* pKey) +{ + if (parser.ReadString(pSection, pKey)) + { + this->clear(); + char* str = parser.value(); + char* context = nullptr; + for (char* cur = strtok_s(str, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context)) + { + Sequence seq; + if (detail::parse_sequence(cur, seq)) + this->push_back(seq); + else if (!INIClass::IsBlank(cur)) + Debug::INIParseFailed(pSection, pKey, cur, "Invalid Sequence name"); + } + } +} + template bool ValueableVector::Load(PhobosStreamReader& Stm, bool RegisterForChange) { @@ -1969,6 +2049,31 @@ void __declspec(noinline) NullableVector::Read(INI_EX& parser, const char* pS } } +template <> +inline void NullableVector::Read(INI_EX& parser, const char* pSection, const char* pKey) +{ + if (parser.ReadString(pSection, pKey)) + { + this->clear(); + auto const non_default = _strcmpi(parser.value(), ""); + this->hasValue = non_default; + + if (non_default) + { + char* str = parser.value(); + char* context = nullptr; + for (char* cur = strtok_s(str, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context)) + { + auto mission = MissionControlClass::FindIndex(cur); + if (mission != Mission::None) + this->push_back(mission); + else if (!INIClass::IsBlank(cur)) + Debug::INIParseFailed(pSection, pKey, cur, "Invalid Mission name"); + } + } + } +} + template bool NullableVector::Load(PhobosStreamReader& Stm, bool RegisterForChange) {