diff --git a/CREDITS.md b/CREDITS.md index 44c87160fb..6997ad09c0 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -418,6 +418,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 6c2070cb3b..7676714a8a 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -1845,6 +1845,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 3109a5d109..7fb4d39009 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -560,6 +560,7 @@ New: - Allow the [use of more precise calculation of repair costs](Fixed-or-Improved-Logics.md#use-more-precise-calculation-of-repair-costs) (by NetsuNegi) - [Customize default mirage disguises per vehicletypes](New-or-Enhanced-Logics.md#default-mirage-disguise-for-individual-vehicletypes) (by NetsuNegi) - [Allow customize jumpjet properties on warhead](Fixed-or-Improved-Logics.md#customizing-locomotor-warhead) (by NetsuNegi) +- [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 a239526268..7357aac5f0 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -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; - } } }; @@ -1177,6 +1171,8 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->JumpjetClimbIgnoreBuilding.Read(exINI, pSection, "JumpjetClimbIgnoreBuilding"); + this->SeparateWeaponTypes.Read(exINI, pSection, "SeparateWeaponTypes"); + // Ares 0.2 this->RadarJamRadius.Read(exINI, pSection, "RadarJamRadius"); @@ -1903,6 +1899,8 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm) .Process(this->Unsellable) .Process(this->TurretShape) + + .Process(this->SeparateWeaponTypes) ; } void TechnoTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index e0cb824ef8..1dbafbb7b0 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -496,6 +496,8 @@ class TechnoTypeExt SHPStruct* TurretShape; + Valueable SeparateWeaponTypes; + ExtData(TechnoTypeClass* OwnerObject) : Extension(OwnerObject) , HealthBar_Hide { false } , HealthBar_HidePips { false } @@ -944,6 +946,8 @@ class TechnoTypeExt , Unsellable {} , TurretShape { nullptr } + + , 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; + } }