beyond-all-reason / beyond-all-reason/RecoilEngine
Feature request : More game specific controls over cai->GenerateAttackCmd() and weapon->AutoTarget()
- Dominant language
- C++
- Stars
- 679
- Forks
- 290
- Avg merge
- 3d 2h
- Merged PRs (30d)
- 40
Description
The current game<->engine interface allows to fine tune unit targetting priorities and behaviour through:
-> Defs level weapons[i] tags badtargetcategory, nochasecategory, etc...
-> Defs level weaponDef.proximityPriority
-> Defs level unitDef.power
-> unit-level runtime AllowWeaponTarget(unitID, -1, -1, 0, range) -> return range = Closest Valid Target for an attack command; within returned range (dispatched in BAR's gadgets.lua as UnitAutoTargetRange)
-> unit-weapon-level runtime AllowWeaponTargetCheck() -> return true/false = can we run AutoTarget() at all?
-> unit-weapon-to-single-unit level AllowWeaponTarget(unitID, targetID, weaponNum, weaponDefID, nil) -> return true/false to validated closest valid target for an auto generated Attack command
-> batch of unit-weapon-to-single-unit level AllowWeaponTarget(unitID, targetID, weaponNum, weaponDefID, defPriority) -> return allowed, modPriority, for AutoTarget() cycles.
-> [[Am i missing some other methods ?]]
Every single game-side priority multiplier must be passed through a full cycle of AutoTarget(), which is a combination of:
- GameHelper -> querry for units within range of attacker
- for iteration within that list
-> process defPriority, filter out non attack-able units
-> Call lua AllowWeaponTarget(); if watched weapon, expect a returned value
-> only keep allowed, and keep the returned priority value
- pass the list to weapon[i]
- weapon -> AutoTarget
- for iteration within the passed list
- search for the first good target within list, break out if found
- else fully perform the iteration and keep best bad target in the list
- send result to SetAttackTarget()
This gives the finer control some situations might require, but in large team games where we expect 100 vs 100 units, all within range of each other, this is very much overkill. And this isn't even fired in idling cases: GenerateAttackCmd takes precedence and just focuses on getting the ClosestValidTarget() tested within range + leash
Some other cases are performed in a very much inneffective way: https://github.com/beyond-all-reason/Beyond-All-Reason/blob/master/luarules/gadgets/unit_aa_targeting_priority.lua
This is a defs level priority mult that is currently enforced at runtime, for each unit-weapon-to-single-unit interactions.
In this particular case; a single UnitDefs.selfPriorityMult = 0.1|1|2|100 tag would be sufficient.
And if concerned that it would affect non aa units; a single weaponDef specific setup call on Initialize() for each affected DefID would also be enough. (cf proposed tags and pseudocode infra)
Moreover, we don't currently have the ability to force using the "Find the best target based on priority" without the downside of looking into the whole targetting process for all weapons of the same DefID (SetWatchWeapon).
I would like to propose some modifications/features to provide tools at all levels.
1/ Defs level additional tag(s):
>``defSelfPriorityMult``: a UnitDef tag, ]0; +inf[; defines how much more (or less) priority, a unit of unitDef defs, will be applied when this unit is being tested.
2/ Runtime modifiable values:
>``selfPriorityMult``: lives in CUnit* unit->selfPriorityMult: same definition as the def tag, except this one can be modified at runtime, depending on unit state
>``weaponDefToUnitDefPriorityMults``: lives in CGameHelper; allows games to initialize weapons via gadget via a config file; and set each weapons' affinities for specific unitDefIDs. values ]0; + inf[
>``unitToTargetUnitPriorityMults``: lives in CUnit*; allows to apply another layer of unit-to-unit fine tuning; outside of the AllowWeaponTarget() loop (precompute -> pass values to engine before the AutoTarget() cycle vs the full for (engine <-> lua) callback loop). values [0; +inf[ --> including 0 as a method to filter out specific targetIDs to be considered by attackerID cf 3 early exit
3/ Early exit in the GenerateWeaponTargets() loop: ``if (unitToTargetUnitPriorityMults[unitID][targetID] == 0) continue;`` add the ability to pre-filter out some targets before the cycle even started.
`` targetPriority *= unitDef.defSelfPriorityMult * unit.selfPriorityMult * weaponDefToUnitDefPriorityMults[weaponDefID][unitDefID] * unitToTargetUnitPriorityMults[unitID][targetID] ``
4/ ``Set/Get UnitWatchWeapons/WatchAllowTarget(unitID, bool)`` -> defines at runtime whether we want to watch a specific unit's weapons (rather than a global weaponDef)
5/ ``eventHandler.WeaponAutoTarget(attackerID, attackerWeaponNum, attackerWeaponDefID, targetID|nullptr)`` -> nil (no expected return value)
Signal lua that an AutoTarget() cycle ended, no watch required; additionally pass the picked target as arg, or nil if no target kept.
Fires per-weapon, once per AutoTarget() cycle. Ideally suppressed entirely by 6/ when AutoTarget had no valid target within range to test for, so the cost at runtime is kept minimal outside of unit-unit encounters.
6/ Avoid calling WeaponAutoTarget (and the whole AutoTarget() route) needlessly: GetClosestEnemy() before even trying to AllowWeaponTargetCheck(); if the closest enemy is further than the tested range, don't even try to query units; and also don't signal an empty AutoTarget() process to lua, but i do realize getting closest enemy is actually already a query in itself. This would need benchmarks to figure out if its best to pre-query, units within range and only fire Allow() if the query isn't empty, or if it's better to query after AllowWeaponTargetCheck() which maybe games use to deny AutoTarget().
I suspect this is actually a game decision:
> Watched and mostly denied AllowWeaponTargetCheck() => do not query before, because denied call will not need to query
> Unwatched or Watched and mostly allowed AllowWeaponTargetCheck() => pre-query and cache results for both lua and cpp usage
7/ Optional: ``Spring.QueryUnitTargets`` -> a modified GetUnitsInX function that actually gives the exact same results as GenerateWeaponTarget() query, before it iterated through it and performed any defPriority calculation. Ideally cached; either read from a current frame cached pre-query, or write into cache for the current frame's query.
All these functions/callins/tags live in synced space only
The expected result is the ability to do things like this:
```lua
local weaponDefsConfigs = VFS.Include(configFile) -- called on initialization
for k,v in pairs(WeaponDefs) do
if weaponDefsConfigs[k] then
Spring.SetWeaponDefToUnitDefPriorityMult(k, v.unitDefID, v.mult)
end
end
local function handleStateChange(unitID, newStateName) -- called from various state change events
-- mults could alternatively be cumulative with custom API: GG.Add|RemoveStatePriorityMult(unitID, newStateName), and currentMult[unitID] tracking
local mult = mults[newStateName]
if mult then
Spring.SetSelfPriorityMult(unitID, mult)
end
end
function gadget:UnitCreated(unitID,unitDefID)
if #UnitDefs[unitDefID].weapons > 0 then
Script.SetWatchUnitAllowTarget(unitID, true) --> watch ALL weaponized units
end
end
local function ExcludeTargetsFromList(unitList, conditionFunc)
for i = 1, #unitList do
local targetID = unitList[i]
if conditionFunc(targetID) then
Spring.SetUnitToTargetUnitPriorityMult(unitID, targetID, 0)
else
Spring.SetUnitToTargetUnitPriorityMult(unitID, targetID, 1)
end
end
end
function gadget:AllowWeaponTarget(attackerID, targetID, _, _, defPriority)
if targetID == -1 then
return HandleAllowWeaponTargetRangeQuery(attackerID, defPriority) -- how far do we search for Closest Valid Target?
end
if defPriority then
Spring.Echo("We should never see this, because we stopped watching after the check")
-- unless this is a watched weapon for specific runtime defPriority manipulations
-- in which case, that route remains valid, the passed defPriority is the one that already has been manipulated by the fed values
-- and already stripped out of the excluded units
return true, defPriority
end
--This is a non-priority related call
return ALLOW_AUTOGENERATED_ATTACK_COMMANDS
end
function gadget:AllowWeaponTargetCheck(attackerID, attackerWeaponNum, attackerWeaponDefID)
-- we can change the range boost of the weapon here, before the query, if we want to include targets outside of range but within leash
if specificAttackerStateThatExcludesSomeTargetsOrChangesSomeMults then
local unitList = Spring.QueryUnitTargets(attackerID, attackerWeaponNum)
unitList = ExcludeTargetsFromList(unitList, conditionFunc)
end
Script.SetWatchUnitAllowTarget(attackerID, false) -- stop watching after we passed our mults to engine
return true
end
function gadget:WeaponAutoTarget(attackerID, attackerWeaponNum, attackerWeaponDefID, targetID)
-- fired regardless of wether a target was found, and wether it's a watched attacker or not
Script.SetWatchUnitAllowTarget(attackerID, true) -- restore watch, so we can watch the next AllowWeaponTargetCheck() callin
target[attackerID][weaponNum] = targetID -- store targetID for GameFramePost() handling
end
function gadget:GameFramePost(f)
-- this is where we can handle the movegoals for all units that have been through
-- AllowWeaponTarget(prio) this frame (toUpdate[index] = unitID iteration?)
end
```
This "project" is in a design phase. I would like to gather your opinions on pertinence, feasibility, and ergonomy of such structure.
Contributor guide
Research direction
Start with GameHelper's GenerateAttackCmd()/GenerateWeaponTargets path and weapon AutoTarget(), then compare the existing luarules/gadgets/unit_aa_targeting_priority.lua behavior. The issue names no tests; benchmark the proposed query paths and review the synced Lua call-ins and runtime priority data. Done means a maintainer-approved, bounded design with feasibility and ergonomics decisions.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, lua
- Domain
- api, game-dev
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100