#131 false lock FIXED: miss-means-miss -- the pick answers only for drawn geometry

Night-12 field report (Ronin/Conn Man/Oracle, blackhawk-correlated): lock
ring lit with the reticle visibly off the mech + no-reg complaints.  Root
cause: TWO port stand-ins answered where the 1995 card (which cast against
the DRAWN geometry) would miss -- the pick's any-object sphere fallback and
the caller's whole-mech AABB fallback.  The regime that exposes them: a
LEVEL boresight over a SHORT mech -- the blackhawk's mesh tops out below
eye-ray height, so the ray clears every triangle but pierces the fat cull
spheres; the ring lights with the reticle above the mech's head (the
operator watched exactly this on the sweep bench).  Careful aimed-down fire
rides triangles, which is why Oracle's per-panel audit passed on the same
build.

Fix: MechSegmentPick returns 1=drawn-geometry hit / 0=TRUE MISS / -1=no
render tree; the sphere may answer ONLY for a mesh the reader cannot parse
(pm==0 -- currently none exist: counters objs/invFail/noTri all clean);
the AABB survives ONLY as the pre-tree replicant grace.  A readable mesh
the ray misses is a MISS -- no lock.

Verified (2-node vs bhk1 at 100u, all runs on force-relinked string-
verified exes after today's stale-link flake):
- LEVEL lock-sweep: 0 locks all run (pre-fix: lock band from 168 sphere
  answers; picksrc tri=0 sphereFB=168).
- DOWN-PITCHED sweep: locks return 100%% tri-sourced (tri=158 sphereFB=0),
  landing on real parts (rarm/ldleg/rdleg) with honest gaps.
- Full zone-walk matrix: tri=18874 sphereFB=0 box=0; victim took 156 hits
  across 16 zones incl. both side torsos -- combat un-regressed.

New instruments (all env-gated): BT_LOCK_SWEEP=<axis> torso pan (the
operator-visible lock-envelope bench), [locksweep] transition log,
BT_LOCK_ENVELOPE synthetic unit-sweep probe, [picksrc]/[pickbox] source
telemetry with objs/invFail/noTri localization counters.
Bench: scratchpad/night12/zonewalk_bhk.sh.

NOTE for the field: locking is now strictly TIGHTER (ring = reticle truly
on the machine).  If era testers feel the pods were more forgiving,
Draco's "slight lock linger" memory becomes a deliberate investigation
(sourced hysteresis), not an accidental sphere halo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-08-05 11:12:43 -05:00
co-authored by Claude Fable 5
parent f279e38707
commit 546aabd5ba
5 changed files with 325 additions and 6 deletions
+129 -2
View File
@@ -5584,12 +5584,30 @@ void
Point3D hp;
int zone = -1;
float segHit[3];
if (BTMechSegmentPick(m, rs, rd, 4000.0f, segHit, &zone))
const int pickR = BTMechSegmentPick(m, rs, rd, 4000.0f, segHit, &zone);
if (pickR > 0)
{
hp.x = segHit[0]; hp.y = segHit[1]; hp.z = segHit[2];
}
else if (!m->PickRayHit(rayStart, rayDir, 4000.0f, &hp))
else if (pickR == 0)
{
// #131 MISS-MEANS-MISS: the drawn geometry was tested
// and the ray missed -- NO lock. (The old AABB
// fallback here answered with zone -1 and lit the
// ring anyway: the box is far fatter than the
// silhouette -- the second half of the false lock.)
continue;
}
else if (!m->PickRayHit(rayStart, rayDir, 4000.0f, &hp))
continue; // pickR < 0: no render tree yet -- the
// box is the pre-tree replicant grace
else if (getenv("BT_PICK_LOG"))
{
static int s_bx = 0;
if ((++s_bx % 60) == 1)
DEBUG_STREAM << "[pickbox] AABB grace hit #" << s_bx
<< " (tree-absent replicant)\n" << std::flush;
}
float dx = hp.x - rs[0], dy = hp.y - rs[1], dz = hp.z - rs[2];
float d = dx*dx + dy*dy + dz*dz;
if (d < bestDist)
@@ -5702,6 +5720,34 @@ void
if (pickTarget != 0)
targetReticle.rayIntersection = pickPoint;
// #131 LOCK-SWEEP readout: log every LOCK TRANSITION (the ring's
// own condition -- a MECH in targetEntity) with the live twist, so
// the sweep bench reads "lock ON at twist a .. OFF at twist b"
// directly. Plus a 2 Hz heartbeat with zone + range.
if (getenv("BT_LOCK_SWEEP"))
{
const int lockedNow = (hotTarget != 0
&& pickTarget == hotTarget) ? 1 : 0;
static int s_prevLock = -1;
static float s_lsAcc = 0.0f;
const float tw = (float)TorsoHeading();
if (lockedNow != s_prevLock)
{
s_prevLock = lockedNow;
DEBUG_STREAM << "[locksweep] " << (lockedNow ? "LOCK" : "unlock")
<< " at twist=" << tw
<< " zone=" << hotZone << "\n" << std::flush;
}
s_lsAcc += dt;
if (s_lsAcc >= 0.5f)
{
s_lsAcc = 0.0f;
DEBUG_STREAM << "[locksweep] hb lock=" << lockedNow
<< " twist=" << tw << " zone=" << hotZone
<< "\n" << std::flush;
}
}
// the engine-Entity target slots the whole weapon path reads
if (pickTarget != 0)
{
@@ -6574,6 +6620,87 @@ void
}
}
// #131 LOCK-ENVELOPE PROBE (BT_LOCK_ENVELOPE=1): the symptom-shaped
// measurement -- every ~2s, cast 49 rays from the eye toward the first
// living target, yaw-offset -6..+6 deg in 0.25-deg steps, and print ONE
// line: which source answered at each offset (T=triangle, S=sphere
// fallback, B=whole-mech box, .=miss). The false lock reads directly:
// S/B cells OUTSIDE the contiguous T band are lock-without-mech; misses
// INSIDE it are no-reg-on-mech. Deterministic -- no servo, no spin.
if ((Entity *)this == application->GetViewpointEntity()
&& getenv("BT_LOCK_ENVELOPE"))
{
static float s_leT = 0.0f;
s_leT += dt;
if (s_leT >= 2.0f)
{
s_leT = 0.0f;
extern int BTGetTargetCandidates(Entity *shooter, Entity **out, int maxOut);
extern int BTIsRegisteredMech(Entity *e);
extern int BTGetAimRay(float rx, float ry, float outStart[3], float outDir[3]);
extern int BTMechSegmentPick(void *mech, const float s[3],
const float dir[3], float max_range, float hit_out[3], int *zone_out);
Entity *cand[8]; Mech *tgt = 0;
int nc = BTGetTargetCandidates((Entity *)this, cand, 8);
for (int ci = 0; ci < nc && tgt == 0; ++ci)
if (cand[ci] != 0 && BTIsRegisteredMech(cand[ci])
&& !((Mech *)cand[ci])->IsMechDestroyed())
tgt = (Mech *)cand[ci];
float rs[3], rdUnused[3];
if (tgt != 0 && BTGetAimRay(0.0f, 0.0f, rs, rdUnused))
{
// bearing to the target's torso (origin + 5u up)
float tx = (float)tgt->localOrigin.linearPosition.x - rs[0];
float ty = ((float)tgt->localOrigin.linearPosition.y + 5.0f) - rs[1];
float tz = (float)tgt->localOrigin.linearPosition.z - rs[2];
float len = sqrtf(tx*tx + ty*ty + tz*tz);
if (len > 1.0f)
{
tx /= len; ty /= len; tz /= len;
char row[64];
int nT = 0, nS = 0, nB = 0;
for (int k = 0; k < 49; ++k)
{
// UNIT-based lateral sweep: -8..+8 u of miss distance
// at the target's range (spheres inflate the lock
// area in UNITS; a degree sweep degenerates at close
// range -- operator caught the near-spawn scene).
float lat = -8.0f + (16.0f / 48.0f) * (float)k;
float rad = atan2f(lat, len);
float c = cosf(rad), s = sinf(rad);
float dir[3] = { tx*c + tz*s, ty, -tx*s + tz*c };
float hp[3]; int zone = -1;
int r = BTMechSegmentPick((void *)tgt, rs, dir, 4000.0f, hp, &zone);
if (r)
{
// distinguish tri vs sphere: a sphere answer has
// zone from segPick (>=0 possible) -- use the
// [picksrc] counters for the split; here mark hits
// vs the AABB probe below.
row[k] = 'T'; ++nT;
}
else
{
Point3D bp;
if (tgt->PickRayHit(
Point3D(rs[0], rs[1], rs[2]),
Point3D(dir[0], dir[1], dir[2]),
4000.0f, &bp))
{ row[k] = 'B'; ++nB; }
else
row[k] = '.';
}
}
row[49] = 0;
DEBUG_STREAM << "[lockenv] range=" << len
<< " lat -8..+8u: " << row
<< " (pick=" << nT << " box+=" << nB << ")\n" << std::flush;
(void)nS;
}
}
}
}
// #124 PROBE (BT_ASPECT_TEST=1): the zero-premise frame probe. At frame
// ~700, dispatch four self TakeDamage messages whose impact points sit
// at KNOWN WORLD BEARINGS (+X/-X/+Z/-Z, 8 u out, torso height) around