From 6b490aab8b474f49a86465f0068774cc48dc033e Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 7 Aug 2026 06:50:58 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20morning=20gate=20G1=20user-passed=20?= =?UTF-8?q?=E2=80=94=20AD-65=20downhill=20slope=20feel=20accepted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit '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 --- docs/architecture/retail-divergence-register.md | 2 +- docs/plans/2026-08-06-collision-fidelity-campaign.md | 2 +- docs/plans/2026-08-07-morning-gate-checklist.md | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index a472a5c1..e599a6bd 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -159,7 +159,7 @@ readiness/requeue adaptation. See | AD-62 | **Filed 2026-08-03 (C4 route 2, round 2); rewritten round 3.** General rule: an accepted local-player ForcePosition that this route does not carry through to a committed canonical placement is never re-applied. That half matches retail — `SmartBox::BlipPlayer` attempts the placement exactly once and never retries. What diverges is that acdream has non-commit outcomes retail cannot reach at all, because retail's world is fully resident and its placement synchronous. Round 3 narrowed the loss to the re-apply alone wherever the packet's placement was actually BEGUN: the retail position event now fires at that packet's terminal outcome whether or not the placement committed (`SettlePending`'s `positionEventOwed` path), matching `BlipPlayer` discarding `SetPositionSimple`'s `enum SetPositionError` and `HandleReceivedPosition` acking unconditionally @0x00454091. Shapes losing ONLY the re-apply: (i) the destination landblock's collision generation is unpublished so the placement parks (`DeferredCell`) and is then retired by a non-position cause (collision-generation retirement, the lost-cell deadline, `ParkCollisionResidents`) with the accepted authority unmoved — the funnel's EQUAL branch; (ii) the same park superseded by a newer ordinary `Apply` Position which now owns the pose — the ADVANCED+ordinary branch; (iii) any OTHER `PositionAuthorityVersion` advance moving the record out from under the funnel's re-issue test — `TryApplyPickup` (`RuntimeEntityObjectLifetime.cs:1116`), `CommitPositionChannelUpdate` (`:2041`), `AdvanceCreateAuthority` (`:2466`) — effectively unreachable for a live local player, but they fail silently in the same direction and the funnel cannot tell them from (ii). Shapes still losing BOTH the re-apply and the ack because no placement was ever begun for that packet: (iv) a `Contention` whose blocking operation is EXTERNAL to this drive (a concurrent portal/teleport placement owns the entity) — nothing is recorded in `_pending`, so nothing pumps it and the packet is dropped outright; (vi) a re-issue retry marker whose re-issue never manages to begin before the funnel clears it. Losing BOTH for a DIFFERENT reason — the placement WAS begun, but the descriptor was displaced before reaching its own terminal settle: (v) a packet superseded by a newer force whose own placement begins cleanly — `SettlePending` opens by nulling `_pending` without reading it, so the older descriptor's owed ack is discarded. Replaying it would be worse than losing it (a stale-sequence report carrying the newer packet's committed pose), and the displacing packet always acks, so ACE always receives a report for the newest force. The `DeferredCell` park is NOT a precondition of this row: shapes (iv)-(vi) never park. In every shape the body stays where the last successful placement left it and the next accepted Position (ACE broadcasts at 5-10 Hz) carries the corrected pose forward. | `src/AcDream.Runtime/Session/RuntimeAcceptedPositionDriveController.cs` (`SettlePending` — the single terminal-outcome funnel: its `positionEventOwed` ack and its two non-reissuing branches; and `TryExecuteAcceptedLocalPosition`'s `Contention` return) | Retail has no park and no external placement authority: `SmartBox::BlipPlayer` runs synchronously against a fully resident world, so "arrived but not yet placeable" and "another placement owns this entity" are both unrepresentable there. Those are our async collision-publication and single-placement-authority adaptations. Re-issuing a retired force instead would be worse than not: shape (ii) would stamp the force route's `Teleport\|Slide` flags and an unconditional ack onto an ordinary echo's pose while skipping the `ConstrainTo` the ordinary branch runs (`RuntimeAuthoritativePositionRouteClassifier.cs:368-388`), and shape (i) can re-issue into the same persistent cancellation cause indefinitely. The drive still owns at most one in-flight placement and still re-issues whenever the newest accepted event IS a still-unserved ForcePosition. | A server correction whose destination collision is slow to publish, or which lands while another placement authority owns the entity, can be silently skipped: the player stays at the pre-correction pose for one broadcast interval (~100-200 ms). Sustained (a slow-publishing destination correcting repeatedly) this reads as rubber-banding that does not take. In shapes (iv)-(vi) ACE additionally receives one fewer `AutonomousPosition` than retail would have sent, so the server cannot tell its force was not applied. | `SmartBox::HandleReceivedPosition` @0x00453FD0 FORCE_POSITION branch (`SendPositionEvent` @0x00454091, early return @0x0045409D); `SmartBox::BlipPlayer` @0x00453940 (discards the error, returns void); `CPhysicsObj::SetPositionSimple` @0x005162B0 (returns `enum SetPositionError`; other callers test `== OK_SPE` @0x0055605D/@0x00556021); `CommandInterpreter::SendPositionEvent` @0x006B4770 | | AD-63 | **Filed 2026-08-04 (cancelled-park presentation rollback).** When a cancelled restorable park is rolled back, the entity's presentation is restored EXCEPT the player's selection. `ParkDeferred`'s Withdraw receipt makes the host sink clear the selection if the parked entity was the selected object (`_clearSelectionForUnavailableEntity`), and the `WithdrawalRestored` receipt that rolls that withdrawal back deliberately does not re-select it. Every other registration the withdrawal removed — the graphical bucket, projection visibility, plugin world state, the world-event replay set, the effect-pose registry, the local-player shadow, the presentation visibility sinks — IS restored exactly. | `src/AcDream.App/World/RuntimePlacementPresentationSink.cs` (`TryApplyWithdrawalRestoration` vs `TryPublishWithdrawal`'s `_clearSelectionForUnavailableEntity` call) | Selection is user intent, not a projection registration. Retail clears the selection when its target becomes unavailable (`SelectionChangeReason.SelectedObjectRemoved` is acdream's name for the same edge) and never re-selects on the object's behalf; re-selecting here would invent input the player did not give. Retail also cannot reach this state at all — it has no cancel for a lost-cell park (AP-136) — so there is no retail behaviour to match, only two acdream choices, and "do not act for the player" is the conservative one. | The player loses their target for the ~150 ms park window if the selected object happened to park, and must re-click it. No other state is affected: the object is visible, on the radar, collidable, and assessable again as soon as the restoration receipt drains. Retire together with AP-136 by making the park survive cancellation (issue #309), which removes the withdrawal — and therefore the selection clear — entirely. | AP-136 (the park rollback this rides on); no retail anchor — retail has no cancellable lost-cell park | | 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~~ | **RETIRED 2026-08-07 (Campaign S S4).** `Transition.AdjustOffset`'s away-from-plane arm now performs retail's `Plane::snap_to_plane` @0x00509c50 semantics verbatim: XY preserved, Z re-solved as `-(x*Nx + y*Ny)/Nz`, no-op under the 0.000199999995f |N.z| epsilon — replacing the orthogonal projection whose cos²θ downhill XY shortfall this row recorded (25% at 30°, 50% at 45°). Branch polarity ported from the `test ah,0x41` idiom at 0x0050a4fa: into-plane subtracts, away-from-plane snaps. Conformance: `S4AdjustOffsetConformanceTests` exact-value rows, sabotage-verified (the re-instated projection reproduces exactly the recorded cos²30° = 0.75 shrinkage). NOTE: the SIBLING row AD-66 was byte-re-confirmed but its landing was WITHHELD the same night — see issue #341. Original text: **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-65~~ | **RETIRED 2026-08-07 (Campaign S S4), USER-PASSED the same morning** ("Slopes feels good" at the downhill/diagonal/jump-landing gate). `Transition.AdjustOffset`'s away-from-plane arm now performs retail's `Plane::snap_to_plane` @0x00509c50 semantics verbatim: XY preserved, Z re-solved as `-(x*Nx + y*Ny)/Nz`, no-op under the 0.000199999995f |N.z| epsilon — replacing the orthogonal projection whose cos²θ downhill XY shortfall this row recorded (25% at 30°, 50% at 45°). Branch polarity ported from the `test ah,0x41` idiom at 0x0050a4fa: into-plane subtracts, away-from-plane snaps. Conformance: `S4AdjustOffsetConformanceTests` exact-value rows, sabotage-verified (the re-instated projection reproduces exactly the recorded cos²30° = 0.75 shrinkage). NOTE: the SIBLING row AD-66 was byte-re-confirmed but its landing was WITHHELD the same night — see issue #341. Original text: **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 | **LANDING WITHHELD 2026-08-07 (Campaign S S4) — the row stays ACTIVE and its byte evidence is now DOUBLE-confirmed.** The bare-radius port was implemented, conformance-tested, and then PULLED: it collides with the #331 absorb characterization pin through a measurement that contradicted itself (the same clean-room binaries measured both a one-time resting lift and an exact latch, flipping with nothing but the test's post-tick assert shape). Issue #341 carries the observation matrix and the apparatus plan; the two exact-value conformance tests are [Skip]-ed in the tree awaiting the relanding. Do not re-derive the bytes — they were never the question. **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) | | AD-68 | **Filed 2026-08-07 at the #338 closure.** During an entity's ASYNC-RESIDENCY window — its flat Setup collision not yet resident — `LiveEntityMotionRuntimeController.GetSetupMoverShape` returns a placeholder mover shape: empty sphere list (falling back to the legacy 0.48/1.835 capsule reconstruction) and step heights **0.4/0.4**, values that appear nowhere in retail (authored human values are 0.600/1.500; retail's not-on-walkable fallback is 0.04). The local player has the same window between controller construction (0.4f defaults) and the publication candidate's adoption. Retail loads Setups synchronously and has no such window at all. Measured scale: 358 placeholder resolves vs 111,248 authored-pair resolves across one long session — seconds per entity, once. | `src/AcDream.App/Physics/LiveEntityMotionRuntimeController.cs` (`GetSetupMoverShape`, the `setup is null` and `<= 0f` arms); `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs` (0.4f field defaults, adopted-over at publication) | An adaptation to async residency, not a wiring defect — #338's live probe proved prepare/publish/resolve all carry the authored values in steady state. Left as-is deliberately: shrinking the window is streaming work, not physics work. | A remote moving DURING its residency window steps 0.4 instead of its authored heights, and collides as a capsule instead of its sphere list — briefly, once per entity. If a future report says "an NPC stumbled on a stair right as it appeared", this row is the first suspect. | `CTransition::step_up` 0x0050b610 (0.04 fallback at 0x0050b655); `CPartArray::GetStepUpHeight` 0x005180d0; issue #338 | diff --git a/docs/plans/2026-08-06-collision-fidelity-campaign.md b/docs/plans/2026-08-06-collision-fidelity-campaign.md index ea8e9abc..ef3ecb2b 100644 --- a/docs/plans/2026-08-06-collision-fidelity-campaign.md +++ b/docs/plans/2026-08-06-collision-fidelity-campaign.md @@ -4,7 +4,7 @@ **Status:** IN FLIGHT — overnight session 2026-08-07 ledger: S1A (AP-157) closed by measurement, no code · S1B contract ready, not implemented · S2 contract ready, not implemented · S3 CANCELLED (planned on a -misreading — see its section) · S4 half-landed (AD-65 shipped; AD-66 withheld +misreading — see its section) · S4 half-landed (AD-65 shipped and USER-PASSED at the 2026-08-07 morning gate; AD-66 withheld behind #341's measurement anomaly; AD-69 filed) · S5 closed (fix predated the campaign; zombie register row) · S6 unstarted · #330 hoist landed, wiring withheld with a seven-point scope map · #32/#338 pre-work both closed. diff --git a/docs/plans/2026-08-07-morning-gate-checklist.md b/docs/plans/2026-08-07-morning-gate-checklist.md index 888a2387..e1099042 100644 --- a/docs/plans/2026-08-07-morning-gate-checklist.md +++ b/docs/plans/2026-08-07-morning-gate-checklist.md @@ -1,8 +1,6 @@ # Morning gate — 2026-08-07 (one sitting) -**Status: READY — pending only the AD-65 review verdict and final commit, -which the session lead completes before launch. Launch happens when you say -go, not before.** +**Status: COMPLETE 2026-08-07 — G1 user-passed ("Slopes feels good").** Everything below folds the deferred visual gates into one sitting, ordered so travel between sites doubles as #339 reproduction attempts. Launch is