diff --git a/CREDITS.md b/CREDITS.md index bdb498d967..29ce594cf8 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -386,6 +386,7 @@ This page lists all the individual contributions to the project by their author. - Fix an unusual use of DeployFireWeapon for InfantryType - Fix the fact that when the selected unit is in a rearmed state, it can unconditionally use attack mouse on the target - Units can customize the attack voice that plays when using more weapons + - When a weapon has `OnlyAttacker=yes`, it prevents other units using that weapon from attacking the same target - **NetsuNegi**: - Forbidding parallel AI queues by type - Jumpjet crash speed fix when crashing onto building diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 0ed79088c9..43e78d0a1f 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -2644,3 +2644,13 @@ CanTarget.MinHealth=0.0 ; floating point value, percents or absolute ```{note} `CanTarget` explicitly requires either `all` or `empty` to be listed for the weapon to be able to fire at cells containing no TechnoTypes. ``` + +### Single Attacker + +- When a weapon has `OnlyAttacker=yes`, it prevents other units using that weapon from attacking the same target. + +In `rulesmd.ini`: +```ini +[SOMEWARHEAD] ; WarheadType +OnlyAttacker=no ; boolean +``` diff --git a/docs/Whats-New.md b/docs/Whats-New.md index e6dee4bdbe..fab2a852d8 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -428,6 +428,7 @@ New: - [Units can customize the attack voice that plays when using more weapons](New-or-Enhanced-Logics.md#multi-voiceattack) (by FlyStar) - Customize squid grapple animation (by NetsuNegi) - [Auto deploy for GI-like infantry](Fixed-or-Improved-Logics.md#auto-deploy-for-gi-like-infantry) (by TaranDahl) +- When a weapon has `OnlyAttacker=yes`, it prevents other units using that weapon from attacking the same target (by FlyStar) 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/Techno/Body.Update.cpp b/src/Ext/Techno/Body.Update.cpp index 8d2d91efb1..e20eda13bf 100644 --- a/src/Ext/Techno/Body.Update.cpp +++ b/src/Ext/Techno/Body.Update.cpp @@ -24,7 +24,8 @@ // It's not recommended to do anything more here it could have a better place for performance consideration void TechnoExt::ExtData::OnEarlyUpdate() { - auto const pType = this->OwnerObject()->GetTechnoType(); + auto const pThis = this->OwnerObject(); + auto const pType = pThis->GetTechnoType(); // Set only if unset or type is changed // Notice that Ares may handle type conversion in the same hook here, which is executed right before this one thankfully @@ -46,6 +47,18 @@ void TechnoExt::ExtData::OnEarlyUpdate() if (this->AttackMoveFollowerTempCount) this->AttackMoveFollowerTempCount--; + + auto& AttackerDatas = this->OnlyAttackData; + if (!AttackerDatas.empty()) + { + for (int index = int(AttackerDatas.size()) - 1; index >= 0; --index) + { + if (AttackerDatas[index].Attacker->Target != pThis) + { + AttackerDatas.erase(AttackerDatas.begin() + index); + } + } + } } void TechnoExt::ExtData::ApplyInterceptor() @@ -2044,3 +2057,48 @@ void TechnoExt::ExtData::UpdateTintValues() calculateTint(Drawing::RGB_To_Int(pShieldType->Tint_Color), static_cast(pShieldType->Tint_Intensity * 1000), pShieldType->Tint_VisibleToHouses); } } + +void TechnoExt::ExtData::AddFirer(WeaponTypeClass* const Weapon, TechnoClass* const Attacker) +{ + if (Attacker->InLimbo) + return; + + const int index = this->FindFirer(Weapon); + const OnlyAttackStruct Data { Weapon ,Attacker }; + + if (index < 0) + { + this->OnlyAttackData.push_back(Data); + } + else + { + this->OnlyAttackData[index] = Data; + } +} + +bool TechnoExt::ExtData::ContainFirer(WeaponTypeClass* const Weapon, TechnoClass* const Attacker) const +{ + const int index = this->FindFirer(Weapon); + + if (index >= 0) + return this->OnlyAttackData[index].Attacker == Attacker; + + return true; +} + +int TechnoExt::ExtData::FindFirer(WeaponTypeClass* const Weapon) const +{ + const auto& AttackerDatas = this->OnlyAttackData; + if (!AttackerDatas.empty()) + { + for (int index = 0; index < int(AttackerDatas.size()); index++) + { + const auto pWeapon = AttackerDatas[index].Weapon; + + if (pWeapon == Weapon && AttackerDatas[index].Attacker) + return index; + } + } + + return -1; +} diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 7521101d2f..53b07f4110 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -731,6 +731,28 @@ bool TechnoExt::IsHealthInThreshold(TechnoClass* pObject, double min, double max return hp <= max && hp >= min; } +// ============================= +// Other + +template +bool TechnoExt::ExtData::OnlyAttackStruct::Serialize(T& Stm) +{ + return Stm + .Process(this->Weapon) + .Process(this->Attacker) + .Success(); +} + +bool TechnoExt::ExtData::OnlyAttackStruct::Load(PhobosStreamReader& Stm, bool RegisterForChange) +{ + return Serialize(Stm); +} + +bool TechnoExt::ExtData::OnlyAttackStruct::Save(PhobosStreamWriter& Stm) const +{ + return const_cast(this)->Serialize(Stm); +} + // ============================= // load / save @@ -793,12 +815,28 @@ void TechnoExt::ExtData::Serialize(T& Stm) .Process(this->TintIntensityAllies) .Process(this->TintIntensityEnemies) .Process(this->AttackMoveFollowerTempCount) + .Process(this->OnlyAttackData) ; } void TechnoExt::ExtData::InvalidatePointer(void* ptr, bool bRemoved) { AnnounceInvalidPointer(this->AirstrikeTargetingMe, ptr); + + if (ptr && bRemoved) + { + auto& AttackerDatas = this->OnlyAttackData; + if (!AttackerDatas.empty()) + { + for (int index = int(AttackerDatas.size()) - 1; index >= 0; --index) + { + if (AttackerDatas[index].Attacker != ptr) + continue; + + AttackerDatas.erase(AttackerDatas.begin() + index); + } + } + } } void TechnoExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 6493623e47..c29ae78e10 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -89,6 +89,20 @@ class TechnoExt int AttackMoveFollowerTempCount; + struct OnlyAttackStruct + { + WeaponTypeClass* Weapon { nullptr }; + TechnoClass* Attacker { nullptr }; + + bool Load(PhobosStreamReader& Stm, bool RegisterForChange); + bool Save(PhobosStreamWriter& Stm) const; + + private: + template + bool Serialize(T& Stm); + }; + std::vector OnlyAttackData; + ExtData(TechnoClass* OwnerObject) : Extension(OwnerObject) , TypeExtData { nullptr } , Shield {} @@ -146,6 +160,7 @@ class TechnoExt , TintIntensityAllies { 0 } , TintIntensityEnemies { 0 } , AttackMoveFollowerTempCount { 0 } + , OnlyAttackData {} { } void OnEarlyUpdate(); @@ -184,6 +199,10 @@ class TechnoExt void ResetDelayedFireTimer(); void UpdateTintValues(); + void AddFirer(WeaponTypeClass* const Weapon, TechnoClass* const Attacker); + bool ContainFirer(WeaponTypeClass* const Weapon, TechnoClass* const Attacker) const; + int FindFirer(WeaponTypeClass* const Weapon) const; + virtual ~ExtData() override; virtual void InvalidatePointer(void* ptr, bool bRemoved) override; virtual void LoadFromStream(PhobosStreamReader& Stm) override; @@ -206,6 +225,10 @@ class TechnoExt switch (abs) { + case AbstractType::Unit: + case AbstractType::Aircraft: + case AbstractType::Building: + case AbstractType::Infantry: case AbstractType::Airstrike: return false; default: diff --git a/src/Ext/Techno/Hooks.Firing.cpp b/src/Ext/Techno/Hooks.Firing.cpp index 1799af3112..3728872357 100644 --- a/src/Ext/Techno/Hooks.Firing.cpp +++ b/src/Ext/Techno/Hooks.Firing.cpp @@ -310,6 +310,11 @@ DEFINE_HOOK(0x6FC339, TechnoClass_CanFire, 0x6) if (pTargetTechno) { + const auto pTargetExt = TechnoExt::ExtMap.Find(pTargetTechno); + + if (pWeaponExt->OnlyAttacker.Get() && !pTargetExt->ContainFirer(pWeapon, pThis)) + return CannotFire; + if (pThis->Berzerk && !EnumFunctions::CanTargetHouse(RulesExt::Global()->BerzerkTargeting, pThis->Owner, pTargetTechno->Owner)) { @@ -332,7 +337,7 @@ DEFINE_HOOK(0x6FC339, TechnoClass_CanFire, 0x6) if (!EnumFunctions::IsTechnoEligible(pTargetTechno, pWHExt->AirstrikeTargets)) return CannotFire; - if (!TechnoExt::ExtMap.Find(pTargetTechno)->TypeExtData->AllowAirstrike.Get(pTargetTechno->AbstractFlags & AbstractFlags::Foot ? true : static_cast(pTargetTechno)->Type->CanC4)) + if (!pTargetExt->TypeExtData->AllowAirstrike.Get(pTargetTechno->AbstractFlags & AbstractFlags::Foot ? true : static_cast(pTargetTechno)->Type->CanC4)) return CannotFire; } } @@ -537,6 +542,24 @@ DEFINE_HOOK(0x6FDDC0, TechnoClass_FireAt_BeforeTruelyFire, 0x6) return 0; } +DEFINE_HOOK(0x6FDE0E, TechnoClass_FireAt_OnlyAttacker, 0x6) +{ + GET(TechnoClass* const, pThis, ESI); + GET(WeaponTypeClass* const, pWeapon, EBX); + GET_BASE(AbstractClass* const, pTarget, 0x8); + + const auto pWeaponExt = WeaponTypeExt::ExtMap.Find(pWeapon); + + if (pWeaponExt->OnlyAttacker.Get() && pTarget == pThis->Target + && pTarget->AbstractFlags & AbstractFlags::Techno) + { + const auto pTargetExt = TechnoExt::ExtMap.Find(static_cast(pTarget)); + pTargetExt->AddFirer(pWeapon, pThis); + } + + return 0; +} + DEFINE_HOOK(0x6FE43B, TechnoClass_FireAt_OpenToppedDmgMult, 0x8) { enum { ApplyDamageMult = 0x6FE45A, ContinueCheck = 0x6FE460 }; diff --git a/src/Ext/WeaponType/Body.cpp b/src/Ext/WeaponType/Body.cpp index 78cbed50c2..2ca1be4925 100644 --- a/src/Ext/WeaponType/Body.cpp +++ b/src/Ext/WeaponType/Body.cpp @@ -152,6 +152,8 @@ void WeaponTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->DelayedFire_AnimOffset.Read(exINI, pSection, "DelayedFire.AnimOffset"); this->DelayedFire_AnimOnTurret.Read(exINI, pSection, "DelayedFire.AnimOnTurret"); + this->OnlyAttacker.Read(exINI, pSection, "OnlyAttacker"); + // handle SkipWeaponPicking if (this->CanTarget != AffectedTarget::All || this->CanTargetHouses != AffectedHouse::All || this->CanTarget_MaxHealth < 1.0 || this->CanTarget_MinHealth > 0.0 @@ -233,6 +235,7 @@ void WeaponTypeExt::ExtData::Serialize(T& Stm) .Process(this->DelayedFire_OnlyOnInitialBurst) .Process(this->DelayedFire_AnimOffset) .Process(this->DelayedFire_AnimOnTurret) + .Process(this->OnlyAttacker) ; }; diff --git a/src/Ext/WeaponType/Body.h b/src/Ext/WeaponType/Body.h index b7aa6e16a3..9d27038b74 100644 --- a/src/Ext/WeaponType/Body.h +++ b/src/Ext/WeaponType/Body.h @@ -92,6 +92,8 @@ class WeaponTypeExt bool SkipWeaponPicking; + Valueable OnlyAttacker; + ExtData(WeaponTypeClass* OwnerObject) : Extension(OwnerObject) , DiskLaser_Radius { DiskLaserClass::Radius } , ProjectileRange { Leptons(100000) } @@ -160,6 +162,8 @@ class WeaponTypeExt , DelayedFire_OnlyOnInitialBurst { false } , DelayedFire_AnimOffset {} , DelayedFire_AnimOnTurret { true } + + , OnlyAttacker { false } { } int GetBurstDelay(int burstIndex) const;