fix #435 (part 1): delete 17 probes that outlived their closed investigations
Each of these was temporary apparatus added to chase one bug, and each was supposed to be deleted in the commit that fixed it. Fourteen closed issues later they were still here: #337's support/wire-mesh trio, #171's sticky timeline, #119's viewer and entity dumps, #113's phantom probe, and a dozen more. 3,493 lines removed; the client now reads 144 environment variables instead of 161, and 47 temporary probes remain instead of 64. This is not only tidying. Every probe leaves a branch on its hot path when unset, several re-read the environment per call rather than caching, and the volume buries the diagnostics that are actually load-bearing. It is also a headless correctness matter: HeadlessStaticStateAudit reflects over PhysicsDiagnostics' flags to refuse a multi-session host when any is set, and cannot see probes that live outside that owner. Four files went entirely — WalkMissDiagnostic.cs, CollisionMeshWireframe.cs and two test files whose only subject was a deleted probe. TransitionTypes.SetContactPlane also sheds its CallerMemberName / CallerLineNumber parameters, which existed solely for #337's cpSrc= attribution and carried the instruction to strip them with the probe family; no call site passed them, so no behavior changes. F2's collision overlay survives and reverts to its proxy-cylinder form, which is what removing the ACDREAM_WIRE_MESH upgrade means. LaunchOptionsDocumentationTests earned its keep here: it refused the deletion until docs/launch-options.md moved the 17 rows into Retired and the frozen direct-read counts came down (PhysicsEngine.cs to zero, TransitionTypes.cs 3 to 2). The documentation could not drift during a cleanup this wide. The 14 probes that name no owning issue are deliberately NOT deleted. Nothing records when they became safe to remove, and guessing is how a future investigation loses apparatus it needed; #435 stays open for their attribution. Full hermetic suite 15,321 passed / 0 failed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
05bfe8d162
commit
0c5057c9ff
34 changed files with 57 additions and 3495 deletions
|
|
@ -34,12 +34,6 @@ public sealed class MotionTableDispatchSink : IInterpretedMotionSink
|
|||
public bool ApplyMotion(uint motion, float speed)
|
||||
{
|
||||
uint result = _sequencer.PerformMovement(MotionTableMovement.Interpreted(motion, speed));
|
||||
// Bug A probe ([remote-landing-after], ACDREAM_PROBE_REMOTE_LANDING):
|
||||
// the MotionTableManagerError code is discarded by this bool return,
|
||||
// so hand it to the diagnostic latch before it is lost. Self-guarded
|
||||
// — one flag test when the probe is off, no behaviour change either
|
||||
// way. TEMPORARY, strips with the rest of the probe family.
|
||||
PhysicsDiagnostics.RecordRemoteLandingDispatch(motion, result);
|
||||
return result == MotionTableManagerError.Success;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -82,10 +82,6 @@ public sealed class StickyManager
|
|||
if (TargetId == 0)
|
||||
return;
|
||||
|
||||
if (PhysicsDiagnostics.ProbeStickyEnabled)
|
||||
Console.WriteLine(FormattableString.Invariant(
|
||||
$"[sticky] guid=0x{_host.Id:X8} UNSTICK target=0x{TargetId:X8}"));
|
||||
|
||||
TargetId = 0;
|
||||
Initialized = false;
|
||||
_host.ClearTarget();
|
||||
|
|
@ -125,10 +121,6 @@ public sealed class StickyManager
|
|||
Initialized = false;
|
||||
StickyTimeoutTime = _host.CurTime + StickyTime;
|
||||
|
||||
if (PhysicsDiagnostics.ProbeStickyEnabled)
|
||||
Console.WriteLine(FormattableString.Invariant(
|
||||
$"[sticky] guid=0x{_host.Id:X8} STICK target=0x{objectId:X8} tgtR={targetRadius:F2} ownR={_host.Radius:F2} lease={StickyTime:F1}s"));
|
||||
|
||||
// set_target(context_id=0, objectId, radius=0.5, quantum=0.5).
|
||||
_host.SetTarget(0, objectId, 0.5f, 0.5);
|
||||
}
|
||||
|
|
@ -149,10 +141,6 @@ public sealed class StickyManager
|
|||
// C0|C3 clear = cur_time > timeout; ACE `>` too), not >=.
|
||||
if (_host.CurTime > StickyTimeoutTime)
|
||||
{
|
||||
if (PhysicsDiagnostics.ProbeStickyEnabled)
|
||||
Console.WriteLine(FormattableString.Invariant(
|
||||
$"[sticky] guid=0x{_host.Id:X8} LEASE-EXPIRE target=0x{TargetId:X8}"));
|
||||
|
||||
TargetId = 0;
|
||||
Initialized = false;
|
||||
_host.ClearTarget();
|
||||
|
|
@ -183,10 +171,6 @@ public sealed class StickyManager
|
|||
|
||||
if (TargetId != 0)
|
||||
{
|
||||
if (PhysicsDiagnostics.ProbeStickyEnabled)
|
||||
Console.WriteLine(FormattableString.Invariant(
|
||||
$"[sticky] guid=0x{_host.Id:X8} TARGET-{info.Status} teardown target=0x{TargetId:X8}"));
|
||||
|
||||
TargetId = 0;
|
||||
Initialized = false;
|
||||
_host.ClearTarget();
|
||||
|
|
@ -268,9 +252,5 @@ public sealed class StickyManager
|
|||
if (heading < -MoveToMath.Epsilon)
|
||||
heading += 360f;
|
||||
offset.SetHeading(heading);
|
||||
|
||||
if (PhysicsDiagnostics.ProbeStickyEnabled)
|
||||
Console.WriteLine(FormattableString.Invariant(
|
||||
$"[sticky] guid=0x{_host.Id:X8} ADJ dist={dist:F3} delta={delta:F3} speed={speed:F1} hdgDelta={heading:F1} live={(target is not null ? 1 : 0)}"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -650,23 +650,6 @@ public sealed class PhysicsDataCache
|
|||
$"[cell-cache] envCellId=0x{envCellId:X8} physicsPolyCount={cellStruct.PhysicsPolygons?.Count ?? 0} resolvedCount={resolved.Count} bspTotalLeafPolys={bspTotalLeafPolys} bspUnmatchedIds={bspUnmatchedIds} {bsStr} portalCount={portals.Count} visibleCells={visibleCellIds.Count} cellBspRoot={(cellStruct.CellBSP?.Root is null ? "null" : "ok")} worldOrigin=({worldOrigin.X:F2},{worldOrigin.Y:F2},{worldOrigin.Z:F2}) {portalTargets}"));
|
||||
}
|
||||
|
||||
if (PhysicsDiagnostics.ProbeWalkMissEnabled)
|
||||
{
|
||||
int walkableCount = 0;
|
||||
foreach (var entry in WalkMissDiagnostic.EnumerateWalkable(
|
||||
resolved, PhysicsGlobals.FloorZ))
|
||||
walkableCount++;
|
||||
|
||||
Console.Write(System.FormattableString.Invariant(
|
||||
$"[floor-polys] cellId=0x{envCellId:X8} walkableCount={walkableCount}"));
|
||||
foreach (var entry in WalkMissDiagnostic.EnumerateWalkable(
|
||||
resolved, PhysicsGlobals.FloorZ))
|
||||
{
|
||||
Console.Write(System.FormattableString.Invariant(
|
||||
$" [id=0x{entry.PolyId:X4} nz={entry.NormalZ:F3} bbox=({entry.BboxMin.X:F2},{entry.BboxMin.Y:F2})..({entry.BboxMax.X:F2},{entry.BboxMax.Y:F2}) planeZ@center={entry.PlaneZAtBboxCenter:F3}]"));
|
||||
}
|
||||
Console.WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1913,15 +1913,6 @@ public sealed class PhysicsEngine
|
|||
? PhysicsResolveCapture.Snapshot(body)
|
||||
: null;
|
||||
|
||||
// #337 (2026-08-06 — TEMPORARY): arm the [support] probe's
|
||||
// contact-plane provenance latch for this resolve, ahead of everything
|
||||
// including the carried-plane seed below. The seed is itself one of
|
||||
// the ten sites that assert a plane, so it stamps its own name and a
|
||||
// capture can read `cpSrc=ResolveWithTransition:<line>` as "carried
|
||||
// from the body, nothing re-derived it this resolve" without needing a
|
||||
// sentinel value for that case. No-op when the probe is off.
|
||||
PhysicsDiagnostics.BeginContactPlaneAttribution();
|
||||
|
||||
// #345 probe (2026-08-08): reset the per-tick transition-phase trace
|
||||
// buffer. Scoped to exactly one resolve/tick — see
|
||||
// PhysicsDiagnostics.BeginTransitFailTrace. No-op when the probe is
|
||||
|
|
@ -1934,38 +1925,6 @@ public sealed class PhysicsEngine
|
|||
transition.ObjectInfo.StepUpHeight = stepUpHeight;
|
||||
transition.ObjectInfo.StepDownHeight = stepDownHeight;
|
||||
|
||||
// #338 (TEMPORARY): the resolver's own reading, taken where the
|
||||
// values actually land rather than at one of two candidate call
|
||||
// sites. The first attempt probed PlayerMovementController and
|
||||
// printed NOTHING across 11,523 live log lines — the wrong one of
|
||||
// its two resolve calls. A silent probe proves nothing, so this
|
||||
// one sits where every caller must pass through. Filtered to the
|
||||
// player so remotes cannot drown it.
|
||||
// #338 self-report, once per process, UNCONDITIONAL. The probe has
|
||||
// now been silent through two placements, and "no output" cannot
|
||||
// distinguish "this site is never reached" from "the flag is
|
||||
// false". This line answers both directly instead of a third round
|
||||
// of inference. One Interlocked per process; strip with the probe.
|
||||
PhysicsDiagnostics.AnnounceStepHeightProbeOnce(
|
||||
(moverFlags & ObjectInfoState.IsPlayer) != 0);
|
||||
|
||||
// The flag test MUST precede the interpolated string: this site runs
|
||||
// per resolve, and building the detail eagerly cost 128 B/resolve
|
||||
// with the probe OFF — caught by Slice I1's zero-allocation gate,
|
||||
// which is exactly what that gate is for.
|
||||
if (PhysicsDiagnostics.ProbeStepHeightsEnabled
|
||||
&& (moverFlags & ObjectInfoState.IsPlayer) != 0)
|
||||
{
|
||||
// The mover id is REQUIRED here (feedback_probe_identity_attribution):
|
||||
// remote players also carry IsPlayer, and the one early
|
||||
// 0.400 reading this probe caught was nearly misattributed to
|
||||
// the local player for exactly that reason — it was a remote
|
||||
// in its Setup-residency window (AD-68).
|
||||
PhysicsDiagnostics.LogStepHeights(
|
||||
"resolve", stepUpHeight, stepDownHeight,
|
||||
$"mover=0x{movingEntityId:X8} onGround={isOnGround} hasBody={body is not null}");
|
||||
}
|
||||
|
||||
transition.ObjectInfo.StepDown = true;
|
||||
// Fix #42 (2026-05-05): the moving entity's ShadowEntry must be
|
||||
// skipped in FindObjCollisions or the sweep collides with self.
|
||||
|
|
@ -2286,33 +2245,6 @@ public sealed class PhysicsEngine
|
|||
bool collisionNormalValid = ci.CollisionNormalValid;
|
||||
Vector3 collisionNormal = ci.CollisionNormal;
|
||||
|
||||
// #42 diagnostic (2026-05-05): trace airborne sweeps to identify the
|
||||
// source of the ~1m XY drift on retail-observed stationary jumps.
|
||||
// Gated on ACDREAM_AIRBORNE_DIAG=1 and !isOnGround. One line per
|
||||
// resolve call. deltaXY = post - target tells us how much the sweep
|
||||
// diverged from the requested target; for a clean stationary +Z
|
||||
// jump we expect (0,0). cp=valid with a tilted normal would confirm
|
||||
// H1 (initial-overlap depenetration → next-step AdjustOffset projects
|
||||
// the +Z offset along a non-+Z normal). User repros at flat plaza /
|
||||
// east hillside / north hillside; if drift direction tracks terrain
|
||||
// orientation, H1 is the cause; if it tracks actor facing, H2 / H3.
|
||||
if (!isOnGround
|
||||
&& Environment.GetEnvironmentVariable("ACDREAM_AIRBORNE_DIAG") == "1")
|
||||
{
|
||||
var post = sp.CheckPos;
|
||||
float dx = post.X - targetPos.X;
|
||||
float dy = post.Y - targetPos.Y;
|
||||
string cpInfo = ci.ContactPlaneValid
|
||||
? $"valid cpN=({ci.ContactPlane.Normal.X:F3},{ci.ContactPlane.Normal.Y:F3},{ci.ContactPlane.Normal.Z:F3})"
|
||||
: "none";
|
||||
Console.WriteLine(
|
||||
$"[SWEEP] airborne pre=({currentPos.X:F3},{currentPos.Y:F3},{currentPos.Z:F3}) " +
|
||||
$"target=({targetPos.X:F3},{targetPos.Y:F3},{targetPos.Z:F3}) " +
|
||||
$"post=({post.X:F3},{post.Y:F3},{post.Z:F3}) " +
|
||||
$"cell={cellId:X8}->{sp.CheckCellId:X8} ok={ok} " +
|
||||
$"deltaXY=({dx:F3},{dy:F3}) cp={cpInfo}");
|
||||
}
|
||||
|
||||
// L.2a slice 1 (2026-05-12): general-purpose resolver probe.
|
||||
// One line per call when PhysicsDiagnostics.ProbeResolveEnabled
|
||||
// is set (env var ACDREAM_PROBE_RESOLVE=1 at startup, or the
|
||||
|
|
@ -2355,67 +2287,6 @@ public sealed class PhysicsEngine
|
|||
$"[resolve] ent=0x{movingEntityId:X8} in=({currentPos.X:F3},{currentPos.Y:F3},{currentPos.Z:F3}) cell=0x{cellId:X8} tgt=({targetPos.X:F3},{targetPos.Y:F3},{targetPos.Z:F3}) out=({probePost.X:F3},{probePost.Y:F3},{probePost.Z:F3}) cell=0x{sp.CheckCellId:X8} ok={ok} groundedIn={isOnGround} cp={probeCp} hit={probeHit} walkable={sp.HasLastWalkablePolygon}"));
|
||||
}
|
||||
|
||||
// #337 [support] probe (2026-08-06 — TEMPORARY, strip with the
|
||||
// physics-probe family). Runs for EVERY body, not just the player:
|
||||
// a corpse sinking through geometry is a plain physics body with
|
||||
// no player-specific logic, so it is the cheapest possible control
|
||||
// on whether the movement code or the geometry is at fault, and it
|
||||
// is invisible to any player-filtered probe.
|
||||
//
|
||||
// The terrain sample below is INDEPENDENT of whatever the sweep
|
||||
// decided — it asks the landblock directly what the ground height
|
||||
// is under the body's own out-XY. Pairing that with the contact
|
||||
// plane's height at the same XY is what separates "terrain is
|
||||
// holding this body up" from "some object surface is". Read-only:
|
||||
// SampleTerrainWalkable takes no locks, mutates nothing, and is
|
||||
// not on the resolve's committed path.
|
||||
if (PhysicsDiagnostics.ProbeSupportEnabled)
|
||||
{
|
||||
Vector3 outPos = sp.CheckPos;
|
||||
TerrainWalkableSample? terrain =
|
||||
SampleTerrainWalkable(outPos.X, outPos.Y);
|
||||
|
||||
bool terrainSampled = terrain.HasValue
|
||||
&& PhysicsDiagnostics.TryPlaneZAt(
|
||||
terrain.Value.Plane, outPos.X, outPos.Y, out _);
|
||||
float terrainZ = float.NaN;
|
||||
if (terrainSampled)
|
||||
{
|
||||
PhysicsDiagnostics.TryPlaneZAt(
|
||||
terrain!.Value.Plane, outPos.X, outPos.Y, out terrainZ);
|
||||
}
|
||||
|
||||
PhysicsDiagnostics.LogSupport(
|
||||
moverId: movingEntityId,
|
||||
isPlayer: (moverFlags & ObjectInfoState.IsPlayer) != 0,
|
||||
inPos: currentPos,
|
||||
inCell: cellId,
|
||||
targetPos: targetPos,
|
||||
outPos: outPos,
|
||||
outCell: sp.CheckCellId,
|
||||
ok: ok,
|
||||
groundedIn: isOnGround,
|
||||
contact: transition.ObjectInfo.Contact,
|
||||
onWalkable: transition.ObjectInfo.OnWalkable,
|
||||
contactPlaneValid: ci.ContactPlaneValid,
|
||||
contactPlane: ci.ContactPlane,
|
||||
contactPlaneCellId: ci.ContactPlaneCellId,
|
||||
contactPlaneIsWater: ci.ContactPlaneIsWater,
|
||||
contactPlaneSource: PhysicsDiagnostics.ContactPlaneSource,
|
||||
lastKnownValid: ci.LastKnownContactPlaneValid,
|
||||
lastKnownPlane: ci.LastKnownContactPlane,
|
||||
terrainSampled: terrainSampled,
|
||||
terrainZ: terrainZ,
|
||||
terrainNormal: terrain?.Plane.Normal ?? Vector3.Zero,
|
||||
terrainCellId: terrain?.CellId ?? 0u,
|
||||
terrainIsWater: terrain?.IsWater ?? false,
|
||||
walkablePolygon: sp.HasWalkablePolygon,
|
||||
lastWalkablePolygon: sp.HasLastWalkablePolygon,
|
||||
stepUpHeight: stepUpHeight,
|
||||
stepDownHeight: stepDownHeight,
|
||||
velocity: body?.Velocity ?? Vector3.Zero);
|
||||
}
|
||||
|
||||
// Phase W Stage 0 (2026-06-02): [cell-swept] probe — swept cell vs static-derived cell.
|
||||
// Emits before the ResolveResult is built so it shows what BOTH paths would return.
|
||||
// No ResolveCellId call here (it has a CellGraph.CurrCell side effect). No behavior change.
|
||||
|
|
|
|||
|
|
@ -446,29 +446,8 @@ public sealed class CollisionInfo
|
|||
public void SetContactPlane(
|
||||
Plane plane,
|
||||
uint cellId,
|
||||
bool isWater = false,
|
||||
// #337 [support] attribution — recorded on PhysicsDiagnostics, not on
|
||||
// this object; see the comment in the body for why. Compiler-supplied
|
||||
// literals: no call site passes these explicitly and none needs to.
|
||||
[System.Runtime.CompilerServices.CallerMemberName] string sourceMember = "",
|
||||
[System.Runtime.CompilerServices.CallerLineNumber] int sourceLine = 0)
|
||||
bool isWater = false)
|
||||
{
|
||||
// #337 attribution (2026-08-06 — TEMPORARY, strip with the probe
|
||||
// family). Recorded ABOVE the no-op guard on purpose: the meaning is
|
||||
// "the last site that ASSERTED this plane", not "the site that first
|
||||
// differed from the previous value" — a sweep that re-derives the
|
||||
// identical plane it was seeded with has still told you where the
|
||||
// plane comes from, and that is the fact the capture needs.
|
||||
//
|
||||
// It lives on PhysicsDiagnostics, NOT on this object. CollisionInfo's
|
||||
// stored members are compared member-for-member by the flat/graph
|
||||
// differential referee and by the scratch-reset poison test; a
|
||||
// diagnostic field there is state those oracles must then be taught to
|
||||
// ignore, which is how a referee stops refereeing. The latch is
|
||||
// [ThreadStatic] and is armed once per resolve — see
|
||||
// PhysicsDiagnostics.BeginContactPlaneAttribution.
|
||||
PhysicsDiagnostics.RecordContactPlaneSource(sourceMember, sourceLine);
|
||||
|
||||
// A6.P3 slice 2 (2026-05-22): no-op-if-unchanged guard. Closes
|
||||
// issue #96 (per-tick CP-write blowup) without removing the
|
||||
// PhysicsEngine.cs L622 seed that step_up depends on. When the
|
||||
|
|
@ -3880,35 +3859,12 @@ public sealed class Transition
|
|||
var oi = ObjectInfo;
|
||||
var ci = CollisionInfo;
|
||||
|
||||
// #334 candidate-disposition probe (2026-08-06 — TEMPORARY, strip with
|
||||
// the physics-probe family). Filtered to the player mover so NPC /
|
||||
// remote dead-reckoning resolves do not pollute the capture, matching
|
||||
// PhysicsResolveCapture's filter. The zero-entry query below is
|
||||
// reported EXPLICITLY: "this cell yielded nothing" is outcome (b) —
|
||||
// a registration gap — and must appear as data, never as silence.
|
||||
bool reachProbe = PhysicsDiagnostics.ProbeReachEnabled && oi.IsPlayer;
|
||||
|
||||
if (objsInCell.Count == 0)
|
||||
{
|
||||
if (reachProbe)
|
||||
PhysicsDiagnostics.LogReachQuery(
|
||||
oi.SelfEntityId, cellId, sp.StepDown,
|
||||
inCell: 0, exempt: 0, reached: 0, rejectedReach: 0,
|
||||
noShape: 0, tested: 0, blocked: 0,
|
||||
currPos: sp.GlobalCurrCenter[0].Origin);
|
||||
return TransitionState.OK;
|
||||
}
|
||||
|
||||
// #42 diagnostic (2026-05-05): identify which static object causes
|
||||
// the airborne first-frame ~1m push.
|
||||
bool airborneDiag = !oi.Contact
|
||||
&& Environment.GetEnvironmentVariable("ACDREAM_AIRBORNE_DIAG") == "1";
|
||||
Vector3 sphereCheckBefore = sp.CheckPos;
|
||||
|
||||
Vector3 checkPos = sp.GlobalSphere[0].Origin;
|
||||
Vector3 currPos = sp.GlobalCurrCenter[0].Origin;
|
||||
float sphereRadius = sp.GlobalSphere[0].Radius;
|
||||
Vector3 movement = checkPos - currPos;
|
||||
|
||||
// Landblock offsets feed the [resolve-bldg] probe only.
|
||||
engine.TryGetLandblockContext(checkPos.X, checkPos.Y,
|
||||
|
|
@ -3919,38 +3875,8 @@ public sealed class Transition
|
|||
// registry mutations through the same reference mid-iteration.
|
||||
using var nearbyObjs = ShadowEntrySnapshot.Capture(objsInCell);
|
||||
|
||||
// #334 probe tallies — see LogReachQuery for what each one decides.
|
||||
// `rejectedReach` is deliberately still REPORTED and structurally 0
|
||||
// since #333 deleted the filter: a run of the acceptance capture whose
|
||||
// [reach-q] lines read rejectedReach=0 where the pre-fix capture read
|
||||
// 7,225 rejections is the evidence the filter is gone, and dropping the
|
||||
// column would make the two captures incomparable.
|
||||
const int rRejected = 0;
|
||||
int rExempt = 0, rReached = 0, rNoShape = 0,
|
||||
rTested = 0, rBlocked = 0;
|
||||
|
||||
foreach (ShadowEntry obj in nearbyObjs.Entries)
|
||||
{
|
||||
// #337 [geom] probe (2026-08-06 — TEMPORARY, strip with the
|
||||
// physics-probe family). Emitted here, at the TOP of the candidate
|
||||
// loop, so it covers every object the mover comes near regardless
|
||||
// of what the exemptions and the reach filter later do with it —
|
||||
// an object whose collision geometry is absent or displaced must
|
||||
// be reported even when nothing ever tests it. Once per GfxObj per
|
||||
// process: the line describes an ASSET, not a moment.
|
||||
if (obj.CollisionType == ShadowCollisionType.BSP
|
||||
&& PhysicsDiagnostics.ShouldLogGeometry(obj.GfxObjId))
|
||||
{
|
||||
PhysicsDiagnostics.LogGeometryFromAssets(
|
||||
gfxObjId: obj.GfxObjId,
|
||||
entityId: obj.EntityId,
|
||||
flat: engine.DataCache.GetFlatGfxObj(obj.GfxObjId),
|
||||
visual: engine.DataCache.GetVisualBounds(obj.GfxObjId),
|
||||
entityWorldPosition: obj.Position,
|
||||
entityScale: obj.Scale,
|
||||
registeredRadius: obj.Radius);
|
||||
}
|
||||
|
||||
// Self-skip — fix #42 (2026-05-05). Mirrors retail
|
||||
// CObjCell::find_obj_collisions at acclient_2013_pseudo_c.txt
|
||||
// 308931: `physobj != arg2->object_info.object` rejects the
|
||||
|
|
@ -3965,12 +3891,6 @@ public sealed class Transition
|
|||
// gfxObj=0x02000001 at exactly the entity's own position).
|
||||
if (oi.SelfEntityId != 0 && obj.EntityId == oi.SelfEntityId)
|
||||
{
|
||||
if (reachProbe)
|
||||
{
|
||||
rExempt++;
|
||||
ProbeReachCandidate(engine, oi, sp, cellId, in obj,
|
||||
"exempt-self", sphereRadius, movement.Length(), currPos);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -3979,12 +3899,6 @@ public sealed class Transition
|
|||
// dispatch; a true result reaches neither branch.
|
||||
if (oi.MissileIgnore(obj.EntityId, obj.State, obj.Flags))
|
||||
{
|
||||
if (reachProbe)
|
||||
{
|
||||
rExempt++;
|
||||
ProbeReachCandidate(engine, oi, sp, cellId, in obj,
|
||||
"exempt-missile", sphereRadius, movement.Length(), currPos);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -3993,7 +3907,6 @@ public sealed class Transition
|
|||
// Cell membership IS the broad phase, and the BSP walk's own root
|
||||
// node bounding-sphere test — correctly centred, unlike the deleted
|
||||
// filter — is the early-out that made this one unnecessary.
|
||||
if (reachProbe) rReached++;
|
||||
|
||||
// Commit C 2026-04-29 — retail exemption block at the top of
|
||||
// CPhysicsObj::FindObjCollisions
|
||||
|
|
@ -4005,12 +3918,6 @@ public sealed class Transition
|
|||
// so this is a cheap fall-through for them.
|
||||
if (CollisionExemption.ShouldSkip(obj.State, obj.Flags, ObjectInfo.State))
|
||||
{
|
||||
if (reachProbe)
|
||||
{
|
||||
rExempt++;
|
||||
ProbeReachCandidate(engine, oi, sp, cellId, in obj,
|
||||
"exempt-rule", sphereRadius, movement.Length(), currPos);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -4040,12 +3947,6 @@ public sealed class Transition
|
|||
if (etherealForTest && sp.StepDown)
|
||||
{
|
||||
// retail pc:276799 — ethereal target not tested in step-down
|
||||
if (reachProbe)
|
||||
{
|
||||
rExempt++;
|
||||
ProbeReachCandidate(engine, oi, sp, cellId, in obj,
|
||||
"exempt-ethereal-stepdown", sphereRadius, movement.Length(), currPos);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
sp.ObstructionEthereal = etherealForTest;
|
||||
|
|
@ -4096,15 +3997,6 @@ public sealed class Transition
|
|||
// clear (pc:276989) fires after shape tests; we clear early here to
|
||||
// leave the flag clean for the next iteration.
|
||||
sp.ObstructionEthereal = false;
|
||||
// #334 outcome (c): the entry IS a candidate, the reach
|
||||
// filter DID admit it, and it still contributes nothing
|
||||
// because no usable physics BSP resolved for its GfxObj.
|
||||
if (reachProbe)
|
||||
{
|
||||
rNoShape++;
|
||||
ProbeReachCandidate(engine, oi, sp, cellId, in obj,
|
||||
"no-shape", sphereRadius, movement.Length(), currPos);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -4172,12 +4064,6 @@ public sealed class Transition
|
|||
Console.WriteLine(System.FormattableString.Invariant(
|
||||
$"[sph-skip-bsp] obj=0x{obj.EntityId:X8} state=0x{obj.State:X8} — HAS_PHYSICS_BSP_PS dispatches BSP-only"));
|
||||
}
|
||||
if (reachProbe)
|
||||
{
|
||||
rExempt++;
|
||||
ProbeReachCandidate(engine, oi, sp, cellId, in obj,
|
||||
"bsp-only-skip", sphereRadius, movement.Length(), currPos);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -4221,12 +4107,6 @@ public sealed class Transition
|
|||
Console.WriteLine(System.FormattableString.Invariant(
|
||||
$"[cyl-skip-bsp] obj=0x{obj.EntityId:X8} state=0x{obj.State:X8} — HAS_PHYSICS_BSP_PS dispatches BSP-only"));
|
||||
}
|
||||
if (reachProbe)
|
||||
{
|
||||
rExempt++;
|
||||
ProbeReachCandidate(engine, oi, sp, cellId, in obj,
|
||||
"bsp-only-skip", sphereRadius, movement.Length(), currPos);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -4390,138 +4270,15 @@ public sealed class Transition
|
|||
// per-object test body before the outer loop continues.
|
||||
sp.ObstructionEthereal = false;
|
||||
|
||||
// #334: this candidate actually reached a shape test. `result` is
|
||||
// post-Layer-2, i.e. what the query will really act on.
|
||||
if (reachProbe)
|
||||
{
|
||||
rTested++;
|
||||
if (result != TransitionState.OK) rBlocked++;
|
||||
ProbeReachCandidate(engine, oi, sp, cellId, in obj,
|
||||
result switch
|
||||
{
|
||||
TransitionState.OK => "tested-ok",
|
||||
TransitionState.Collided => "tested-collided",
|
||||
TransitionState.Adjusted => "tested-adjusted",
|
||||
TransitionState.Slid => "tested-slid",
|
||||
_ => "tested-invalid",
|
||||
},
|
||||
sphereRadius, movement.Length(), currPos);
|
||||
}
|
||||
|
||||
if (result != TransitionState.OK)
|
||||
{
|
||||
if (airborneDiag)
|
||||
{
|
||||
var sphereCheckAfter = sp.CheckPos;
|
||||
var d = sphereCheckAfter - sphereCheckBefore;
|
||||
Console.WriteLine(
|
||||
$"[SWEEP-OBJ] type={obj.CollisionType} gfxObj=0x{obj.GfxObjId:X8} " +
|
||||
$"objPos=({obj.Position.X:F3},{obj.Position.Y:F3},{obj.Position.Z:F3}) " +
|
||||
$"objR={obj.Radius:F3} cylH={obj.CylHeight:F3} " +
|
||||
$"state={result} pushDelta=({d.X:F3},{d.Y:F3},{d.Z:F3})");
|
||||
}
|
||||
// #334 (TEMPORARY): the early exit is a real query outcome, so
|
||||
// it must be summarised too — otherwise the summary would
|
||||
// under-report exactly the queries where something DID block,
|
||||
// and "blocked" is the control that proves the probe can see a
|
||||
// working collision as well as a missing one.
|
||||
if (reachProbe)
|
||||
PhysicsDiagnostics.LogReachQuery(
|
||||
oi.SelfEntityId, cellId, sp.StepDown,
|
||||
nearbyObjs.Entries.Length, rExempt, rReached,
|
||||
rRejected, rNoShape, rTested, rBlocked, currPos);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (reachProbe)
|
||||
PhysicsDiagnostics.LogReachQuery(
|
||||
oi.SelfEntityId, cellId, sp.StepDown,
|
||||
nearbyObjs.Entries.Length, rExempt, rReached,
|
||||
rRejected, rNoShape, rTested, rBlocked, currPos);
|
||||
|
||||
return TransitionState.OK;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #334 candidate-disposition probe helper (2026-08-06 — TEMPORARY, strip
|
||||
/// with the physics-probe family). Static, and takes everything by
|
||||
/// parameter, so it introduces no closure display class into
|
||||
/// <see cref="FindObjCollisionsInCell"/> — Slice I1's 0 B/resolve budget
|
||||
/// must hold with the probe compiled in and switched off.
|
||||
///
|
||||
/// <para>
|
||||
/// The target's physics-BSP ROOT sphere is resolved through the SAME
|
||||
/// production accessor registration used
|
||||
/// (<c>GetFlatGfxObj(id).PhysicsBsp</c>'s root node, per
|
||||
/// <c>LiveEntityCollisionBuilder</c> and <c>ShadowShapeBuilder</c>), so the
|
||||
/// probe cannot report geometry that differs from what the registry
|
||||
/// actually emitted. AP-156's lesson was exactly that: one resolver.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private static void ProbeReachCandidate(
|
||||
PhysicsEngine engine,
|
||||
ObjectInfo oi,
|
||||
SpherePath sp,
|
||||
uint cellId,
|
||||
in ShadowEntry obj,
|
||||
string disposition,
|
||||
float sphereRadius,
|
||||
float movementLen,
|
||||
Vector3 currPos)
|
||||
{
|
||||
bool xyOnly = obj.CollisionType == ShadowCollisionType.Cylinder;
|
||||
|
||||
Vector3 dOrigin = currPos - obj.Position;
|
||||
float distOrigin = xyOnly
|
||||
? MathF.Sqrt(dOrigin.X * dOrigin.X + dOrigin.Y * dOrigin.Y)
|
||||
: dOrigin.Length();
|
||||
|
||||
// World-space offset from the part ORIGIN (what the DELETED filter
|
||||
// measured against) to the BSP root sphere CENTRE (what it should have
|
||||
// measured against). Both distances are still emitted after #333: the
|
||||
// pair is what proved the diagnosis, and it stays comparable across the
|
||||
// pre-fix and post-fix captures. Zero for non-BSP shapes, whose
|
||||
// Position already IS their centre.
|
||||
Vector3 bspCentreOffset = Vector3.Zero;
|
||||
if (obj.CollisionType == ShadowCollisionType.BSP)
|
||||
{
|
||||
var flatBsp = engine.DataCache?.GetFlatGfxObj(obj.GfxObjId)?.PhysicsBsp;
|
||||
if (flatBsp is { RootIndex: >= 0 })
|
||||
{
|
||||
bspCentreOffset = Vector3.Transform(
|
||||
flatBsp.Nodes[flatBsp.RootIndex].BoundingSphere.Origin * obj.Scale,
|
||||
obj.Rotation);
|
||||
}
|
||||
}
|
||||
|
||||
Vector3 dCentre = currPos - (obj.Position + bspCentreOffset);
|
||||
float distCentre = xyOnly
|
||||
? MathF.Sqrt(dCentre.X * dCentre.X + dCentre.Y * dCentre.Y)
|
||||
: dCentre.Length();
|
||||
|
||||
float budget = sphereRadius + obj.Radius + movementLen + 2f;
|
||||
|
||||
PhysicsDiagnostics.LogReachCandidate(
|
||||
moverId: oi.SelfEntityId,
|
||||
entityId: obj.EntityId,
|
||||
gfxObjId: obj.GfxObjId,
|
||||
cellId: cellId,
|
||||
shape: obj.CollisionType,
|
||||
disposition: disposition,
|
||||
stepDown: sp.StepDown,
|
||||
distOrigin: distOrigin,
|
||||
distCenter: distCentre,
|
||||
objRadius: obj.Radius,
|
||||
sphereRadius: sphereRadius,
|
||||
movementLen: movementLen,
|
||||
budget: budget,
|
||||
centerBudget: budget - 2f,
|
||||
objPos: obj.Position,
|
||||
bspCentreOffset: bspCentreOffset,
|
||||
currPos: currPos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BR-7 / A6.P4 (2026-06-11). The retail BUILDING collision channel —
|
||||
/// <c>CSortCell::find_collisions</c> (Ghidra 0x005340a0): an outdoor
|
||||
|
|
|
|||
|
|
@ -1,173 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// ISSUES #83 H-disambiguation spike (2026-05-21). Pure-function
|
||||
/// aggregator over a <see cref="CellPhysics.Resolved"/> dict — picks
|
||||
/// the nearest walkable-eligible polygon to a given foot position
|
||||
/// (cell-local space) and reports XY-containment + vertical gap so
|
||||
/// the <c>[walk-miss]</c> emission site can disambiguate H1/H2/H3
|
||||
/// without re-walking the dictionary itself.
|
||||
///
|
||||
/// <para>
|
||||
/// Also enumerates walkable polygons for the one-shot
|
||||
/// <c>[floor-polys]</c> dump at cell-cache time.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Spec: <c>docs/superpowers/specs/2026-05-21-indoor-walk-miss-probe-design.md</c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class WalkMissDiagnostic
|
||||
{
|
||||
public readonly struct AggregateResult
|
||||
{
|
||||
public bool Found { get; init; }
|
||||
public ushort PolyId { get; init; }
|
||||
public bool ContainsFootXY { get; init; }
|
||||
public float Dz { get; init; }
|
||||
public float NormalZ { get; init; }
|
||||
}
|
||||
|
||||
public readonly struct WalkableEntry
|
||||
{
|
||||
public ushort PolyId { get; init; }
|
||||
public float NormalZ { get; init; }
|
||||
public Vector3 BboxMin { get; init; }
|
||||
public Vector3 BboxMax { get; init; }
|
||||
public float PlaneZAtBboxCenter { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Walks <paramref name="resolved"/>, considering only polygons
|
||||
/// whose plane normal Z is at least <paramref name="floorZ"/>
|
||||
/// (walkable slope). Selection rule:
|
||||
/// <list type="number">
|
||||
/// <item><description>Polygons whose local-XY bounding box contains
|
||||
/// <paramref name="footLocal"/>'s XY are preferred. Among them,
|
||||
/// the one with smallest <c>|dz|</c> wins.</description></item>
|
||||
/// <item><description>If no poly contains the foot XY, the poly
|
||||
/// with smallest <c>|dz|</c> across all walkable polys wins,
|
||||
/// and <see cref="AggregateResult.ContainsFootXY"/> is false.</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static AggregateResult AggregateNearestWalkable(
|
||||
IReadOnlyDictionary<ushort, ResolvedPolygon> resolved,
|
||||
Vector3 footLocal,
|
||||
float floorZ)
|
||||
{
|
||||
bool bestFound = false;
|
||||
bool bestContainsFootXY = false;
|
||||
ushort bestPolyId = 0;
|
||||
float bestAbsDz = float.MaxValue;
|
||||
float bestSignedDz = 0f;
|
||||
float bestNormalZ = 0f;
|
||||
|
||||
foreach (var kvp in resolved)
|
||||
{
|
||||
var poly = kvp.Value;
|
||||
if (poly.Plane.Normal.Z < floorZ) continue;
|
||||
if (poly.Vertices.Length < 3) continue;
|
||||
|
||||
// Local-XY bounding box.
|
||||
float minX = float.MaxValue, minY = float.MaxValue;
|
||||
float maxX = float.MinValue, maxY = float.MinValue;
|
||||
for (int i = 0; i < poly.Vertices.Length; i++)
|
||||
{
|
||||
var v = poly.Vertices[i];
|
||||
if (v.X < minX) minX = v.X;
|
||||
if (v.Y < minY) minY = v.Y;
|
||||
if (v.X > maxX) maxX = v.X;
|
||||
if (v.Y > maxY) maxY = v.Y;
|
||||
}
|
||||
bool containsFootXY =
|
||||
footLocal.X >= minX && footLocal.X <= maxX &&
|
||||
footLocal.Y >= minY && footLocal.Y <= maxY;
|
||||
|
||||
// Signed vertical gap from foot to the polygon's plane at
|
||||
// the foot's XY: plane.D + n.x*X + n.y*Y + n.z*Z = 0
|
||||
// => planeZ = -(D + n.x*X + n.y*Y) / n.z
|
||||
// => dz = footZ - planeZ
|
||||
float planeZ = -(poly.Plane.D
|
||||
+ poly.Plane.Normal.X * footLocal.X
|
||||
+ poly.Plane.Normal.Y * footLocal.Y)
|
||||
/ poly.Plane.Normal.Z;
|
||||
float signedDz = footLocal.Z - planeZ;
|
||||
float absDz = MathF.Abs(signedDz);
|
||||
|
||||
// Preference: prefer XY-containing polys. Among the
|
||||
// preferred set, smallest |dz| wins.
|
||||
bool preferOver = !bestFound
|
||||
|| (containsFootXY && !bestContainsFootXY)
|
||||
|| (containsFootXY == bestContainsFootXY && absDz < bestAbsDz);
|
||||
|
||||
if (preferOver)
|
||||
{
|
||||
bestFound = true;
|
||||
bestContainsFootXY = containsFootXY;
|
||||
bestPolyId = kvp.Key;
|
||||
bestAbsDz = absDz;
|
||||
bestSignedDz = signedDz;
|
||||
bestNormalZ = poly.Plane.Normal.Z;
|
||||
}
|
||||
}
|
||||
|
||||
return new AggregateResult
|
||||
{
|
||||
Found = bestFound,
|
||||
PolyId = bestPolyId,
|
||||
ContainsFootXY = bestContainsFootXY,
|
||||
Dz = bestSignedDz,
|
||||
NormalZ = bestNormalZ,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates walkable-eligible polygons (normal Z >= floorZ)
|
||||
/// with their local-XY bounding boxes and plane Z at the bbox
|
||||
/// center. Used by the one-shot <c>[floor-polys]</c> cell-load
|
||||
/// dump.
|
||||
/// </summary>
|
||||
public static IEnumerable<WalkableEntry> EnumerateWalkable(
|
||||
IReadOnlyDictionary<ushort, ResolvedPolygon> resolved,
|
||||
float floorZ)
|
||||
{
|
||||
foreach (var kvp in resolved)
|
||||
{
|
||||
var poly = kvp.Value;
|
||||
if (poly.Plane.Normal.Z < floorZ) continue;
|
||||
if (poly.Vertices.Length < 3) continue;
|
||||
|
||||
float minX = float.MaxValue, minY = float.MaxValue, minZ = float.MaxValue;
|
||||
float maxX = float.MinValue, maxY = float.MinValue, maxZ = float.MinValue;
|
||||
for (int i = 0; i < poly.Vertices.Length; i++)
|
||||
{
|
||||
var v = poly.Vertices[i];
|
||||
if (v.X < minX) minX = v.X;
|
||||
if (v.Y < minY) minY = v.Y;
|
||||
if (v.Z < minZ) minZ = v.Z;
|
||||
if (v.X > maxX) maxX = v.X;
|
||||
if (v.Y > maxY) maxY = v.Y;
|
||||
if (v.Z > maxZ) maxZ = v.Z;
|
||||
}
|
||||
|
||||
float cx = (minX + maxX) * 0.5f;
|
||||
float cy = (minY + maxY) * 0.5f;
|
||||
float planeZAtCenter = -(poly.Plane.D
|
||||
+ poly.Plane.Normal.X * cx
|
||||
+ poly.Plane.Normal.Y * cy)
|
||||
/ poly.Plane.Normal.Z;
|
||||
|
||||
yield return new WalkableEntry
|
||||
{
|
||||
PolyId = kvp.Key,
|
||||
NormalZ = poly.Plane.Normal.Z,
|
||||
BboxMin = new Vector3(minX, minY, minZ),
|
||||
BboxMax = new Vector3(maxX, maxY, maxZ),
|
||||
PlaneZAtBboxCenter = planeZAtCenter,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue