Commit graph

592 commits

Author SHA1 Message Date
Erik
78b981cca0 fix(runtime): transient collision-seal failure no longer terminal for login (#357)
Login could hang forever at reveal ready=True with the world never
revealed: UI and sky drawn, geometry absent, client healthy. The player's
first-entry conductor was being terminally dropped by a TRANSIENT
condition.

Mechanism, pinned by probes: the C3c-F2 rearm guard validates the exact
destination cell's prefix admissibility before moving the dormant lease
out of AwaitingCell, but the placement transaction's ring search touches
NEIGHBOUR landblocks and TrySealCollisionEvaluationAuthority covers every
touched prefix. A hard login recenter admits nine landblocks at once, so
a rearm taken while a neighbour's admission was still registered passed
the guard and failed the seal. The operation was left in
AwaitingPreparation, IsDormantLocalActivationAwaitingCell went false, and
EvaluateActivation had no way to say 'retry' - it fell through to
RejectedAuthority, which RuntimeFirstEntryDriveController treats as
terminal. The local player left the pump (pending=0), the movement
controller never published, auto-entry never fired, the reveal never
completed. Timing-flipped: the same binary worked when the rearm landed
outside a neighbour's admission window, then lost that race consistently.

Fix is classification, not state: EvaluateActivation reports DeferredCell
when the abort happens while the dormant lease is still current
(IsDormantLocalActivationLeaseCurrent), so the conductor keeps retrying.
The operation deliberately stays in AwaitingPreparation - the retry
re-runs the full evaluation against fresh state, which is the recovery
contract the publication-state tests already pin (the SAME token
evaluates Evaluated once the authority settles). Genuine discards still
report RejectedAuthority. A first attempt that re-parked the lease to
AwaitingCell was rejected by the test matrix: recovery would then need
the rearm gate, which is stricter than the seal, and the
reentrant-restriction-mutation recoveries hung in DeferredCell.

Seven publication-state tests move their transient-abort assertion from
RejectedAuthority to DeferredCell; the two genuinely-terminal tests
(lease retired) are unchanged. The [wake]/[rearm]/[pump] probes that
pinned the mechanism stay behind ACDREAM_PROBE_PARK=1 with the rest of
the C4 family.

Exonerated by experiment before the fix: ACE (wire capture shows
PlayerCreate sent; retail logs in fine) and the portal-cue commit
2914e43a (full revert stalled identically).

Verified: 2/2 live logins reach auto-entered player mode and reveal
event=complete, with the probe showing seal-refused -> retry -> recovery
in flight; full Release suite 11,740 passed / 4 skipped / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 12:32:53 +02:00
Erik
b80ba797cf docs: file #356 closed-issue record for the focus-loss crash
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 12:02:57 +02:00
Erik
972c7ab3b8 fix(input): focus loss no longer faults on an unpublished movement controller (#356)
Losing window focus calls CameraPointerInputController.HandleFocusChanged
-> MouseLookController.EndForLifecycle -> PlayerMovementController
.EndMouseLook, and EnsurePublishedForRuntimeOperation throws when the
controller exists but is not yet published (mid-login) or already retired
(post-logout). A focus callback can land in either window, so a simple
alt-tab during the login stream took the whole process down with an
unhandled InvalidOperationException. Hit live during the Campaign A
listening-session launches.

EndAndRestoreCursor already guarded 'no controller'; publication state is
the finer-grained form of the same condition, so the guard is completed
with the new CanExecuteLiveMovement predicate (the exact lifecycle set
EnsurePublishedForRuntimeOperation accepts) rather than wrapping the call
in a catch. Cursor restore still runs unconditionally - presentation is
always safe. Published-controller behaviour is unchanged.

Also files issue #357: the login placement stall this session exposed
(reveal ready=True, player Place edge never executes, world never opens).
That one is NOT fixed here - full evidence chain, wire capture, and probe
output are in the issue. It is a placement-domain bug and blocks the
Campaign A listening gate.

Suite: green except the known load-dependent measurement flake
(RuntimeCollisionReportingStateTests allocation pin), which passes in
isolation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 12:02:38 +02:00
Erik
c69b3bde04 fix(audio): Campaign A slice A1 — retail's sound probability gate (#355)
The SoundTable probability field is a Bernoulli play/skip gate applied at
the play site (SoundManager::PlayProbability @0x005500E0), not a selection
weight — and variant selection (SoundManager::GetSound @0x00550680) is a
uniform index over (n-1) that ignores probability entirely. SoundCookbook
did the opposite: a cumulative-distribution walk weighted BY probability,
short-circuiting single-entry lists before rolling at all.

A dat census says 4,183 of 4,184 entries are single-entry and 686 of those
carry probability < 1.0, so the gate was categorically absent: Speak1 idle
chatter authored at 0.05 fired every trigger (~20x too often), wound/attack/
swoosh variants never dropped, and six 0.0001 entries always played.

Split into retail's two steps (PickVariant + PlayProbability, composed by
Select) over a new ISoundRandom modelling both retail roll ranges: the
variant roll clamped below 1.0 (0x00797D48) and the gate's 1/32767 grid,
which is why 0.0001 resolves to ~1.2e-4. PickVariant reproduces retail's
(n-1) off-by-one verbatim per the port-faithfully rule — the last variant
of a multi-entry sound is unreachable, costing exactly one wave
(0x0A00051E) in the shipped dats.

Also removes invented mechanism this review disproved: the dead Core
SoundEntry/ISoundCache scaffold (PitchMin/PitchMax, Loop, Is3D — retail
never calls SetFrequency, never sets the loop flag, and creates every
gameplay buffer 2D), the engine's pitch plumbing, the int 0..7 priority
cast (the dat field is a float in [0,1]; 4,100 entries collapsed to 0),
and the clamp-at-the-field on volume (an unbounded gain retail clamps only
after the distance divide).

Tests rewritten as conformance against the disassembled values, replacing
a self-referential suite that pinned the wrong model.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 21:28:16 +02:00
Erik
6bb4cfa795 feat(ui): the spell-bar drop ring — retail's authored drag-accept state, and the ring exposed a real drop off-by-one
Some checks failed
Headless portability / portable-headless (ubuntu-latest) (push) Has been cancelled
Headless portability / portable-headless (windows-latest) (push) Has been cancelled
Headless portability / linux-graphical (push) Has been cancelled
Headless portability / linux-vulkan (push) Has been cancelled
The green ring is retail's own art: every UIItem cell carries an
authored DragAccept child (catalog 0x21000037, child 0x1000045A), and
the spell bar's drag-over handler (SpellCastSubMenu::OnItemListDragOver
@0x004C5990) flips it to the Accept state (0x10000040 -> surface
0x060011F9) for any spell payload. Ported through a per-slot
SetDragAcceptVisual seam + a catalog DragOverAcceptance hook; other
lists are untouched (null acceptance = neutral). A polarity error in
our older docs (Accept/Reject state ids swapped) was corrected against
three independent sources; the shipped art was always right, only the
labels lied.

