diff --git a/CREDITS.md b/CREDITS.md index 99727ed0e0..65edd27099 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -419,6 +419,7 @@ This page lists all the individual contributions to the project by their author. - Fixed an issue where parachute units would die upon landing if bridges were destroyed during their descent - Custom hover vehicles shutdown drowning death - SHP turret vehicles support the use of `*tur.shp` files + - 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 5b7f63a4eb..5e81a7bd6f 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -1872,6 +1872,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|deployfire|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 23bf39f7e2..0ad5e3170d 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -562,6 +562,7 @@ New: - [Allow customize jumpjet properties on warhead](Fixed-or-Improved-Logics.md#customizing-locomotor-warhead) (by NetsuNegi) - Customize effects range of power plant enhancer (by NetsuNegi) - Allow each side to customize the color when the proportion of working miners is higher than `HarvesterCounter.ConditionYellow` (by Noble_Fish) +- [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 a76000114e..39a6dbce02 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -1,4 +1,4 @@ -#include "Body.h" +#include "Body.h" #include @@ -448,33 +448,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; - } } }; @@ -1192,6 +1186,8 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) || ExtraThreatCoefficient_Facing.Get(RulesExt::Global()->ExtraThreatCoefficient_Facing) != 0 || ExtraThreatCoefficient_DistanceToLastTarget.Get(RulesExt::Global()->ExtraThreatCoefficient_DistanceToLastTarget) != 0; + this->SeparateWeaponTypes.Read(exINI, pSection, "SeparateWeaponTypes"); + // Ares 0.2 this->RadarJamRadius.Read(exINI, pSection, "RadarJamRadius"); @@ -1927,6 +1923,8 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm) .Process(this->ExtraThreatCoefficient_InRangeDistance) .Process(this->ExtraThreatCoefficient_Facing) .Process(this->ExtraThreatCoefficient_DistanceToLastTarget) + + .Process(this->SeparateWeaponTypes) ; } void TechnoTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index 5d30e8c562..e0cf9d861a 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include #include @@ -505,6 +505,8 @@ class TechnoTypeExt SHPStruct* TurretShape; + Valueable SeparateWeaponTypes; + ExtData(TechnoTypeClass* OwnerObject) : Extension(OwnerObject) , HealthBar_Hide { false } , HealthBar_HidePips { false } @@ -962,6 +964,8 @@ class TechnoTypeExt , ExtraThreatCoefficient_InRangeDistance {} , ExtraThreatCoefficient_Facing {} , ExtraThreatCoefficient_DistanceToLastTarget {} + + , 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 856355209f..89a1e05621 100644 --- a/src/Utilities/TemplateDef.h +++ b/src/Utilities/TemplateDef.h @@ -1509,6 +1509,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; + } }