MP: world structures (garages/walls) TARGETABLE -- boresight ray-tests the static collision tree (task #50)

The boresight's non-mech pick only sampled the VISUAL heightfield (BTGroundRayHit),
which on arena1 is a single flat 'sky'-named ground mesh (and class-42 BuildTables
likewise holds only 'sky') -- so shots passed THROUGH the garages/walls and only mechs
moved the HUD range axis (user-reported regression: "buildings used to move the range
axis; now only mechs do, and you can only fire at a peer").

Real fix: the boresight now ALSO ray-tests the ZONE'S STATIC COLLISION SOLID TREE --
the same geometry that already blocks the mech's walk. Authentic engine mechanism:
Mover::FindBoxedSolidHitBy already tests the static world via
zone->GetCollisionRoot()->FindBoundingBoxHitBy(line). Factored its "static world" tail
into Mover::FindStaticSolidHitBy(Line*) (static solids only, no movers), wrapped by
Mech::WorldStructurePick(start,dir,range,&hit) (builds the world-space Line, reads the
entry point back via line->FindEnd since HitByBounded clips line->length=enter).

Boresight pick order is now: closest MECH (PickRayHit, damage zones + lock) -> closest
STRUCTURE (WorldStructurePick; occludes a mech BEHIND it; designates the gBTTerrainEntity
sentinel + entry point, so the range caret reads the structure distance and NO lock ring
draws, mech4.cpp:4529) -> flat ground (BTGroundRayHit) -> sky (fire-at-nothing). Also
un-skips arena1's misnamed-'sky' flat ground so the ground tier works (btvisgnd
geometry-aware skip + [mapent]/[rendent] census).

Verified headless: a BT_WSWEEP horizontal ray-fan on arena1 tracks position (3/24 hits
near the boundary -> 17-21/24 inside the interior garage cluster -> 3/24 past it =
DISCRETE interior solids, not an enclosing box), no crash/assert/AV. Interactive aim
(BTGetAimRay) can't run headless (no window -> noRay), so the sweep is the headless proof;
interactive aim is user-verified.

Note: FindBoundingBoxUnder (the ground/containedByNode BoundingBoxTree) is DOWNWARD-only
(gravity/ground-snap: *height = FindDistanceBelowBounded), useless for a horizontal
boresight; the static SOLID tree's FindBoundingBoxHitBy is the only ray-vs-world query.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-15 10:08:45 -05:00
co-authored by Claude Opus 4.8
parent ee6e19e86e
commit de8f6d02c1
9 changed files with 222 additions and 4 deletions
+26
View File
@@ -104,6 +104,32 @@ bug was fixed en route: every
renderable rebuild stomped `mCamera` back to the chase eye (btl4vid `mViewInside` now persists renderable rebuild stomped `mCamera` back to the chase eye (btl4vid `mViewInside` now persists
the chosen view — the aim camera feed and the V toggle both depend on it). the chosen view — the aim camera feed and the V toggle both depend on it).
**WORLD STRUCTURES ARE TARGETABLE via the STATIC COLLISION TREE (task #50, 2026-07-15) [T2].**
The `BTGroundRayHit` "terrain" tier only samples the VISUAL heightfield, which on arena1 is a
single flat `'sky'`-named ground mesh (`[visgnd] sampler ready: 1 terrain instances`) — and the
class-42 map-instance stream (`BuildTables`) likewise holds only `'sky'`. So the garages/walls
were invisible to the pick → shots passed through them, and only mechs moved the range axis (the
user-reported regression). The FIX: the boresight now also ray-tests the **zone's static collision
solid tree** — the SAME geometry that blocks the mech's walk. Mechanism is authentic engine code:
`Mover::FindBoxedSolidHitBy` (MOVER.cpp) already tests movers, doors, AND the static world
(`application->GetInterestManager()->GetInterestZone(interestZoneID)->GetCollisionRoot()->FindBoundingBoxHitBy(line)`).
We factored its "test against the static world" tail into **`Mover::FindStaticSolidHitBy(Line*)`**
(static solids only, no movers), wrapped by **`Mech::WorldStructurePick(start,dir,range,&hit)`**
(mech.cpp — builds the world-space `Line`, reads the entry point back via `line->FindEnd` since
`FindBoundingBoxHitBy``HitByBounded` clips `line->length = enter`). The pick order in mech4.cpp is
now: closest MECH (`PickRayHit`, damage zones + lock) → closest STRUCTURE (`WorldStructurePick`,
occludes a mech BEHIND it) → flat ground (`BTGroundRayHit`) → sky (fire-at-nothing). A structure
hit designates `mech+0x388 = gBTTerrainEntity` (the no-damage-zone sentinel) + `0x37c` = the entry
point, so the range caret reads the structure distance and NO lock ring draws (mech4.cpp:4529, the
`des != hotTarget` branch), exactly the reported authentic behavior. ⚠ There is NO ray query
against the ground/`containedByNode` `BoundingBoxTree``FindBoundingBoxUnder` is DOWNWARD-only
(gravity/ground-snap: `*height = FindDistanceBelowBounded`), useless for a horizontal boresight;
the static SOLID tree (`BoxedSolidTree::FindBoundingBoxHitBy`) is the only ray-vs-world query.
Verified: a `BT_WSWEEP` horizontal ray-fan on arena1 tracks position (3/24 hits near the boundary
→ 17-21/24 inside the interior garage cluster → 3/24 past it — discrete interior solids, not just
an enclosing box), no crash. The interactive boresight (`BTGetAimRay`) can't be exercised headless
(no window → `noRay`), so the SWEEP is the headless proof; interactive aim is user-verified.
## Firing arc + muzzles ## Firing arc + muzzles
There is NO firing-arc field in the binary (`Emitter::FireWeapon` fires whenever HasActiveTarget) — There is NO firing-arc field in the binary (`Emitter::FireWeapon` fires whenever HasActiveTarget) —
the 1995 game got away with it because it was **cockpit-only** (a beam behind you is off-camera). The the 1995 game got away with it because it was **cockpit-only** (a beam behind you is off-camera). The
+11
View File
@@ -154,6 +154,17 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
centred); (c) pre-burial, the pick still centred); (c) pre-burial, the pick still
tests the sinking wreck's collision box at its parked position (pre-existing wreck-targeting tests the sinking wreck's collision box at its parked position (pre-existing wreck-targeting
behavior). behavior).
- **✅ WORLD STRUCTURES (garages/walls) now targetable (task #50, 2026-07-15)** — the world-pick's
non-mech tier previously only sampled the visual heightfield (`BTGroundRayHit`), which on arena1
is a single flat `'sky'` ground mesh → shots passed THROUGH the garages and only mechs moved the
range axis (user-reported regression). Resolved by ray-testing the **zone's static collision
solid tree** (the geometry that already blocks the walk) via the authentic engine query
`Mover::FindBoxedSolidHitBy`'s static-world tail — factored into `Mover::FindStaticSolidHitBy` +
`Mech::WorldStructurePick`. See [[combat-damage]] "WORLD STRUCTURES ARE TARGETABLE". This also
closes the old "0x388 writer is un-exported / unknown world-pick mechanism" residue: the
authentic non-mech world pick is the static-solid-tree ray query (`BoxedSolidTree::FindBoundingBoxHitBy`),
the same one `FindBoxedSolidHitBy` uses for mover-vs-world collision. Interactive aim is
user-verified (headless has no `BTGetAimRay`); the `BT_WSWEEP` ray-fan is the headless proof.
- **Cockpit HUD leftovers (tasks #35-#38 residue, 2026-07-08)** — the reticle + 7 weapon pips + - **Cockpit HUD leftovers (tasks #35-#38 residue, 2026-07-08)** — the reticle + 7 weapon pips +
ALL the instrument dynamics are LIVE (the Execute @004cdcf0 gap was RECOVERED via capstone ALL the instrument dynamics are LIVE (the Execute @004cdcf0 gap was RECOVERED via capstone
disasm, task #37; the authentic Lock producer + the simple-X mode landed in #38 — see disasm, task #37; the authentic Lock producer + the simple-X mode landed in #38 — see
+28
View File
@@ -1137,6 +1137,34 @@ BoxedSolid*
return solid; return solid;
} }
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// The "static world" tail of FindBoxedSolidHitBy on its own: ray-test ONLY the
// zone's static solid tree (world structures -- garages, walls, props), NOT the
// tangible movers/doors. Same tree the mech's walk collides against, so a shot
// designated through this lands on exactly the geometry that blocks the mech.
// FindBoundingBoxHitBy clips line->length to the hit distance (HitByBounded ->
// line->length = enter), so the caller reads the entry point via line->FindEnd.
//
BoxedSolid*
Mover::FindStaticSolidHitBy(
Line *line
)
{
Check(this);
Check(line);
InterestManager *interest_mgr = application->GetInterestManager();
Check(interest_mgr);
InterestZone *zone = interest_mgr->GetInterestZone(interestZoneID);
Check(zone);
BoxedSolidTree* tree = zone->GetCollisionRoot();
Check(tree);
BoxedSolid *result = (BoxedSolid*)tree->FindBoundingBoxHitBy(line);
Check_Fpu();
return result;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// //
BoxedSolidCollisionList* BoxedSolidCollisionList*
+10
View File
@@ -300,6 +300,16 @@ public:
Line *line, Line *line,
Entity *except_by Entity *except_by
); );
// STATIC-WORLD-ONLY ray query: the "test against the static world" tail of
// FindBoxedSolidHitBy factored out -- ray-tests ONLY the zone's static solid
// tree (the world structures: garages, walls, props), skipping the tangible
// movers/doors. Used by the weapon boresight pick so a shot lands on the SAME
// static geometry that already blocks the mech's walk (mechs are picked
// separately, with their damage zones/lock). Clips line->length to the entry.
BoxedSolid*
FindStaticSolidHitBy(
Line *line
);
BoxedSolidCollisionList* BoxedSolidCollisionList*
CollideCenterOfMotion( CollideCenterOfMotion(
Line *line, Line *line,
+17
View File
@@ -247,6 +247,23 @@ void
else if (gBTTerrainEntity == 0) else if (gBTTerrainEntity == 0)
gBTTerrainEntity = entity; // else the first world entity gBTTerrainEntity = entity; // else the first world entity
} }
// CENSUS: what world-geometry entities does the RENDER tree walk?
// (the garages/structures may be here even if not in the map's class-42
// instance stream that BuildTables reads.)
if (getenv("BT_GROUND_LOG"))
{
Derivation *dv = entity->GetClassDerivations();
const int isTer = entity->IsDerivedFrom(*Terrain::GetClassDerivations())?1:0;
const Point3D &ep = entity->localOrigin.linearPosition;
// tag NON-terrain entities distinctly -- these are the structures
// (garages/walls/props); log their world XZ so we can spawn beside
// one and confirm it is a collision solid the boresight now hits.
DEBUG_STREAM << (isTer ? "[rendent]" : "[rendent-STRUCT]")
<< " class='" << (dv && dv->className ? dv->className : "?")
<< "' terrain=" << isTer
<< " pos=(" << ep.x << "," << ep.y << "," << ep.z << ")"
<< "\n" << std::flush;
}
} }
DPLRenderer::MakeEntityRenderables( DPLRenderer::MakeEntityRenderables(
entity, model_resource, view_type); entity, model_resource, view_type);
+8
View File
@@ -168,6 +168,14 @@ bool BuildTables()
const float *pos = (const float *)(base + p + 48); const float *pos = (const float *)(base + p + 48);
p += (mlen + 3) & ~3u; p += (mlen + 3) & ~3u;
// CENSUS (BT_GROUND_LOG): log EVERY map entity's class + name, so we can see
// what class the garages/structures are (the class-42 filter may drop them).
if (getenv("BT_GROUND_LOG"))
{
ResourceDescription *crd = rf->FindResourceDescription(rid);
DEBUG_STREAM << "[mapent] cls=" << cls << " mflags=" << mflags
<< " name='" << (crd ? crd->resourceName : "?") << "'\n" << std::flush;
}
if (mflags & 0x2) continue; // include marker if (mflags & 0x2) continue; // include marker
if (cls != 42) continue; // terrain/static cultural only if (cls != 42) continue; // terrain/static cultural only
+31
View File
@@ -539,6 +539,37 @@ Logical
return True; return True;
} }
//
// World-STRUCTURE boresight pick. Ray-test the zone's STATIC collision solid
// tree (the world geometry -- garages, walls, props -- that the mech's walk
// already collides against) along the boresight, and hand back the entry point
// of the closest structure the ray strikes. This is the authentic non-mech
// world pick: Mover::FindStaticSolidHitBy is the "test against the static world"
// tail of the engine's own FindBoxedSolidHitBy (MOVER.cpp), which is what the
// mover-vs-world collision uses -- so a shot lands on EXACTLY the geometry that
// blocks the walk (the user's "structures block the mech, but weapons don't").
//
// The ray is WORLD-space (the static tree is world-space, unlike a mech's local
// collision template). FindBoundingBoxHitBy clips the Line's length to the
// entry distance (BoundingBox::HitByBounded -> line->length = enter), so
// line->FindEnd yields the world entry point on the struck solid.
//
Logical
Mech::WorldStructurePick(const Point3D &start, const Vector3D &dir,
Scalar max_range, Point3D *hit_world)
{
Scalar dlen = (Scalar)Sqrt(dir.x*dir.x + dir.y*dir.y + dir.z*dir.z);
if (dlen < 1e-6f)
return False;
Line ray(start, UnitVector(dir.x/dlen, dir.y/dlen, dir.z/dlen), max_range);
BoxedSolid *solid = FindStaticSolidHitBy(&ray); // static world only
if (solid == 0)
return False;
if (hit_world != 0)
ray.FindEnd(hit_world); // clipped length -> entry point
return True;
}
// //
// The roster torso (sinkSourceSubsystem @0x438, ClassID 0xBC5) -- parity with // The roster torso (sinkSourceSubsystem @0x438, ClassID 0xBC5) -- parity with
// the binary's *(mech+0x438). // the binary's *(mech+0x438).
+8
View File
@@ -693,6 +693,14 @@ public:
Logical PickRayHit(const Point3D &start, const Vector3D &dir, Logical PickRayHit(const Point3D &start, const Vector3D &dir,
Scalar max_range, Point3D *hit_world); Scalar max_range, Point3D *hit_world);
// World-STRUCTURE boresight pick: ray-test the zone's STATIC collision solid
// tree (garages/walls/props -- the geometry that blocks the walk) along the
// boresight and return the entry point of the closest structure hit. The
// authentic non-mech world pick (Mover::FindStaticSolidHitBy, the static-world
// half of the engine's FindBoxedSolidHitBy). Defined in mech.cpp.
Logical WorldStructurePick(const Point3D &start, const Vector3D &dir,
Scalar max_range, Point3D *hit_world);
// Per-weapon beam render walk (task #33; extracted task #51 so it runs for // Per-weapon beam render walk (task #33; extracted task #51 so it runs for
// EVERY mech -- player, dummy masters, and MP replicants whose emitters // EVERY mech -- player, dummy masters, and MP replicants whose emitters
// carry replicated discharge state). Defined in mech4.cpp. // carry replicated discharge state). Defined in mech4.cpp.
+83 -4
View File
@@ -4325,6 +4325,39 @@ void
} }
} }
// WORLD-STRUCTURE SWEEP PROBE (BT_WSWEEP, task #50 verification): once a
// second cast a horizontal fan of rays from the eyepoint against the
// STATIC collision solid tree (WorldStructurePick -> FindStaticSolidHitBy)
// and log the hit count + nearest range. Confirms arena1's structures
// (garages/walls) ARE in the collision tree the boresight now queries --
// independent of the render sentinel or interactive aim. Diag, off by default.
if (getenv("BT_WSWEEP"))
{
static float s_ws = 0.0f; s_ws += dt;
if (s_ws >= 1.0f)
{
s_ws = 0.0f;
Point3D eye = localOrigin.linearPosition; eye.y += 30.0f;
int hits = 0; float nearest = 1e30f; float ndir = -1.0f;
for (int a = 0; a < 24; ++a)
{
double th = (double)a * (6.2831853071795862 / 24.0);
Vector3D d((float)cos(th), 0.0f, (float)sin(th));
Point3D hp;
if (WorldStructurePick(eye, d, 2000.0f, &hp))
{
++hits;
float dx = hp.x-eye.x, dz = hp.z-eye.z;
float r = (float)Sqrt(dx*dx + dz*dz);
if (r < nearest) { nearest = r; ndir = (float)(th * 57.29578); }
}
}
DEBUG_STREAM << "[wsweep] eye=(" << eye.x << "," << eye.z
<< ") struct-hits=" << hits << "/24 nearest="
<< (hits ? nearest : -1.0f) << " dir=" << ndir << "deg\n" << std::flush;
}
}
float rs[3], rd[3]; float rs[3], rd[3];
if (!BTGetAimRay(gBTAimX, gBTAimY, rs, rd)) if (!BTGetAimRay(gBTAimX, gBTAimY, rs, rd))
{ {
@@ -4356,16 +4389,62 @@ void
hotPoint = hp; hotPoint = hp;
} }
} }
if (hotTarget != 0) // WORLD-STRUCTURE pick (task #50): ray-test the STATIC collision
// solid tree (garages/walls/props -- the geometry that already
// BLOCKS the mech's walk) along the boresight. A structure CLOSER
// than the mech hit occludes it (the shot stops at the wall); with
// no mech, a structure hit IS the designation. This is the
// authentic non-mech world pick: Mech::WorldStructurePick ->
// Mover::FindStaticSolidHitBy, the "test against the static world"
// half of the engine's own FindBoxedSolidHitBy (MOVER.cpp) -- so a
// shot lands on exactly the geometry the walk collides against
// (the user's "structures block the mech, but weapons pass through").
Point3D structPoint;
float structDist = 1e30f;
bool haveStruct = false;
if (gBTTerrainEntity != 0)
{ {
pickTarget = hotTarget; Point3D sp;
if (WorldStructurePick(rayStart, rayDir, 1200.0f, &sp))
{
float dx = sp.x-rs[0], dy = sp.y-rs[1], dz = sp.z-rs[2];
structDist = dx*dx + dy*dy + dz*dz;
structPoint = sp;
haveStruct = true;
static float s_wp = 0.0f; s_wp += dt;
if (getenv("BT_GROUND_LOG") && s_wp >= 1.0f)
{
s_wp = 0.0f;
DEBUG_STREAM << "[wpick] structure hit at ("
<< sp.x << "," << sp.y << "," << sp.z << ") dist="
<< (float)Sqrt(structDist)
<< (hotTarget ? " (mech also under boresight)" : "")
<< "\n" << std::flush;
}
}
}
if (hotTarget != 0 && (!haveStruct || bestDist <= structDist))
{
pickTarget = hotTarget; // mech is the closest hit
pickPoint = hotPoint; pickPoint = hotPoint;
++gAimHits; ++gAimHits;
} }
else if (haveStruct)
{
// a world structure is the closest hit (garage/wall) -- or it
// occludes a mech behind it. Designate it as the non-mech world
// pick (the geometry rides in the POINT; the sentinel entity has
// no damage-zone table -> no lock ring, HudSim part_013.c:5620).
pickTarget = gBTTerrainEntity;
pickPoint = structPoint;
++gAimGround;
}
else else
{ {
// the ground downrange of the guns (max = the pod's HUD // no mech, no structure -> the flat ground downrange of the
// range scale; past it the shot is a sky shot -> no target) // 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, extern bool BTGroundRayHit(float,float,float, float,float,float,
float, float*,float*,float*); float, float*,float*,float*);
float hx, hy, hz; float hx, hy, hz;