The ring shares ONE landing computation with the drop
(FavoriteDropIndex) — and that requirement exposed a genuine #354
off-by-one: the empty-tail path double-applied the -1 adjustment
(retail gates it on the lift's removal @0x004C7157), landing a
reordered spell second-to-last instead of last. Fixed;
discriminator-verified both ways. AP-172 narrowed + its false
empty-tail claim corrected.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-08-08 20:18:51 +02:00
Erik
81a9d85a1d fix(ui): spell-bar drag-reorder works — the per-frame rebuild was destroying the dragged cell (#354)
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
Everything already existed — the drag payloads, the favorite wire pair
(0x1E3 add-at-position / 0x1E4 remove, byte-confirmed against retail's
Event_AddSpellFavorite @0x006A0F70 and ACE), the insert-shift state
ops. The bug: lifting a favorite fires SpellbookChanged, the next
per-frame Tick rebuilt the bar, the rebuild flushed and recreated
every cell, and UiRoot's subtree-removal safety net canceled the
in-flight drag whose source had just been destroyed — one frame after
every lift, before any drop could land.

The rebuild now defers for the duration of the drag gesture, and the
drop ports retail's own -1-if-lifted-before-target index adjustment
(SpellCastSubMenu::AddFavorite @0x004C7060) so final positions are
byte-identical: insert-shift, not swap; drag-out still deletes (the
lift's removal stands on a missed drop, retail's shape). The
real-pointer-pipeline test fails against the pre-fix code with the
exact cancellation and passes after; a discriminator pins that
physical-item drop handlers reject the spell payload.

AP-172 files the one presentation divergence (mid-drag reflow happens
on release, not continuously) — renumbered from the agent's AP-171
draft, which collided with the same-day double-click row. #354 filed
and closed.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-08-08 18:28:47 +02:00
Erik
0a996a1a91 docs: world-interaction program CLOSEOUT — all six slices complete, vendors user-accepted end to end
Slices 5+6 close together on the user's final gates ("Ok fixed" on the
live purse repaint, the slider/wrap/double-click passes before it).
The plan carries the full closeout: what shipped, the four-review /
34-defect + eleven-live-finding audit trail, the two latent crashers
the arc exposed, and the deferred remainder with its issues and
register rows. CLAUDE.md's Current state flips the program to
COMPLETE; #353 closes user-passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 17:56:42 +02:00
Erik
d674b99f56 feat(ui): double-click-to-buy (AP-171, user-approved) + #353 toolbar text fixes — authored right-justify and two-line name wrap (Fable)
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
Double-clicking a vendor shop item now buys through the Buy button's
exact quantity/price path — retail has NO double-click-to-buy (the
named table sweep's negative evidence stands); the user chose the
addition explicitly and AP-171 records it.

#353 (pre-existing, user-reported): the stack-count entry is AUTHORED
HJustify=2 — right-justified flush against the slider on its own row —
and UiField already supported RightAligned; nobody had honored the
authored value. The name element is AUTHORED two lines tall (H=31,
W=140): long names now word-wrap at the authored pixel width onto a
second centered row via two stacked one-line labels reusing the
existing centered draw path (WrapNameTwoLines: greedy word break, no
hyphenation, second row clips like retail).

Ten SelectedObjectController structure tests updated from
single-label to first-label access. Lesson re-learned the hard way:
the first "green" run used a stale TEST assembly (only the App
project had been rebuilt) — the clean-room caught it, per
feedback_stale_build_artifacts. Full App 4,329/3 and Core 4,381/1
verified green on properly rebuilt assemblies; the one transient
Core Release failure did not reproduce and is noted on #351.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 17:42:02 +02:00
Erik
1688863366 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>
2026-08-08 17:24:49 +02:00
Erik
33b45ee581 fix(ui): vendor dropdown polish — authored arrow-cap with open/closed flip, downward popup, left-aligned rows
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
Three gate findings, each settled by authored data rather than
invention: the button face is retail's two-piece assembly and the
17x19 arrow-cap 0x1000034E now renders with its authored
Normal(closed)/Highlight(open) states; the popup direction is an
AUTHORED attribute (UIElement_Menu::Open pc:120210-120252 — bool
attr 5, chat authors upward=true, the vendor menu authors nothing and
defaults downward), so both menus are now byte-faithful with no
special case; and the 19/20px text indents were chat-specific
checkbox/LED clearances the vendor rows don't author — measured
against the live retail font, "Spell Components" overflowed by 11px
and now fits with 8px to spare. Chat's menu defaults are bit-identical
and its tests untouched.

AP-161's arrow-cap note closes. #351 files the pre-existing FarLoad
Debug flake (three sightings today, never in clean-room Release).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-08-08 10:59:59 +02:00
Erik
58c8de264e fix(render): #350 — the shadow ledger's lifetime counters are 64-bit; the 2h42m overflow is closed
RenderDeltaApplyResult's nine counters accumulate for the lifetime of
a world generation (the cumulative sum is never reset in place) yet
were int where every sibling lifetime counter in the same class was
already ulong/long. A 2h42m single-generation session (login to crash
with zero portals) overflowed one through ordinary per-tick churn from
SynchronizeActiveSources' two call sites per frame. Introduced
0eb66485 (2026-07-24); first reached by the vendor buy gate because
parking at a shop for hours produced the project's first multi-hour
unbroken generation. The vendor materializer was exonerated by routing
analysis (shop-item Ingest/Remove reaches inventory deltas only, never
the render journal).

Widened to long; the per-tick builder stays int and widens implicitly;
the arithmetic stays checked. All 68 render-shadow tests green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 10:29:39 +02:00
Erik
9d3df5f627 fix(app): #348 — cursor switches ride a process-lifetime native cache; the per-flip Win32 handle leak is closed
Silk's per-mouse ICursor recreates the native Win32 cursor on every
Image assignment; a per-frame cursor alternation (the pick cursor
flickering between kinds while hovering an ANIMATED NPC — exactly the
stand-at-a-vendor posture) allocated a fresh USER handle each flip
until CreateCursor died with "Not enough memory" and took the render
loop with it (vendor-gate.log, exit 82 — surfaced as one clean stack
by #343's fix, as designed).

GlfwCursorCache restores retail's own shape: each distinct
MediaDescCursor is created ONCE for the process lifetime
(glfwCreateCursor, rejected media cached as permanent misses) and
switching is an O(1) zero-allocation glfwSetCursor. The AP-72
missing-art standard-cursor fallback rides the same cache
(Arrow/Hand/Crosshair/IBeam; anything else keeps the Silk path).
Graphical hosts attach after the native window exists; tests and
windowless hosts keep the Silk path untouched. RetailCursorManager's
dedup and PlanApplication logic are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 18:26:17 +02:00
Erik
fa0c053ebf docs(physics): #347 closed WITHOUT a code change — retail's glide alternates exactly as ours does; AD-70 retired as a wrong inference
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 round-2 cdb capture is decisive: during a live retail glide,
edge_slide fired ~1.5 times per find_transitional_position — the
arm/move alternation's exact signature (3 entries on the arming tick,
0 on the moving tick) — with cliff_slide in lockstep, step_down at
2.5x, step_up 0, and every stack sample on our identical call path.
cliff_slide's bytes match our port and ACE's (compare constant at
0x794610 verified 0.0), and the user could not distinguish the two
clients side by side. The "retail redirects within the tick" premise
misread round-1's set_sliding_normal cadence (per-event, not
per-tick, so its 1:1 ratio with edge never discriminated anything).

The alternation-tolerant assertion in Issue345SteepSlopeGlideTests is
therefore the CORRECT retail-shape pin from both sides; its comment
now cites the capture instead of calling the shape a residual. The
#269 note is honest the other way: the hope that a within-tick port
would explain that feel residual is withdrawn with the premise.

The temporary Scratch347 diagnostic is deleted. Capture evidence:
345-glide-stacks.cdb.log (repo root, untracked, cited from the
contract's RESOLUTION section).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:05:53 +02:00
Erik
ab89ebdf92 fix(physics): #345 — a grounded mover glides along a too-steep face; validate_walkable's return is scoped as retail's bytes scope it
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
Retail's OBJECTINFO::validate_walkable @0x0050d010 initializes its
return slot to OK (0x0050d025) and assigns ADJUSTED only inside the
below-plane guard, immediately after the push executes (0x0050d249).
The guard-fail path — grounded, OnWalkable, plane too steep — jumps
past the contact write, the push, and the assignment (0x0050d1b9 ->
0x0050d251): retail deliberately IGNORES the steep plane at primary
validation so the insert proceeds, the step-down phase fails on the
steep landing, and the edge family produces the per-tick lateral
glide. ACE flattened this into an unconditional return Adjusted
(ObjectInfo.cs:169) and we inherited it; our TransitionalInsert then
retried the byte-identical Adjusted forever — the user's
stop-instead-of-slide.

Evidence chain: the user's retail observation (the axiom), the live
cdb glide profile (edge_slide/cliff_slide 594 each in lockstep,
step_up 0), the D0 implementer's correct STOP (fixtures reproduced
the stuck fingerprint while faithfully executing the ACE-shaped
reading — refuting the reading, not the code), and the capstone
byte-decode both Opus reviewers re-derived independently, including
the stack-slot frame arithmetic and every ret site's eax.

The conformance fixture is the live topology: flat and steep terrain
triangles sharing ONE cell's diagonal (a cell-boundary face does NOT
reproduce the loop — the cell-scoped primary sample never validates a
neighbour's triangle — and is pinned as supplementary). Sabotage:
restoring the unconditional Adjusted reds the discriminator with the
exact stuck position (0.325 m lateral, 28/30 stuck ticks vs 2.602 m /
14/30 fixed; reviewer B's independent five-angle table is monotone
10-85 degrees). Stuck ticks are counted from positions so the
assertion survives the eventual probe strip.

In-game glide gate PASSED 2026-08-08: "Well it works, we are sliding.
I cant detect any speed change from retail."

Filed alongside: #347 + AD-70 (our glide alternates arm/move at half
retail's per-tick rate — retail redirects within the tick; next up by
user direction), AD-71 (the guard's mutable WalkableAllowance operand
vs retail's fixed is_valid_walkable global — now return-value-bearing),
and the reviewers' named residuals in the #345 closure entry
(placement-arm flip, other-cell coverage gap, EdgeSlide-less
projectiles, ACE's server-side shared misport predicting remote
drift-then-snap on steep terrain). The unported IsViewer arm of
validate_walkable is noted in the D0 doc.

Suite: clean-room complete solution 11,271 passed / 4 skipped / 0
failed; Core assembly re-run green after the review-driven test
hardening.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 13:32:51 +02:00
Erik
10efb5b1f9 fix(physics): AD-66 relands — the push-out uses retail's bare radius; plant-then-lift complete (#341 closed)
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
Third attempt, landed on evidence where the first two correctly refused:
the ten-run stability gate passed 10/10 bit-identical (0x42667451, two
clean-room cycles among the runs), the recalibrated golden's every value
measured with derivations rather than guessed, and the historical
measurement flip stands recorded as unexplained-but-unreproducible
after 37 hunt runs plus these 10 found no divergence anywhere.

The mechanism, completing the S4b byte-pin: validate_walkable plants
the sphere at perpendicular r*N.z (byte-faithful, untouched); this push
fires once per settle and lifts to tangent equilibrium dist=r, where
the trigger goes quiet — retail's slope hover, arriving via the push
exactly as the original substitution's own comment predicted retail
had. Sabotage: restoring radius*N.z reddens the discriminating
exact-value test verbatim. AD-65 conformance, the uphill no-flap
guard, and the #331 absorb pin all green untouched.

AD-66 retired (the campaign's last withheld row); AD-69's seam-frame
correction deliberately unbundled, stays active as its own follow-up.
Clean-room suite 11,267 / 4 / 0 — the suite's two AD-66 skips are gone.

User's "port the retail pair" decision is now fully executed; the
hover-look slope gate is the remaining acceptance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 12:01:15 +02:00
Erik
67a77526de docs: #341 boundary hunt — the flip is unreproducible at 37/37 bit-identical runs; reland unblocked under its original gate
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 11:44:20 +02:00
Erik
3dd41c66e1 docs: #345 live retail trace — the glide is cliff_slide firing every tick; our insert loop never routes there
cdb on the PDB-paired retail client during the user's 45-degree glide:
edge_slide/cliff_slide 594 each in lockstep, set_sliding_normal 538,
step_up ZERO. Ours: 18 edge-family firings total, stuck ticks
dead-looping on insert retries. The divergent branch is
transitional_insert's handling of the refused steep walkable — retail
proceeds into the step-down-failed/edge path per tick, we retry from
scratch. The D0 code-reading's 'retries from scratch, retail-identical'
conclusion is corrected by the runtime evidence: the profile is what
the decomp reading could not see.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 11:22:27 +02:00
Erik
2b5367a81a docs: #345 retail observed — it GLIDES, scaling with angle; divergence confirmed, upstream of the proven-faithful response path
The user's side-by-side is the axiom: retail glides laterally along the
steep hillside, faster with more approach angle; acdream stops dead.
D0's line-by-line faithfulness of the response path relocates the
divergence upstream — the request shape reaching the transition, the
broadphase question, or state seeding. Next: the cdb live-trace session
at the slope, per the workflow's Step -1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 11:18:44 +02:00
Erik
18289f95f8 docs: #345 D0 verdict — the stuck-tick fingerprint is retail's own algorithm; fix attempt correctly stopped
Five links traced from the named decomp: the below-push never executes
(OnWalkable guard — the probe printed the wrong guard pair), the
from-scratch retry is retail-identical, and validate_transition's
failure path manufactures every captured field including the (0,0,1)
default. Stopping dead may simply BE retail. Two validations remain:
the user observing their RETAIL client at a comparable slope (the
cheapest decisive test there is), and — only if retail visibly
slides — the find_cell_list broadphase question via the cdb toolchain.
The mechanism paragraph's wrong-cause framing is retained and
corrected in place, per the register's own honesty pattern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 11:11:19 +02:00
Erik
2098fa6690 docs: #345 mechanism caught — ValidateWalkable's below-push Adjusts every attempt but the adjustment never carries forward
The trace shows 275 uniform player stuck ticks: target 0.268m below a
just-too-steep plane (N.z=0.599 vs FloorZ 0.664), below-push Adjusted
on every transition attempt with the IDENTICAL dist each time — the
adjust is recomputed from scratch instead of feeding the next attempt,
attempts exhaust, the tick yields zero. The fix contract's question is
now a single retail cross-read: does transitional_insert feed the
adjusted CheckPos forward, or branch differently on Adjusted-with-
non-walkable. The probe paid for itself in one one-minute run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:53:24 +02:00
Erik
6c6664a685 fix(app): #343 — a wounded render loop defers the native release instead of throwing over the real failure
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
Root cause pinned by IL-decompiling Silk.NET.Windowing.Common:
ViewImplementationBase._inRenderLoop is set at DoRender/DoUpdate entry
and cleared ONLY on normal return, so a throwing frame callback leaves
it armed forever and any later Dispose -> Reset throws "You cannot call
Reset inside of the render loop", exit 82, replacing the original
wounding exception in the report.

The fix mirrors Silk's own bracket exactly: GameWindow._renderLoopArmed
set at OnUpdate/OnRender entry, cleared only on their normal return —
deliberately NOT in a finally, so it tracks the wound the same way
Silk's private field does. ReleaseNativeWindow checks it before
disposing: armed -> best-effort Close() (swallowed so it can never
become the reported failure), no Dispose, and a new terminal status
CompleteWithDeferredNativeRelease with Error kept null — the original
exception stays the primary report. Healthy paths (OnClosing's
in-loop completion, Run()'s tail release) are byte-unchanged, and the
new PublishNativeWindow parameters default to null so every existing
caller and test behaves identically.

Sabotage: disabling the armed-check flipped the deferral test to
Expected CompleteWithDeferredNativeRelease / Actual Complete —
the guard is what the test exercises. Clean-room suite 11,262 / 6 / 1,
the 1 being #340's documented load flake (passed standalone; second
recorded firing noted in its entry).

Queue: #344 done, #343 done; next #345's instrumented mechanism
session, then #341's boundary hunt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:28:29 +02:00
Erik
52bdf4df71 fix(world): #344 — a mid-teleport world-frame disagreement defers the projection instead of crashing
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
During a portal transit the two world-frame owners legitimately rebase
on different edges (Runtime at TeleportAdvanced, streaming only after
old-window retirement), and a spawn projection landing in that window
hit the #283 invariant as an unhandled render-path throw — the crash
the user hit entering a dungeon.

The guard's check is unchanged; only the disagreement RESPONSE is
discriminated on the canonical transit authority
(RuntimeWorldTransitState.IsTeleportActive, the same field the App
layer already reads for portal-in-flight): in transit -> the
materializer's existing "not yet" return, parking the projection on
its established retry rides (OnLandblockLoaded's re-attempt loop,
whose ordering guarantees agreement on retry because the recenter
coordinator adopts the new origin BEFORE unblocking new landblock
loads — verified at source; plus OnPosition recovery and
OnAppearance). Outside transit -> still throws: genuine corruption
stays loud. The implementer explicitly ruled out riding the Runtime
placement pump, which would have acknowledged-and-discarded the
completion receipt and silently dropped the entity forever.

Sabotage: removing the discriminator reddened the pre-existing #283
throw tests as well as the new not-in-transit test — the sabotage
defeats the original contract, not merely the new coverage. Four new
tests cover defer, defer-then-agree-then-succeed (projected exactly
once), throw-outside-transit, and the agreeing pass-through.

Clean-room suite: 11,261 passed / 6 skipped / 0 failed. #346 filed for
a sixth, distinct load-sensitive allocation flake observed during the
runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:07:45 +02:00
Erik
1a9c057a82 docs: #345 A/B complete — pre-existing, Campaign S exonerated wholesale
The pre-campaign binary shows the identical 0.0%-yield signature across
eleven consecutive sustained-input windows at the same slope with the
same protocol. Older defect, newly noticed. The throwaway A/B worktree
is removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 09:43:32 +02:00
Erik
da88b50ad4 docs: file #345 — angled input into a steep slope is fully eaten upstream of the face, UP collision normal, no slide
The user's report measured two ways: the branch probe shows the steep
face reached only 18 times with a HEALTHY applied cliff-slide (the #32
retention visibly working — lastN is the flat ground, not the face);
the flight recorder shows the actual stop is 838 uniform ticks on
nearly-flat approach terrain eating 100% of angled input with
collisionNormal straight UP and no sliding latch. Not the absorb, not
the slide family. Regression status unknown — the A/B against the
pre-campaign binary is mandated as step 1 before any theory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 09:41:11 +02:00
Erik
00c03a33a5 docs: #341 round 2 — the flip survives a same-session ABA, property reads ruled out; AD-66 stays withheld
The reland's ten-run protocol was stable-with-lift, then the
recalibrated form flipped on its first run, and a same-binary ABA
isolated it to assertion CODE SHAPE alone with a hardcoded-constant
control killing the property-read hypothesis. Best remaining story:
JIT inlining/tiering of the settle chain differs by caller IL shape and
some computation in it sits on an exact float boundary deciding WHEN
the one-time lift happens. First discriminating experiment for the
investigation: DOTNET_TieredCompilation=0. The stop clause fired twice;
production is unchanged; no third reland before the boundary is found.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 09:03:34 +02:00
Erik
712244b8aa docs: S4b's STOP fired — retail is PLANT-THEN-LIFT; the reland maps to AD-66's bare radius alone
The contract's D0 byte-pin refuted the tangent-placement premise:
validate_walkable @0x0050d010 is planted for every normal mover
(Ghidra + BN + ACE agree; ours is already byte-faithful; only the
camera branch is tangent). With the byte-proven bare-radius push-out
the coherent retail mechanism is plant-then-lift — the push fires once
per settle, raises the body to tangent equilibrium, and both checks go
quiet there. The slope float comes from the push, not the placement,
exactly as the original substitution's own comment argued. The 84%
live fire rate was measured against our push-disabled steady state;
the #341 assert-shape flip now reads as order-dependent settle state.
The user's 'port the retail pair' therefore maps to relanding AD-66's
bare radius alone, with the ten-run stability protocol.

The STOP rule paid for itself: an implementer without it would have
made ValidateWalkable DIVERGE from retail in the name of faithfulness.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 08:46:16 +02:00
Erik
205379c6d6 fix(streaming): #339 — a packed EnvCell geom id no longer misroutes into the 32-bit prepare arm
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 reveal hang (three live occurrences: portal-space, login twice) was
an unhandled OverflowException on the render frame's readiness
evaluation: EnsureRenderDataReady found a packed 64-bit EnvCell geometry
id OWNED but DESCRIPTOR-LESS — the release path removes the descriptor
while render data parks on the LRU, IncrementRefCount restores ownership
on a revisit, and the scheduler's PrepareEnvCellGeomMeshDataAsync has
not yet re-registered — and fell through to the Setup/GfxObj arm, whose
checked((uint)id) cast threw. After that the reveal was never evaluated
again.

The fix corrects the TYPE DISPATCH rather than suppressing anything:
packed ids (bit 33, GetEnvCellGeomId) answer "not yet" in the
acquire-to-prepare window — the true answer, since the scheduler
re-registers on the same landblock build — and PrepareMeshDataAsync's
blind cast becomes a typed, loud invariant failure naming the id kind
and the issue, so a future caller repeating the confusion gets a
diagnosis instead of three live hangs.

Validation: the crash was DETERMINISTIC at login cell 0xA8B4002F (two
consecutive hard failures); with the fix the same login revealed
cleanly and a full session — 16,585 entities, five portal generations,
the user-passed Session-B dungeon gate — ran with zero overflows and
zero guard fires. Clean-room suite: 11,253 passed / 6 skipped /
0 failed.

Found because the Session-B gate launch finally captured the stack the
earlier #339 hangs never printed. #343 (the wounded loop's Reset-in-
render-loop shutdown) and #344 (the mid-teleport world-frame race the
same evening surfaced) are filed separately and unfixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 08:37:43 +02:00
Erik
42de5f18ff docs: Session B dungeon gate user-passed; #344 filed — mid-teleport world-frame race
S1B + S2's shared gate passed live ('Feels good!'), run INSIDE the
dungeon the #344 crash had conveniently saved the character at. #344
filed with its two halves kept separate: the world-frame guard is
CORRECT (it refused a 28.8 km mis-projection — do not weaken it); the
defect is the teleport-recentre ordering race plus the unhandled-crash
failure mode where a deferred retry belongs. AD-64/#324 family.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 08:33:43 +02:00
Erik
ad4a970ec9 docs: #339 mechanism caught with a full stack — packed EnvCell geom id hits a 32-bit checked cast on the render path
The Session-B gate launch reproduced the reveal hang at login and the
log finally carries the crash: PrepareMeshDataAsync's Setup/GfxObj arm
does checked((uint)id) on a packed 64-bit EnvCell geometry id, reached
because EnsureRenderDataReady found the id OWNED but DESCRIPTOR-LESS —
the release path removes the descriptor while render data parks on the
LRU, re-acquire restores ownership, and nothing re-registers the
descriptor until a full prepare is re-requested. The OverflowException
rides the render frame's readiness evaluation and the reveal is never
evaluated again. Demote-then-revisit ordering explains the
intermittency. S1B/S2 are not implicated: neither touches this
pipeline and the signature predates both.

