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>
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>
Both static sites (LandblockPhysicsPublisher, the headless-only
LandblockPhysicsContentBuilder) emitted an authored Setup Sphere as a
base-anchored Cylinder of radius r and height 2r. The live path emits a
Sphere for the same data, so the same object collided differently by
arrival route, and the narrow phase met a flat cap where retail meets a
curved surface. Both sites now mirror ShadowShapeBuilder.FromSetup's
Sphere block exactly.
Combined Opus review: PASS. Its numeric verification of the dispatch
test's geometry (head-sphere clearance 0.201 m for the true sphere; the
cylinder counterfactual inside by 0.10 m XY with the Z band overlapping)
is what makes the discrimination claim more than a sabotage anecdote,
and its F8 finding is applied: the test now carries a POSITIVE control —
aiming straight through the boulder's centre must block — so a
membership/seed regression can no longer masquerade as a curve-hit
pass. F4 applied: CylHeight is asserted, not inferred (the C4 lesson).
F3 applied: the deleted Quaternion.Inverse base composition is recorded
as internally coherent for the old cylinder's world-Z axis — the defect
was the shape TYPE, not that rotation math.
The review also verified the deleted-cylinder blast radius: the F2
overlay's drawn span is IDENTICAL for both shapes (old [c-r, c+r], new
[c-r, c+r]); the flood sphere's centre rises by exactly r, which cannot
change outdoor membership (XY rectangle) and lands the indoor half on
Session B's dungeon gate alongside S1B; and the sphere-branch flood is
now pinned uncapped by a genuine eleventh-shape A/B test.
PublishStaticCollision — the headless static path — gains its first
test ever.
AP-155 is NARROWED, not deleted (review F13): the has-BSP source split
(entity.MeshRefs vs setup.Parts + AnimPartChanged) survives and keeps
the row active. The shared-primitive-emitter refactor that would make
route independence a compile-time property is the filed follow-up
(review F20).
Population: 3,506 of 5,935 installed Setups, structurally equal to
AP-157's third-branch count (byte-identical classifier — three
independent routes agree: 3,605 - 99 = 3,506).
Clean-room suite at implementation: 11,253 passed / 6 skipped / 0
failed on landed S1B. Post-review-hardening: Publisher tests 25/25,
Content tests 2/2, both green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
AP-83/AP-91 claim no current mover sets PerfectClip. False: the camera
probe sets it, faithfully to retail's camera flags. The containment
proof therefore has a real question — whether the viewer exemptions or
the camera sweep's shape cut every chain to the ACE-derived TOI tails —
and an honest fallback if they do not: the rows get rewritten with the
camera named as the live population rather than 'unreachable'. Guard
either way, so a future flag change cannot silently start executing
math nobody re-verified.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The suspect line is pinned at scoping: our ValidateWalkable measures the
sphere's VERTICAL bottom against the plane (planted rest, perp = r*N.z)
while our AdjustSphereToPlane is already a faithful tangent port — we
mix the two geometries today and the planted one wins on terrain. D0
byte-pins retail's validate_walkable distance basis with an explicit
STOP if it refutes the premise; D2 retests the #341 harness flip ten
ways under the mechanism's stability prediction; the gate teaches the
user that slightly hovering feet on steep slopes is the CORRECT retail
look. Queued behind S1B and S2 — one physics slice in flight at a time.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
The six-part sitting ran 2026-08-07 on the post-S4 binary: nine-stop
tour, portal repetition, the cancelled-teleport vanish-and-return,
equipped-item teleport, two-client observation, and the fresh-process
logout/relaunch cycle. Log evidence: 19 reveal generations, 17
materializations, one cancel correctly superseded by its immediate
replacement, zero hangs, zero wait-cues. The graceful-logout path
cleared the ACE session instantly and login returned to the exact last
location.
Recorded honestly rather than roundly: the placement probe families were
not armed during the sitting, so route-7's thin cause=propagate evidence
was not thickened and 4b-3's cause=cellless case remains unexercised
with an unestablished trigger. Both close as user-passed with thin log
evidence; the symptom-side gates are the regression net.
The probe-family strip is unblocked and queued behind the in-flight
Campaign S slice for build-slot reasons.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
'Slopes feels good' at the downhill/diagonal/jump-landing gate, on a
launch whose capture self-verified the binary identity and the authored
step heights. The overnight program's one owed visual gate is closed;
Campaign S continues at S1B.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
The slice text claimed a door's collision stays where the shut door was.
AP-84's own row refutes the scenario: an open door is ETHEREAL (#150)
and bypasses collision entirely, so a door only ever collides in its
registered default pose — the approximation is behaviourally equivalent
for the entire known BSP-part weenie population, and the row's risk
column already carries the revisit trigger for animated non-ethereal
BSP movers if they ever appear.
The campaign's governing lesson says register rows are leads that must
be re-measured; the dual of that is that a CORRECT row must not be
'fixed'. The plan itself committed the reading failure this time. No
fix, no gate, no door row in the morning sitting.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Applying feedback_register_revert_resurrection to its own discovery:
auditing every row in a8a7d64b's register hunk found the inverse defect
in the same diff. The revert resurrected retired AD-55 AND deleted
then-active AD-56 (the plumb-fall freeze adaptation, split out of TS-4
by the commit being reverted). Nothing ever restored it, and its
condition came back to life when TS-4's Path-6 shortcut deletion
re-landed for real at Slice 2B — so for eight days a live, test-pinned,
deliberately-documented behaviour had no register row, which is 'a bug
twice over' by the register's own rule.
Restored verbatim from 5e2be19b with a provenance header. One revert,
two silent register corruptions in opposite directions — the memory
rule's audit step is not optional. AD section 50 -> 51.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Sledding constant has been the byte-confirmed cos(10 deg) =
0.98480775f in production since 252e8068 (2026-07-30), which also struck
the register row. Five hours later a8a7d64b — reverting the UNRELATED
TS-4 commit 5e2be19b — restored this file's older hunk and resurrected
the un-struck row text while leaving the code fixed. The zombie row then
cost tonight's session a full duplicate byte-derivation: the stale row
said 0.99999536f was live, so the binary was re-read to prove what
252e8068's own commit message already contained verbatim.
Tonight's derivation note is corrected to what it actually is — an
independent confirmation of the week-old fix — and S5 collapses to this
bookkeeping: no code change, no feel gate owed; the user has been
playing on the fixed constant for a week.
Process rule filed to memory (feedback_register_revert_resurrection):
after any revert whose diff touches the register, re-verify EVERY row in
the touched hunks; and before acting on a row's 'our code does X' claim,
grep the cited file for the claimed expression first. AD section
51 -> 50 active rows.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both static publication paths emit an authored Setup Sphere as a
base-anchored Cylinder (r, 2r) while the live path emits a Sphere for
the same data — different narrow-phase dispatch and a route-dependent
collision difference for the same object. The fix mirrors FromSetup's
step-3 emission at both sites; the contract's first test is the
route-independence property asserted shape-for-shape, and the dispatch
test picks geometry where cap-hit and curve-hit verdicts differ so the
sphere verdict is observable, not inferred.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
snap_to_plane @0x00509c50 semantics extracted at scoping: XY preserved,
Z re-solved so the offset lies in the plane, no-op under the 0.0002
|N.z| epsilon — versus our orthogonal projection, which is exactly the
cos-squared downhill shortfall AD-65 recorded. Branch polarity pinned
from the test ah,0x41 idiom at 0x0050a4fa: into-plane subtracts,
away-from-plane snaps.
AD-66's port carries a mandatory regression scenario: the original
substitution was empirically motivated (uphill contact-flap), so the
contract requires that exact scenario as a test and a full STOP if the
faithful port genuinely reds it — re-filing the divergence as deliberate
is the session lead's call, not the implementer's tune.
Priority note: S4 jumps ahead of S1B in the overnight queue on felt
value — 25-50% downhill XY shortfall is daily-feel, while S1B's
over-inclusion is zero-felt fidelity. The campaign's
membership-before-query ordering is about masking, and an over-inclusive
residual masks nothing downstream.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Campaign S S1A, both outcomes the measure-first rule exists for.
AP-157's CylHeight half is RETIRED as a non-divergence: retail's own
cylsphere overload (CObjCell::find_cell_list @0x0052b9f0) copies
localtoglobal(low_pt) + radius per cylsphere, capped at 10, and never
reads height — retail collapses a cylsphere to a base-point sphere
exactly as acdream does.
The sorting-sphere half measured REAL against retail's registration set
— 1,812 of 3,343 evaluated Setups (54%) fail containment at 1 mm, worst
shortfall 18.135 m — and then PROVEN collision-unreachable: for this
branch the flood spheres and the collision-test geometry are the same
per-part Sphere list, so every omitted cell is one the entity's test
geometry cannot reach, and retail's wider sorting-sphere registrations
are narrow-phase rejects on retail too. Fix deferred to the next
bake-schema revision rather than performing Slice I3 surgery for zero
behavioural delta. The measurement test stays in the tree as the
permanent record (population cross-checked against the dispatch test's
independently-committed constants: 3,506 = 3,605 - 99).
AD-55 is byte-decoded and RESOLVED against our constant: the binary
loads qword [0x007c6b28] = pi/18 exactly and executes FCOS — retail's
Sledding flatness threshold is cos(10 deg) = 0.984808. Our 0.99999536f
is cos(0.17453 DEGREES): the radian literal misread as degrees, which
makes the object-friction arm unreachable on real terrain (nothing is
flatter than 0.175 deg). Evidence note carries the full instruction
listing and the polarity of the test ah,0x41 / jp idiom; the one-line
fix + conformance test is S5, queued behind the running implementation
slice for build-slot reasons.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Campaign S night shift. #330's contract pins the scoping facts so the
implementer inherits measurements instead of re-deriving them: the
builder is already presentation-free logic (the only App coupling is one
identity-guard parameter), headless has full content (_content.Dats +
prepared PhysicsDataCache), shadow-sync already runs in Runtime once a
shadow exists, and the no-window inbound route is host-disjoint per
AD-64 so registration wired there cannot double-register on the
graphical host. The local-player ProvenShapeless pin is explicitly OUT —
fixing it blind risks the K-series gates.
AP-159's S1B contract maps the pseudo-C line ranges for the part-array
find_transit_cells overload, the box-vs-cell BSP traversal, and flags
the adjacent overload's Binary Ninja signature artifact for the
mandatory pseudocode step to resolve. House rule carried: a new
traversal in two representations ships with an exact differential
referee, and the direction assertion (membership strictly shrinks) is a
test, not an assumption.
Also settled at scoping, evidence in the S1A brief: AP-157's CylHeight
half is a NON-divergence — CObjCell::find_cell_list's cylsphere overload
@0x0052b9f0 copies localtoglobal(low_pt) + radius per cylsphere, capped
at 10, and never reads height; acdream's base-point cylinder flood is
exactly retail's behaviour. Register correction follows with S1A's
measurement numbers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
Report-only investigation of the local player running off cliff edges. No
production or test code changed.
Retail's rim slide is CTransition::cliff_slide (0x0050a6d0), whose direction is
cross(steep contact normal, last_known_contact_plane.N) — it needs the surface
the mover was standing on as its second vector. Disassembly of the PDB-paired
binary shows COLLISIONINFO::set_contact_plane (0x00509d80, 22 bytes) writes only
contact_plane_valid / contact_plane / contact_plane_is_water; the last-known
group has exactly four semantic writers in retail, all outside the per-substep
collision response (CTransition::init_contact_plane 0x0050e850,
init_last_known_contact_plane 0x0050e8e0, the validate_transition tail
0x0050ad07, and the clears).
acdream's CollisionInfo.SetContactPlane latches the last-known group on every
write, at all 13 call sites. The step-down probe's own steep plane therefore
overwrites the ground reference before EdgeSlideAfterStepDownFailed reads it,
CliffSlide's cross product goes to zero, its degenerate OK return displaces
nothing, and TransitionalInsert's retry accepts the candidate hanging over the
drop. The latch dates to 9ea8ae51 (2026-04-13) and was never retail-verified,
matching the user's "pre-existing, not a regression".
Also established: every edge_slide branch, gate and threshold in acdream is
byte-exact against 0x0050b3d0 / 0x0050b812 / 0x0050b886 (the step-down probe
schedule is confirmed against the disassembly, which corrects a Binary Ninja
stack-slot misread at 0x0050b8ba); 204d0ae0 deleted four remote-only forgeries
and never touched the sweep, the edge family or contact-plane maintenance, so
it could not have covered the local player; #134 is the same family with an
unverified 2026-07-09 triage closure; and six claims in #32's own text are
stale at HEAD.
The causal link to the user's cliff is a hypothesis, not a proof — it needs
edge_slide to take branch 2. ACDREAM_DUMP_EDGE_SLIDE=1 already prints the
branch and both normals at the deciding site; the report carries the run
command and a six-row decision table. No code before that run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
The previous fix said the local origin/main ref had not been written
since April and was three months stale. It was not stale: a fresh
git fetch origin returned it unchanged at f6275f45, so the REMOTE was
genuinely 412 commits behind and the ref was accurate all along.
The original error was therefore never a caching artefact — it was
reading a lagging mirror as if it were the live remote and concluding
the campaign was unpushed. Recorded that way so the rule is usable:
origin here is a mirror and may lag arbitrarily; measure against
github/main or git branch --contains.
Both remotes are now at parity, 0ce54a5c.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The handoff's opening said "Nothing is pushed — the branch does not exist
on the remote, and there are 388+ unpushed commits ahead of origin/main."
All three clauses were wrong, in the direction that would most alarm a
successor into thinking the campaign could be lost.
main and github/main are both d4e956b4, and git branch --contains
7b3e2895 lists main. The campaign was merged and pushed the same day.
The measurement error is recorded because it will recur: this repo has
TWO remotes. github is live; origin (git.snakedesert.se) has a cached
origin/main ref pointing at f6275f45 whose ref file has not been written
since 2026-04-27. main is 412 commits ahead of that three-month-stale
ref, which is where "388+ unpushed" came from. Measuring push state
against origin/* here without checking the ref's age produces a false
alarm every time.
Also opens Campaign S as the ACTIVE work ahead of the remaining M4
vendor slices, per user direction: "I think its better to fix these kind
of things before we add new stuff. Physics and collisions are vital."
Clean-room gate at a0690947, all 43 bin/obj directories deleted first
per the closeout's own rule 4: 11,231 passed / 4 skipped / 0 failed
across all nine projects.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Plans the twelve remaining collision-domain items. Slice boundaries come
from file coupling, not from how the symptoms group: AP-157, the AP-156
residual and AP-159/#335 all edit ShadowObjectRegistry (two of them the
same function), so they are ONE slice against a pinned contract per
feedback_dont_parallelize_coupled_plan_slices.
Ordering is membership-before-query, because we just watched a
membership fix (AP-156) be made invisible by a query-site defect
(AP-158) directly downstream of it.
Every slice opens with a measurement that can cancel it. In this domain
over the last two weeks: AP-155's recorded direction was inverted,
AP-156's risk column was wrong (and that is why #334 hid inside it),
AP-22 described an unreachable branch, and #331's headline claim was
refuted. One row in four was materially wrong about its own population,
direction, or existence.
Two items are deliberately reclassified. AP-83/AP-91 are NOT fixes —
the x87 PerfectClip tails do not decompile, so there is no retail text
to port; the honest deliverable is proving the branch unreachable.
AD-65's "live lead for #269" framing is retracted: #269 closed
2026-07-31 and AD-65's sign is opposite to that symptom.
#32 and #338 are pre-work, not slices — both are blocked on a
measurement far cheaper than the fix, and #32 is the outstanding half of
the user's original two-bug report, so it does not sit behind six
slices.
Live gates batched into four sessions; #330 needs none and is the
parallel track for whenever a gate is blocking.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
Found while fixing AP-156 and deliberately NOT bundled into it: the shadow
broadphase at TransitionTypes.cs:3756-3764 measures `currPos - obj.Position`
against `sphereRadius + obj.Radius + movement + 2f`, where `obj.Position` is
the PART ORIGIN but `obj.Radius` is the BSP root bounding sphere's radius,
measured about that sphere's own centre. The same discarded origin, one layer
down.
It admits a real contact only when the sphere's centre is within about
`movement + 2` metres of the part origin. For Setup 0x02000255 that offset is
9.911 m against a budget near 2.5 m, so a mover touching the upper half of the
prop is discarded before BSPQuery runs.
This is newly load-bearing: before AP-156 those objects were mostly not in the
cell at all, so the filter never got to reject them. AP-156 puts them in the
right cells and this becomes the next gate. It is the first place to look if
the connected session finds a tall prop that still does not block.
Filed rather than fixed because it is a different code path with an unanswered
retail question — the `+ 2f` slack and the movement term look like acdream's
own broadphase rather than a port of anything in CPhysicsObj::FindObjCollisions
@0x0050f050, in which case it needs a divergence row of its own before it is
touched. Bundling it would also make AP-156's connected gate un-attributable,
which is exactly the fault that split AP-155.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The AP-152 retail review (docs/research/2026-08-06-ap152-review-retail.md)
FAILED `4abd1b5e` and is right. `ShadowObjectRegistry.BuildFloodSpheres` took
each physics-BSP part's ROOT BOUNDING SPHERE RADIUS
(FlatCollisionAssetBuilder.cs:393 -> LiveEntityCollisionBuilder.cs:137) and
centred it on the PART ORIGIN (ShadowShapeBuilder.cs:194), discarding the root
sphere's own Origin.
Re-measured independently against the installed client_portal.dat, reproducing
the reviewer's numbers exactly: 376 of 973 physics-BSP parts have
|origin| > radius/2, worst 20.762 m on a 27.708 m sphere (gfx 0x010036DD,
Setup 0x0200129A). Over the 172 Setups AP-152 moved onto that path the emitted
flood FAILED TO CONTAIN the object's own BSP sphere for 170 of them (73
CylSphere-bearing, 97 Sphere-bearing), worst shortfall 9.911 m on Setup
0x02000255 — whose one part's sphere sits 9.911 m above the part origin — and
for 43 the post-AP-152 flood was strictly SMALLER than the pre-AP-152 one.
Indoor flooding is 3-D (CellTransit.cs:601 routes every id & 0xFFFF >= 0x0100
candidate through FindTransitCellsSphere), so a tall prop or door slab was
absent from EnvCells it physically occupies and therefore never a broadphase
candidate there (TransitionTypes.cs:3763 iterates only entries already in the
cell). That is the #98 / #168 class AP-152 exists to remove.
Retail, re-disassembled from the PDB-paired binary (check_exe_pdb.py MATCH,
CodeView GUID 9e847e2f-777c-4bd9-886c-22256bb87f32), every address resolved
back through named-retail/symbols.json:
CGfxObj::physics_sphere is [gfxobj+0x74] (physics_bsp is [+0x78], as
CPartArray::CacheHasPhysicsBSP @0x00518110 reads at 0x00518127), and
acclient pseudo-C 0x00534b5b assigns it BSPTREE::GetSphere(physics_bsp).
BSPTREE::GetSphere @0x005397e0
8b01 mov eax,[ecx] ; BSPTREE::root_node
83c004 add eax,4 ; past BSPNODE::vfptr -> CSphere sphere
So retail's per-part flood sphere IS the BSP root bounding sphere,
ORIGIN INCLUDED (acclient.h: BSPNODE { vfptr; CSphere sphere; ... },
CSphere { Vector3 center; float radius; } -> radius at +0xc).
CPhysicsObj::find_bbox_cell_list @0x00510fc0 adds the object's own cell and
then walks the PART ARRAY: 0x00511012 call 0x518160
(CPartArray::calc_cross_cells_static), which dispatches [edx+0x7c] with
(num_parts, parts, cellarray). Its EnvCell body,
CEnvCell::find_transit_cells @0x0052cae0:
0x0052cb31 mov edx,[eax+0x20] ; CPhysicsPart::gfxobj (CGfxObj**)
0x0052cb36 mov esi,[ecx+0x74] ; physics_sphere (else +0x90 drawing)
0x0052cb4c add eax,0x30 ; CPhysicsPart::pos
0x0052cb5a call Position::localtolocal ; transform the sphere CENTRE
0x0052cb65 fadd [esi+0xc] ; only NOW the radius
Retail transforms the centre through the part's own Position before it ever
touches the radius. Carrying the radius alone is not an approximation of
that; it is a different sphere.
Changes:
* `ShadowShape` gains `BoundsCenter` — the bounding sphere's centre in the
shape's own local frame, scaled like LocalPosition and Radius. Zero for
Cylinder/Sphere shapes, whose LocalPosition already IS their centre.
* `ShadowShapeBuilder.FromSetup` gains a `physicsBspBounds` resolver that
supplies radius AND centre from ONE call, replacing the placeholder radius
plus a downstream substitution. `LiveEntityCollisionBuilder` now holds a
single `Func<uint, FlatCollisionSphere?>` and derives its dispatch predicate
from it, so the gate and the geometry cannot disagree and the radius cannot
be taken while the origin is dropped. That split is what produced this bug;
it no longer exists.
* `FromLandblockBspParts` carries the centre too. A landblock-baked part array
is the same CPartArray walk, so stair runs, fences and rock clusters had the
identical defect. Both storage forms (flat BSP and the graph fallback) are
covered.
* `BuildFloodSpheres` places each sphere at
partWorldPos + rotate(BoundsCenter, partWorldRot), composed exactly as the
ShadowEntry rows are.
* The 10-sphere clamp now applies to the CYLSPHERE branch only. Retail's clamp
is inside CObjCell::find_cell_list @0x0052b9f0
(0x0052ba21 cmp eax,0xa / 0x0052ba28 mov ebp,0xa); the BSP walk has none and
the sorting-sphere overload @0x0052b990 takes one sphere. 7 installed Setups
carry more than 10 physics-BSP parts (max 49, Setup 0x02001A91) and their
tail parts were dropped from the flood entirely. Without this the new
containment assertion would have covered shapes production never floods
from.
Register. AP-155 was two divergences with different code paths, populations
and gates under one id; it is NARROWED to its static-publication half and its
flood half is split out as AP-156 WITH ITS DIRECTION CORRECTED. AP-155(b)
recorded the approximation as over-inclusive — "floods MORE cells rather than
fewer, the safe direction for membership" — and that false direction was the
stated reason the residual was safe to defer. It was under-inclusive for 170
of 172. AP-156 records the correction, this fix, and the one genuine residual:
acdream's sphere-vs-portal traversal where retail walks each part's sphere
against the cell's own portal planes. AP-155(b)'s "acdream approximates
retail's bounding BOX" was wrong too — find_bbox_cell_list forms no box.
AP-157 filed for the review's F4: retail's third branch floods from ONE
CPartArray::GetSortingSphere @0x00518b00 ([partArray+0x54]+0x70 =
CSetup::sorting_sphere; 4,154 of 5,935 installed Setups carry a non-zero one)
where acdream floods from every Sphere shape, and acdream's cylinder flood
ignores CylHeight. Deliberately NOT bundled here: different branch, disjoint
population, different live gate. Active AP rows 107 -> 109, literal count.
Tests. Both flood tests the review named substituted a CONCENTRIC Radius = 14f
at LocalPosition = Zero — the one configuration in which the defect cannot
appear. Every fixture is now off-centre by default, and
`FromSetup_CylSphereAndBspSetup_FloodsTheBspFootprint` drives the production
`physicsBspBounds` seam instead of hand-substituting. Five new facts: the
flood centres on BoundsCenter not the part origin; it rotates BoundsCenter by
the part rotation; it caps cylspheres at ten but never the BSP parts; the
landblock path carries the scaled centre in both storage forms; and an
installed-DAT containment sweep asserting every emitted BSP flood sphere
contains that part's real bounding sphere at entity scale 1.75, behind four
external controls — 973 parts, 376 off-centre, 172 affected, and 170
would-fail-if-the-origin-were-discarded, the last of which fails if the
population ever stops exercising the field.
Nine sabotages, each reverted and re-verified:
A drop BoundsCenter from the flood -> 3 Core
B rotate by entity rot, not part rot -> 1 Core (the rotation fact only)
C FromSetup discards the origin -> 1 Core + 2 App + 1 Content
(the shipped defect, now caught in three projects)
D drop entScale on BoundsCenter -> 2 App + 1 Content
E landblock flat branch drops the centre -> 1 Core
F landblock graph branch drops it -> 1 Core
G drop partScale on the landblock centre -> 1 Core
H re-apply the 10-cap to every branch -> 1 Core
I remove the cylsphere cap -> 1 Core
AP-152's own two sabotages re-run against this tree: the step-0 gate disabled
still reddens exactly its five facts with Headless 89/89 green, and
cylinder-first flooding still reddens exactly one.
Clean Release build after deleting all 44 bin/obj: 0 errors, 21 pre-existing
warnings. Complete suite 11,208 passed / 4 skipped / 0 failed, +5 on the
11,203 baseline at 4abd1b5e — Core 4264 -> 4268, Content 126 -> 127, App
unchanged (one rename, not an addition). No new skips.
NOT yet gated live. This moves shadow-cell membership for real objects, in
both directions, and the connected session must look for both: props and doors
that START blocking from a neighbouring cell (the 73 CylSphere+BSP Setups),
AND ones that STOP blocking (the 99 Sphere+BSP Setups can shrink; 43 shrink
below their pre-4abd1b5e size, which is the regression this fixes). Tall
indoor props and door slabs — the ones whose sphere sits metres above the part
origin — are where the change is largest.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The register row predicted "catching or stopping on a doorway sill". That
symptom could not have been occurring. `Transition.BspOnlyDispatch`
(TransitionTypes.cs:1348, landed 2026-05-25 as A6.P7) already skipped both
primitive branches (:3911, :3954) whenever the target's wire PhysicsState
carries HAS_PHYSICS_BSP_PS, and ACE sets that bit from CSetup.HasPhysicsBSP
for every affected Setup. The extra primitive was never tested for collision.
The live defect was CELL MEMBERSHIP. The same shape list feeds
`ShadowObjectRegistry.BuildFloodSpheres`, which had no such guard and
preferred Cylinders over everything whenever any Cylinder existed — retail's
SECOND priority applied ahead of its first. For the 73 CylSphere+BSP Setups
acdream therefore flooded shadow cells from the cylinder and never from the
slab: an object absent from cells it physically occupies, which is the
#98 / #168 symptom class, not the door-collision class the row named.
Retail, re-disassembled from the PDB-paired binary (v11.4186, CodeView GUID
9e847e2f-777c-4bd9-886c-22256bb87f32, check_exe_pdb.py MATCH) rather than
taken from Binary Ninja, which drops flag tests:
CPhysicsObj::FindObjCollisions @0x0050f050
0x0050f165 test dword [esi+0xa8], 0x10000
0x0050f16f je 0x50f1a2 ; clear -> primitive dispatch
0x0050f18d call 0x518180 ; CPartArray::FindObjCollisions
0x0050f19d jmp 0x50f2b0 ; UNCONDITIONAL, past BOTH primitive loops
; (CylSphere 0x50f1a2, Sphere 0x50f21d)
0x0050f1d6 jae 0x50f317 ; CylSphere loop exhausted -> RETURN
0x0050f22f je 0x50f31b ; zero Spheres -> RETURN seeded OK_TS
CPhysicsObj::calc_cross_cells @0x00515230
0x00515285 test dword [esi+0xa8], 0x10000
0x0051528f jne 0x515305 -> CPhysicsObj::find_bbox_cell_list @0x00510fc0
0x005152d1 call 0x52b9f0 ; cylsphere branch, below the jump
0x005152fb call 0x52b990 ; sorting-sphere branch, below the jump
Priority at both consumers: BSP -> CylSphere -> Sphere -> nothing. BSP wins.
Every address above was resolved back to its symbol by exact lookup in
named-retail/symbols.json.
Changes:
* `ShadowShapeBuilder.FromSetup` gains a step-0 dispatch gate. Steps 1 and 2
are skipped entirely when any part's EFFECTIVE GfxObj carries a physics
BSP. The gate and step 3 now share one `EffectivePartGfxObjId` helper, so
they cannot read different identities — a gate on `setup.Parts` would,
after an ObjDesc swap, suppress the primitives while step 3 emitted
nothing and `Build` returned null, deleting the entity's collision.
Emission order is unchanged. This also removes acdream's undeclared
reliance on the server sending the flag: the gate is derived from the
parts, exactly as CPartArray::CacheHasPhysicsBSP @0x00518110 derives it.
* `ShadowObjectRegistry.BuildFloodSpheres` now applies calc_cross_cells'
own order: BSP, else Cylinder, else everything. Given the gate above this
is a no-op for every shape list acdream produces (FromSetup is now
exclusive; both landblock-static publishers already emit homogeneous
lists), so the measured membership delta remains attributable to the
gate alone. It is kept for the same reason BspOnlyDispatch is kept: retail
genuinely dispatches here, and it guards a future additive producer.
`Transition.BspOnlyDispatch` is deliberately untouched.
Register: AP-152 RETIRED with its four false statements corrected — the risk
statement (the symptom was already inert); "small and centred at the part
origin" (max primitive is 6.714 m, and 0x0200086E's sphere origin is
(0.759, 0.165, 5.842)); the cottage door's "~14 cm base Sphere" (it is
0.100 m; 0.141 is Setup.Radius, which AP-22 proved is never collision
geometry); and naming one pinning test where two existed. AP-153/154/155
filed: retail's dispatch flag is cached once at InitPartArrayObject+0x7e
where acdream's gate is live; the query-time guard takes a client-derived
flag off the wire; and the static publishers emit Setup Spheres as
height-capped Cylinders while BuildFloodSpheres approximates retail's
bounding box with bounding spheres.
Tests. Both pinning tests corrected, neither deleted:
`FromSetup_DoorSetup_ProducesFourShapes` -> `..._EmitsBspPartsOnly`;
`FromSetup_DoorSetup_SphereAtExpectedLocalOffset` re-hosted on
`_ => false`, the DAT-real configuration for the 3,605 Sphere-only Setups.
`FromSetup_ScaleFactor_MultipliesAllRadiiAndOffsets` was the campaign's
eighth green test covering nothing — its assertions sat inside
`if (CollisionType == Cylinder)` on a fixture with zero CylSpheres, so only
`Scale == 2.0f` ever ran. Proved empirically: with the sphere radius scale
deleted, the old body passes and the corrected body fails. Three new facts:
the effective-identity gate, the App-layer CylSphere+BSP registration (no
App fixture combined the two before), and the flood-set dispatch. One new
installed-DAT sweep pins 172 affected Setups (73 CylSphere+BSP, 99
Sphere+BSP) behind external bucket controls, re-measured independently and
agreeing exactly with the filing commit's separate sweep.
All eight sabotages run and reported; every discriminating fact reddens in
the intended direction and only there. Clean Release build after deleting
every bin/obj: 0 errors. Complete suite 11,203 passed / 4 skipped / 0
failed, +5 on the 11,198 baseline at ec29a732 — exactly the five added
facts, no new skips.
Blast radius, corrected: the FromSetup half is graphical-only (its sole
production caller is LiveEntityCollisionBuilder in AcDream.App, which
AcDream.Headless cannot reference — Headless -> Runtime -> Core/Content).
The BuildFloodSpheres half lives in AcDream.Core and DOES execute in
Headless via LandblockPhysicsContentBuilder, but is behaviour-neutral there
because both of that builder's registrations pass homogeneous lists.
Headless suite green at 89/89.
NOT yet gated live: this changes shadow-cell membership for 22 Setups used
by 151 Door weenies and 38 stationary props. Needs a connected session.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#331 reported that `PhysicsEngine.ResolveWithTransition` refuses ALL uphill
motion whenever a `body:` is supplied. It does not. It refuses a step whose
sub-step offset is exactly anti-parallel to a live sliding normal — the
#137-family absorb this project already recorded as retail-faithful.
Measured on the same fixture, same gradient, same body, varying only the
heading relative to the slope gradient:
(0, -0.1, 0) cross-slope 0 -> zero movement, latched
(0.0001,-0.1, 0) cross-slope 0.0001 m -> zero movement, latched
(0.001, -0.1, 0) cross-slope 0.001 m -> climbs 0.176 m in 5 ticks
(0.01, -0.1, 0) cross-slope 0.01 m -> climbs 0.176 m in 5 ticks
The threshold is retail's own F_EPSILON small-offset abort (0.0002 m): about
0.11 degrees off the exact gradient at a 0.1 m step. `RemoteRampHarness`
builds a ramp whose gradient is exactly along Y and the original probe pushed
exactly along -Y, so it hit the measure-zero case with probability 1.
The latch itself is production-real in mechanism — a pure gravity fall under
the production RuntimeRemotePhysicsUpdater, with no fixture settle seam
involved, lands leaving Contact|OnWalkable|Sliding with slidingNormal (0,1,0)
— but every link is faithful to retail, verified in the PDB-paired binary
rather than Binary Ninja (BN typed find_transitional_position `void` and
dropped the load-bearing return value):
validate_walkable sets collision_normal from the terrain plane when
OBJECTINFO CONTACT is clear 0x0050d251 / 0x0050d261 / 0x0050d26c
validate_transition converts it unconditionally 0x0050ac19-0x0050ac30
set_sliding_normal zeroes Z AND re-normalizes 0x0050a060
SetPositionInternal persists SLIDING_TS 0x005154c2 / 0x005154e1
get_object_info re-seeds it next frame 0x00511d44 / 0x00511d4f
find_transitional_position returns
`i != 0 && state == OK` on the step-0 abort 0x0050c0ed -> 0x0050c089
ACE agrees (Transition.cs:1027, CollisionInfo.cs:58). No production code
changed; no divergence introduced, so no register row.
What lands is the coverage whose absence made this invisible — nothing in the
suite asserted that a body-bearing mover makes uphill progress on a walkable
slope, and the test that found #331 passed vacuously because the body never
moved:
RuntimeRemoteUphillProgressTests.ARemoteWithABodyClimbsAWalkableSlopeAndKeepsItsFeetOnIt
per-tick climb + surface tracking under a realistic off-gradient heading.
SAB-A1 AdjustOffset -> Vector3.Zero reddens at tick 1
SAB-A2 fixture gradient -> 0 (flat) reddens at tick 1
RuntimeRemoteUphillProgressTests.AnExactlyUpSlopeOffsetIsAbsorbedByThePersistedSlidingNormal
characterization pin for the absorb, with the retail anchors inline.
SAB-B1 delete the get_object_info sliding seed reddens (climbs to 57.7544)
SAB-A1 reddens
SAB-A2 reddens
NON-discriminating, measured and documented: making the final tick
exactly up-slope leaves it green — by then the latch is already cleared.
RemoteRampHarness gains a warning block naming the axis-alignment trap so the
next vacuous uphill assertion is caught at authoring time.
Suite re-measured from a full clean (43 bin/obj removed): 11,198 passed /
4 skipped / 0 failed, against the 11,196/4/0 baseline at 0d62a5ff — exactly
the two tests added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Campaign closed by user direction after the #280 connected gate passed.
GATE RESULT. #280 user-accepted: "now portal space takes longer but terrain is
complete when I exit" — both halves of the specified criteria, a measurably
longer hold and a complete destination on reveal. Probe evidence: three Portal
reveals plus a Login reveal, every one at radius=12 where pre-fix it was a
hardcoded 1, each portal hold raising the wait cue at ~5.0 s before completing.
An accidental but genuine A/B came out of the same session. An earlier run set
ACDREAM_PROBE_REVEAL_RADIUS=1 — that variable is a radius VALUE, not an on/off
flag — which forced the pre-fix window. The user saw the original defect under
it and not under radius=12. That is the before/after pair the gate asked for,
obtained by mistake. Recorded prominently because the same mistake would
silently reproduce the bug for the next person.
WHAT IS NOT CLAIMED. The ledger closes with most connected gates outstanding
BY USER DIRECTION, not because they were discharged: D-1's two reachability
scenarios, AP-136's six-step park protocol, route-7 thickening (the
remote-teleport probe recorded ZERO lines), the two-client observation, the
nine-stop soak, and the lifecycle/reconnect route. The closeout's section 2.6
is a table of exactly this, and both the campaign plan banner and this commit
say that anyone citing "the campaign passed" must cite it alongside.
THE PROBE FAMILY IS DELIBERATELY NOT STRIPPED. Closing the campaign would
normally retire the six ACDREAM_PROBE_* flags, but their gates were never run,
and stripping now would delete precisely the instrumentation those owed gates
need — the failure the handoff's own rule exists to prevent. Honouring that
rule means not stripping even though the campaign is closing.
ACDREAM_PROBE_REVEAL_RADIUS is also kept despite #280 closing, because AP-149
and #326 are open and would both want the same A/B harness.
#280 is marked CLOSED in ISSUES with its gate evidence, and its residual
AP-149 is restated there: our outer ring accepts terrain-only readiness where
retail's PreFetchCells also requires each landblock's LandBlockInfo and every
building's EnvCells, so distant SCENERY may still fill in after reveal even
though terrain does not. Not folded in — it costs further hold time and is a
game-feel call.
Memory updated with the campaign's closed state and the follow-up order:
#331 first, then AP-152, #330, AD-65.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the automated half of C5c. Everything still owed needs the user at a
live client, and the probe strip cannot precede it.
AUTOMATED GATE — PASS. Complete Release suite on the final binary at
7b3e2895, run after deleting all 44 bin/obj directories rather than trusting
a rebuild flag: 11,196 passed / 4 skipped / 0 failed across all nine
projects. The clean-build precaution is not ceremony — this session had three
incidents of a runner serving a DLL that still contained deleted code, one of
them under -t:Rebuild. Campaign net: 11,106 at 02578441 -> 11,196, +90, with
no new skip anywhere and none of #302/#308/#321 firing.
STATE. Every implementation item in the placement cutover is landed and
dual-reviewed: C5b, #280, #276's remainder, AP-22 and AD-10, all with both
lenses PASS. #309 was accepted as a standing divergence by user decision
rather than fixed. What remains is connected/visual work plus the ledger
close.
WHAT THE HANDOFF CARRIES that a reader would otherwise have to rediscover:
- The connected gates owed, with the detail that matters — #280's route needs
a LIFESTONE leg because the original repro was a recall, not a /teleloc;
D-1's two reachability scenarios have never been reproduced live; and
AP-136's six-step park check SURVIVES #309's deferral because it validates
the shipped rollback path, not the deferred fix. Strip the probes after
those, never before.
- Twelve issues filed (#321-#332). #331 is flagged first: its discriminator
turned out to be the `body:` parameter rather than the fixture, it
reproduces under the local player's own call profile on ramps as shallow as
1.1 degrees, and nothing in the suite asserts uphill progress on a walkable
slope — the test that found it passed vacuously.
- Register movement, including AD-65's corrected magnitude (25%/50%, not
13%/29% — the row stated cos^2 and quantified 1-cos) and why that matters:
it is a live lead for #269, and #269's existing do-not-retry covers friction
and jump chains, not AdjustOffset.
- The bisect hazard from the C4 handoff, carried forward verbatim.
- Nine process findings stated as rules, each paid for this session. The two
that cost the most: a blast-radius survey only reaches as far as the call
graph its author walked (C5b missed an entire host with 11,000 tests
green), and a test is not evidence until sabotage proves it discriminates
(seven green-but-empty tests found or avoided).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both AD-10 review lenses PASS; the deletion stands. These are the findings
they raised. One production file touched, comment-only.
AD-65 WAS UNDERSTATED BY HALF, and it is the finding that matters. The row
states the factor as cos^2(theta) and then quantified 1-cos(theta): "13% at
30 degrees, 29% at 45". The correct figures are 25% and 50%. This is not
algebra alone — #331's probe in the same push measures 0.0735 m travelled for
a 0.1 m request at 30.96 degrees, i.e. 26.5% short, which is exactly
cos^2(30.96). AD-65 is a LEAD for #269's slope-slide residual; at the
understated magnitude it reads as marginal and could have been dismissed. At
50% short at 45 degrees it is a serious candidate. I repeated the wrong figure
in conversation before the review caught it.
"VERBATIM/FAITHFUL PORT" of Transition.AdjustOffset was asserted in five
places and was false as of the very next commit, which filed AD-65 and AD-66
against that same function. Corrected to "structurally exact, with exactly two
filed divergences" in the register row and the production doc comment.
RECORDED, and it favours the change: the redundancy measurement is CONTINGENT
on AD-65 — the two mechanisms agree today partly because both under-travel
downhill. That makes this deletion a PREREQUISITE for fixing AD-65 rather than
merely compatible with it; had the projection survived, correcting
AdjustOffset would have re-introduced a disagreement between two live
projections. The record claimed no such thing and should have.
UNTESTED AXIS recorded: the contract's T2 — its mandatory wrong-plane-versus-
right-plane discriminator — was dropped without record, breaching the
contract's own clause requiring exactly that to be written down. The
consequence is precise: the deletion is measured, but the change's only
claimed BENEFIT (a walkable non-terrain surface now gets the committed contact
plane instead of terrain far below) has zero automated coverage and rests on
source reasoning. Stated in the row rather than left implied.
#331 SEVERITY RAISED from UNKNOWN — the discriminator is known and it is not
the fixture. With `body: null` the same uphill sweep climbs (ok=True, moved
(0, -0.0999, +0.060)); with a body supplied it returns ok=False and zero
movement, under a call profile identical to the local player's
(IsPlayer|EdgeSlide + the human two-sphere Setup). A diagonal request keeps
cross-slope X and zeroes only up-slope Y, and it fires on a 1.1 degree ramp.
So "confined to the synthetic fixture" is no longer the comfortable default:
the failing call shape is the shape production uses. Nothing in the suite
asserts uphill progress on a walkable slope, which is why it was invisible —
the test that found it passed vacuously, because the body never moved.
Also: malformed XML doc on ComposeOffset (duplicate </summary> swallowed the
retirement note from tooling) fixed; the placement-cutover plan's item 5 and
its stale "After C5" line now record AP-22 and AD-10 as retired.
Core builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two findings from the AD-10 work that are out of its scope and are filed
rather than absorbed. Neither is caused by AD-10 and neither is affected by
its deletion.
#331 — PhysicsEngine.ResolveWithTransition returns ok=False and the
unchanged input position for EVERY uphill step on the synthetic
constant-gradient terrain ramp, while the identical downhill step succeeds
and produces a correct slope-following result. Probed and ruled out:
gradient (fails at 2.9 degrees as at 31), step size, cell boundaries
(five start positions with the cell id recomputed), and Z seating. It is
not an axis bug either — inverting the ramp so it rises along +Y makes +Y
the failing direction, so the failure tracks the slope.
Filed with severity UNKNOWN on purpose. Players demonstrably walk uphill
in acdream and the local player runs the same call, so either production
terrain differs from what the fixture publishes (AddLandblock only, no
flat-collision statics) or something in the live arguments does. That was
not traced, and guessing which would be exactly the kind of inference this
campaign keeps getting burned by. The issue names the one probe that
decides it.
It surfaced because an uphill counterpart to the AD-10 tracking test was
written, PASSED, and was then found vacuous — the body never moved, so it
"stayed on the surface" by standing still. That test was dropped rather
than shipped. Any future uphill assertion against that harness is vacuous
the same way until this is resolved, which is reason enough to record it
even if production is fine.
#332 — Headless bots appear to have no remote dead-reckoning at all.
RuntimeRemotePhysicsUpdater has exactly one production instantiation,
AcDream.App/Physics/RemotePhysicsUpdater.cs:46, and src/AcDream.Headless/
never names it or RemoteMotion. Remote entities on that host would move
only at UpdatePosition cadence. Filed as an observation for the headless
owner to judge, adjacent to #330 but a separate mechanism.
#332 also records the reasoning trap it exposes, because it inverts the
C5b lesson rather than repeating it: RemoteMotionCombiner is in Core and
RuntimeRemotePhysicsUpdater is in Runtime, so "therefore headless runs it"
is the natural correction to C5b's graphical-only survey — and it is
wrong. Assembly placement is not reachability; the instantiation census
is. AD-10 designed no headless gate for exactly this reason, and a passing
one would have been vacuous evidence.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both were found inside acdream's port of CTransition::adjust_offset while
retiring AD-10, and neither had a register row — grep confirms no existing
row mentions snap_to_plane, SnapToPlane, naturalResting, or away-plane.
Filed as AD-65 and AD-66. Neither is fixed here: both change LOCAL-PLAYER
movement feel and need their own visual gate, and folding them into the
remote-movement change would have put a local-player regression behind the
wrong acceptance test.
AD-65 — the `collisionAngle > 0` arm substitutes `result -= N * angle` for
retail's Plane::snap_to_plane call, making the if and else arms
byte-identical. snap_to_plane (0x00509c50) writes only v.z and leaves XY
alone, so acdream descends slopes 13% slow at 30 degrees and 29% slow at
45. Uphill is correct. Recorded as a LEAD for the open #269 slope-slide
residual, explicitly not a diagnosis — the direction fits but nothing here
establishes causation. #269's friction and jump chains are byte-exonerated
and are not re-audited; adjust_offset is a different function.
AD-66 — the safety push-out substitutes `radius * Normal.Z` for retail's
bare `radius` in both the trigger and the zDist numerator, knowingly and
with a written rationale. The rationale may be right; the missing row is
the defect. The code comment's "ACE and the published pseudocode have the
original threshold" understates the case — the retail binary has it.
Both directions are byte-verified against the PDB-paired v11.4186 binary
(GUID 9e847e2f-777c-4bd9-886c-22256bb87f32, check_exe_pdb.py MATCH), not
inferred from pseudo-C: Binary Ninja renders every x87 comparison in this
function as the fnstsw/test-ah mush and cannot be read for branch
direction. AD-65's row records the exact three instructions and the FPU
condition-code reasoning; AD-66's records the four operand loads showing
neither site multiplies by N.z, plus both float constants read from the
image (0x795344 = 0.0f, 0x7c6878 = 0.00019999999494757503f).
AD-10's own retail anchor was corrected in the previous commit for the
same reason: pc:272296-272346 truncated the sliding-normal validity gate
at the head and the whole safety push-out block at the tail.
No code change; no test change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>