fix(vendor): the range watcher measures retail's cylinder-gap — the acceptance-band self-close is dead (Fable, from the live trace)
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run

The vendor-verify gate's trace proved the entire walk-to-use chain
succeeding — arrival natural, Use dispatched, UseDone, the full
117-item ApproachVendor — and the panel still never appeared: the
range watcher's plain center-distance shortcut (AP-160) closed the
session the same frame it opened. The walk stops where the server
accepts (cylinder-gap: center minus both radii), which lands ~4.3 m
center against the vendor's authored 3 m — inside the acceptance
band, outside the watcher's bare-center check.

EnforceRange now measures cylinder-gap with both radii resolved
through the SAME ResolveObjectTableHost seam the movement arrival
uses — the seam whose absence was AP-160's original justification,
created by the previous commit's fix. The watcher and the walk agree
by construction. Unresolvable hosts degrade an operand to center
distance (close-early only, never holding a session ACE ended);
heights pass 0 (the host surface exposes radius only). AP-160
narrowed; #352 files the deferred cylinder-vs-center discriminating
unit test (needs a 38-member host fake; the live gate covered the
behavior today).

Clean-room complete solution: 11,536 passed / 4 skipped / 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-08 17:24:49 +02:00
parent 02b735ba4a
commit 1688863366
3 changed files with 43 additions and 16 deletions

View file

@ -24,6 +24,16 @@ What does NOT go here:
- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending. - Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
- Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed. - Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed.
## #352 — Vendor range-watcher cylinder metric: discriminating unit test deferred
**Status:** OPEN (filed 2026-08-08). The EnforceRange cylinder-gap fix
(acceptance-band self-close, vendor-verify-gate.log evidence) landed with
the existing 17-range/lifecycle tests green but WITHOUT a unit test that
discriminates cylinder-vs-center (needs an IPhysicsObjHost fake — 38
members — bound via BindObjectTableHostResolver; center>radius while
cylinder<=radius stays open, radii-sabotage closes). Write it next
session; the live gate covered the behavior.
## #351 — LandblockBuildOriginTests.FarLoad_StripsEnvCellsAndPhysics flake (Debug, load-sensitive) ## #351 — LandblockBuildOriginTests.FarLoad_StripsEnvCellsAndPhysics flake (Debug, load-sensitive)
**Status:** OPEN (filed 2026-08-08). Seen three times today in Debug runs **Status:** OPEN (filed 2026-08-08). Seen three times today in Debug runs

File diff suppressed because one or more lines are too long

View file

@ -87,18 +87,15 @@ public static class RuntimeVendorRangeQuery
/// </para> /// </para>
/// ///
/// <para> /// <para>
/// <b>Distance metric divergence (register AP-160):</b> retail/ACE close /// <b>Distance metric (AP-160 residual narrowed 2026-08-08):</b> the
/// on CYLINDER-GAP distance — both objects' own collision radius and /// watcher measures retail's CYLINDER-GAP distance
/// height subtracted from the center distance /// (<c>Position::cylinder_distance</c>) with both radii resolved through
/// (<c>Position::cylinder_distance</c>/ACE's <c>GetCylinderDistance</c>). /// the same <c>ResolveObjectTableHost</c> seam the movement arrival
/// Runtime does not resolve a live per-NPC collision radius/height /// uses — the plain-center-distance shortcut self-closed sessions the
/// outside the App-layer's Setup-cylinder resolver /// server had just opened (the walk-to-use acceptance band; see the
/// (<c>WorldSelectionQuery</c>, App-only — out of reach per the /// inline comment at the range check). Heights pass 0 (the host surface
/// Core-structure rules), so this uses plain 3D center-to-center /// exposes radius only); an unresolvable host degrades that operand to
/// distance via <see cref="ObjectRangeMath.ObjectsInRange"/>'s /// center distance, which only ever closes EARLY.
/// <c>useRadii: false</c> branch instead. Effect: the panel can close up
/// to (player radius + vendor radius) sooner than exact retail —
/// typically well under a meter for a two-legged NPC.
/// </para> /// </para>
/// </summary> /// </summary>
public static void EnforceRange(GameRuntime runtime) public static void EnforceRange(GameRuntime runtime)
@ -148,15 +145,35 @@ public static class RuntimeVendorRangeQuery
// an EXACT position match. That is retail's own behavior for a // an EXACT position match. That is retail's own behavior for a
// radius-0 (or unauthored) handler, not a bug to paper over. // radius-0 (or unauthored) handler, not a bug to paper over.
float useRadius = vendorRecord.Snapshot.UseRadius ?? 0f; float useRadius = vendorRecord.Snapshot.UseRadius ?? 0f;
// #vendor-verify-gate 2026-08-08: the watcher MUST measure with the
// same metric the walk-to-use arrival and ACE's acceptance use —
// cylinder-gap (center distance minus both bodies' radii), retail's
// Position::cylinder_distance. With plain center distance there is
// an acceptance band (bare UseRadius .. UseRadius + both radii,
// observed live: open accepted at 4.29 m center vs radius 3) where
// the server OPENS the session and this watcher instantly
// self-closed it — the panel flashed shut within one frame. The
// radii come from the same GetObjectA seam the movement arrival
// resolves through (ResolveObjectTableHost, lazy-minimal hosts
// included); an unresolvable host contributes 0 and degrades to
// center distance, which can only close EARLY, never hold a session
// ACE has ended.
float playerRadius =
runtime.EntityObjects.Physics.ResolveObjectTableHost(playerGuid)
?.Radius ?? 0f;
float vendorRadius =
runtime.EntityObjects.Physics.ResolveObjectTableHost(vendorId)
?.Radius ?? 0f;
bool inRange = ObjectRangeMath.ObjectsInRange( bool inRange = ObjectRangeMath.ObjectsInRange(
AbsolutePosition(playerPosition), AbsolutePosition(playerPosition),
0f, playerRadius,
0f, 0f,
AbsolutePosition(vendorPosition), AbsolutePosition(vendorPosition),
0f, vendorRadius,
0f, 0f,
useRadius, useRadius,
useRadii: false, useRadii: true,
ignoreZDelta: false); ignoreZDelta: false);
if (!inRange) if (!inRange)