Root-cause fix deliberately NOT designed in this session's tail — the
invariant to restore is owned-implies-descriptor (or a legitimate
re-request from retained data), which needs the acquire-path read the
next session starts with. Catch-and-continue and skip-64-bit-ids are
named as the band-aids they are.

#343 filed alongside: the wounded render loop's shutdown throws Silk's
Reset-inside-render-loop and abandons incomplete, exit 82.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 08:14:49 +02:00
Erik
b3e43d22c9 fix(physics): S1B — indoor cell membership admits on the part BOX, as retail does (#335, AP-159 narrowed)
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
CellTransit.FindTransitCellsBox ports CEnvCell::find_transit_cells'
part-array overload @0x0052cae0 line-for-line: per-portal x per-part
order, the sphere cheap-reject at F_EPSILON+radius, the box admit whose
"Straddle or crossing-side" rule is exactly retail's `eax != side` under
the PDB Sidedness enum, leads-outside placed AFTER the admit, the
unconditional unloaded-neighbour hint without the sphere overload's
re-test, the destination box_intersects_cell gate with its deliberate
no-break, and add_all_outside_cells after the loop. The box-vs-cell BSP
traversal lands in BOTH representations behind the flat-authoritative
dispatcher with a graph referee whose 20,000 installed comparisons are
pinned by assertion (review F5), zero mismatch.

Dual Opus review: PASS on both lenses. The mandatory D0 pseudocode pass
caught that the contract's own supplementary note misattributed the box
block to the sphere overload — it belongs to a SECOND
check_building_transit overload @0x0052c680, whose portal-side
convention is INVERTED and whose admit differs; the pseudocode doc now
records that trap plus two byte confirmations made at review:
which_side @0x00444720 is strictly > eps for POSITIVE, and
intersect_box's in-plane early exit returns CROSSING(3)
(jp @0x005aa1bc -> mov eax,3), settling review items b1/b2 for the
future bridge porter. The bridge itself stays unported as AP-159's
explicit remainder.

The review also retired #335's severity premise honestly: "over-
inclusive only, never a missed one" is wrong at production shape ratios,
where the box (whole-vertex AABB) legitimately exceeds the sphere
(physics-polygon root sphere). Measured, both populations: rigged
(box << sphere) — 1,520 placements, 978 cells removed, 0 added;
production-ratio (box >= sphere) — 950 placements, 20 removed, 1 ADDED
through the loaded-neighbour gate, which is retail's direction, not a
defect. The no-op guard (review F4) asserts removal is nonzero so an
unwired admit cannot pass silently.

Process note: the implementer authored against this session's worktree
at bec5c69d, 25 commits stale — the recorded worktree-base class. All
six files were byte-identical between bases, the diff transplanted
losslessly, and every verdict-bearing run (referee, direction sweeps,
this clean-room) was re-executed on current main. S2's uncommitted
phase-1 edits were stashed for this landing so the suite verdicts
exactly one changeset.

Also untracks 341-slope-capture.jsonl (an accidental add) and
gitignores it.

Clean-room suite: 11,248 passed / 6 skipped / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 07:46:57 +02:00
Erik
eec50bbd24 docs: #341 decision — user directs porting the retail pair (tangent rest + bare trigger, one slice)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 07:19:20 +02:00
Erik
4885629fbd docs: #341 — Ghidra cross-check confirms the tangent-rest solve, BN's garbled denominator restored
The divide-by-itself artifact in the BN text resolves in Ghidra to
t = (dist -/+ r) / dot(N, stepDir): a ray-vs-plane interpolation to
perpendicular-distance == radius. Two independent decompilers agree on
the mechanism the #341 resolution rests on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 07:17:58 +02:00
Erik
6439b9d48e docs: #341 mechanism RESOLVED — trigger and resting geometry are one family
CPolygon::adjust_sphere_to_plane @0x00538210 solves the walkable
placement for perpendicular distance == radius: retail rests the sphere
TANGENT to the slope, which makes its bare-radius push-out trigger
structurally inert — and explains why our planted rest (perp = r*N.z)
makes our substituted trigger inert here by the same algebra. Each
engine's trigger matches its own resting geometry; the live A/B's 84%
fire rate is what mixing retail's trigger with our placement produces.

AD-66 is therefore not a standalone row: the faithful unit is the pair
(tangent placement + bare trigger), ported together or divergent
together. The visible corollary of the retail pair — feet floating by
r*(sec(theta)-1) on slopes, up to ~20 cm near the walkable limit — is
why the decision is queued for the user's eyes rather than taken
silently under the retail-first default.

From capture to mechanism in one morning: the user's two-minute slope
run plus one decomp read did what the overnight harness could not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 07:17:22 +02:00
Erik
ca0fad1482 docs: #341 live A/B — the user's slope run shows the bare-radius trigger would fire on 84% of grounded slope ticks
3,870 player resolves captured at Rithwic with ACDREAM_CAPTURE_RESOLVE.
Of 2,955 contact-seeded ticks the RETAINED trigger fired zero times —
the current push-out is inert in ordinary play — while retail's bare
trigger would have fired on 2,471 (84%), lifts 2-88 mm, p50 27 mm. The
body rests at r*N.z every grounded tick, so landing AD-66 alone would
engage the push on virtually every slope step and fight the foot
planting: the oscillation the original substitution was written against.

The exposed question sits upstream: our placement plants the sphere
vertically (perpendicular r*N.z); if retail's walkable contact rests
tangent (perpendicular r), retail's bare trigger is inert in retail
exactly as ours is here, and the trigger cannot be ported without the
placement geometry. That retail-side verification is now the apparatus
session's first task, and it also gives the harness flip a mechanical
suspect: settle-state order dependence, not physics.

Measured from the user's own two-minute run rather than argued — the
cheapest decisive instrument of the campaign so far.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 07:15:12 +02:00
Erik
d73125d3b0 fix(physics): S4/AD-65 — the away-from-plane response snaps to the surface, as retail does
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
Campaign S slice S4, the half that landed. Retail's CTransition::
adjust_offset @0x0050a370 branches on dot(offset, contactPlane.N) at
0x0050a4fa: moving INTO the plane subtracts the normal component
(0x0050a529), moving AWAY calls Plane::snap_to_plane @0x00509c50 —
which preserves X and Y and re-solves ONLY Z so the offset lies in the
plane (the d terms cancel algebraically), no-op under the
0.000199999995f |N.z| epsilon. acdream ran the orthogonal projection in
BOTH directions, shrinking downhill XY travel by cos^2(theta): 25% at
30 degrees, 50% at 45 — AD-65's recorded shortfall, now retired.

The combined Opus review independently re-derived the algebra, the
branch polarity, the epsilon's bit-identity (17b75139), and the
sabotage magnitude (the re-instated projection yields X = 0.75 =
cos^2 30 exactly), and verified the delta is 4 non-comment lines with
the into-plane arm, the crease arm, and both no-plane arms untouched.
Its blast-radius sweep found the away arm exercised but NOT
discriminated by any pre-existing test — every one asserts lower
bounds the snap over-satisfies — so the two new exact-value tests are
the only discriminating coverage, recorded in the test's class doc,
and the felt 33-100% downhill speed-up is the morning gate's one row.

AD-66 (the push-out's bare radius) is WITHHELD: byte-confirmed twice,
implemented, then pulled after the same clean-room binaries measured
contradictory absorbed-tick outcomes flipping with nothing but test
assert shape — issue #341 carries the observation matrix and the
apparatus plan; its two exact-value tests are [Skip]-ed; the retained
substitution's rationale is restored at the site per review F1, with
the review's remaining findings (F2/F3/F4/F5/F6) applied and F8 filed
as #342. AD-69 filed: the same block omits retail's get_block_offset
seam-frame correction, deferred to the AD-66 relanding for
attributability. #340 filed: a fifth load-sensitive flake.

Review verdict: PASS. AD-65 is provably unable to reach the #341
anomaly's code path (the absorb scenario takes the crease arm).
Clean-room suite: 11,239 passed / 6 skipped / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 02:45:03 +02:00
Erik
55b07f6a62 refactor(physics): hoist the live-entity collision builder to Runtime (#330 groundwork)
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
LiveEntityCollisionBuilder and LiveEntityDefaultPoseResolver move from
AcDream.App.Physics to AcDream.Runtime.Physics with no behaviour change
— diff-verified byte-identical shape math by both review lenses. The
Build signature's App-record parameter is replaced by presentation-free
primitives with identical guard semantics, INCLUDING the
FinalPhysicsState read the contract had missed and the implementer
surfaced rather than dropped. Visibility stays internal: Runtime's
existing InternalsVisibleTo grants already cover every consumer, so the
implementation's public widening is reverted per the architecture
review's finding 11.

The registration WIRING is deliberately WITHHELD. Both Opus lenses
failed it, converging: a shadow registered at spawn freezes there
(RuntimeRemotePhysicsUpdater is Runtime-homed but App-driven — nothing
headless ticks it), so a walking NPC becomes a phantom obstacle at its
spawn point while the real NPC still passes through the bot; three of
five shadow-lifetime edges leaked (pickup leaves a permanent invisible
collider, supersession orphans a duplicate, generation reset never
unregisters and the K-ledger convergence oracle only checks retained
shadows AFTER disposal clears them); and headless cannot resolve BSP
collision assets at all, so doors and chests would still be
walk-through. The frozen-shadow root was the SESSION LEAD's contract
error (fact 3), not the implementer's.

#330 stays OPEN, rewritten as the seven-point scope map the reviews
produced — the honest overnight deliverable is that map, not a
half-mechanism carrying new divergences.

Suite 11,235 passed / 4 skipped / 0 failed (the withheld seam's two
tests account for the delta from the implementation run's 11,237).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 01:49:13 +02:00
Erik
8c97084289 docs: close #338 — headline refuted by full-capture statistics; AD-68 files the real residual
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 three-site probe answered it in one run: prepare and publish carry
the authored 0.600/1.500 to the publication candidate, and resolve
receives exactly those values for the entire session after one early
0.400 reading. Re-reading the ORIGINAL 337-support.log with statistics
instead of an eyeball: authored pair 111,248 lines, 0.400 pair 358. The
filing was built on an early line of a 255k-line capture; the alleged
mechanism (values never wired to the mover) does not exist.

The 358 are AD-68, now registered: GetSetupMoverShape's placeholder
(empty spheres -> legacy capsule, 0.4/0.4 steps) during an entity's
async Setup-residency window, plus the local player's own seconds-long
window between controller construction and publication-candidate
adoption. Retail loads synchronously and has no such window. Left as-is
deliberately: shrinking it is streaming work.

The filing still paid for itself: three false doc-comment claims
corrected in PlayerMovementController (retail '~0.4 m' twice, and an
ApplyStepHeights writer that never existed anywhere in the tree —
replaced with the real writer chain), retail's actual fallback pinned at
0.04 (CTransition::step_up @0x0050b655), and the resolve probe now
prints the mover id, because the early 0.400 was most plausibly a
REMOTE player — remotes also carry IsPlayer — and the guid rule
(feedback_probe_identity_attribution) exists precisely to stop that
misread.

No production behaviour changed; nothing for the morning gate. AD
section 50 -> 51. Suite 11,234 / 4 / 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 00:44:42 +02:00
Erik
1dc81710f3 docs: close #32 — local edge-slide user-passed; AD-67 filed for the kept cell-id write
Both halves of #32 are now closed: remote at 204d0ae0 (user-passed
2026-08-04), local at 332045c7 (user-passed 2026-08-07 at the Rithwic
cliff, on the first launch whose capture printed the fixed binary's own
assembly path). The research doc carries the outcome banner: the live
capture landed in decision-table row 1 verbatim and Section 7's fix
shipped unchanged.

AD-67 records the one deliberate residual: the narrowed SetContactPlane
still writes ContactPlaneCellId, which retail writes only at
init_contact_plane (0x0050e8ca). Kept on the research doc's own advice —
our consumers want the current value — and not bundled into the fix
commit, where a second behaviour change would have made the user's
cliff gate ambiguous. AD section 49 -> 50.

Section 3.5's blast-radius items stay open as watch items, now strictly
MORE reachable than before the fix (last-known validity is narrower, so
the StopVelocity recovery and phase-3 reset take their invalid branches
more often). Carried onto Campaign S slice S4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 00:38:02 +02:00
Erik
9b9bb6515f docs: the #32 'fix failed' verdict is VOID — the tested binary never contained the fix
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
Two checkouts, one relative launch path. Edits and builds ran in the
main repo; every client launch ran from a PowerShell shell whose cwd was
still the session worktree, so 'dotnet run --project src\AcDream.App\...'
executed the worktree's 08-06 22:35 binary — #333 present, #32 fix,
InitContactPlane and every #338 probe absent. Byte-proof both ways: 0
occurrences of the fix strings in the worktree's Core.dll, both present
in the main repo's.

Everything the previous entry concluded is therefore void: the
byte-identical capture was the OLD code re-running (expected), the three
probe silences were one fact (the instrumented binary never ran), and
the 26,358-write attribution table is pre-fix baseline data of the old
binary only. #32's fix returns to UNTESTED, with no evidence against it.

The verification that was supposed to catch this confirmed the wrong
binary: the DLL byte-check ran against the OTHER checkout's bin. So the
self-report now prints typeof(PhysicsDiagnostics).Assembly.Location as
its second line — binary identity becomes a recorded fact inside every
capture instead of an inference from file timestamps afterwards. Memory
updated with the multi-checkout rule: absolute launch paths, verify each
shell's cwd before the first launch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 00:27:57 +02:00
Erik
ee4d328408 docs: #32's setter split is necessary but NOT sufficient — and the instrument is not trustworthy yet
The 332045c7 fix produced a byte-identical live capture at Rithwic: same
six events, same curN == lastN, same apply=False. The user still falls
through. The commit is not wrong and is not reverted — it restores
retail's setter split and is sabotage-verified — but it closes a writer
that is not the operative one here.

Caller-attributed capture puts 26,358 last-known writes in
PhysicsEngine.ResolveWithTransition, whose only such writes are
check_contact's FAILURE branch seeding from body.ContactPlane. Recorded
with the loop that suggests (2041-2044 seeds ci from body; 2168-2174
writes body from ci) explicitly marked NOT PROVEN, because it rests on
line-number mapping from an optimised build where inlining makes
attribution approximate.

Filed with a BLOCKER at the top: an unconditional one-shot WriteLine at
the head of that same method printed zero times. Both facts cannot hold.
Three conclusions were drawn from probe silence this session and all
three were premature, so the entry says to settle assembly identity with
a check that cannot be explained away before any further work on #32 or
#338 — rather than let a fourth inference compound the first three.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 00:25:05 +02:00
Erik
375cc0f950 docs: file #339 — stuck in portal space, destination reveal never becomes ready
Captured live 2026-08-07 with the raw log attached rather than
summarised. Generation 2 to cell 0x3032001C: render, composites and
collision are all False at begin and still all False at cancel, so
complete and world-visible never fire and the five-second wait cue sits
there until the client is closed.

Filed rather than chased, per user direction. Two things recorded
because they will otherwise be assumed: the same destination succeeded
TWICE in the previous session, so it is intermittent rather than a
broken landblock; and while it is mechanically very likely unrelated to
the #32 contact-plane change landed minutes earlier (different
subsystem, different thread), it fired on the first run after it, so the
entry says to A/B against a binary without #32 before ruling it out
rather than asserting independence.

Also flags, without assuming either way, that #280's D-1 was an
unrecoverable portal hang with the same visible symptom — this is either
that regressing or a second mechanism wearing its face.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 00:17:48 +02:00
Erik
801ff5fd44 docs: #338 answered — retail DOES read the authored step height, so this is real
The gating question is closed against the decomp rather than reasoned
from our source. CTransition::step_up @0x0050b610 defaults step_up_height
to 0.0399999991f and substitutes object_info.step_up_height when
(state & 2); step_down has the same shape at 0x0050b852 and reads the
authored value unconditionally at 0x0050c232.

Two things fall out. Retail's fallback is 0.04, not 0.4 — our value
matches neither the fallback nor the authored 0.600/1.500. And state
bit 0x2 is OnWalkable, so retail applies the authored height only while
standing on walkable ground. We already port that gate faithfully in
Transition.DoStepUp, including the stepDownHeight = oi.StepUpHeight
assignment that reads oddly but is exactly what retail passes. The gate
is not the defect; only the value fed into it is.

The local player is the only affected population: its controller fields
initialise to 0.4f, while remotes and live entities get Setup-derived
values. The property's doc comment names PlayerModeController.
ApplyStepHeights as the authoritative writer — that method does not
exist anywhere in the tree; the identifier appears once, in the comment.

Deliberately NOT fixed. A real writer does exist further out, and
RuntimeSetPositionMoverPreparation does compute the Setup-derived value,
so the plumbing is there. Whether it runs for the local player or runs
and is overwritten is unproven — the probe reading 0.400 says the
controller held its default, not why. Setting the field without knowing
which path won would be a coin flip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 22:45:45 +02:00
Erik
3171f43002 docs: file #338 — player steps at 0.400 where Setup 0x02000001 authors 0.600/1.500
Spotted in the #337 [support] capture and deliberately left out of that
fix so the fix stayed falsifiable. Filed with what is NOT established
attached: whether retail reads the authored Setup field at all is the
first question, and the entry says to grep named-retail before touching
anything. The #337 lineage already burned two diagnoses reasoned from
source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 22:17:28 +02:00
Erik
ea83b043df fix(physics): delete the query-site broadphase reach filter (#333, closing #337)
Transition.FindObjCollisionsInCell discarded a shadow candidate when
  |currPos - obj.Position| > sphereRadius + obj.Radius + movement.Length() + 2f

obj.Position is the part ORIGIN; obj.Radius is the physics-BSP ROOT
BOUNDING SPHERE's radius, measured about a centre AP-156 established is
frequently metres from that origin (376 of 973 installed physics-BSP
parts sit further from their part origin than half their own radius,
worst 20.762 m). Geometry deep inside the real bounding sphere was
therefore thrown away before BSPQuery ever ran: solid near the origin,
permeable in a bounded shell beyond it. For the Neftet rock 0xC8766009 /
gfx=0x01004751 the two points are 23.556 m apart, which is #337 — wedged
on the plateau, jumps sinking into the mesh, corpses falling through. A
live capture recorded 7,225 rejections on that one owner, every single
one with wouldAcceptAtCenter=True.

Deleted rather than re-centred. Retail has no distance pre-filter,
disassembled from the PDB-paired v11.4186 binary (CodeView GUID
9e847e2f-777c-4bd9-886c-22256bb87f32) rather than read from Binary Ninja:

  CObjCell::find_obj_collisions @0x0052b750 walks shadow_object_list and
  calls CPhysicsObj::FindObjCollisions (0x0052b78b) UNCONDITIONALLY; its
  only early-out is insert_type == INITIAL_PLACEMENT_INSERT (0x0052b759).
  CPhysicsObj::FindObjCollisions @0x0050f050 contains no float compare at
  all. CPartArray::FindObjCollisions @0x00518180 is a bare do/while over
  parts, and CPhysicsPart::find_obj_collisions @0x0050d8d0 is two null
  checks plus a call. Retail's only spatial rejection is the BSP node
  bounding-sphere test inside the walk — correctly centred, which is
  exactly what the deleted filter was not.

Re-centring it (carry BoundsCenter on ShadowEntry) would have preserved
an invention retail does not have, including a +2f slack and a
movement.Length() term with no retail counterpart, and left a second
reach budget to be tuned forever. Retail's own cross-cell slack constant
is F_EPSILON = 0.0002 m, not 2 m.

The method's comment claimed the filter was "the analog of the part
sorting-sphere early-outs inside retail's CPhysicsObj::FindObjCollisions
— response-neutral, pure perf". Both halves were false and cost #333 and
#337; it is replaced by the disassembly above.

Gate: Issue333BroadphaseReachFilterTests drives the production path
end-to-end (ResolveWithTransition -> FindObjCollisionsInCell ->
CollisionTraversal) on a DAT-free fixture so it runs everywhere, as a
discriminating pair. Sabotage-verified: restore the pre-check and
OffCentreBspFloorStopsAFallingMover reaches z=37.800 — exactly the
unobstructed fall, blockedAtLeastOnce=False — while
CentredBspFloorStopsAFallingMover keeps passing. Without the control a
fixture unable to fall would pass the first test for the wrong reason.

Issue337's skipped TheBroadphaseAdmitsTheSurfaceTheMoverIsStandingOn
asserted the now-deleted predicate and could never have gone green; it
is rewritten as installed-DAT evidence pinning BOTH halves of the
diagnosis and is no longer skipped.

Perf measured, not assumed (Release, synthetic all-BSP cell, per
ResolveWithTransition): at 38 candidates — the live maximum — 10.61 us ->
16.68 us (1.57x); at a deliberately unreachable 200, 17.34 -> 39.48 us
(2.28x); ~0.16 us per additional candidate tested. Over 19,701 live
[reach-q] samples the in-cell count is p50 = 9, p99 = 32, max 38.

The ACDREAM_PROBE_REACH rejectedReach column is kept and is now
structurally 0, so a post-fix capture stays comparable with the pre-fix
one; dropping it would make the two incomparable.

AP-158 retired (110 active AP rows). #333 and #337 closed pending the
user's live acceptance at Neftet.

Solution suite 11,231 passed / 4 skipped / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 22:16:53 +02:00
Erik
5a1eeace73 docs(physics): #337 diagnosed — it is #333's query-site broadphase, not the mesh
Report-only. No production code changed.

The collision mesh is present, correctly shaped, correctly placed in the
world, and the BSP traversal reaches every part of it. The mover never gets
as far as the query. FindObjCollisionsInCell's per-object broadphase measures
the mover's distance to the shadow entry's Position — the part ORIGIN — and
compares it against obj.Radius, which is the physics-BSP ROOT BOUNDING
SPHERE's radius. For 0xC8766009 those two points are 23.556 m apart, so a
mover standing on its plateau is inside the real bounding sphere by ~20 m of
margin and is still rejected. Same defect AP-156 fixed in the flood and #334
fixed in the registration extent walk, left in place at the query site.

Measured, not inferred. An offline replay against the installed DAT
reconstructs all eleven landblock-0x8766 owners and matches the live [geom]
placement exactly (0xC8766002 at (84.699,100.082,13.000) yaw -45.00 vs the
log's objPos + bspCentreOffset). At the position the client fell through, the
production swept query returns a hit on poly 31 at 0.037-0.366 m while the
filter rejects the candidate: distToOrigin=60.434 > maxReach=59.697, distance
to the bounding-sphere CENTRE 37.083 m against a 56.909 m radius. The live
capture recorded that rejection 7,225 times with the probe's own
wouldAcceptAtCenter=True on every one.

Bounded because the dead zone is the shell between maxReach and the true
sphere, up to ~23.5 m thick on the far side. movement.Length() is a budget
term: a 0.25 m walking step gives shortfall +0.60, a 0.72 m step +0.14, and
~0.86 m passes — which is exactly why jumping over the spot works, walking
into it does not, and a corpse falls through.

Three hypotheses refuted by measurement, not by argument:

- "the rock's own mesh never collides" — true of 0xC8766002 and it is
  INNOCENT; its geometry is 22.8 m from the wedge and it has zero brute-force
  hits over a 12,493-point lattice covering the plateau. It is a candidate
  only because it is a 130x147 m owner. The rock actually walked on is
  0xC8766009.
- wrong world transform — the offline placement reproduces the runtime
  exactly, and a uniform displacement cannot produce a bounded pocket.
- BSP traversal hole — a referee ran the production walk against brute force
  at 7,770 on-surface probes across all eleven owners plus 137,423 lattice
  points. Mismatch 0 everywhere. A 0.5 m hole map also shows continuous
  upward-facing coverage across the whole wedge region.

[geom]'s verdict=coincident was never able to decide this: LogGeometry
compares the physics box against the visual box in the object's OWN LOCAL
FRAME, so it proves shape agreement and says nothing about world placement.
Recorded in the doc so the next reader does not re-trust it.

Retail has no per-object distance filter on the BSP branch. Verified
instruction-by-instruction with cdb against the PDB-paired v11.4186 binary:
CPartArray::FindObjCollisions @0x00518180 is 14 instructions of bare
do/while over parts[i]; CPhysicsPart::find_obj_collisions @0x0050d8d0 is 17
instructions of two null checks plus the call to CGfxObj::find_obj_collisions
@0x00534700. No compare, no float math in either. The in-tree comment calling
the filter a retail analog and response-neutral is wrong on both counts.

The support=object cpNz=1.0000 readings inside the rock are not the rock:
ValidateTransition:6076 is retail's stationary-fall failsafe manufacturing a
flat plane through the sphere bottom, and :5997 is the LastKnownContactPlane
restore holding a stale plane. Both are retail-correct responses to a stuck
body, and they are why the client believes it is standing while ACE rejects
the position.

Preferred fix is to delete the pre-check for BSP entries and correct the
comment; fallback is to measure to the bounding-sphere centre, which also
needs BoundsCenter carried on ShadowEntry. Neither is landed.

The reproducer was confirmed to FAIL when un-skipped, with the numbers above
— this campaign has caught eleven green tests covering nothing, so a fixture
that cannot distinguish the bug is worse than none.

Gates: bin/obj deleted, Release build 0 errors, Core suite 4,287 passed /
2 skipped / 0 failed (baseline 4,286/1 plus three new dumps and the one
deliberately skipped reproducer).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 21:16:31 +02:00
Erik
49a7e90652 probe(physics): ACDREAM_PROBE_SUPPORT + ACDREAM_WIRE_MESH — separate #337's three candidates
The user is wedged at the top of Neftet rock plateaus, jumps sink into the
mesh, and a corpse falls straight through. ACDREAM_PROBE_REACH already ruled
out its own domain: blocked=0, every candidate tested-ok. Three candidates
remain — terrain support, a collision mesh not where its visual is, or the
transition wedging on an unobstructed path.

ACDREAM_PROBE_RESOLVE alone cannot separate them. It prints a three-value
contact-plane token, no plane normal, no plane height, no terrain sample and
no plane provenance, so all three produce the same line. Two additions:

[support] — one line per resolve for EVERY body, not just the player. A corpse
is a plain physics body with no player-specific logic, so its fall-through is
the cheapest available control on "movement code vs geometry data", and it is
invisible to any player-filtered probe. The line samples the outdoor terrain
INDEPENDENTLY at the body's own out-XY and prints the contact plane's own
height at that same XY. Two heights at one point make support=terrain /
object / none a measurement rather than an inference, and cpSrc= names the
site that asserted the plane so provenance and classification cross-check.

[geom] — once per GfxObj that comes near a mover: the object's physics-BSP
vertex cloud against its visual mesh AABB in the same local frame, through the
same prepared accessors the resolver queries. verdict=coincident REFUTES the
working hypothesis for that object outright; no-physics-bsp / empty-physics-bsp
/ displaced / extent-mismatch each name a specific data defect. Built to
refute, not to confirm — two diagnoses on this defect's lineage have already
been refuted by measurement.

ACDREAM_WIRE_MESH upgrades the existing F2 overlay, which drew a broadphase
proxy cylinder for BSP objects and so could not answer the question at all, to
the real physics-BSP polygon edges (cyan) beside the visual mesh box (magenta)
and the terrain surface (yellow). Own class per code-structure rule 1.

The provenance latch lives on PhysicsDiagnostics, not on CollisionInfo. Two
fields there first — the obvious home — broke the flat/graph differential
referee and the scratch-reset poison test, both of which compare CollisionInfo
member-for-member. Teaching either to skip a member is a one-line green fix
that puts a permanent hole in a referee whose whole job is comparing
everything. Captured as feedback_probe_state_off_compared_types.

Seven tests cover the support classifier's boundaries: a wrong classifier does
not fail to answer, it answers confidently wrong.

Gates: Release build 0 errors; complete suite 11,225 passed / 4 skipped / 0
failed from a cleaned tree — baseline 11,218/4/0 plus exactly the seven new
tests, skips unchanged.

Issue #337 filed with the symptom set, what is ruled out, and a table of what
each possible output means. All of this is TEMPORARY and recorded for
stripping with the physics-probe family.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 19:49:59 +02:00
Erik
13fcf38138 fix(physics): port retail's find_bbox_cell_list outdoor extent walk (#334)
acdream had never implemented retail's SECOND cell-membership algorithm.
CPhysicsObj::calc_cross_cells @0x00515230 tests HAS_PHYSICS_BSP_PS at
0x00515285 and jumps (0x0051528f jne 0x515305) to find_bbox_cell_list
@0x00510fc0 for a BSP-bearing object; everything below that jump is the
OTHER algorithm, CObjCell::find_cell_list, and that is all we had. Every
object, BSP-bearing or not, was routed through it.

That path's outdoor expansion is a HARD CAP of one cell in each direction.
CellTransit.AddAllOutsideCells computes minRad = radius, maxRad = 24 - radius
and adds at most the eight neighbours of the sphere's own cell, so for any
radius >= 12 m both boundary tests are unconditionally true and the result is
exactly 3x3. Widening the radius or adding a second sphere is mechanically
incapable of adding a tenth cell. The user's live probe measured the
consequence directly: standing inside a Neftet formation, inCell=2 exempt=2
reached=0 -- the geometry was not a candidate at all.

The port. AddAllOutsideCellsFromParts is CLandCell::add_all_outside_cells
@0x00533360 plus add_cell_block @0x005331d0: base landcell from the FIRST
part's own adjust_to_outside, baseX/baseY within-block, each part's authored
CGfxObj::gfx_bound_box re-fit through all eight corners
(BBox::LocalToGlobal @0x005b2120), floor(v / square_length) where
square_length = 0x7c920c = 24.0f, four accumulators seeded to zero, ONE
rectangle unioned across all parts, FILLED, in GLOBAL lcoords so it crosses
landblocks freely, clamped only to [0, 0x7f8).
BuildShadowCellSetFromParts is find_bbox_cell_list's worklist.
RegisterMultiPart dispatches on the same flag retail does, and
BuildFloodSpheres' BSP arm is deleted rather than left unreachable.

Disassembled from the PDB-paired 2013-09-06 binary, not read from Binary
Ninja: BN mis-renders four separate constructs inside add_all_outside_cells
alone -- a dropped `and eax,0xffff` on baseX, a neg/sbb/and select shown as
identically zero, a wrong get_landcell argument, and both x87 flag tests as
`unimplemented {test ah}`.

ShadowPartGeometry pairs the BSP root sphere with the authored box so no
resolver can answer one and leave the other call site to synthesize a
substitute -- the AP-156 invariant applied a second time, since that split is
what produced AP-156 and then this. The box comes from
FlatGfxObjVisualBounds, already computed by exactly CGfxObj::init_end's
algorithm and already in the prepared package: no bake change, no DAT re-read.

Cost, measured over the installed DATs before any code was written: 1,258
physics-BSP GfxObjs, cells/object p50 4, p90 4, p99 12, max 49. The port is
CHEAPER than the old 3x3 = 9 for 98.97% of them. Row totals (shapes x cells)
over all 1,031 landblocks with BSP owners fall 97,173 -> 15,607 (0.161x);
dense Arwic 0xC6A9 falls 342 -> 43. One landblock more than doubles.

Precondition confirmed before pinning any expected cell set: 0x010046D8's box
is 96 m x 96 m about cell (2,2) = 0x87640013, which independently corroborates
the 3x3-centred-there diagnosis, and its rectangle does contain 0x87640011 and
0x87640019 -- the two cells the probe measured empty.

Register: AP-156's outdoor half CLOSED and its risk column CORRECTED (it read
"extra broadphase candidates, never a missed one", which generalised the indoor
direction to the whole row and is why #334 sat inside it unnoticed). AP-159 +
issue #335 file the unported indoor arm; AD-49 records the seed-time rectangle.
Issue #336 files a fourth load-sensitive test flake seen once during the gate.

Ten tests, every one sabotage-verified in both directions across eight
mutations (dispatch, 8-corner refit, floor-vs-truncation, union-vs-per-part,
map clamp, adjust guard, landblock clamp, box-path-for-everything). The
strongest is an installed-DAT replay of the user's own probe evidence.
Suite 11,208 -> 11,218 passed / 4 skipped / 0 failed; the +10 is exactly the
new tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 19:07:06 +02:00
Erik
f0588725cf docs: #334 measured in game — a landblock-spanning object is registered by one sphere
The reach-filter theory I proposed is REFUTED by measurement, and the real
cause is found. Probe evidence committed as 334-neftet-probe.log (8,401 lines,
ACDREAM_PROBE_REACH at b61f5fd4).

Standing INSIDE the formation, the collision system reports inCell=2 exempt=2
reached=0 rejectedReach=0 — two candidates, both the player's own body spheres.
Nothing was rejected because nothing was there. That kills AP-158 as the cause:
the formation is not in the candidate set at all.

The blocking part the user found is what makes it diagnostic. One object does
collide — gfx=0x010046D8, a BSP with objR=69.471, the landblock's baked rock
geometry — and it appears in cells 0x8764000A and 0x87640012 while being absent
from 0x87640011, 0x87640019 and 0x87630018. Same object, same landblock,
present in one cell and missing from the one directly beside it (grid (2,1)
versus (2,0)).

Cause: BuildFloodSpheres derives cell membership from a single bounding sphere
per part. A 69.471 m radius cannot reach every cell of a 192 m landblock, so
cells beyond it get no registration and the player walks through. Retail does
not use a sphere here — calc_cross_cells 0x00515230 routes BSP objects to
find_bbox_cell_list 0x00510fc0 -> calc_cross_cells_static 0x00518160, a walk
over the object's extent.

Recorded explicitly because the null result was misleadable: AP-156 (b52967de)
did NOT fix this and was never going to. AP-156 corrected the sphere's
POSITION; this is about its COVERAGE. Sequential halves of one weakness, not
competing explanations — and without that note the next reader would reasonably
conclude AP-156 had failed.

Process note worth keeping: I proposed the reach filter, and a DAT sweep would
have "confirmed" it by finding exactly the oversized objects I predicted. The
user insisted on measuring in game instead, which produced the opposite answer
in one run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 17:56:08 +02:00
Erik
d4e956b4e2 docs: file #334 — large static formations lose collision at their boundaries
User-reported in live play at Neftet: solid on approach, permeable at a
boundary between two formations, and no floor above — jumping over lands you
through.

NOT a regression from today's collision work, and that was established by
measurement rather than argument: a client was built at 52175aa1 — before
AP-22, AP-152, AP-156, AD-10 and #276's remainder — and the user reproduced
both this and the cliff-edge symptom identically on it. Pre-existing, simply
never filed.

The leading candidate is already filed as AP-158 / #333: the broadphase
measures reach from the part ORIGIN against a `+ 2 m` budget, where retail has
no distance pre-filter at all (CObjCell::find_obj_collisions 0x0052b750
dispatches unconditionally, its only early-out being INITIAL_PLACEMENT_INSERT).
118 of 477 unique BSP GfxObjs exceed that budget and 46 exceed 5 m — and a
"large stone formation" is exactly the class whose geometry sits many metres
from its origin. Solid in the middle, permeable at the edges, no floor
overhead, follows directly.

That also explains why AP-156 did not fix it, which is worth recording because
the null result looked like a failed fix: AP-156 put the object in the right
CELLS; AP-158 is why it is still rejected WITHIN them. Sequential, not
alternative — this issue is the observable proving the second half still bites.

The issue names the one measurement that settles it (the offending Setup's BSP
root-sphere origin offset) and states plainly that no fix should be attempted
first — the `+ 2f` slack is invented, so widening it would be tuning a
non-retail constant rather than removing it.

The user's other report — running off cliff edges instead of stopping — is
already covered by #32 (HIGH, open since 2026-04-29), whose row states
"Local-player edge-slide is unchanged by this work" after its remote half
closed. No new issue filed for it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 17:07:52 +02:00
Erik
e6457cc849 fix(physics): close the AP-156 fix review — real containment oracle, type-level invariant, AP-158
Both review lenses PASSED; this is the cleanup, not a rescue. Evidence:
docs/research/2026-08-06-ap156-review-closure.md (the review itself is
committed alongside it as the received artifact).

R1 — the load-bearing containment test could not fail. Its truth and flood
values were two hand-copies of the same expression over the same part set,
so the shortfall was algebraically identically zero for any DAT input. The
oracle is now PHYSICS-POLYGON VERTICES — a different DAT field from the
bounding sphere the builder emits, so the two sides can genuinely disagree.
Sabotage-verified three ways after full cleans: dropping the bounds centre
in production reddens it (428 Setups, worst 35.869 m on 0x0200129A, matching
an independent out-of-repo sweep exactly); dropping only the scale on the
centre reddens it (326); and corrupting the TEST's own bounds oracle reddens
it (467) where under the shipped oracle that same corruption was invisible
by algebra. Renamed accordingly. A6's stale "cap control" comment corrected:
that loop is the test's own uncapped re-implementation and cannot observe a
cap regression — the cap is covered in Core.

R2 — the population was understated. 172 is AP-152's DISPATCH population;
AP-156's is 530 BSP-bearing Setups, of which 525 have a flood sphere move
and 428 fail vertex containment before the fix (412 at a 1 cm tolerance —
the review's figure; the gap is 16 Setups between 1.4 mm and 10 mm, real
geometry). 0 fail after, at any tolerance down to zero. Corrected in the
AP-156 row, the section-3 header, the C5c handoff and two test docstrings.
Dated review artifacts are left as written — "170 of 172" was correct for
what they measured, and rewriting evidence to match a later measurement
loses provenance.

A1 — BoundsCenter = default reopened at the type what the commit closed at
the seam. Dropping the default alone would NOT have closed the review's own
scenario (a copied Cylinder call site would write Vector3.Zero explicitly
and stay green), so ShadowShape's constructor is now private and BSP shapes
are built only through ShadowShape.Bsp(..., FlatCollisionSphere localBounds),
which takes radius and centre as ONE value and scales them together. There
is no expression a caller can write that carries one and drops the other.
22 construction sites converted; the same sabotage now reddens 5 Core tests
where the review's sabotage A reached 4, because both BSP producers share
one scaling path.

A2 — #333 is real and bigger than filed, and its retail question is
answered. I disassembled CObjCell::find_obj_collisions @0x0052b750 from the
PDB-paired binary myself (check_exe_pdb.py MATCH) rather than inheriting the
claim: its only early-out is sphere_path.insert_type == INITIAL_PLACEMENT_
INSERT, then it calls FindObjCollisions on every unparented non-self shadow
object UNCONDITIONALLY. Retail has NO distance pre-filter, so acdream's
"+ movement + 2f" reach filter is an invention with no register row — filed
as AP-158, carrying the disassembly, the F_EPSILON = 0.0002 m contrast, and
the measured blast radius (118 of 477 unique installed physics-BSP GfxObjs
exceed its ~2.5 m budget, 46 exceed 5 m). Active AP rows 109 -> 110.

Recorded prominently in three places a reader will hit: TALL PROPS MAY SHOW
NO VISIBLE CHANGE UNTIL #333 LANDS, and a null result at the connected gate
is EXPECTED, not evidence against AP-156.

LOW items. R3: the comment claiming the cited evidence justified the whole
cap line is corrected, but int.MaxValue on the sorting-sphere branch stays —
capping at 1 would take Spheres[0], and retail's one sphere is
CSetup::sorting_sphere, a different DAT field; capping keeps the wrong field
AND flips the substitution under-inclusive (#98/#168 direction). AP-157
already owns it. R4: acdream scales the flood sphere where retail's
find_transit_cells never reads gfxobj_scale — added as a second residual on
AP-156. R5: retail's slack constant carried into AP-158 and #333. A3: the
per-call delegate allocation is back to a cached field, still derived from
the single bounds resolver. A5: noted; b52967de's message cannot be amended.

Gates: all 44 bin/obj deleted before every verdict-deciding build, each test
run gated on a verified "Build succeeded" in the same invocation. Release
build 0 errors / 21 pre-existing warnings. Complete suite 11,208 passed /
4 skipped / 0 failed — reconciles exactly with the e2b2d04c baseline; one
test renamed, none added, removed or skipped. Nothing conflated with the
known load-sensitive flakes #302 / #308 / #321.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 16:44:48 +02:00