diff --git a/CREDITS.md b/CREDITS.md index ba87b71b0d..483c4b7773 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -411,6 +411,7 @@ This page lists all the individual contributions to the project by their author. - Vehicle Deployment Enhancement - Fix an issue where miners affected by `Passengers/DeployFire` were unable to unload minerals - Fix an issue where mining vehicles could not move after leaving a tank bunker + - Separation of AutoTarget for `DeployFireWeapon`, `OpenTransportWeapon`, and `NoAmmoWeapon` - **NetsuNegi**: - Forbidding parallel AI queues by type - Jumpjet crash speed fix when crashing onto building diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md index e3c3e714b9..6ecd96da9a 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -1799,6 +1799,17 @@ In `rulesmd.ini`: RadarInvisibleToHouse= ; Affected House Enumeration (none|owner/self|allies/ally|team|enemies/enemy|all), default to enemy if RadarInvisible=true, none otherwise ``` +### Separation AutoTarget + +- In vanilla, when a unit's `TurretCount` is not greater than 0 and `IsGattling=yes` is not set, it calculates the sum of all weapons' `Damage` and all weapons' `AmbientDamage`, then divides by the number of weapons (rounding towards zero). If the result is less than 0, the unit is considered to use a repair weapon; otherwise, it is an offensive weapon. This may cause units that have both repair and damage weapons to be unable to properly use `DeployFireWeapon`, `OpenTransportWeapon`, and `NoAmmoWeapon`. Now this issue can be resolved by the newly added `SeparateWeaponTypes` list. + - When using separation auto target, if `GuardRange` is not set, the effective range will use the current weapon's own `Range`, rather than the maximum `Range` among all the unit's weapons. Functions such as whether it can attack ground or air targets, and whether it uses repair behavior, which were originally based on average calculations, are now entirely determined by the current weapon's own settings. + +In `rulesmd.ini`: +```ini +[SOMETECHNO] ; TechnoType +SeparateWeaponTypes=none ; List of SeparateWeaponType Enumeration (none|deployfireweapon|opentransport|noammo|all) +``` + ### Subterranean unit travel height and speed - It is now possible to control the height at which units with subterranean (Tunnel) `Locomotor` travel, globally or per TechnoType. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 2f983f2f3c..2084e7e2a5 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -539,6 +539,7 @@ New: - [Penetrates damage on transporter](New-or-Enhanced-Logics.md#penetrates-damage-on-transporter) (by NetsuNegi) - Added amount limit of `LimboKill` (by NetsuNegi) - [Customizations for techno type target scan/guard range](Fixed-or-Improved-Logics.md#target-scan-guard-range-customizations) (by Starkku) +- [Separation of AutoTarget for `DeployFireWeapon`, `OpenTransportWeapon`, and `NoAmmoWeapon`](Fixed-or-Improved-Logics.md#separation-autotarget) (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/Infantry/Hooks.cpp b/src/Ext/Infantry/Hooks.cpp index 668855d8c3..fe654e9642 100644 --- a/src/Ext/Infantry/Hooks.cpp +++ b/src/Ext/Infantry/Hooks.cpp @@ -84,6 +84,16 @@ DEFINE_HOOK(0x51EE6B, InfantryClass_WhatAction_ObjectClass_InfiltrateForceAttack return WhatActionObjectTemp::Fire ? 0x51F05E : 0; } +// Setting Ares' NoSelfGuardArea to yes will also disable this feature. I'm not sure if it should be removed. +DEFINE_HOOK(0x51E748, InfantryClass_WhatAction_ObjectClass_SkipGuardArea, 0x8) +{ + GET(InfantryClass* const, pThis, EDI); + GET(const Action, action, EBP); + enum { SkipGameCode = 0x51E7A6 }; + + return (action == Action::Self_Deploy || pThis->IsDeployed()) ? SkipGameCode : 0; +} + DEFINE_HOOK(0x51ECC0, InfantryClass_WhatAction_ObjectClass_IsAreaFire, 0xA) { enum { IsAreaFire = 0x51ECE5, NotAreaFire = 0x51ECEC }; diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 2eba7f26b3..a064471b42 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -452,33 +452,27 @@ void TechnoTypeExt::ExtData::UpdateAdditionalAttributes() if (!pWeapon) return; + const int combatDamage = (pWeapon->Damage + pWeapon->AmbientDamage); + const ThreatType threats = pWeapon->Projectile ? pWeapon->AllowedThreats() : ThreatType::Normal; + const bool attackFriendlies = WeaponTypeExt::ExtMap.Find(pWeapon)->AttackFriendlies.Get(false); + if (isElite) { - if (pWeapon->Projectile) - this->ThreatTypes.Y |= pWeapon->AllowedThreats(); - - this->CombatDamages.Y += (pWeapon->Damage + pWeapon->AmbientDamage); + this->ThreatTypes.Y |= threats; + this->CombatDamages.Y += combatDamage; eliteNum++; - if (!this->AttackFriendlies.Y - && WeaponTypeExt::ExtMap.Find(pWeapon)->AttackFriendlies.Get(false)) - { + if (!this->AttackFriendlies.Y && attackFriendlies) this->AttackFriendlies.Y = true; - } } else { - if (pWeapon->Projectile) - this->ThreatTypes.X |= pWeapon->AllowedThreats(); - - this->CombatDamages.X += (pWeapon->Damage + pWeapon->AmbientDamage); + this->ThreatTypes.X |= threats; + this->CombatDamages.X += combatDamage; num++; - if (!this->AttackFriendlies.X - && WeaponTypeExt::ExtMap.Find(pWeapon)->AttackFriendlies.Get(false)) - { + if (!this->AttackFriendlies.X && attackFriendlies) this->AttackFriendlies.X = true; - } } }; @@ -1157,6 +1151,8 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->PenetratesTransport_FatalRateMultiplier.Read(exINI, pSection, "PenetratesTransport.FatalRateMultiplier"); this->PenetratesTransport_DamageMultiplier.Read(exINI, pSection, "PenetratesTransport.DamageMultiplier"); + this->SeparateWeaponTypes.Read(exINI, pSection, "SeparateWeaponTypes"); + // Ares 0.2 this->RadarJamRadius.Read(exINI, pSection, "RadarJamRadius"); @@ -1865,6 +1861,8 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm) .Process(this->PenetratesTransport_PassThroughMultiplier) .Process(this->PenetratesTransport_FatalRateMultiplier) .Process(this->PenetratesTransport_DamageMultiplier) + + .Process(this->SeparateWeaponTypes) ; } void TechnoTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index 04cfaabc66..e83d136d97 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -482,6 +482,8 @@ class TechnoTypeExt Valueable PenetratesTransport_FatalRateMultiplier; Valueable PenetratesTransport_DamageMultiplier; + Valueable SeparateWeaponTypes; + ExtData(TechnoTypeClass* OwnerObject) : Extension(OwnerObject) , HealthBar_Hide { false } , HealthBar_HidePips { false } @@ -917,6 +919,8 @@ class TechnoTypeExt , PenetratesTransport_PassThroughMultiplier { 1.0 } , PenetratesTransport_FatalRateMultiplier { 1.0 } , PenetratesTransport_DamageMultiplier { 1.0 } + + , SeparateWeaponTypes { SeparateWeaponType::None } { } virtual ~ExtData() = default; diff --git a/src/Ext/TechnoType/Hooks.MultiWeapon.cpp b/src/Ext/TechnoType/Hooks.MultiWeapon.cpp index 6fcda76ce2..4a14a00717 100644 --- a/src/Ext/TechnoType/Hooks.MultiWeapon.cpp +++ b/src/Ext/TechnoType/Hooks.MultiWeapon.cpp @@ -143,6 +143,24 @@ DEFINE_HOOK(0x7090A0, TechnoClass_VoiceAttack, 0x7) return 0x7091C7; } +bool __forceinline IsDeployed(TechnoClass* const pThis, const AbstractType rtti) +{ + switch (rtti) + { + case AbstractType::Infantry: + return static_cast(pThis)->IsDeployed(); + case AbstractType::Unit: + { + auto const pUnit = static_cast(pThis); + + return pUnit->Deployed || (pUnit->Type->DeployFire && + pThis->CurrentMission == Mission::Unload); + } + default: + return false; + } +} + static __forceinline ThreatType GetThreatType(TechnoClass* pThis, TechnoTypeExt::ExtData* pTypeExt, ThreatType result) { const ThreatType flags = pThis->Veterancy.IsElite() ? pTypeExt->ThreatTypes.Y : pTypeExt->ThreatTypes.X; @@ -157,9 +175,9 @@ DEFINE_HOOK(0x7431C9, FootClass_SelectAutoTarget_MultiWeapon, 0x7) // UnitClas GET(FootClass*, pThis, ESI); GET(const ThreatType, result, EDI); - const bool isUnit = R->Origin() == 0x7431C9; const auto pTypeExt = TechnoExt::ExtMap.Find(pThis)->TypeExtData; const auto pType = pTypeExt->OwnerObject(); + const bool isUnit = R->Origin() == 0x7431C9; if (isUnit && !pType->IsGattling && pType->TurretCount > 0 @@ -168,6 +186,57 @@ DEFINE_HOOK(0x7431C9, FootClass_SelectAutoTarget_MultiWeapon, 0x7) // UnitClas return UnitGunner; } + const SeparateWeaponType weaponTypes = pTypeExt->SeparateWeaponTypes; + + if (weaponTypes & SeparateWeaponType::DeployFire) + { + const AbstractType rtti = isUnit ? AbstractType::Unit : AbstractType::Infantry; + const int deployFireWeapon = pType->DeployFireWeapon; + + if (IsDeployed(pThis, rtti) && pType->DeployFire && deployFireWeapon >= 0) + { + ThreatType flags = result; + + if (const auto pWeapon = pThis->GetWeapon(deployFireWeapon)->WeaponType) + flags |= pWeapon->AllowedThreats(); + + R->EDI(flags); + return isUnit ? UnitReturn : InfantryReturn; + } + } + + if (weaponTypes & SeparateWeaponType::OpenTransport) + { + const int openTransportWeapon = pType->OpenTransportWeapon; + + if (pThis->InOpenToppedTransport && openTransportWeapon >= 0) + { + ThreatType flags = result; + + if (const auto pWeapon = pThis->GetWeapon(openTransportWeapon)->WeaponType) + flags |= pWeapon->AllowedThreats(); + + R->EDI(flags); + return isUnit ? UnitReturn : InfantryReturn; + } + } + + if (weaponTypes & SeparateWeaponType::NoAmmo) + { + const int noAmmoWeapon = pTypeExt->NoAmmoWeapon; + + if (pType->Ammo >= 0 && noAmmoWeapon >= 0 && pThis->Ammo <= pTypeExt->NoAmmoAmount) + { + ThreatType flags = result; + + if (const auto pWeapon = pThis->GetWeapon(noAmmoWeapon)->WeaponType) + flags |= pWeapon->AllowedThreats(); + + R->EDI(flags); + return isUnit ? UnitReturn : InfantryReturn; + } + } + R->EDI(GetThreatType(pThis, pTypeExt, result)); return isUnit ? UnitReturn : InfantryReturn; } @@ -185,7 +254,26 @@ DEFINE_HOOK(0x445F04, BuildingClass_SelectAutoTarget_MultiWeapon, 0xA) return Continue; } - R->EDI(GetThreatType(pThis, TechnoTypeExt::ExtMap.Find(pThis->Type), result)); + const auto pType = pThis->Type; + const auto pTypeExt = TechnoTypeExt::ExtMap.Find(pType); + + if (pTypeExt->SeparateWeaponTypes & SeparateWeaponType::NoAmmo) + { + const int noAmmoWeapon = pTypeExt->NoAmmoWeapon; + + if (pType->Ammo >= 0 && noAmmoWeapon >= 0 && pThis->Ammo <= pTypeExt->NoAmmoAmount) + { + ThreatType flags = result; + + if (const auto pWeapon = pThis->GetWeapon(noAmmoWeapon)->WeaponType) + flags |= pWeapon->AllowedThreats(); + + R->EDI(flags); + return ReturnThreatType; + } + } + + R->EDI(GetThreatType(pThis, pTypeExt, result)); return ReturnThreatType; } @@ -215,7 +303,56 @@ DEFINE_HOOK(0x6F398E, TechnoClass_CombatDamage_MultiWeapon, 0x7) return GunnerDamage; } + const SeparateWeaponType weaponTypes = pTypeExt->SeparateWeaponTypes; + + if (weaponTypes & SeparateWeaponType::DeployFire) + { + const int deployFireWeapon = pType->DeployFireWeapon; + + if (IsDeployed(pThis, rtti) && pType->DeployFire && deployFireWeapon >= 0) + { + int damage = 0; + + if (auto const pWeapon = pThis->GetWeapon(deployFireWeapon)->WeaponType) + damage = (pWeapon->Damage + pWeapon->AmbientDamage); + + R->EAX(damage); + return ReturnDamage; + } + } + + if (weaponTypes & SeparateWeaponType::OpenTransport) + { + const int openTransportWeapon = pType->OpenTransportWeapon; + + if (pThis->InOpenToppedTransport && openTransportWeapon >= 0) + { + int damage = 0; + + if (auto const pWeapon = pThis->GetWeapon(openTransportWeapon)->WeaponType) + damage = (pWeapon->Damage + pWeapon->AmbientDamage); + + R->EAX(damage); + return ReturnDamage; + } + } + + if (weaponTypes & SeparateWeaponType::NoAmmo) + { + const int noAmmoWeapon = pTypeExt->NoAmmoWeapon; + + if (pType->Ammo >= 0 && noAmmoWeapon >= 0 && pThis->Ammo <= pTypeExt->NoAmmoAmount) + { + int damage = 0; + + if (auto const pWeapon = pThis->GetWeapon(noAmmoWeapon)->WeaponType) + damage = (pWeapon->Damage + pWeapon->AmbientDamage); + + R->EAX(damage); + return ReturnDamage; + } + } + R->EAX(pThis->Veterancy.IsElite() ? pTypeExt->CombatDamages.Y : pTypeExt->CombatDamages.X); return ReturnDamage; } - diff --git a/src/Utilities/Enum.h b/src/Utilities/Enum.h index 2109fdf943..8c89bf3a7f 100644 --- a/src/Utilities/Enum.h +++ b/src/Utilities/Enum.h @@ -394,3 +394,15 @@ class MouseCursorHotSpotY return false; } }; + +enum class SeparateWeaponType : unsigned char +{ + None = 0x0, + DeployFire = 0x1, + OpenTransport = 0x2, + NoAmmo = 0x4, + + All = DeployFire | OpenTransport | NoAmmo +}; + +MAKE_ENUM_FLAGS(SeparateWeaponType); diff --git a/src/Utilities/TemplateDef.h b/src/Utilities/TemplateDef.h index fa93406465..fbfdabfc8b 100644 --- a/src/Utilities/TemplateDef.h +++ b/src/Utilities/TemplateDef.h @@ -1508,6 +1508,50 @@ if(_strcmpi(parser.value(), #name) == 0){ value = __uuidof(name ## LocomotionCla Debug::INIParseFailed(pSection, pKey, pCur); } } + + template <> + inline bool read(SeparateWeaponType& value, INI_EX& parser, const char* pSection, const char* pKey) + { + if (parser.ReadString(pSection, pKey)) + { + static const std::pair Names[] = + { + {"deployfire", SeparateWeaponType::DeployFire}, + {"opentransport", SeparateWeaponType::OpenTransport}, + {"noammo", SeparateWeaponType::NoAmmo}, + {"all", SeparateWeaponType::All}, + { "none", SeparateWeaponType::None }, + }; + + auto parsed = SeparateWeaponType::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 separate weapon type"); + return false; + } + } + + value = parsed; + return true; + } + + return false; + } }