Skip to content
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 @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions docs/New-or-Enhanced-Logics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions docs/Whats-New.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion src/Ext/Rules/Body.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#include "Body.h"
#include "Body.h"

#include <Ext/TechnoType/Body.h>
#include <New/Type/RadTypeClass.h>
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
;
}

Expand Down
4 changes: 4 additions & 0 deletions src/Ext/Rules/Body.h
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,8 @@ class RulesExt
Valueable<Mission> AIParadropMission;

Valueable<bool> DefaultToGuardArea;
Valueable<bool> ExtendedAutoTargeting;
Valueable<int> ExtendedAutoTargeting_SwitchTargetThreshold;

Valueable<bool> CylinderRangefinding;

Expand Down Expand Up @@ -570,6 +572,8 @@ class RulesExt
, DefaultToGuardArea { false }

, CylinderRangefinding { false }
, ExtendedAutoTargeting { false }
, ExtendedAutoTargeting_SwitchTargetThreshold { 1000 }
{ }

virtual ~ExtData() = default;
Expand Down
244 changes: 244 additions & 0 deletions src/Ext/Techno/Hooks.Targeting.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,247 @@ 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)) // OpportunityFire, in-range only
{
auto crd = pThis->GetCoords();
ExtendedAutoTargetingContext::OldTarget = pOldTarget;
ExtendedAutoTargetingContext::OldThreat = (int)pThis->ThreatCoeffients(static_cast<ObjectClass*>(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<ObjectClass*>(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<ObjectClass*>(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<ObjectClass*>(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(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