feat(R3-W4): ground transitions + lifecycle verbatim; K-fix18 DELETED (closes J8, J10, J11-shape, J12, J13, J18, J19)

Core (dedicated agent, independently reviewed): HitGround 0x00528ac0 /
LeaveGround 0x00528b00 verbatim (creature+gravity gates, the
RemoveLinkAnimations seam — K-fix18's retail mechanism — velocity via
GetLeaveGroundVelocity with the autonomous flag, jump-state resets,
apply_current_movement re-sync); enter_default_state 0x00528c80 per A8
(fresh states, InitializeMotionTables seam, sentinel APPENDED without
draining pending_motions — pinned, Initted=1, LeaveGround tail);
Initted gates; the A3 IsThePlayer dual dispatch in
apply_current_movement / ReportExhaustion / SetWeenieObject /
SetPhysicsObject (a remote player routes INTERPRETED — the
ACE-divergence pin); set_hold_run 0x00528b70 + SetHoldKey 0x00528bb0
(XOR guard, None-only-from-Run); adjust_motion creature guard wired
(TS-34 retired); PhysicsBody.LastMoveWasAutonomous +
set_local_velocity(autonomous). Port discovery: retail's
apply_raw_movement 0x005287e0 / apply_interpreted_movement 0x00528600
ARE the already-shipped D6.2a/funnel functions — the dual dispatch
composes them instead of duplicating.

App cutover (orchestrator): the skipTransitionLink flag + both K-fix18
call sites DELETED (AP-74 retired). MotionInterpreter.DefaultSink routes
apply_current_movement's interpreted branch through the REAL funnel
dispatch when a sink is bound — so a remote's LeaveGround engages
Falling via the contact-gated funnel, replacing the forced SetCycle
(J19); the per-remote MotionTableDispatchSink is now PERSISTENT
(EnsureRemoteMotionBindings: DefaultSink + RemoveLinkAnimations +
InitializeMotionTables seams, idempotent from both the UM and
VectorUpdate paths; wire velocity re-applied after LeaveGround so it
stays authoritative). Player: seams bound to the player sequencer; the
controller's grounded→airborne EDGE now fires LeaveGround (jump()
clears OnWalkable and the same frame's transition detection fires it —
retail's order; walk-off-a-ledge gets the momentum fallback + link
strip it never had); the manual jump-block LeaveGround deleted;
LastMoveWasAutonomous set at the controller chokepoint (W6 refines).

Trace S8 re-expressed as the retail mechanism (Falling dispatch +
RemoveAllLinkAnimations = same final state the flag produced). 43 new
lifecycle tests. Registers: TS-34 + AP-74 retired; TS-38, AP-77, AP-78
added. Full suite: 3,665 passed. Live smoke: in-world clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-02 23:01:31 +02:00
parent af4764443f
commit e214acdf23
8 changed files with 1575 additions and 124 deletions

View file

@ -102,7 +102,7 @@ accepted-divergence entries (#96, #49, #50).
---
## 3. Documented approximation (AP) — 72 rows
## 3. Documented approximation (AP) — 73 rows
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|---|---|---|---|---|---|
@ -179,9 +179,10 @@ accepted-divergence entries (#96, #49, #50).
---
| AP-74 | **K-fix18 `skipTransitionLink`** post-Q4 expression: `SetCycle(..., skipTransitionLink:true)` strips the just-dispatched transition links via `CSequence.RemoveAllLinkAnimations` so Falling engages instantly on jump start; retail gets the same strip from `LeaveGround``CPhysicsObj::RemoveLinkAnimations` (0x0050fe20) fired by the physics layer detecting ground departure (R2-Q4 rehost of the 2026-04-26 K-fix18) | `src/AcDream.Core/Physics/AnimationSequencer.cs` (`SetCycle` skip branch); GameWindow :4830/:10201 callers | Same primitive retail uses, fired from the caller instead of the ground-transition event; pending-queue tick counts intentionally not zeroed (retail's RemoveLinkAnimations doesn't touch the manager queue — the countdown chain absorbs the stall) | If a code path sets the flag outside a genuine ground-departure, links retail would play get eaten; queue completion for the stripped entry reports late (next sentinel) | `LeaveGround` 0x00528b00 → `RemoveLinkAnimations` 0x0050fe20; retire in R3-W4 (flag + call sites deleted, LeaveGround fires it) |
| AP-75 | **Adapter-boundary `adjust_motion` + locomotion velocity/omega synthesis**: `SetCycle` still (a) remaps TurnLeft/SideStepLeft/WalkBackward to their mirrors with negated speed BEFORE dispatch (retail adjusts in `CMotionInterp` — R3 scope; GameWindow's local-player path passes raw ids), and (b) post-dispatch overwrites sequence velocity (low-bytes 05/06/07/0F/10) and omega (0D/0E, only when dat-silent) with the retail locomotion constants — retail drives BODY velocity from `get_state_velocity`, not the sequence accumulators (R2-Q4 carry-over of the pre-Q4 adapter tail) | `src/AcDream.Core/Physics/AnimationSequencer.cs` (`SetCycle` head remap + tail synthesis) | (a) preserves unadjusted GameWindow callers until R3-W6 unifies the local player onto MotionInterpreter; (b) preserves the remote-DR and local Option-B velocity consumers until R6 root motion drives the body (sibling of IA-3) | (a) none while callers stay in the known set; (b) a dat whose locomotion MotionData carries a REAL velocity different from the constants gets overwritten — exotic-creature speed skew (same class as IA-3's risk) | `CMotionInterp::adjust_motion` @305343; `get_state_velocity` 0x00528960; retire (a) R3-W6, (b) R6 |
| AP-76 | **Remote rotation from the ObservedOmega side-channel**: the R2-Q5 sink callbacks (`MotionTableDispatchSink.TurnApplied/TurnStopped`) seed `RemoteMotion.ObservedOmega = (0,0,-(pi/2)*signedTurnSpeed)` on wire turns and zero it on turn-stops; GameWindow's per-tick step applies ObservedOmega (preferring it over sequence omega) to the remote body's orientation. Retail rotates the body from the SEQUENCE omega inside the per-tick apply_physics chain (CSequence::apply_physics -> CPhysicsObj omega integration) (carried verbatim from the deleted RemoteMotionSink; H17) | `src/AcDream.App/Rendering/GameWindow.cs` (sink callbacks in OnLiveMotionUpdated + the omegaToApply step in TickAnimations) | Same angular rate retail derives (pi/2 rad/s x turn speed); starts rotation the same tick as the wire turn without waiting for R6's per-tick order; UpdatePosition orientation snaps bound any drift | If the dat's turn modifier omega differs from the pi/2 constant, remote rotation rate diverges until an orientation snap; double-application risk if R6 lands apply_physics-driven rotation without deleting this seam | retire in R6 (retail per-tick order: apply_physics drives remote rotation) |
| AP-77 | **`apply_current_movement`'s interpreted-branch tail (`ApplyCurrentMovementInterpreted`) writes body velocity DIRECTLY via `get_state_velocity`/`set_local_velocity` when grounded, instead of dispatching through an `IInterpretedMotionSink`** — retail's real `apply_interpreted_movement` (0x00528600) drives velocity indirectly through `DoInterpretedMotion`'s animation-table backend (the SAME function the funnel's `ApplyInterpretedMovement` already uses WITH a sink); this dual-dispatch call site (`HitGround`/`LeaveGround`/`ReportExhaustion`/hold-key toggles/`SetWeenieObject`/`SetPhysicsObject`) has no sink threaded through it | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`ApplyCurrentMovementInterpreted`) | Direct continuation of the pre-R3-W4 `apply_current_movement` approximation (R3-W4 plan explicitly keeps this shape rather than relocating it — "the direct grounded-velocity write MOVES to the controller-side call site unchanged"); correct for the grounded, no-animation-table-yet state acdream is in today | A MotionTable whose locomotion cycle bakes a DIFFERENT velocity than `get_state_velocity`'s constants would silently diverge from what the animation actually plays, since this path never touches the animation backend at all | `CMotionInterp::apply_interpreted_movement` 0x00528600; retire when a sink is threaded through `apply_current_movement`'s callers (R6 root motion) |
| AP-78 | **`MotionInterpreter.RemoveLinkAnimations`/`InitializeMotionTables` are no-op `Action?` seams** — retail's `CPhysicsObj::RemoveLinkAnimations` (0x0050fe20, called from `HitGround`/`LeaveGround`) and `CPhysicsObj::InitializeMotionTables` (called from `enter_default_state`) have no real implementation bound at the Core layer; this is ALSO the retail mechanism K-fix18's App-side `SetCycle(skipTransitionLink:true)` hack (AP-74) stands in for — R3-W4 ports the CALL SITES (`HitGround`/`LeaveGround`/`EnterDefaultState` all invoke the seam), but the App-side binding to the entity's `AnimationSequencer`/`CSequence` core, and the K-fix18 hack's deletion, are a separate (orchestrator) commit | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`RemoveLinkAnimations`, `InitializeMotionTables` properties) | Matches the existing `UnstickFromObject`/`InterruptCurrentMovement` `Action?` seam convention (R3-W2/W3); Core has no GL/sequencer dependency to bind against (Code Structure Rule #2) | Until the App layer binds `RemoveLinkAnimations`, `HitGround`/`LeaveGround`/`EnterDefaultState` silently skip the link-strip — no different from before R3-W4 (K-fix18 still covers it) until the orchestrator's binding + K-fix18 deletion land together | `CPhysicsObj::RemoveLinkAnimations` 0x0050fe20; `CPhysicsObj::InitializeMotionTables`; retire (seam→binding) when the orchestrator wires the App-side call, AP-74 retires in the SAME commit that deletes K-fix18 |
## 4. Temporary stopgap (TS) — 36 rows (TS-37 is a retired-row historical note, not an active count)
@ -219,10 +220,10 @@ accepted-divergence entries (#96, #49, #50).
| TS-31 | Squelch toggle absent (no `/squelch` slash command, no clickable name-tags to silence); retail's squelch list filters incoming chat lines | `src/AcDream.Core/Chat/ChatLog.cs` | Squelch is a social / moderation feature deferred to post-M1.5; the data structure (`ChatLog`) has no squelch set today | Any player can spam all clients; clickable-name-tag contextual menu (used in retail to squelch, tell, add-to-friends) is absent | `ChatFilter::IsSquelched`; retail right-click player name → Squelch menu |
| TS-32 | `ClientObjectTable` has no pre-queue for a child `CreateObject` that arrives before its parent (out-of-order PARENTED create); such objects are ingested as root objects and their `ContainerId` links a not-yet-known container. Retail's `null_object_table` + `null_weenie_object_table` hold unresolvable objects until the parent arrives | `src/AcDream.Core/Items/ClientObjectTable.cs` (`Ingest`) | PD↔`CreateObject` ordering is handled (upsert semantics); out-of-order PARENTED creates are observed only at high packet loss or in vendor/corpse multi-object bursts on non-loopback links; deferred to D.5.5+ | A container's child object arriving before the container is ingested as a root item — it won't appear in `GetContents` until the next `RecordMembership` or a move event corrects the parent link | `CObjectMaint::null_object_table` / `null_weenie_object_table` (acclient.h / named-retail pc) |
| TS-33 | Outbound position-send tracker over-stamped after MoveToState: `NotePositionSent` writes last-sent position + cell + contact-plane after BOTH the MTS and AP sends, but retail's `SendMovementEvent` (0x006b4680) stamps ONLY `last_sent_position_time` after an MTS — only `SendPositionEvent` (0x006b4770, the AP path) stamps all three. Broader: acdream gates APs on a plain interval (`HeartbeatDue`) where retail uses `ShouldSendPositionEvent` (0x006b45e0 — interval OR cell-change OR contact-plane-change) | `src/AcDream.App/Rendering/GameWindow.cs:8331` (MTS `NotePositionSent`) | D5 audit-only in the L.2b wire-parity slice; the full cadence port (`ShouldSendPositionEvent` gate + split MTS/AP stamping) is a dedicated follow-up slice | AP heartbeat cadence diverges from retail — acdream may suppress or reorder autonomous-position sends differently after movement-state sends; on a real network this shifts the position-correction rhythm | `CommandInterpreter::SendMovementEvent` 0x006b4680, `SendPositionEvent` 0x006b4770, `ShouldSendPositionEvent` 0x006b45e0 |
| TS-34 | `adjust_motion`'s creature guard is a no-op — retail returns early for a non-creature weenie (`!IsCreature()`), but acdream's `IWeenieObject` has no `IsCreature()`, so the guard always proceeds | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`adjust_motion`) | Correct today: the only caller of the local `adjust_motion`/`apply_raw_movement` path is the player (a creature); no non-creature drives it | If a non-creature weenie is ever routed through `adjust_motion` (e.g. a missile with a weenie), its motion would be wrongly normalized instead of skipped | `CMotionInterp::adjust_motion` 0x00528010 creature guard; add `IsCreature` to IWeenieObject |
| TS-35 | `PhysicsBody.IsFullyConstrained` is a stub property (default `false`, never set by any physics code), read by `jump_is_allowed`'s verbatim `IsFullyConstrained` gate (raw 305524-305525) | `src/AcDream.Core/Physics/PhysicsBody.cs` (`IsFullyConstrained`) | R3-W3 needed the read site to port `jump_is_allowed`'s full chain; the WRITE side (per-cell contact-plane constraint tracking — a mover pinned between opposing walkable surfaces / doorway jamming) doesn't exist yet | A body that retail would consider fully constrained (and thus refuse to jump, 0x47) never gets blocked here — jumps succeed in geometry retail would reject. Low practical risk today: the constrained-mover scenario (doorway wedge, opposing-wall pin) is rare and mostly a physics-digest topic already tracked separately | `CPhysicsObj::IsFullyConstrained` 0x0050f730; `jump_is_allowed` 0x005282b0 |
| TS-36 | `MotionInterpreter.InterruptCurrentMovement` is a no-op `Action?` seam — retail's `CPhysicsObj::interrupt_current_movement` (called unconditionally at the top of `jump()` and inside `StopCompletely`) has no real implementation to call yet | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`InterruptCurrentMovement`, R3-W3; called from `jump()`) | R4 (MoveToManager) is where `cancel_moveto`/in-flight-transition-interrupt actually lands; until then there is no in-flight transition to cancel (no MoveToManager exists), so the no-op is behaviorally inert, not a masked bug | Once MoveToManager exists (R4) and a jump fires mid-auto-walk/mid-moveto without this seam being wired to the real cancel, the queued transition would keep running alongside the new jump — silent double-motion | `CPhysicsObj::interrupt_current_movement`; `jump` 0x00528780 @305800 |
| TS-37 | RETIRED misattribution note (not a live divergence — kept here as the historical record R3-W3 closes): the S2a port had `contact_allows_move` (0x00528240) arm `StandingLongJump` as a side effect, explicitly flagged "PRE-EXISTING acdream side effect (not part of 0x00528240)". R3-W3 deletes that side effect; `ChargeJump` (0x005281c0) is now the ONLY arming site, matching retail exactly. No further action — recorded per the register's retire-in-same-commit rule | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`contact_allows_move`, `ChargeJump`) | N/A — retired | N/A — retired | `CMotionInterp::charge_jump` 0x005281c0 @305448 |
| TS-38 | `MotionInterpreter.Initted` defaults to `true` in both constructors, not retail's `false` — retail's `CMotionInterp` is never observed pre-`enter_default_state` (every real construction path calls it before exposing the interpreter); acdream's constructors are used directly by ~40 pre-existing tests and both App call sites as complete, immediately-usable objects with no separate "enter default state" step | `src/AcDream.Core/Physics/MotionInterpreter.cs` (`Initted` property + both constructors) | Defaulting `true` is the C# equivalent of "the constructor already did what `enter_default_state` would have done to this flag" — `EnterDefaultState()` remains available, verbatim, for the REST of retail's reset semantics (state defaults, sentinel enqueue, `LeaveGround` tail) when a caller wants them | None observed: no code path needs `apply_current_movement`/`ReportExhaustion` to no-op before an explicit `EnterDefaultState()` call, since nothing constructs a `MotionInterpreter` and defers initialization today. If a future caller DOES need staged construction (build now, `EnterDefaultState()` later), it must explicitly set `Initted = false` first | `CMotionInterp::enter_default_state` 0x00528c80 @306124 sets `initted = 1`; retire if/when construction is staged through `EnterDefaultState()` uniformly |
---