World-pick targeting: 0x388 = whatever is downrange -- mech, terrain, or nothing (task #41)

User video evidence (lasers firing at nothing) + the colleague's
torso-locked report + the binary all reconcile into one model:

The boresight pick hits the WORLD, and the target slot mech+0x388 holds
whatever entity is downrange:
- enemy mech under the boresight -> aimed target (hull point -> STEP-6
  zone under the boresight, hotbox + lock ring, damage);
- else the TERRAIN downrange (BTGroundRayHit to 1200) -> the beam and
  missiles fire at the scenery ("firing at nothing", as in the pod
  videos), range caret reads the ground distance (authentic :5639), no
  damage;
- else (sky) -> no target, and the weapon's own double 0x388 gate
  refuses the discharge.

Binary evidence for non-mech targets: HudSimulation :5620 explicitly
handles a target WITHOUT damage zones (target->0x120 == 0) -- dead code
if only mechs were ever targeted. The pick is automatic every frame
(0x388: 11 reads / 0 direct stores in CODE).

Implementation: gBTTerrainEntity captured in MakeEntityRenderables
(Terrain::GetClassDerivations); mech4 pick = enemy PickRayHit else
ground ray -> terrain entity + ground point; damage/hotbox/lock only
for the mech pick; the task-40 enemy auto-converge REMOVED (facing away
now fires at the scenery, not magically at the enemy -- the user's
complaint). Verified all three states headless (BT_AIM 0,0 / .6,.5 /
0,-.8): aimed zone hits / terrain beams + 0 damage / no discharge.

Fourth (and evidence-complete) targeting model iteration; KB updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-09 00:06:00 -05:00
co-authored by Claude Fable 5
parent 60ed54e008
commit e30a61a62f
4 changed files with 133 additions and 102 deletions
+24 -21
View File
@@ -58,27 +58,30 @@ basis via `BTSetAimCamera`, the render loop publishes proj._11/_22 + backbuffer
`Mech::PickRayHit` (world ray → local frame → `BoundingBox::HitBy` slab test vs the collision
template's ExtentBox → world hit point).
**FIRING NEEDS A TARGET BUT NOT A MANUAL LOCK / PINPOINT AIM (task #40, binary-verified [T1]).**
The weapon fire path is DOUBLY gated on `mech+0x388 != 0`: `Emitter::EmitterSimulation`
(`FUN_004baa88` :7689) only calls FireWeapon with a target, AND `Emitter::FireWeapon`
(`FUN_004bace8` :7727) wraps its ENTIRE body (incl. `beamFlag`=+0x46c=1 :7749) in the same check —
so **no target ⇒ no beam, ever** (you cannot fire into truly empty space). BUT a capstone scan of
the whole CODE section finds **11 reads of `+0x388` and ZERO direct stores** — the target is
written INDIRECTLY (a message/selector via a `this`-pointer), i.e. the game **AUTO-ACQUIRES** the
target for you; there is no manual "lock to fire." The spinning-ring **LOCK** (HudSimulation
5619-5634) is a SEPARATE, stricter state layered on top (host-zone < 0.75 damage AND targeted-zone
< 1.0). **Port model:** `mech+0x388` = the enemy whenever it is ALIVE (`!IsMechDestroyed()`) — the
pod's auto-target — so firing works freely with the enemy merely present, NO pinpoint needed
(verified: `BT_AIM="0.5 0.2"` off-hull → fires + lands damage; before this fix off-hull dealt 0).
A pinpoint hull hit (`PickRayHit``hotTarget`) UPGRADES the shot: `mech+0x37c` = the picked hull
point → STEP-6 resolves the zone under the crosshair (aimed fire; center-aim → head-band zone 13),
and lights the lock ring. Off-hull, `0x37c` = the target's centre mass (body hits, near-centre
zone). (⚠ THREE earlier cuts were wrong and are corrected: a STICKY designation — locked forever;
a MOUSE cursor slew — a free cursor the pod never had; and a PINPOINT-required fire gate — you had
to boresight the hull exactly to fire at all. All from over-reading the RP-shared `Reticle` struct;
removed.) The HUD draws the aim group at the boresight ([0x9a] translate), the designator hotbox at
the target's projected point (subB9 hot / subB8 designated, `BTProjectHotBox`/`BTProjectToReticle`),
and the edge arrows when off-screen/behind. `BT_FIRE_ARC` is now an EXPLICIT OPT-IN presentation clamp (unset = authentic
**THE WORLD-PICK TARGET MODEL (task #41, the reconciliation — binary + pod-video evidence).**
The target slot `mech+0x388` holds **whatever entity is downrange of the torso boresight** — a
mech OR a WORLD entity (terrain/mesa/building). Evidence: (a) the weapon fire path is DOUBLY
gated on `0x388 != 0` (`FUN_004baa88` :7689 AND `FUN_004bace8` :7727 wrap everything incl.
`beamFlag`=+0x46c :7749 — no target ⇒ no discharge [T1]); (b) pod demo VIDEOS show lasers firing
with no mech targeted — reconciled by (c): HudSimulation :5620 **explicitly handles a target
WITHOUT damage zones** (`target->0x120 == 0`) — dead code unless non-mech entities get targeted;
and (d) `0x388` has **11 reads / ZERO direct stores** across CODE (capstone scan) — the pick is
written indirectly, AUTOMATICALLY, every frame; never a manual player lock. So: **fire at the
enemy under your boresight → aimed zone damage; fire anywhere else → the beam hits the scenery
downrange ("firing at nothing"); only a true sky shot (nothing within range) refuses to
discharge.** The spinning-ring **LOCK** (HudSimulation 5619-5634) is a SEPARATE, stricter state
(mech target only + host-zone < 0.75 damage + targeted-zone < 1.0). **Port implementation:** the
boresight ray tests the enemy's collision box (`PickRayHit` → aimed hull point → STEP-6 zone
under the boresight + hotbox + lock ring), else the terrain (`BTGroundRayHit` to 1200 →
`gBTTerrainEntity` + the ground point — beam/missiles fly to the scenery, NO damage dispatch;
the range caret authentically reads the ground distance, :5639), else no target (sky — the
double gate refuses). Verified all three states (`BT_AIM` 0,0 / 0.6,0.5 / 0,0.8). (⚠ This is
the FOURTH targeting model iteration; the first three — sticky lock, mouse cursor, enemy
auto-converge — all over-read the RP-shared `Reticle` struct or under-read the pick. The
world-pick model is the one that fits ALL the evidence.) The HUD draws the aim group at the
boresight ([0x9a] translate), the designator hotbox at the mech target's projected point (subB9
hot / subB8 designated, `BTProjectHotBox`/`BTProjectToReticle`), and the edge arrows when
off-screen/behind. `BT_FIRE_ARC` is now an EXPLICIT OPT-IN presentation clamp (unset = authentic
no-arc). The old hardwired lock, the ±30°-default cone, and the projectile path's gEnemyMech
fallback are all REMOVED. LMB fires lasers / RMB missiles (with SPACE/CTRL). ⚠ A view-selection
bug was fixed en route: every
+21
View File
@@ -58,11 +58,19 @@
#if !defined(APP_HPP)
# include <app.hpp>
#endif
#if !defined(TERRAIN_HPP)
# include <terrain.hpp> // Terrain::GetClassDerivations (world-pick target, task #41)
#endif
#include <string.h>
#include <math.h>
#include <time.h> // clock() -- the threat-trail ages (task #37)
// WORLD-PICK TARGET (task #41): a live Terrain entity mech4 cites in
// mech+0x388 when the boresight pick lands on the ground (the binary's target
// slot holds non-mech world entities). Captured in MakeEntityRenderables.
Entity *gBTTerrainEntity = 0;
//
// Material-name substitution placeholders. Mirrors RPL4VideoRenderer's
@@ -205,6 +213,19 @@ void
// `this_instance` in the CulturalIcon/Landmark arm has been fixed in
// L4VIDEO.cpp.
//
// WORLD-PICK TARGET support (task #41): the boresight pick can hit
// TERRAIN (the binary's target slot holds non-mech world entities --
// HudSimulation part_013.c:5620 explicitly handles a target without
// damage zones). Capture a live Terrain entity for mech4's ground
// pick to cite in mech+0x388 (the pick POINT carries the geometry;
// which specific instance matters only to damage, which never routes
// to terrain).
{
extern Entity *gBTTerrainEntity;
if (gBTTerrainEntity == 0
&& entity->IsDerivedFrom(*Terrain::GetClassDerivations()))
gBTTerrainEntity = entity;
}
DPLRenderer::MakeEntityRenderables(
entity, model_resource, view_type);
break;
+79 -77
View File
@@ -2286,38 +2286,62 @@ void
}
}
// --- TARGETING (task #36 -- the AUTHENTIC Reticle acquisition,
// engine/MUNGA/RETICLE.h [T0]): the pilot slews the crosshair
// (gBTAimX/Y); a pick ray through it tests the enemy's collision
// volume; the mech UNDER the crosshair becomes the designated target
// (sticky -- it persists when the crosshair drifts off, marked by
// the HUD designator; re-hover to refresh, burial clears it). The
// designation feeds the engine-Entity target slots the whole weapon
// path reads (target position @0x37c, target entity @0x388, zone
// @0x38c). While the crosshair is ON the target ("hot"), the aim
// point is the PICKED HULL POINT -- the STEP-6 cylinder lookup then
// resolves damage to the zone under the crosshair (aimed fire).
Entity *hotTarget = 0; // mech under the crosshair NOW
// --- TARGETING: the WORLD-PICK model (task #41, the reconciliation of
// ALL the evidence). The boresight ray picks whatever is DOWNRANGE:
// 1. the enemy mech's collision volume -> aimed target (hull point
// -> STEP-6 zone under the boresight; hotbox + lock ring);
// 2. else the TERRAIN (BTGroundRayHit) -> the ground point becomes
// the target -- the beam fires at the scenery ("firing at
// nothing", seen in the pod demo videos), no damage;
// 3. else (sky, nothing within range) -> NO target, and the
// weapon's own double gate (FUN_004baa88:7689 + FUN_004bace8:
// 7727 -- both require mech+0x388 != 0) refuses the discharge.
// Binary evidence for non-mech targets: HudSimulation :5620
// explicitly handles a target WITHOUT damage zones (target->0x120
// == 0) -- dead code if only mechs were ever targeted. The pick is
// AUTOMATIC every frame (0x388 has 11 reads / 0 direct stores in
// CODE -- written indirectly, never a manual player lock).
Entity *hotTarget = 0; // the enemy mech under the boresight
Point3D hotPoint; // picked world point on its hull
static int gAimNoRay = 0, gAimNoPick = 0, gAimHits = 0; // 1Hz diagnostics
Entity *pickTarget = 0; // what the boresight ray hit (mech/terrain)
Point3D pickPoint; // where it hit
static int gAimNoRay = 0, gAimGround = 0, gAimHits = 0; // 1Hz diagnostics
{
extern int BTGetAimRay(float rx, float ry, float outStart[3], float outDir[3]);
extern Entity *gBTTerrainEntity; // captured by MakeEntityRenderables
float rs[3], rd[3];
if (!BTGetAimRay(gBTAimX, gBTAimY, rs, rd))
{
++gAimNoRay;
}
else if (gEnemyMech != 0)
else
{
Point3D rayStart(rs[0], rs[1], rs[2]);
Vector3D rayDir(rd[0], rd[1], rd[2]);
if (((Mech *)gEnemyMech)->PickRayHit(rayStart, rayDir, 4000.0f, &hotPoint))
if (gEnemyMech != 0 && !((Mech *)gEnemyMech)->IsMechDestroyed()
&& ((Mech *)gEnemyMech)->PickRayHit(rayStart, rayDir, 4000.0f, &hotPoint))
{
hotTarget = gEnemyMech;
hotTarget = gEnemyMech;
pickTarget = gEnemyMech;
pickPoint = hotPoint;
++gAimHits;
}
else
++gAimNoPick;
{
// the ground downrange of the guns (max = the pod's HUD
// range scale; past it the shot is a sky shot -> no target)
extern bool BTGroundRayHit(float,float,float, float,float,float,
float, float*,float*,float*);
float hx, hy, hz;
if (gBTTerrainEntity != 0 &&
BTGroundRayHit(rs[0], rs[1], rs[2], rd[0], rd[1], rd[2],
1200.0f, &hx, &hy, &hz))
{
pickTarget = gBTTerrainEntity;
pickPoint.x = hx; pickPoint.y = hy; pickPoint.z = hz;
++gAimGround;
}
}
}
// The Reticle struct (the mech's TargetReticle attribute): position,
@@ -2325,53 +2349,44 @@ void
// damage delivery (the authentic percent-table roll, STEP 6).
targetReticle.reticlePosition.x = gBTAimX;
targetReticle.reticlePosition.y = gBTAimY;
targetReticle.targetEntity = hotTarget;
targetReticle.targetEntity = pickTarget;
targetReticle.targetDamageZone = -1;
if (hotTarget != 0)
targetReticle.rayIntersection = hotPoint;
if (pickTarget != 0)
targetReticle.rayIntersection = pickPoint;
// TARGET (mech+0x388) is AUTO-ACQUIRED, not manually locked (task #40,
// binary-confirmed): the weapon's fire path (Emitter FUN_004baa88 :7689
// + FireWeapon FUN_004bace8 :7727) is DOUBLY gated on mech+0x388 != 0 --
// no target, NO beam, ever -- but 0x388 is WRITTEN INDIRECTLY (a
// disasm scan of the whole CODE section finds 11 reads and ZERO direct
// stores), i.e. the game AUTO-SELECTS the target for you; you never
// manually "lock" to fire. The spinning-ring LOCK (HudSimulation
// 5619-5634) is a SEPARATE, stricter state layered on top. So: 0x388
// = the enemy whenever it is alive (the pod's auto-target); firing
// needs only that, NOT a pinpoint boresight hit. A pinpoint hull hit
// (hotTarget) UPGRADES the shot to aimed-zone damage + the lock ring.
Entity *autoTarget =
(gEnemyMech != 0 && !((Mech *)gEnemyMech)->IsMechDestroyed())
? gEnemyMech : 0;
if (autoTarget != 0)
// the engine-Entity target slots the whole weapon path reads
if (pickTarget != 0)
{
MECH_TARGET_ENTITY(this) = autoTarget;
MECH_TARGET_ENTITY(this) = pickTarget;
MECH_TARGET_SUBIDX(this) = -1;
if (hotTarget != 0)
MECH_TARGET_POS(this) = hotPoint; // aimed: the picked hull point
else
{
// no pinpoint pick -> converge on the target's centre mass
// (the beam still fires; the zone resolves near-centre).
Point3D cm = ((Mech *)autoTarget)->localOrigin.linearPosition;
cm.y += kMuzzleHeight;
MECH_TARGET_POS(this) = cm;
}
MECH_TARGET_POS(this) = pickPoint; // beam endpoint = the pick
}
else
{
MECH_TARGET_ENTITY(this) = 0; // no living enemy -> no target
MECH_TARGET_ENTITY(this) = 0; // sky: no target, no discharge
MECH_TARGET_SUBIDX(this) = -1;
}
}
// HUD feeds: the range caret + the hotbox (world point + state) + the
// recovered-Execute instruments (compass, twist tape, group mask).
// The range caret tracks the PICK (authentic: :5639 computes it from
// mech+0x37c whatever the target is -- a terrain pick reads the ground
// distance); the hotbox + lock exist ONLY for a mech target (:5620 --
// a target with no damage zones gets neither).
{
extern void BTSetHudTargetRange(Scalar range);
Entity *des = MECH_TARGET_ENTITY(this);
if (des != 0)
if (des != 0 && des != hotTarget)
{
// terrain pick: range to the ground point; no hotbox, no ring.
Point3D tp = MECH_TARGET_POS(this);
float hddx = tp.x - localOrigin.linearPosition.x;
float hddz = tp.z - localOrigin.linearPosition.z;
BTSetHudTargetRange((Scalar)sqrtf(hddx*hddx + hddz*hddz));
gBTHudLockState = 0;
}
else if (des != 0)
{
Mech *dm = (Mech *)des;
Point3D dp = dm->localOrigin.linearPosition;
@@ -2426,7 +2441,8 @@ void
{
Point3D enemyPos = ((Mech *)gEnemyMech)->localOrigin.linearPosition;
const int designated = (MECH_TARGET_ENTITY(this) != 0);
// damage routes ONLY when the boresight pick is the enemy mech
const int designated = (hotTarget != 0);
float ddx = enemyPos.x - localOrigin.linearPosition.x;
float ddy = enemyPos.y - localOrigin.linearPosition.y;
@@ -2438,14 +2454,15 @@ void
{
gTargetLogAccum = 0.0f;
DEBUG_STREAM << "[target] aim=(" << gBTAimX << "," << gBTAimY << ")"
<< (hotTarget != 0 ? " HOT-aimed (zone under crosshair)"
: (designated ? " auto-target (fire freely, body hits)" : " no living target"))
<< (hotTarget != 0 ? " ENEMY under boresight (aimed)"
: (pickTarget != 0 ? " terrain downrange (beam at scenery)"
: " sky (no target, no discharge)"))
<< " range=" << range
<< (range <= kWeaponRange ? " IN RANGE" : "")
<< " [aimedHits=" << gAimHits << " noRay=" << gAimNoRay
<< " offHull=" << gAimNoPick << "]"
<< " [enemyPicks=" << gAimHits << " groundPicks=" << gAimGround
<< " noRay=" << gAimNoRay << "]"
<< "\n" << std::flush;
gAimHits = 0; gAimNoRay = 0; gAimNoPick = 0;
gAimHits = 0; gAimNoRay = 0; gAimGround = 0;
}
// --- FIRING (bring-up): on the trigger, with a target in range and the
@@ -2502,29 +2519,14 @@ void
gFireCooldown = kFireCooldown;
++gShotCount;
// Impact point (task #36): while the crosshair is ON the target the
// shot lands at the PICKED HULL POINT -- the STEP-6 cylinder lookup
// resolves damage to the zone under the crosshair (aimed fire: leg
// shots hit legs, torso shots hit the torso). Off-crosshair (the
// sticky designation converging), the shot enters on the surface
// toward the shooter at chest height (the unaimed synth point --
// exactly the axis point would be angularly degenerate for the
// cylinder sector lookup).
Point3D impact;
if (hotTarget != 0)
{
impact = hotPoint;
}
else
{
impact = enemyPos;
if (range > 1e-3f)
{
impact.x -= (ddx / range) * 3.0f; // ~torso radius toward shooter
impact.z -= (ddz / range) * 3.0f;
}
impact.y += kMuzzleHeight; // chest height (beam aim height)
}
// Impact point (tasks #36/#41): damage happens ONLY when the enemy
// is under the boresight (designated == hotTarget != 0, checked in
// the gate above), so the shot always lands at the PICKED HULL
// POINT -- the STEP-6 cylinder lookup resolves damage to the zone
// under the boresight (aimed fire: leg shots hit legs, torso shots
// the torso). Terrain picks never reach here (beam + scenery
// impact only, no damage dispatch).
Point3D impact = hotPoint;
Origin exp_origin = ((Mech *)gEnemyMech)->localOrigin;
exp_origin.linearPosition = impact; // at the hit point
+9 -4
View File
@@ -620,13 +620,18 @@ void
// reconstruction; the byte-exact world-entity Projectile is blocked by the 2007 engine
// Entity base mismatch -- see BTPushProjectile / mech4.cpp). Speed = |launchVelocity|.
char *o = (char *)owner; // inherited MechSubsystem::owner (the Mech)
void *target = (o != 0) ? *(void **)(o + 0x388) : 0; // owner's locked target entity (null in bring-up)
Point3D targetPos = (o != 0) ? *(Point3D *)(o + 0x37c) : muzzle; // owner target point
// The owner's target slots (raw 0x388/0x37c -- the same raw offsets mech4's
// targeting block writes; a consistent producer/consumer pair on our
// compiled object). Task #41: the slots hold the WORLD PICK -- the enemy
// mech under the boresight, or the terrain downrange (a missile fired at
// the scenery flies to the ground point and detonates without damage), or
// null at the sky (BTPushProjectile refuses; the weapon's own 0x388 gate
// wouldn't have fired either).
void *target = (o != 0) ? *(void **)(o + 0x388) : 0;
Point3D targetPos = (o != 0) ? *(Point3D *)(o + 0x37c) : muzzle;
Scalar speed = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x
+ launchVelocity.y*launchVelocity.y
+ launchVelocity.z*launchVelocity.z);
// Always launch -- BTPushProjectile falls back to gEnemyMech when the owner's target
// slot isn't populated (bring-up).
BTPushProjectile(muzzle, o, target, targetPos, speed, damageData.damageAmount);
simulationFlags |= 0x1; // replication-dirty