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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -99,33 +99,6 @@ public static class RenderingDiagnostics
|
|||
public static bool ProbeVisibilityEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_VIS") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// #119-residual viewer/flood capture (2026-06-11): one <c>[viewer]</c>
|
||||
/// line per CHANGE of (root cell, flood size, OutsideView poly count,
|
||||
/// player cell), with the projection EYE at mm precision on every line —
|
||||
/// the capture half of the tower-ascent capture→replay loop
|
||||
/// (TowerAscentReplayTests replays the captured pairs deterministically).
|
||||
/// Light: silent while the visibility state is stable; a tower climb
|
||||
/// emits a few dozen lines. Initial state from
|
||||
/// <c>ACDREAM_PROBE_VIEWER=1</c>.
|
||||
/// </summary>
|
||||
public static bool ProbeViewerEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_VIEWER") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// #131 (2026-06-12) outside-stage dynamics probe. When true, the renderer
|
||||
/// emits one <c>[outstage]</c> line per CHANGE of the outside-stage
|
||||
/// routing + per-slice cone verdict set under an interior root (which
|
||||
/// outdoor dynamics were routed to the landscape slice, which survived the
|
||||
/// slice viewcone), and GameWindow emits one <c>[outstage-pt]</c> line per
|
||||
/// change of the slice Scene-particle id set + matched-emitter count.
|
||||
/// Built for the portal-swirl-missing-through-doorway capture. Light:
|
||||
/// silent while the set is stable. Initial state from
|
||||
/// <c>ACDREAM_PROBE_OUTSTAGE=1</c>.
|
||||
/// </summary>
|
||||
public static bool ProbeOutStageEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_OUTSTAGE") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Phase U.4c (2026-05-31) flap-convergence probe. When true, the portal
|
||||
/// visibility pass emits, EVERY frame the camera root is an indoor cell, a
|
||||
|
|
@ -144,21 +117,6 @@ public static class RenderingDiagnostics
|
|||
public static bool ProbeFlapEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_FLAP") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Issue #78 (2026-05-31) cell-shell render probe. When true,
|
||||
/// <c>EnvCellRenderer.Render</c> emits one <c>[shell]</c> line per opaque-pass
|
||||
/// call: per visible (filtered) cell — is it present in the prepared snapshot,
|
||||
/// how many gfxObjs + instances, and per-gfxObj batch count / index count /
|
||||
/// translucent / zero-bindless-handle (missing texture) — plus the pass totals.
|
||||
/// This directly answers WHY interior walls/ceilings don't appear: no geometry
|
||||
/// prepared for the cell (cell absent / 0 instances), drawn-but-invisible
|
||||
/// (zeroHandle / translucent against the clear color), or prepared+drawn (so the
|
||||
/// fault is elsewhere — depth/occlusion). Throwaway apparatus — strip once the
|
||||
/// indoor-enclosure render is fixed. Initial state from <c>ACDREAM_PROBE_SHELL=1</c>.
|
||||
/// </summary>
|
||||
public static bool ProbeShellEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_SHELL") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Flap root-cause apparatus (2026-06-07). When true, the indoor render path emits ONE
|
||||
/// <c>[pv-input]</c> line per frame with the EXACT PortalVisibilityBuilder.Build inputs at HIGH
|
||||
|
|
@ -202,21 +160,6 @@ public static class RenderingDiagnostics
|
|||
public static bool ProbeClipRouteEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_CLIPROUTE") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// #105 white-indoor-textures apparatus (2026-06-10). When true, <c>WbMeshAdapter.Tick</c>
|
||||
/// emits one <c>[tex-flush]</c> line whenever the staged-texture-update picture changes:
|
||||
/// pending layer updates across all shared atlases BEFORE and AFTER the per-frame
|
||||
/// <c>ObjectMeshManager.GenerateMipmaps()</c> flush, plus arrays-with-pending / total-array
|
||||
/// counts. The broken contract this pins: <c>TextureAtlasManager.AddTexture</c> only STAGES
|
||||
/// pixel data (PBO + pending list); without the per-frame flush (WB GameScene.cs:975) the
|
||||
/// data never reaches the GL texture and the batch samples undefined content behind a valid
|
||||
/// bindless handle — the classic white walls. A healthy run shows <c>after=0</c> on every
|
||||
/// line; a stuck <c>before==after>0</c> at standstill is the #105 mechanism live.
|
||||
/// Initial state from <c>ACDREAM_PROBE_TEXFLUSH=1</c>.
|
||||
/// </summary>
|
||||
public static bool ProbeTexFlushEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_TEXFLUSH") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Bounded-propagation port apparatus (2026-06-08). When true, PortalVisibilityBuilder.Build emits
|
||||
/// one [portal-churn] summary line per call: per-cell pop count (re-pops = churn), total re-enqueues,
|
||||
|
|
@ -228,52 +171,6 @@ public static class RenderingDiagnostics
|
|||
public static bool ProbePortalChurnEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_PORTAL_CHURN") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// BR-2 phantom-site probe (2026-06-11; plan
|
||||
/// <c>docs/plans/2026-06-11-building-render-port-plan.md</c> §BR-2 first
|
||||
/// task). The BR-1 pre-check proved the #113 phantom residual cannot be
|
||||
/// GfxObj portal fills (never extracted); the surviving suspects are
|
||||
/// cell-side. When true, <c>RetailPViewRenderer</c> emits, print-on-change
|
||||
/// per cell: <c>[phantom-shell]</c> — per shell-pass cell, the clip-enable
|
||||
/// state and each drawn slice's slot + plane count, flagging the pass-all
|
||||
/// cases (NoClipSlice fallback for slot-less cells; assembler slot-0
|
||||
/// scissor fallback) — and <c>[phantom-objs]</c> — per object-list cell,
|
||||
/// the entity-bucket size drawn unclipped/un-viewcone'd. Reproducing the
|
||||
/// phantom with this on pins which mechanism draws it (shells → BR-2/BR-3;
|
||||
/// statics → BR-5). Throwaway apparatus — strip when the phantom closes.
|
||||
/// Initial state from <c>ACDREAM_PROBE_PHANTOM=1</c>.
|
||||
/// </summary>
|
||||
public static bool ProbePhantomEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_PHANTOM") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// #133 A7 (2026-06-13) dungeon-lighting objective probe. When true,
|
||||
/// the per-frame scene-lighting build emits ONE <c>[light]</c> line
|
||||
/// roughly every second (wall-clock rate-limited like WB-DIAG) via
|
||||
/// <see cref="EmitLight"/>:
|
||||
/// <code>
|
||||
/// [light] insideCell=<bool> ambient=(r,g,b) sun=<intensity>
|
||||
/// registeredLights=<N> activeLights=<uCellAmbient.w> playerCell=0x<id>
|
||||
/// </code>
|
||||
/// This is the self-verification signal for the dungeon-dim question:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><c>insideCell=true ambient=(0.20,0.20,0.20) sun=0</c>
|
||||
/// confirms the indoor branch fired (retail flat ambient, sun killed).</description></item>
|
||||
/// <item><description><c>registeredLights</c> is the count of dat-baked
|
||||
/// point/spot lights (<c>Setup.Lights</c>) registered with the
|
||||
/// <c>LightManager</c> — if this is 0 in a dungeon, the cell's static
|
||||
/// objects carry no baked torches (so the only illumination IS the
|
||||
/// 0.2 ambient → dim).</description></item>
|
||||
/// <item><description><c>activeLights</c> is <c>uCellAmbient.w</c> — the
|
||||
/// shader's active-slot count, which INCLUDES the (zeroed) sun slot
|
||||
/// indoors. So <c>activeLights=1 registeredLights=0</c> = "only the dead
|
||||
/// sun slot, no torches in range".</description></item>
|
||||
/// </list>
|
||||
/// Output-only, inert when off. Initial state from <c>ACDREAM_PROBE_LIGHT=1</c>.
|
||||
/// </summary>
|
||||
public static bool ProbeLightEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIGHT") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// A7.L1 (2026-07-06) light-pool SET-COMPOSITION probe — the apparatus the
|
||||
/// <c>[light]</c> counts could not provide (the #176/#177 discriminator: the bug
|
||||
|
|
@ -496,94 +393,8 @@ public static class RenderingDiagnostics
|
|||
/// </summary>
|
||||
internal static void ResetVisibilityProbeForTests() => _lastVisRootCellId = 0;
|
||||
|
||||
// Wall-clock rate-limit gate for EmitLight. Ticks (100 ns) is plenty —
|
||||
// we only need ~1 Hz and avoid a Stopwatch allocation/field. Sentinel 0
|
||||
// = "never emitted" so the first call always fires.
|
||||
private static long _lastLightEmitTicks;
|
||||
private const long LightEmitIntervalTicks = 10_000_000; // 1 s in 100-ns ticks
|
||||
|
||||
/// <summary>
|
||||
/// #133 A7 — emit ONE rate-limited <c>[light]</c> line describing the
|
||||
/// current scene-lighting state, followed (when <paramref name="lights"/>
|
||||
/// is supplied) by up to three <c>[light-detail]</c> lines for the nearest
|
||||
/// ACTIVE point/spot lights. Cheap no-op when
|
||||
/// <see cref="ProbeLightEnabled"/> is false; otherwise fires at most
|
||||
/// once per second. Pull the values from the spot where
|
||||
/// <c>GameWindow.UpdateSunFromSky</c> set <c>Lighting.CurrentAmbient</c>
|
||||
/// / <c>Lighting.Sun</c> and where <c>SceneLightingUbo.Build</c> computed
|
||||
/// the active-slot count.
|
||||
/// <para>
|
||||
/// The <c>[light-detail]</c> lines are the answer to the "candle-spotlight"
|
||||
/// question — they expose each torch's REAL dat-derived runtime values
|
||||
/// (<c>range=</c> Falloff metres, <c>intensity=</c>, <c>cone=</c> radians,
|
||||
/// <c>color=</c>, <c>distToViewer=</c>) so it is visible in launch.log
|
||||
/// whether dungeon torches are tiny-range points or wide cones and at what
|
||||
/// intensity — without a screenshot:
|
||||
/// <code>
|
||||
/// [light-detail] kind=Point range=<Falloff m> intensity=<I> cone=<rad> color=(r,g,b) distToViewer=<m>
|
||||
/// </code>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="insideCell">The <c>playerInsideCell</c> value driving the indoor branch.</param>
|
||||
/// <param name="ambientR">Cell ambient red (xyz of <c>uCellAmbient</c>).</param>
|
||||
/// <param name="ambientG">Cell ambient green.</param>
|
||||
/// <param name="ambientB">Cell ambient blue.</param>
|
||||
/// <param name="sunIntensity">The sun <c>LightSource.Intensity</c> (0 indoors).</param>
|
||||
/// <param name="registeredLights">Total point/spot lights registered with the LightManager.</param>
|
||||
/// <param name="activeLights"><c>uCellAmbient.w</c> — shader active-slot count (includes the zeroed sun slot indoors).</param>
|
||||
/// <param name="playerCellId">The player's current cell id (0 if unresolved → outside).</param>
|
||||
/// <param name="lights">The ticked <c>LightManager</c> (its <c>Active</c> list, sorted nearest-first by the
|
||||
/// just-completed Tick). When non-null, drives the <c>[light-detail]</c> lines. Optional so existing call
|
||||
/// sites / tests that only want the aggregate line keep compiling.</param>
|
||||
public static void EmitLight(bool insideCell,
|
||||
float ambientR, float ambientG, float ambientB,
|
||||
float sunIntensity,
|
||||
int registeredLights,
|
||||
int activeLights,
|
||||
uint playerCellId,
|
||||
AcDream.Core.Lighting.LightManager? lights = null)
|
||||
{
|
||||
if (!ProbeLightEnabled) return;
|
||||
|
||||
long now = DateTime.UtcNow.Ticks;
|
||||
if (_lastLightEmitTicks != 0 && (now - _lastLightEmitTicks) < LightEmitIntervalTicks)
|
||||
return;
|
||||
_lastLightEmitTicks = now;
|
||||
|
||||
var ci = System.Globalization.CultureInfo.InvariantCulture;
|
||||
Console.WriteLine(string.Format(ci,
|
||||
"[light] insideCell={0} ambient=({1:0.###},{2:0.###},{3:0.###}) sun={4:0.###} registeredLights={5} activeLights={6} playerCell=0x{7:X8}",
|
||||
insideCell, ambientR, ambientG, ambientB, sunIntensity,
|
||||
registeredLights, activeLights, playerCellId));
|
||||
|
||||
// #133 A7 (2026-06-13) — per-light detail for the "spotlight bubble"
|
||||
// question. Dump the actual runtime dat-derived values of the nearest
|
||||
// ~3 ACTIVE point/spot lights so the real Falloff/Intensity/ConeAngle
|
||||
// are visible in launch.log (are torch ranges 1m or 10m? points or
|
||||
// spots? what intensity?). The sun (Directional, slot 0) is skipped —
|
||||
// it carries no Range/cone meaning. DistSq is already cached by
|
||||
// LightManager.Tick this frame, so the active list is sorted nearest-
|
||||
// first; we just take the first few non-directional entries.
|
||||
if (lights is null) return;
|
||||
var active = lights.Active;
|
||||
int shown = 0;
|
||||
const int MaxDetail = 3;
|
||||
for (int i = 0; i < active.Length && shown < MaxDetail; i++)
|
||||
{
|
||||
var ls = active[i];
|
||||
if (ls is null) continue;
|
||||
if (ls.Kind == AcDream.Core.Lighting.LightKind.Directional) continue;
|
||||
|
||||
float dist = ls.DistSq >= 0f ? MathF.Sqrt(ls.DistSq) : 0f;
|
||||
Console.WriteLine(string.Format(ci,
|
||||
"[light-detail] kind={0} range={1:0.###} intensity={2:0.###} cone={3:0.####} color=({4:0.###},{5:0.###},{6:0.###}) distToViewer={7:0.###} owner=0x{8:X8} cell=0x{9:X8} dyn={10}",
|
||||
ls.Kind, ls.Range, ls.Intensity, ls.ConeAngle,
|
||||
ls.ColorLinear.X, ls.ColorLinear.Y, ls.ColorLinear.Z, dist,
|
||||
ls.OwnerId, ls.CellId, ls.IsDynamic ? 1 : 0));
|
||||
shown++;
|
||||
}
|
||||
}
|
||||
|
||||
// Wall-clock rate-limit gate for EmitIndoorLight (shares the 1 s interval).
|
||||
private static long _lastIndoorLightEmitTicks;
|
||||
|
||||
|
|
@ -694,27 +505,6 @@ public static class RenderingDiagnostics
|
|||
/// </summary>
|
||||
public static bool IsEnvCellId(ulong id) => (id & 0xFFFFu) >= 0x0100u;
|
||||
|
||||
/// <summary>
|
||||
/// #119 tower-staircase decisive probe (2026-06-11). Comma-separated
|
||||
/// Setup / GfxObj source ids (hex, optional 0x prefix) from
|
||||
/// <c>ACDREAM_DUMP_ENTITY</c>. Any <c>WorldEntity</c> whose
|
||||
/// <c>SourceGfxObjOrSetupId</c> is in this set emits:
|
||||
/// (a) a <c>[dump-entity] HYDRATE</c> dump at MeshRef construction time
|
||||
/// (<c>GameWindow.BuildInteriorEntitiesForStreaming</c>) — per-part
|
||||
/// placement-frame translations + dropped-part accounting — discriminating
|
||||
/// hydration-time corruption (H-A: SetupMesh.Flatten identity fallback /
|
||||
/// silent gfx-null part drops under degraded dat reads);
|
||||
/// (b) a <c>[dump-entity] DRAW</c> dump in <c>WbDrawDispatcher</c> at first
|
||||
/// draw — live MeshRefs translations + Tier-1 classification cache state —
|
||||
/// re-emitted compactly whenever that state changes (H-B: stale/partial
|
||||
/// cached batch set); and
|
||||
/// (c) rate-limited <c>[dump-entity] WALK-REJECT</c> lines when the
|
||||
/// dispatcher's walk filters the entity out (absence-of-draw attribution).
|
||||
/// Empty set = probe off; every call site early-outs on <c>Count == 0</c>.
|
||||
/// </summary>
|
||||
public static IReadOnlySet<uint> DumpEntitySourceIds { get; } =
|
||||
ParseDumpEntityIds(Environment.GetEnvironmentVariable("ACDREAM_DUMP_ENTITY"));
|
||||
|
||||
/// <summary>
|
||||
/// Parse the <c>ACDREAM_DUMP_ENTITY</c> value: comma-separated hex ids,
|
||||
/// optional 0x prefix, whitespace tolerated, malformed segments ignored
|
||||
|
|
@ -794,52 +584,4 @@ public static class RenderingDiagnostics
|
|||
/// </summary>
|
||||
public static string? FrameHistoryPath { get; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_FRAME_HISTORY");
|
||||
|
||||
// ── #337 collision-mesh wireframe (2026-08-06 — TEMPORARY) ──────────────
|
||||
//
|
||||
// The F2 collision overlay already existed, but for a BSP object it drew a
|
||||
// proxy cylinder sized from the REGISTERED BROADPHASE RADIUS. That shows
|
||||
// where the collision system thinks the object roughly is; it cannot show
|
||||
// where the collision SURFACES are, which is the only thing that answers
|
||||
// "is the collision geometry where the visual geometry is". The knobs
|
||||
// below turn F2 into the real answer: the actual physics-BSP polygon
|
||||
// edges, in world space, next to the same object's visual mesh box.
|
||||
//
|
||||
// Off by default, so F2 keeps its old cheap behaviour for anyone who wants
|
||||
// it and this costs nothing until asked for.
|
||||
|
||||
/// <summary>
|
||||
/// When true, the F2 collision overlay draws each nearby object's REAL
|
||||
/// physics-BSP polygon edges (cyan) and, beside them, the same object's
|
||||
/// visual mesh bounding box (magenta), plus the terrain triangle under the
|
||||
/// player (yellow). Any separation between the cyan surfaces and the
|
||||
/// object you can see is the "collision is not where the visual is"
|
||||
/// defect, read directly off the screen instead of inferred from a log.
|
||||
/// Initial state from <c>ACDREAM_WIRE_MESH=1</c>.
|
||||
/// TEMPORARY — strip with the #337 probe family.
|
||||
/// </summary>
|
||||
public static bool CollisionMeshWireframeEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_WIRE_MESH") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Radius in metres around the player within which
|
||||
/// <see cref="CollisionMeshWireframeEnabled"/> resolves polygon geometry.
|
||||
/// A whole landblock of rock is far more geometry than a line list wants;
|
||||
/// 30 m covers everything you can wedge against. Override with
|
||||
/// <c>ACDREAM_WIRE_RADIUS=<metres></c>.
|
||||
/// </summary>
|
||||
public static float CollisionMeshWireframeRadius { get; set; } =
|
||||
ParsePositiveFloat(
|
||||
Environment.GetEnvironmentVariable("ACDREAM_WIRE_RADIUS"),
|
||||
fallback: 30f);
|
||||
|
||||
private static float ParsePositiveFloat(string? raw, float fallback)
|
||||
=> float.TryParse(
|
||||
raw,
|
||||
System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out float value)
|
||||
&& value > 0f
|
||||
? value
|
||||
: fallback;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue