Weapons: estimate the post-aim muzzle position for pre-aim line-of-fire tests - #3327
Draft
eun-ice wants to merge 1 commit into
Draft
Weapons: estimate the post-aim muzzle position for pre-aim line-of-fire tests#3327eun-ice wants to merge 1 commit into
eun-ice wants to merge 1 commit into
Conversation
…re tests
TryTarget(preFire=false), used for target selection and by
SlowUpdate/HoldIfTargetInvalid, traced line of fire from the AimFromWeapon
piece (or, for Cannon/MissileLauncher/StarburstLauncher, the current muzzle),
while the shot itself is tested from the muzzle after the turret has turned.
Where the two disagree a unit either aims and then never fires, or drops a
target it could have hit.
Add an optional unit def weapon tag
aimFromEstimate = {pivotX, pivotY, pivotZ, lateral, forward, barrelForward, barrelUp}
describing the muzzle as a yaw pivot plus offsets turning with yaw only and
with yaw and pitch (AimFromEstimate.h; one sqrt, no trig, linear in the
offsets so it can be fitted offline). With the tag, GetAimFromPos(tgtPos)
returns the predicted muzzle for that target using the direction the aiming
script would be asked for (CalcWantedDir for cannons, trajectoryHeight for
missiles). Without it the AimFromWeapon piece is used as before; the three
weapon classes that overrode GetAimFromPos to the current muzzle now follow
the same rule.
Weapons that alternate between barrels use one estimate for the mean muzzle:
which barrel fires next cannot be predicted from the QueryWeapon piece the
engine holds at aiming time (measured on BAR's Storm, AK and Pawn the relation
differs per script and burst state), so per-barrel entries were dropped.
Also add Spring.GetUnitWeaponAimFromPos(unitID, weaponNum, x, y, z) and fix
Spring.GetUnitWeaponTryTarget(unitID, weaponNum, x, y, z), which built
SWeaponTarget(nullptr, pos) and therefore always tested (0, 0, 0).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
eun-ice
added a commit
to eun-ice/RecoilEngine
that referenced
this pull request
Sep 4, 2026
…fire A mobile unit inside 0.9 * maxRange of its attack target whose weapons all fail the preliminary line-of-fire test used to plain-return (unit target) or stop (ground target) and retry from the same spot every SlowUpdate. With the test now traced from the predicted muzzle (beyond-all-reason#3327) the answer is honest, but a unit parked behind a rock or a crest still never fires (beyond-all-reason#3242). The obvious fix, treating "in range but no solution" like "out of range" and closing in, has a known failure mode: in a group the rear units are blocked by the front units, walk past them, and thereby block the units they passed, so the whole ball creeps forward. The CommandAI cannot tell the two cases apart because the line-of-fire test only returns a bool. The plain return has the same failure mode already: the goal set with the attack order is the target itself, so a unit that enters range without a solution just keeps walking, which the false positives of the old pre-aim test used to hide. * TraceRay: add Collision::NOMOBILEFRIENDLIES, a trace-only flag (never part of a weapon's avoidFlags) that skips allied units that are able to move. Allied structures, features and terrain are still scanned. Honoured by TraceRay, TestCone, TestTrajectoryCone and the MissileLauncher trajectory scan. * CWeapon::TryTargetRotate / TryTargetHeading take optional extra avoid flags for one test. * CMobileCAI: when no weapon has a solution although the target is in range, test again with mobile allies ignored. If that still fails the blocker is static (terrain, feature, allied wall or building) and the unit keeps closing in like an out-of-range unit would, for unit and ground targets. If it passes, only an allied unit is in the way and it may move out of it, so the unit stops and waits instead of walking past it. The approach is bounded: it stops at 20% of maxRange (so artillery does not walk up to a target that sits behind a wall), an approach that fails (unreachable goal) is not retried for 10 seconds, stopToAttack units always wait, and the strafeToAttack, gunship, very-close and owner-rotation branches keep their old behaviour. Hold-position units follow the same logic; an explicit attack order already makes them approach an out-of-range target and the temp-order case is cancelled earlier as before. Fixes beyond-all-reason#3242.
This was referenced Sep 4, 2026
eun-ice
added a commit
to eun-ice/RecoilEngine
that referenced
this pull request
Sep 5, 2026
…fire A mobile unit inside 0.9 * maxRange of its attack target whose weapons all fail the preliminary line-of-fire test used to plain-return (unit target) or stop (ground target) and retry from the same spot every SlowUpdate. With the test now traced from the predicted muzzle (beyond-all-reason#3327) the answer is honest, but a unit parked behind a rock or a crest still never fires (beyond-all-reason#3242). The obvious fix, treating "in range but no solution" like "out of range" and closing in, has a known failure mode: in a group the rear units are blocked by the front units, walk past them, and thereby block the units they passed, so the whole ball creeps forward. The CommandAI cannot tell the two cases apart because the line-of-fire test only returns a bool. The plain return has the same failure mode already: the goal set with the attack order is the target itself, so a unit that enters range without a solution just keeps walking, which the false positives of the old pre-aim test used to hide. * TraceRay: add Collision::NOMOBILEFRIENDLIES, a trace-only flag (never part of a weapon's avoidFlags) that skips allied units that are able to move. Allied structures, features and terrain are still scanned. Honoured by TraceRay, TestCone, TestTrajectoryCone and the MissileLauncher trajectory scan. * CWeapon::TryTargetRotate / TryTargetHeading take optional extra avoid flags for one test. * CMobileCAI: when no weapon has a solution although the target is in range, test again with mobile allies ignored. If that still fails the blocker is static (terrain, feature, allied wall or building) and the unit keeps closing in like an out-of-range unit would, for unit and ground targets. If it passes, only an allied unit is in the way and it may move out of it, so the unit stops and waits instead of walking past it. The approach is bounded: it stops at 20% of maxRange (so artillery does not walk up to a target that sits behind a wall), an approach that fails (unreachable goal) is not retried for 10 seconds, stopToAttack units always wait, and the strafeToAttack, gunship, very-close and owner-rotation branches keep their old behaviour. Hold-position units follow the same logic; an explicit attack order already makes them approach an out-of-range target and the temp-order case is cancelled earlier as before. Fixes beyond-all-reason#3242.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR improves the estimated muzzle position for the preliminary line-of-fire check before a unit shoots.
Part of #3242 (attackers stopping without being able to fire when obstructed by terrain): this makes the engine's pre-aim "can I shoot from here" answer match the real shot more closely, which is the prerequisite for any CommandAI change to keep moving. Also beyond-all-reason/Beyond-All-Reason#9055 (Rocko on a ridge).
BAR companion PR beyond-all-reason/Beyond-All-Reason#9090
Problem
Shooting happens in five phases:
This PR improves the preliminary line-of-fire check to use a better estimate of where the muzzle position might be after aiming, at the cost of a a few multiplications and a
sqrtCurrently the engine does the preliminary line-of-fire check with either a static
aimFromPosgiven by the unit, or for Cannon/MissileLauncher/StarburstLauncher the actual muzzle position inCWeapon::TryTarget(pos, trg, preFire=false)When the actual fire check happens after aiming (
CWeapon::TryTarget(pos, trg, preFire=true)) the actual muzzle position is used, which might be different from the estimated muzzle position before aiming.This can lead to issues:
So neither the current
muzzlePosnor the staticaimFromPosis a good estimate for where the muzzle will be after aiming.Idea
The idea of this PR is to allow unit designers to specify a tuple of values to the engine so the engine can make a better estimate of the actual muzzle pos in the preliminary check. If no such values are specified, the engine behaves as before (with the exception of using
aimFromPosfor Cannon/MissileLauncher/StarburstLauncher.This PR is an AI proposed simple calculation that only adds one
sqrtand a few multiplications to reach a much better estimate where the muzzle position would be after aiming.Change
aimFromEstimate = {pivotX, pivotY, pivotZ, lateral, forward, barrelForward, barrelUp}(unit space). It models the muzzle as a yaw pivot plus an offset turning with yaw
only plus an offset turning with yaw and pitch; evaluation is one
sqrt, no trig(
Sim/Weapons/AimFromEstimate.h). The expression is linear in the five offsets,so the values can be fitted offline from observed (aim direction, muzzle) pairs.
CWeapon::GetAimFromPos(tgtPos, useMuzzle)returns the estimated post-aim muzzlefor
tgtPoswhen the tag is present, elseaimFromPosas before. The direction fedinto the estimate is what the aiming script would be asked for:
CCannon::CalcWantedDirfor cannons,
+trajectoryHeightfor missiles, straight line otherwise (new virtualGetWantedDirFor).GetAimFromPostoreturn the current muzzle; they follow the same rule as every other weapon type.
Spring.GetUnitWeaponAimFromPos(unitID, weaponNum, x, y, z) -> px, py, pz, isEstimateexposes the position the pre-aim test is traced from (debug widgets, tests).
Spring.GetUnitWeaponTryTarget(unitID, weaponNum, x, y, z)builtSWeaponTarget(nullptr, pos, true), whose constructor zeroesgroundPoswhen theunit is null, so it always tested
(0,0,0)and returnedfalse.classes above now use the
AimFromWeaponpiece instead of the current muzzle forthe pre-aim test (changelog caveat).
One tuple per weapon, also for weapons whose
QueryWeaponalternates between barrels(they get the mean muzzle, ~half the barrel spacing of residual error). Per-barrel
tuples were tried and measured: the engine re-queries
QueryWeapononly right before ashot, and whether the piece it holds at aiming time is the barrel that fires next depends
on where the script advances its counter and on burst state — Storm fires the same piece
(26/31), AK mostly the other one (35/86 same), Pawn alternates in a 4-shot pattern. No
selection rule beats the mean, so the simpler tag stays.
Validation
test_AimFromEstimate(new, 902 assertions): the trig-free formula against anexplicitly rotated two-axis turret reference for four geometries × 16 yaws × 7
pitches, plus degenerate inputs.
checktarget: 29/30 pass; the failingtestUnitSyncis the pre-existingduplicate build/install base-content configuration failure on this machine.
tweakunits. Each unit aims at ~50 ground targets around it; for every shot theposition the pre-aim test is traced from (
Spring.GetUnitWeaponAimFromPos, readbefore the turret turns) is compared with the muzzle position at
ProjectileCreated.Columns are the mean distance in elmos between those two points:
AimFromWeaponpiece;hand-tuned fixed aim position could do;
The last column counts pre-aim decisions that disagree with the real shot on a ring
of terrain pillars raised around the unit afterwards: stall = pre-aim clear but the
shot is blocked (unit aims and never fires), refuse = pre-aim blocked but the shot
would be clear (unit drops a target it could hit), today → with the estimate,
out of n targets.
(armrock/corstorm/corak/armpw alternate between two barrels; the mean-muzzle tuple is used, the Pawn gains nothing and the generator skips it.)
PR's mechanism (pre-aim check from the raised arm passes, shot from the lowered arm is
blocked). Note that this PR alone does not make the stuck Sheldons of Attack Command: Attackers can stop without being able to fire when obstructed by terrain #3242 move: with a
correct pre-aim answer they are rejected before aiming, but
CMobileCAI::ExecuteAttackstill parks a unit that is inside 0.9 × range without a firing solution — that is the
remaining CommandAI part of Attack Command: Attackers can stop without being able to fire when obstructed by terrain #3242. The Wolverine slope stall is a separate heading bug
(Attack Command: Attackers can stop without being able to fire on slopes #3324, Weapons: Fix AimWeapon script heading parameter #3325).
AI disclosure
Formula, fitting tool, engine change and tests were written with Claude Code
(Anthropic); the author reviewed every change and ran the validation above.