From 1dc81710f31b39c7cc65254b72799fadee442f6a Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 7 Aug 2026 00:38:02 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20close=20#32=20=E2=80=94=20local=20edge-?= =?UTF-8?q?slide=20user-passed;=20AD-67=20filed=20for=20the=20kept=20cell-?= =?UTF-8?q?id=20write?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/ISSUES.md | 34 ++++++++++++++++++- .../retail-divergence-register.md | 3 +- ...2026-08-06-32-local-edge-slide-research.md | 11 ++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 064fdd7b..c8f310ad 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -24,6 +24,35 @@ What does NOT go here: - Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending. - Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed. +## #32 — CLOSED 2026-08-07: local edge-slide fixed at `332045c7`, USER-PASSED on its first genuine live run + +**"Yes works now."** — the user at the Rithwic cliff, on the first launch that +actually contained the fix (assembly identity printed in the capture: +`acdream/src/AcDream.App/bin/Release/net10.0/AcDream.Core.dll`). +The player now slides along the rim instead of running off. + +The fix is the `set_contact_plane` / `init_contact_plane` split +(`COLLISIONINFO::set_contact_plane` @0x00509d80 writes the contact group only; +`CTransition::init_contact_plane` @0x0050e850 seeds both), gated by +`Issue32LastKnownContactPlaneTests` (sabotage-verified pair + control). +Remote half was closed 2026-08-04 at `204d0ae0`; with this, the issue's +edge-slide family is done. **Register: AD-67 filed in the closeout commit** — +the narrowed setter still writes `ContactPlaneCellId`, which retail writes only +in `init_contact_plane` (@0x0050e8ca); acdream's callers rely on the current +cell id. + +**The blast-radius items from research §3.5 stay OPEN as watch items, now +strictly more reachable than before the fix:** last-known validity is +narrower, so `ValidateTransition`'s `StopVelocity` recovery and +`transitional_insert`'s phase-3 reset take their last-known-INVALID branches +more often. Nothing observed misbehaving in the passing session; check here +first if slope-feel or landing regressions appear. Carried into Campaign S's +S4 slice notes. + +**The three prior update entries below record a wrong-binary detour** (the fix +was tested against a client that did not contain it) — retained as the record; +their verdicts are void and superseded by this closure. + ## #32 UPDATE 2026-08-07, SECOND CORRECTION — the fix was NEVER IN THE TESTED BINARY; every conclusion in the entry below is void **Root cause of the contradiction: two checkouts and a relative launch path.** @@ -11135,7 +11164,10 @@ retail counterpart, so "widen the budget" would be tuning an invented constant ## #32 — Retail edge-slide / cliff-slide / precipice-slide incomplete -**Status:** REMOTE HALF CLOSED — fixed `204d0ae0`, **user-passed 2026-08-04** +**Status:** CLOSED 2026-08-07 — both halves. Local half fixed `332045c7` +(set/init contact-plane split), user-passed 2026-08-07 at the Rithwic cliff; +see the closure entry near the top of this file. Remote half below. +**Original status line:** REMOTE HALF CLOSED — fixed `204d0ae0`, **user-passed 2026-08-04** ("it lands and slides correctly now"). A remote observed in acdream now slides down a steep face under gravity instead of freezing on it and then blipping. The three recorded gaps below (LeaveGround chatter bound, the `!Ok` airborne diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index f0c8b2f7..5ec625d5 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -62,7 +62,7 @@ accepted-divergence entries (#96, #49, #50). --- -## 2. Adaptation (AD) — 49 active rows (AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice) +## 2. Adaptation (AD) — 50 active rows (AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice) Recent retirements: AD-3/AD-4 retired 2026-07-31 by exact active/per-candidate visible-cell availability, full-catalog containment-root validation, and the @@ -160,6 +160,7 @@ readiness/requeue adaptation. See | AD-64 | **Filed 2026-08-05 at the C5b architecture review's D1 fix.** The graphical and no-window hosts run parallel, non-shared inbound entity routes — `LiveEntitySessionController` → `LiveEntityNetworkUpdateController.OnPosition` versus `RuntimeLiveEntitySessionController.OnPositionUpdated` — and AD-60's W2 wire-cell commit is therefore expressed TWICE. The committed VALUE is shared exactly (one owner, `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, including the landblock-vs-cell preserve branch); what is duplicated is the REACHABILITY decision — which packets may reach it. The graphical host encodes that decision implicitly, as the set of early returns strewn through a 400-line `OnPosition` (authority gate on `Rejected`, the local force arm on every drive status except `NotApplicable`, the missile arm, the `ChildUnparentDisposition` Superseded/Pending arm, the initial-create residence gate inside `RebucketLiveEntity`). The no-window host encodes it explicitly, in one method, `TryCommitAcceptedWireCell`, whose gates were derived from those returns one by one. Two of the graphical gates have no no-window analogue and are deliberately absent rather than reproduced: the `ChildUnparentDisposition` arm is presentation recovery this host does not perform, and the residence gate's `MaterializationResidence is AwaitRuntimePlacement` half is App presentation bookkeeping whose no-window equivalent is unconditionally true for a residence-backed record. **CORRECTED 2026-08-05 at the C5b closeout (architecture finding L-B): "deliberately absent" was presented as the complete list of differences and it was not — there are three more, and the row's own "derived from those returns one by one" phrasing was the claim that made them invisible.** (a) **The residence gate is WEAKER than the merge's own.** Both hosts' wire-cell commits gate on `TryGetInitialCreateResidence` (= `RuntimeInitialCreateResidenceState.TryGetCurrent`), while `RuntimeEntityObjectLifetime.TryApplyPosition`'s FIFO enqueue branch gates on `TryGetPendingInitialResidence` (= `TryGetTransaction` = `TryGetCurrent` OR a completed-but-unretired lease, `RuntimeInitialCreateResidenceState.cs:729-748`). In that window the merge enqueues the packet as a continuation while the commit reads "no residence" and writes the wire cell AHEAD of the continuation that will replay it. Host-symmetric and pre-existing — the graphical `RebucketLiveEntity` has the identical pair — but this row previously claimed the `AwaitRuntimePlacement` half was the only deliberately-absent piece of the residence gate, which is false. (b) **The missile gates are two different expressions.** The graphical route PREFERS `earlyRemoteRoute.OperationKind is RuntimeSetPositionOperationKind.ProjectileAuthoritative` and falls back to the `Missile`-flag / bound-projectile conjunction only when the classification is null; the no-window route ALWAYS uses the conjunction, because it classifies nothing for a remote. They agree today (the conjunction is what the classifier's own projectile test is built from), but they are separately maintained and only the conjunction is reachable on one side — a change to the classifier's projectile predicate moves one host and not the other. (c) **The pre-merge PAYLOAD gate was absent entirely, and is now present.** The graphical route validates the wire payload before the merge (`LiveEntityNetworkUpdateController.OnPosition`'s `payloadIsValid` from `ProjectileController.CanAcceptPositionPayload` — despite the name, not projectile-scoped; it runs for every guid — consumed by `LiveEntityInboundAuthorityGate.TryAcceptPosition`'s `!payloadIsValid` return). The no-window route had no equivalent, so since D1 an unvalidated `update.Position.LandblockId` reached `CommitWireCellRebucket`, whose own doc calls `0` "the withdrawal shape" (cell 0 + landblock 0) — silently de-residencing the entity in the exact field `RuntimeEntityObjectViews.Snapshot` hands every bot as `CellId` and `RuntimeSetPositionState.IsAffectedCollisionResident` reads. Fixed at the closeout by applying the same predicate at the same point: `RuntimeAuthoritativePositionRouteClassifier.IsValidCreateWirePosition` plus the finite-velocity term, the pair `RuntimeEntityObjectLifetime.TryApplyPosition` already applies on its initial-residence branch. Rejecting BEFORE the merge (not merely before the commit) is what makes the hosts symmetric — neither lets an invalid payload advance the timestamp gate — and is pinned by `RuntimeLiveEntitySessionControllerTests.InvalidPositionPayload_IsRefusedBeforeTheMerge_InANoWindowHost`, sabotage-verified in both directions (gate removed -> red at the withdrawal-shape assertion; gate moved to guard only the commit -> red at the pose assertion). The no-window host also has no W3 (`TryAdoptWireCellAfterRouting`) analogue and needs none — it performs no remote contact routing at all. | `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (`OnPosition`, the implicit gate set); `src/AcDream.App/World/LiveEntityRuntime.cs` (`RebucketLiveEntity`'s residence early return); `src/AcDream.Runtime/Session/RuntimeLiveEntitySessionController.cs` (`TryCommitAcceptedWireCell`, `IsMissilePacket`) | Retail has one client and therefore one route; there is no retail shape to match, only acdream's own two-host structure. The alternative — unifying the two session controllers so the decision exists once — is the genuinely correct fix and is filed as issue #324, but it is campaign-sized: it has to reconcile presentation recovery, hydration, the equipped-child renderer, and the remote routing arms that only one of the two hosts has. Duplicating a small, individually test-gated decision is the cheaper correct thing meanwhile; duplicating it SILENTLY, which is what the pre-D1 state amounted to (one host simply had none of it), is what this row exists to stop. | The two decisions can drift: a future change to one host's reachability rules will not be caught by the other host's tests. Concretely, if the graphical route later adds an early return, the no-window host keeps committing on that packet shape, and vice versa. Bounded by the eight-sabotage gate the D1 fix left behind, plus the closeout's ninth (the payload gate, red in both directions) — every arm of `TryCommitAcceptedWireCell` and both directions of the force rule are individually red-verified — so drift shows up as a test that must be deliberately changed, not as a silent divergence. **That bound does NOT cover the three differences added at the closeout**: the weaker residence predicate (a) and the missile-expression split (b) have no discriminating test on either side, because in both cases the two hosts currently AGREE and the divergence is structural rather than behavioural. They are recorded here precisely because nothing else will catch them. Retire with #324. | No retail anchor — acdream-only host-structure deviation. Adjacent rows: AD-60 (the W2/W3 channel list), AP-146/#320 (the local player's cell edges) | | AD-65 | **Filed 2026-08-06 (found while retiring AD-10; NOT fixed here).** `Transition.AdjustOffset`'s `collisionAngle > 0` arm — the body moving AWAY from its contact plane — substitutes `result -= N * collisionAngle` for retail's `Plane::snap_to_plane` call, making the `if` and the `else` arms byte-identical. Retail's two arms are genuinely different: `snap_to_plane` (0x00509c50) writes ONLY `v.z = -(v.x*N.x + v.y*N.y) / N.z` and leaves X and Y untouched, while the into-plane arm subtracts the full normal component. So for a horizontal step of length d on a slope of angle theta, retail DESCENDS with XY preserved at d and Z dropping d*tan(theta) (speed along the plane d/cos theta), whereas acdream shrinks XY to d*cos^2(theta) (speed along the plane d*cos theta). acdream therefore descends slopes SLOWER than retail by cos^2(theta) in XY: **25% slow at 30 degrees, 50% at 45 degrees**. **MAGNITUDE CORRECTED 2026-08-06 at the AD-10 retail review (F1): this row first said 13%/29%, which is 1-cos(theta) -- the wrong formula for its own stated cos^2(theta) factor, and half the true value.** The correction is confirmed by measurement, not just algebra: #331's probe records 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). This matters because the row is a LEAD for #269's slope-slide residual -- at the understated magnitude the lead reads as marginal and could be dismissed. Uphill (`collisionAngle <= 0`) is correct and identical to retail. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`AdjustOffset`, the `else` arm commented "Moving away from contact plane: snap to plane surface" — the comment names snap_to_plane but the code does not call it) | Not justified — this is an unexamined substitution, not a decision. It is filed rather than fixed because it changes LOCAL-PLAYER movement feel and so needs its own visual gate; folding it into a remote-movement change would put a local-player regression behind the wrong acceptance test. | Downhill locomotion is 25-50% slow in XY across the walkable slope range (corrected 2026-08-06; was understated as 13-29%), for every mover that runs the sweep (local player, remotes, projectiles). **Recorded as a LEAD, not a diagnosis, for the open #269 slope-slide feel residual** (Campaign P): the direction is right (downhill-only, XY-shortening) but nothing here establishes causation, and #269 still needs its live cdb A/B. Note #269's friction and jump chains are byte-exonerated and must not be re-audited; `adjust_offset` is a different function and is not covered by that do-not-retry. | `CTransition::adjust_offset` 0x0050a370, pc:272271-272393; the branch at `0050a4fa fcomp [0x795344]` / `0050a502 test ah,0x41` / `0050a505 jne 0x50a515` — disassembled from the PDB-paired v11.4186 binary (GUID 9e847e2f-777c-4bd9-886c-22256bb87f32), 0x795344 = 0.0f (bytes 00000000). FPU C0 is "less" and C3 is "equal", so `jne` on `ah & 0x41` takes the SUBTRACT branch at 0x50a515 when `cAngle <= 0` and falls through to `call 0x509c50` (`Plane::snap_to_plane`, pc:271852) when `cAngle > 0`. Binary Ninja renders all four comparisons in this function as the `fnstsw`/`test ah` mush and cannot be read for direction. | | AD-66 | **Filed 2026-08-06 (found while retiring AD-10; NOT fixed here).** `Transition.AdjustOffset`'s safety push-out substitutes `naturalRestingDist = radius * ContactPlane.Normal.Z` for retail's bare `radius` in BOTH the trigger comparison and the `zDist` numerator. The substitution is deliberate and carries a written rationale in the code (the LocalSphere origin sits at (0, 0, radius) along WORLD Z, so a sphere resting on a tilted plane is `radius * N.z` from it, and the bare threshold would fire spuriously on every slope and lift the feet by r*(sec theta - 1) — 7 cm at 30 degrees, 48 cm at 60). The rationale may well be correct. What is missing is the register row: an intentional deviation from a byte-confirmed retail constant with no row is precisely what this register exists to catch, and the code comment's claim that "ACE and the published pseudocode have the original threshold" understates it — the retail BINARY has it. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`AdjustOffset`, the `ci.ContactPlaneCellId != 0 && !ci.ContactPlaneIsWater` block) | Argued at length in the code comment and empirically motivated (the uncorrected threshold reportedly broke ValidateWalkable's contact check on steep slopes and flickered the Falling animation while running uphill). Filed to make the deviation auditable, not to assert it is wrong. | If the sphere-origin premise is mistaken, the push-out under-fires on slopes and a genuinely penetrating sphere is left below its contact plane. Conversely, if the premise is right, retail itself has the spurious lift and acdream is deliberately smoother than retail on slopes — a feel divergence in the same family as, and possibly interacting with, AD-65 and #269. | `CTransition::adjust_offset` 0x0050a370; disassembled from the PDB-paired v11.4186 binary: `0050a5c4 fld [ecx+0xc]` loads the bare `global_sphere->radius` and `0050a5c7 fsub [0x7c6878]` subtracts 0.00019999999494757503f (bytes 17b75139) for the trigger; `0050a5dc fsubr [ecx+0xc]` reloads the bare radius for the numerator before `0050a5df fdiv [esi+8]` divides by `contact_plane.N.z`. Neither site multiplies by N.z. | +| AD-67 | **Filed 2026-08-07 at the #32 closeout.** The narrowed `CollisionInfo.SetContactPlane` still writes `ContactPlaneCellId`, which retail's `COLLISIONINFO::set_contact_plane` @0x00509d80 does not — retail writes the cell id only in `CTransition::init_contact_plane` (@0x0050e8ca). Kept deliberately at the #32 fix on the research doc's own advice: acdream's consumers (the `[support]` probe's provenance, water-plane bookkeeping, `AdjustOffset`'s `ContactPlaneCellId != 0` gate) rely on the cell id being current per contact write, and retail's equivalent state travels a different route the port has not needed. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`SetContactPlane`, the `ContactPlaneCellId = cellId` line) | The #32 fix removed the four LAST-KNOWN writes — the defect — and deliberately did not also change this contact-group field in the same commit; two behaviour changes in one fix would have made the user's cliff gate ambiguous. | A consumer that assumes the cell id changes ONLY at transition seed time (retail's timing) would observe it changing per contact write instead. No such consumer is known; `AdjustOffset`'s gate wants the current value. | `COLLISIONINFO::set_contact_plane` 0x00509d80 (22 bytes, no cell-id write); `CTransition::init_contact_plane` 0x0050e850 (cell id at 0x0050e8ca) | --- diff --git a/docs/research/2026-08-06-32-local-edge-slide-research.md b/docs/research/2026-08-06-32-local-edge-slide-research.md index 747a00c7..f4d356a2 100644 --- a/docs/research/2026-08-06-32-local-edge-slide-research.md +++ b/docs/research/2026-08-06-32-local-edge-slide-research.md @@ -1,5 +1,16 @@ # Issue #32 (local half) — the player runs off cliff edges instead of stopping or sliding +**OUTCOME 2026-08-07 — Section 6's capture landed in decision-table ROW 1 +verbatim** (six `branch2/steep-cliffslide` events at Rithwic, every one +`curN == lastN == (-0.954, 0.000, 0.301)`, `angle=0.0000`, `apply=False`, +outcome `degenerate-cross/last-known`), **the Section 7 fix shipped at +`332045c7`, and the user passed it live**: "Yes works now." One caveat for the +record: the first live "test" of the fix ran a stale checkout's binary and +produced a void failure verdict — see the #32 entries in `docs/ISSUES.md` for +that detour and the assembly-identity rule it produced. Section 3.5's +cell-id note is now register row **AD-67**; its blast-radius items are carried +as watch items on Campaign S slice S4. + **Date:** 2026-08-06 **Mode:** research / report-only. No production or test code was written or changed. **Worktree:** `.claude/worktrees/peaceful-visvesvaraya-e0a196`, branch