Skip to content

[Minor] Units of the same type do not attack the same target #1807

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CREDITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions docs/New-or-Enhanced-Logics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
1 change: 1 addition & 0 deletions docs/Whats-New.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
60 changes: 59 additions & 1 deletion src/Ext/Techno/Body.Update.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -2044,3 +2057,48 @@ void TechnoExt::ExtData::UpdateTintValues()
calculateTint(Drawing::RGB_To_Int(pShieldType->Tint_Color), static_cast<int>(pShieldType->Tint_Intensity * 1000), pShieldType->Tint_VisibleToHouses);
}
}

void TechnoExt::ExtData::AddFirer(WeaponTypeClass* const Weapon, TechnoClass* const Attacker)
{
if (Attacker->InLimbo)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why shouldn't the limbo Firestarter be recorded?

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;
}
38 changes: 38 additions & 0 deletions src/Ext/Techno/Body.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,28 @@ bool TechnoExt::IsHealthInThreshold(TechnoClass* pObject, double min, double max
return hp <= max && hp >= min;
}

// =============================
// Other

template <typename T>
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<OnlyAttackStruct*>(this)->Serialize(Stm);
}

// =============================
// load / save

Expand Down Expand Up @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions src/Ext/Techno/Body.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <typename T>
bool Serialize(T& Stm);
};
std::vector<OnlyAttackStruct> OnlyAttackData;

ExtData(TechnoClass* OwnerObject) : Extension<TechnoClass>(OwnerObject)
, TypeExtData { nullptr }
, Shield {}
Expand Down Expand Up @@ -146,6 +160,7 @@ class TechnoExt
, TintIntensityAllies { 0 }
, TintIntensityEnemies { 0 }
, AttackMoveFollowerTempCount { 0 }
, OnlyAttackData {}
{ }

void OnEarlyUpdate();
Expand Down Expand Up @@ -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;
Expand All @@ -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:
Expand Down
25 changes: 24 additions & 1 deletion src/Ext/Techno/Hooks.Firing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need .Get() I think?

return CannotFire;

if (pThis->Berzerk
&& !EnumFunctions::CanTargetHouse(RulesExt::Global()->BerzerkTargeting, pThis->Owner, pTargetTechno->Owner))
{
Expand All @@ -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<BuildingClass*>(pTargetTechno)->Type->CanC4))
if (!pTargetExt->TypeExtData->AllowAirstrike.Get(pTargetTechno->AbstractFlags & AbstractFlags::Foot ? true : static_cast<BuildingClass*>(pTargetTechno)->Type->CanC4))
return CannotFire;
}
}
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

&& pTarget->AbstractFlags & AbstractFlags::Techno)
{
const auto pTargetExt = TechnoExt::ExtMap.Find(static_cast<TechnoClass*>(pTarget));
pTargetExt->AddFirer(pWeapon, pThis);
}

return 0;
}

DEFINE_HOOK(0x6FE43B, TechnoClass_FireAt_OpenToppedDmgMult, 0x8)
{
enum { ApplyDamageMult = 0x6FE45A, ContinueCheck = 0x6FE460 };
Expand Down
3 changes: 3 additions & 0 deletions src/Ext/WeaponType/Body.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
;
};

Expand Down
4 changes: 4 additions & 0 deletions src/Ext/WeaponType/Body.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ class WeaponTypeExt

bool SkipWeaponPicking;

Valueable<bool> OnlyAttacker;

ExtData(WeaponTypeClass* OwnerObject) : Extension<WeaponTypeClass>(OwnerObject)
, DiskLaser_Radius { DiskLaserClass::Radius }
, ProjectileRange { Leptons(100000) }
Expand Down Expand Up @@ -160,6 +162,8 @@ class WeaponTypeExt
, DelayedFire_OnlyOnInitialBurst { false }
, DelayedFire_AnimOffset {}
, DelayedFire_AnimOnTurret { true }

, OnlyAttacker { false }
{ }

int GetBurstDelay(int burstIndex) const;
Expand Down
Loading