diff --git a/CREDITS.md b/CREDITS.md index ecf82be902..ecaad2d98f 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -692,6 +692,7 @@ This page lists all the individual contributions to the project by their author. - Fix an issue where units recruited by a team with `AreTeamMembersRecruitable=false` cannot be recruited even if they have been liberated by that team - Global default value for `DefaultToGuardArea` - Weapon range finding in cylinder + - Extended auto-targeting - **solar-III (凤九歌)** - Target scanning delay customization (documentation) - Skip target scanning function calling for unarmed technos (documentation) diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 0e2c666d66..c14ed146c7 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -1660,6 +1660,20 @@ RateDown.Cover.Value=0 ; integer RateDown.Cover.AmmoBelow=-2 ; integer ``` +### Extended auto-targeting + +- Now you can activate multiple auto-targeting optimizations by setting `ExtendedAutoTargeting=true`. These include: + - When using stop, guard, or mouse commands, refresh the auto-targeting cooldown. + - When the target becomes invalid, refresh the auto-targeting cooldown (In vanilla, this cooldown will be reduced to less than 10 frames). + - When a unit is on a task that allows auto-targeting but already has a target, it will still auto-target. If a target with a threat level higher than the original target by more than `ExtendedAutoTargeting.SwitchTargetThreshold` is found, switch the target. + +In `rulesmd.ini`: +```ini +[General] +ExtendedAutoTargeting=false ; boolean +ExtendedAutoTargeting.SwitchTargetThreshold=1000 ; integer +``` + ### Firing offsets for specific Burst shots - You can now specify separate firing offsets for each of the shots fired by weapon with `Burst` via using `(Elite)(Prone/Deployed)PrimaryFire|SecondaryFire|WeaponX|FLH.BurstN` keys, depending on which weapons your TechnoType makes use of. *N* in `BurstN` is zero-based burst shot index, and the values are parsed sequentially until no value for either regular or elite weapon is present, with elite weapon defaulting to regular weapon FLH if only it is missing. If no burst-index specific value is available, value from the base key (f.ex `PrimaryFireFLH`) is used. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 975aeed572..a2030e5ed0 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -535,6 +535,7 @@ New: - Option to scale `PowerSurplus` setting if enabled to current power drain with `PowerSurplus.ScaleToDrainAmount` (by Starkku) - Global default value for `DefaultToGuardArea` (by TaranDahl) - [Weapon range finding in cylinder](New-or-Enhanced-Logics.md#range-finding-in-cylinder) (by TaranDahl) +- [Extended auto-targeting](New-or-Enhanced-Logics.md#extended-auto-targeting) (by TaranDahl) 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/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index ca47eeb3f8..363a840a3d 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -1,4 +1,4 @@ -#include "Body.h" +#include "Body.h" #include #include @@ -364,6 +364,9 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->CylinderRangefinding.Read(exINI, GameStrings::General, "CylinderRangefinding"); + this->ExtendedAutoTargeting.Read(exINI, GameStrings::General, "ExtendedAutoTargeting"); + this->ExtendedAutoTargeting_SwitchTargetThreshold.Read(exINI, GameStrings::General, "ExtendedAutoTargeting.SwitchTargetThreshold"); + // Section AITargetTypes int itemsCount = pINI->GetKeyCount("AITargetTypes"); for (int i = 0; i < itemsCount; ++i) @@ -663,6 +666,8 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->AIParadropMission) .Process(this->DefaultToGuardArea) .Process(this->CylinderRangefinding) + .Process(this->ExtendedAutoTargeting) + .Process(this->ExtendedAutoTargeting_SwitchTargetThreshold) ; } diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index 4001f0238b..e141852433 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -311,6 +311,8 @@ class RulesExt Valueable AIParadropMission; Valueable DefaultToGuardArea; + Valueable ExtendedAutoTargeting; + Valueable ExtendedAutoTargeting_SwitchTargetThreshold; Valueable CylinderRangefinding; @@ -570,6 +572,8 @@ class RulesExt , DefaultToGuardArea { false } , CylinderRangefinding { false } + , ExtendedAutoTargeting { false } + , ExtendedAutoTargeting_SwitchTargetThreshold { 1000 } { } virtual ~ExtData() = default; diff --git a/src/Ext/Techno/Hooks.Targeting.cpp b/src/Ext/Techno/Hooks.Targeting.cpp index ff1da00c3d..1ed33114f9 100644 --- a/src/Ext/Techno/Hooks.Targeting.cpp +++ b/src/Ext/Techno/Hooks.Targeting.cpp @@ -69,3 +69,262 @@ DEFINE_HOOK(0x6F7CE2, TechnoClass_CanAutoTargetObject_IronCurtain, 0x6) return 0; } + +#pragma region ExtendedAutoTargeting + +namespace ExtendedAutoTargetingContext +{ + AbstractClass* OldTarget = nullptr; + int OldThreat = -1; + + AbstractClass* NewTarget = nullptr; + int NewThreat = -1; + + void Clear() + { + OldTarget = nullptr; + OldThreat = -1; + NewTarget = nullptr; + NewThreat = -1; + } +} + +// Record old target +DEFINE_HOOK(0x6FA6C9, TechnoClass_Update_RecordOldTarget, 0x6) +{ + if (!RulesExt::Global()->ExtendedAutoTargeting) + return 0; + + GET(TechnoClass*, pThis, ESI); + + auto pOldTarget = pThis->Target; + + if (pOldTarget && (pOldTarget->AbstractFlags & AbstractFlags::Techno) != AbstractFlags::None && pThis->IsCloseEnoughToAttack(pOldTarget) && pThis->ShouldLoseTargetNow) // OpportunityFire, in-range only + { + auto crd = pThis->GetCoords(); + ExtendedAutoTargetingContext::OldTarget = pOldTarget; + ExtendedAutoTargetingContext::OldThreat = (int)pThis->ThreatCoeffients(static_cast(pOldTarget), &crd); + pThis->Target = 0; + } + + return 0; +} + +DEFINE_HOOK(0x4DF3A0, TechnoClass_UpdateAttackMove_RecordOldTarget1, 0x6) +{ + if (!RulesExt::Global()->ExtendedAutoTargeting) + return 0; + + GET(FootClass*, pThis, ECX); + + auto pOldTarget = pThis->Target; + + if (pOldTarget && (pOldTarget->AbstractFlags & AbstractFlags::Techno) != AbstractFlags::None && pThis->InAuxiliarySearchRange(pOldTarget)) // AttackMove has its own range checking + { + auto crd = pThis->GetCoords(); + ExtendedAutoTargetingContext::OldTarget = pOldTarget; + ExtendedAutoTargetingContext::OldThreat = (int)pThis->ThreatCoeffients(static_cast(pOldTarget), &crd); + pThis->Target = 0; + pThis->HaveAttackMoveTarget = false; + } + + return 0; +} + +DEFINE_HOOK(0x4DF42A, TechnoClass_UpdateAttackMove_SkipCheck, 0x6) +{ + if (!RulesExt::Global()->ExtendedAutoTargeting) + return 0; + + enum { SkipCheck = 0x4DF462, FuncEnd = 0x4DF4AB }; + + GET(FootClass*, pThis, ESI); + + return pThis->MegaTarget ? SkipCheck : FuncEnd; +} + +DEFINE_HOOK(0x4D6ED1, TechnoClass_MissionAreaGuard_RecordOldTarget, 0x6) +{ + if (!RulesExt::Global()->ExtendedAutoTargeting) + return 0; + + GET(TechnoClass*, pThis, ESI); + + auto pOldTarget = pThis->Target; + + if (pOldTarget && (pOldTarget->AbstractFlags & AbstractFlags::Techno) != AbstractFlags::None && pThis->CanPassiveAcquireTargets() && pThis->TargetingTimerFinished() && pThis->DistanceFrom(pOldTarget) <= pThis->GetGuardRange(1)) // AreaGuard, use GetGuardRange + { + auto crd = pThis->ArchiveTarget->GetCoords(); + ExtendedAutoTargetingContext::OldTarget = pOldTarget; + ExtendedAutoTargetingContext::OldThreat = (int)pThis->ThreatCoeffients(static_cast(pOldTarget), &crd); + pThis->Target = 0; + } + + return 0; +} + +// Use old target when targeting +DEFINE_HOOK(0x6F8E1F, TechnoClass_SelectAutoTarget_UseContext, 0x6) +{ + if (!RulesExt::Global()->ExtendedAutoTargeting) + return 0; + + R->Stack(STACK_OFFSET(0x6C, -0x4C), ExtendedAutoTargetingContext::OldTarget); + R->Stack(STACK_OFFSET(0x6C, -0x50), ExtendedAutoTargetingContext::OldThreat); + return 0; +} + +// Record new target +DEFINE_HOOK(0x6F936F, TechnoClass_SelectAutoTarget_RecordNew1, 0x8) +{ + if (!RulesExt::Global()->ExtendedAutoTargeting) + return 0; + + GET(AbstractClass*, pBestTarget, EBP); + GET(int, bestThreat, EAX); + + if (ExtendedAutoTargetingContext::OldTarget) + { + ExtendedAutoTargetingContext::NewTarget = pBestTarget; + ExtendedAutoTargetingContext::NewThreat = bestThreat; + } + + return 0; +} + +DEFINE_HOOK(0x6F955B, TechnoClass_SelectAutoTarget_RecordNew2, 0x8) +{ + if (!RulesExt::Global()->ExtendedAutoTargeting) + return 0; + + GET(AbstractClass*, pBestTarget, EDX); + GET(int, bestThreat, EAX); + + if (ExtendedAutoTargetingContext::OldTarget) + { + ExtendedAutoTargetingContext::NewTarget = pBestTarget; + ExtendedAutoTargetingContext::NewThreat = bestThreat; + } + + return 0; +} + +// Check if new target has enough threat +DEFINE_HOOK(0x709938, TechnoClass_TargetAndEstimateDamage_CheckContext, 0x6) +{ + if (!RulesExt::Global()->ExtendedAutoTargeting) + return 0; + + GET(TechnoClass*, pThis, ESI); + + if (ExtendedAutoTargetingContext::OldTarget && ExtendedAutoTargetingContext::NewTarget && ExtendedAutoTargetingContext::OldTarget != ExtendedAutoTargetingContext::NewTarget) + { + auto crd = pThis->GetCoords(); + //if (ExtendedAutoTargetingContext::NewThreat < ExtendedAutoTargetingContext::OldThreat + RulesExt::Global()->ExtendedAutoTargeting_SwitchTargetThreshold) + if (pThis->ThreatCoeffients(static_cast(ExtendedAutoTargetingContext::NewTarget), &crd) < ExtendedAutoTargetingContext::OldThreat + RulesExt::Global()->ExtendedAutoTargeting_SwitchTargetThreshold) + R->EAX(ExtendedAutoTargetingContext::OldTarget); + } + + return 0; +} + +// Reset context +DEFINE_HOOK_AGAIN(0x6FA6F5, ExtendedAutoTargetingContext_Clear, 0x5); // Update +DEFINE_HOOK_AGAIN(0x4DF4AB, ExtendedAutoTargetingContext_Clear, 0x5); // UpdateAttackMove +DEFINE_HOOK(0x4DF41E, ExtendedAutoTargetingContext_Clear, 0x7) // UpdateAttackMove +{ + ExtendedAutoTargetingContext::Clear(); + return 0; +} + +DEFINE_HOOK(0x6FA6E6, TechnoClass_Update_SetIsOpportunityTarget, 0x6) +{ + if (!RulesExt::Global()->ExtendedAutoTargeting) + return 0; + + GET(TechnoClass*, pThis, ESI); + + // This field indicate that the target is acquired by OpportunityFire, thus should be cleared if out of range. + // Must set it manually because the SetTarget in TargetAndEstimateDamage will reset it. + if (pThis->Target == ExtendedAutoTargetingContext::OldTarget) + pThis->ShouldLoseTargetNow = true; + + return 0; +} + +DEFINE_HOOK(0x4D6F0C, FootClass_MissionAreaGuard_AfterTargeting, 0x6) // AreaGuard +{ + GET(FootClass*, pThis, ESI); + + if (pThis->Target) + pThis->vt_entry_53C(0); + + ExtendedAutoTargetingContext::Clear(); + return 0; +} + +// Stop command +DEFINE_HOOK(0x4C757D, EventClass_RespondToEvent_IDLE_ClearTargetingTimer, 0x6) +{ + if (!RulesExt::Global()->ExtendedAutoTargeting) + return 0; + + GET(TechnoClass*, pThis, ESI); + pThis->TargetingTimer.Start(0); + return 0; +} + +// Clicked mission +DEFINE_HOOK(0x4C72E8, EventClass_RespondToEvent_MegaMission_ClearTargetingTimer, 0x6) +{ + if (!RulesExt::Global()->ExtendedAutoTargeting) + return 0; + + GET(TechnoClass*, pThis, EDI); + pThis->TargetingTimer.Start(0); + return 0; +} + +// Target expired. +DEFINE_HOOK(0x7079D1, TechnoClass_PointerExpired_ClearTargetingTimer, 0x6) +{ + if (!RulesExt::Global()->ExtendedAutoTargeting) + return 0; + + GET(TechnoClass*, pThis, ESI); + pThis->TargetingTimer.Start(0); + + if (pThis->MegaMissionIsAttackMove()) + pThis->UpdateTimer.Start(0); + + return 0; +} + +// ContinueMegaMission +DEFINE_HOOK(0x4DF320, FootClass_ContinueMegaMission_Start, 0x6) +{ + if (!RulesExt::Global()->ExtendedAutoTargeting) + return 0; + + enum { RETN = 0x4DF395 }; + + GET(FootClass*, pThis, ECX); + + if (!pThis->Target) + { + pThis->HaveAttackMoveTarget = 0; + pThis->HaveAttackMoveTarget = pThis->TargetAndEstimateDamage(pThis->Location, ThreatType::Range); + return pThis->HaveAttackMoveTarget ? RETN : 0; + } + + return 0; +} + +// This is the bug fixed by #2078. +// The new feature of auto-targeting when having a target will not take effect unless this fix is enabled. +DEFINE_HOOK(0x6F9AF4, TEST, 0x6) +{ + return 0x6F9B1B; +} + +#pragma endregion