From 2b5e8a67387e7dc84e0bf9e32ad1b152e72ed38a Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 3 Jul 2026 19:42:04 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20R5-V1=20landed=20=E2=80=94=20wiring=20h?= =?UTF-8?q?andoff,=20register=20TS-35=20correction,=20#167,=20roadmap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Housekeeping around the R5-V1 core port (3d89446d): - r5-wiring-handoff.md: the V2/V3/V4 GameWindow-wiring plan (per-entity IPhysicsObjHost, AP-79→voyeur behavioral-equivalence anchor, TS-39 sticky integration, mt-0 flags, #164, facade). These are the visual-gated slices. - ISSUES #167: ConstraintManager leash unported — arming (SmartBox) + two x87 distance constants BN elided; deferred (needs cdb/Ghidra). TS-35 stays. - Register TS-35 corrected: R5 recon proved the "write side" is the ConstraintManager server-position rubber-band leash, NOT the earlier "per-cell contact-plane / doorway-jamming" guess. Oracle addresses fixed. - Roadmap Phase R: R5-V1 shipped, R2-R4 visual pass PASSED, next = wiring. Co-Authored-By: Claude Fable 5 --- docs/ISSUES.md | 38 +++++ .../retail-divergence-register.md | 2 +- docs/plans/2026-04-11-roadmap.md | 12 +- .../r5-wiring-handoff.md | 133 ++++++++++++++++++ 4 files changed, 181 insertions(+), 4 deletions(-) create mode 100644 docs/research/2026-07-03-r5-managers/r5-wiring-handoff.md diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 0d531831..c3f3061a 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -46,6 +46,44 @@ Copy this block when adding a new issue: --- +## #167 — ConstraintManager leash unported (arming + two unknown x87 constants) + +**Status:** OPEN (deferred, filed 2026-07-03 during R5-V1) +**Severity:** LOW (server-position rubber-band + jump-during-rubber-band gate) +**Component:** physics, constraint + +**Description:** R5-V1 ported `ConstraintManager` (the server-position +rubber-band leash) as a Core class for structural completeness of +`PositionManager`, but it is never ARMED in acdream. Retail arms the leash +ONLY from `SmartBox::HandleReceivedPosition` (0x00453fd0) — on every inbound +server position packet, anchoring the mover to self (remotes) or the received +position (player) — with a start/max distance band from +`CPhysicsObj::GetStartConstraintDistance` (0x0050ebc0) and +`GetMaxConstraintDistance` (0x0050ec10). acdream's position reconciliation is +not `SmartBox`, so nothing calls `PositionManager.ConstrainTo`, and +`IsFullyConstrained` stays false (= register **TS-35**'s current stub +behavior — jump never blocked by the leash). + +**Blockers:** (1) the two distance constants are **x87 float returns BN +elided** — `GetStart/MaxConstraintDistance` decompile to a bare +`this->m_position;` expression with the actual returned value lost to the +FPU-return-elision artifact. Recovering them needs a live cdb read of `st0` +after the call (retail debugger toolchain) or a Ghidra re-decompile with a +corrected float-return signature. (2) The arming site (`SmartBox`'s inbound +position-reconciliation branches A/B/C) has no acdream equivalent yet — wiring +it means teaching acdream's position path to re-anchor the leash on every +server position update, then feeding the `adjust_offset` taper into the body +integration (same chokepoint as the sticky wiring, R5-V3). + +**Where:** `src/AcDream.Core/Physics/Motion/ConstraintManager.cs` (class, +unarmed); the read gate is `PhysicsBody.IsFullyConstrained` (TS-35) via +`jump_is_allowed`. Decomp: `docs/research/2026-07-03-r5-managers/`. + +**Acceptance:** the two constants are recovered (cdb/Ghidra), acdream arms the +leash on inbound server positions, `IsFullyConstrained` fires while +rubber-banding, and a jump attempt inside the tight leash is blocked +(0x47) matching retail; TS-35 + this issue retire together. + ## #160 — Remote moveto: run animation pace vs actual movement speed mismatch **Status:** CLOSED (2026-07-03, `41006e79`, user-verified same session). diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index a2533f12..5e469273 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -219,7 +219,7 @@ accepted-divergence entries (#96, #49, #50). | 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), AND acdream's frame-changed diff compares POSITION only (`ApproxPositionEqual`) where retail's `Frame::is_equal` compares the full frame incl. ORIENTATION — a stationary heading change (R4-V5: the MoveToManager's `HandleTurnToHeading` arrival snap, `set_heading(send:true)`) never triggers an AP, so the server keeps the stale facing until the player next moves. Masked against ACE (ACE rotates server-side on its own mt-8/9 / close-range-use paths and broadcasts the result) | `src/AcDream.App/Rendering/GameWindow.cs:8331` (MTS `NotePositionSent`); `src/AcDream.App/Input/PlayerMovementController.cs` (`ApproxPositionEqual` + the heartbeat diff); the player MoveToManager `setHeading` seam in `EnterPlayerModeNow` (the `send` flag's would-be consumer) | D5 audit-only in the L.2b wire-parity slice; the full cadence port (`ShouldSendPositionEvent` gate + split MTS/AP stamping + full-frame `Frame::is_equal` diff) is a dedicated follow-up slice (R7 outbound) | AP heartbeat cadence diverges from retail — acdream may suppress or reorder autonomous-position sends differently after movement-state sends, and a stationary server-commanded turn leaves observers with stale facing until the next movement; on a real network this shifts the position-correction rhythm | `CommandInterpreter::SendMovementEvent` 0x006b4680, `SendPositionEvent` 0x006b4770, `ShouldSendPositionEvent` 0x006b45e0, `Frame::is_equal` (pc:700263) | | TS-39 | `MoveToManager.StickTo` and `Unstick` are unbound no-op seams (both remote and player bindings leave them null) — retail's sticky arrival hands off to `PositionManager::StickTo(tlid, radius, height)` (BeginNextNode §4b reads the three fields BEFORE CleanUp zeroes them — that ordering IS ported) and every `PerformMovement` head calls `unstick_from_object` → `PositionManager::UnStick`; acdream's sticky moveto (wire bit 0x80) completes-and-stops instead of sticking | `src/AcDream.Core/Physics/Motion/MoveToManager.cs` (`StickTo`/`Unstick` seam properties); GameWindow `EnsureRemoteMotionBindings` + `EnterPlayerModeNow` (neither binds them) | PositionManager/StickyManager bodies were NOT extracted in the R4 research (call shapes only, decomp §9f/§9g — do-not-invent); the seams + verbatim call ordering land now, the bodies with R5 | A server /follow-style sticky MoveTo arrives, walks to the target, then STOPS instead of sticking — the entity drifts off the target as it moves until the next re-emit; unstick ordering differences invisible until StickyManager exists | `PositionManager::StickTo` / `UnStick` call shapes (r4-moveto-decomp.md §4b/§3a); R5 StickyManager port retires this row | | TS-40 | Retail's `physics_obj->cell` ("placed in the world") is proxied by the explicit `PhysicsBody.InWorld` flag — set by `SnapToCell` (local player placement) and `RemoteMotion` construction (remotes exist only for world entities); consumed by `CMotionInterp`'s detached-object link-strip guards (`if (cell == 0) RemoveLinkAnimations`, raw @305627). Replaces the UNREGISTERED `CellPosition.ObjCellId == 0` proxy, which only the local player ever seeded (#145 `SnapToCell`), so every REMOTE body read "detached" and every dispatched transition link (door swings, remote walk↔run links) was stripped the same tick it was appended — the 2026-07-03 door-snap bug | `src/AcDream.Core/Physics/PhysicsBody.cs` (`InWorld`); `src/AcDream.Core/Physics/MotionInterpreter.cs` (3 guard sites) | acdream has no per-body CObjCell pointer; a boolean placement flag carries exactly the guard's retail meaning until cell-pointer plumbing exists | A body used without either placement path (a future entity class constructing bodies directly) reads detached and loses transition links until its creation site sets the flag | `CMotionInterp::DoInterpretedMotion` 0x00528360 tail @305627; `CPhysicsObj::RemoveLinkAnimations` | -| 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-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. **R5-V1 CORRECTED the mechanism** (the earlier "per-cell contact-plane / doorway-jamming" guess was WRONG): the write side is the **ConstraintManager server-position rubber-band leash** — armed by `SmartBox::HandleReceivedPosition` on every inbound server position, `IsFullyConstrained` = `max*0.9 < offset`. R5-V1 ported `ConstraintManager` (`src/AcDream.Core/Physics/Motion/ConstraintManager.cs`) but does NOT arm it (no acdream `SmartBox` + two x87 distance constants BN elided) — so this read stays false. Arming = issue #167 | A body retail would consider fully constrained (still rubber-banding toward a server position inside the tight leash) never refuses the jump (0x47) — a jump succeeds mid-rubber-band where retail blocks it. Low practical risk (the leash band is tight + short-lived) | `CPhysicsObj::IsFullyConstrained` 0x0050ec60 → `ConstraintManager::IsFullyConstrained` 0x005560d0; `jump_is_allowed` 0x005282b0; arming `SmartBox::HandleReceivedPosition` 0x00453fd0 (issue #167) | | 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 | diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index fedaf27b..19b4d05a 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -718,9 +718,15 @@ diagnostic scaffolding, not yet the final collision system. whole B.6 auto-walk DELETED; the P1 autonomous-echo gate ported verbatim; TS-36 bound — input/jump/teleport cancel movetos through the retail interrupt chain; retired AD-8/AD-9/AP-8/AP-9/AD-26/TS-36; trail in the - plan doc). R2+R3+R4 share ONE pending user visual pass. NEXT: the visual - pass, then R5 (MovementManager facade + StickyManager + ConstraintManager - + full TargetManager — retires TS-39/AP-79's adapter). + plan doc). R2+R3+R4 visual pass PASSED 2026-07-03 (#161–#163 closed; + #164–#166 filed). **R5-V1 SHIPPED 2026-07-03 `3d89446d`** — PositionManager + facade + StickyManager + ConstraintManager + the full TargetManager voyeur + system ported to Core, fully tested, UNWIRED (purely additive; renamed the + misnamed `Physics.PositionManager` combiner → `RemoteMotionCombiner`); + ConstraintManager unarmed (issue #167, TS-35 stays). Decomp/ACE/plan: + `docs/research/2026-07-03-r5-managers/`. NEXT: R5-V2/V3/V4 — the visual-gated + GameWindow wiring (per-entity `IPhysicsObjHost`, retire AP-79 + TS-39, apply + mt-0 sticky/longjump, #164, facade); plan in `r5-wiring-handoff.md`. - **L.2g — Inbound motion interpretation (remote-entity CMotionInterp funnel).** ACTIVE 2026-07-02. Port retail's inbound motion pipeline verbatim for ALL remote entities (players, NPCs, monsters — one funnel, user-approved): diff --git a/docs/research/2026-07-03-r5-managers/r5-wiring-handoff.md b/docs/research/2026-07-03-r5-managers/r5-wiring-handoff.md new file mode 100644 index 00000000..81bbf6ac --- /dev/null +++ b/docs/research/2026-07-03-r5-managers/r5-wiring-handoff.md @@ -0,0 +1,133 @@ +# R5 wiring handoff — V1 landed, V2/V3/V4 are the visual-gated wiring + +Successor to `2026-07-03-r5-entry-handoff.md`. **R5-V1 shipped** (`3d89446d`): +the retail movement-manager family is ported to Core as faithful, fully-tested, +UNWIRED classes. The remaining slices (V2/V3/V4) are GameWindow integration +whose acceptance is **visual verification against the running client** — the one +thing the CLAUDE.md says requires the user. Do them one at a time, visual-gate +each, do NOT stack. + +## What V1 landed (`3d89446d`, full suite 4006 green) + +New Core (`src/AcDream.Core/Physics/Motion/`), all with conformance tests: +- `StickyManager`, `ConstraintManager`, `PositionManager` (facade), + `TargetManager` + `TargettedVoyeurInfo`, `IPhysicsObjHost` (the CPhysicsObj + back-pointer seam the App implements per entity), `MotionDeltaFrame`. +- `TargetInfo` extended to the full retail 10-field struct (additive defaults). +- `MoveToMath`: `CylinderDistanceNoZ` (signed), `NormalizeCheckSmall`, + `GlobalToLocalVec`. +- **Rename**: `AcDream.Core.Physics.PositionManager` (the misnamed remote + anim+interp combiner) → `RemoteMotionCombiner`. This freed the name for the + faithful facade and removed the ambiguity that breaks any file importing both + `AcDream.Core.Physics` + `.Motion` — **GameWindow imports both**, so the + wiring below could not compile without this rename. + +Decomp + ACE cross-ref + decoded math: this directory (`r5-*-decomp.md`, +`r5-ace-crossref.md`, `r5-port-plan.md`). The port plan §2 has the decoded x87 +mush (sticky steer, constraint taper, voyeur gates) — trust it over the raw BN. + +## The wiring model (all slices) + +acdream has no per-entity `CPhysicsObj`. Introduce one `IPhysicsObjHost` per +entity — **in a dedicated App class `EntityPhysicsHost`, NOT inline in +GameWindow** (code-structure rule #1). GameWindow keeps a +`Dictionary _physicsHosts`; each host's `GetObjectA` +is `id => _physicsHosts.GetValueOrDefault(id)` (cross-entity resolve for the +voyeur round-trip). Construct a host in `EnsureRemoteMotionBindings` (remotes) +and `EnterPlayerModeNow` (player); remove it + `NotifyVoyeurOfEvent(ExitWorld)` +on despawn/exit-world. + +Host accessor mapping (behavioral-equivalence anchor — match AP-79 exactly): +- `Position` = the entity's **`WorldEntity.Position`** (world-space) via + `_entitiesByServerGuid[id].Position`. This is EXACTLY the source the AP-79 + poll used for a target (`trackedEnt.Position`), so for the moveto case + (quantum 0, `GetInterpolatedPosition` = Position) the voyeur system delivers + the identical position the adapter did. +- `Velocity` = body velocity (only used for quantum>0, i.e. sticky). +- `Radius` = setup cylsphere radius (today MoveToManager passes 0 — the P4 note; + wiring the real radius here is the "finally needed" bit). +- `InContact` = transient Contact bit (constraint gate; V3). +- `MinterpMaxSpeed` = `interp.GetMaxSpeed()` or null (sticky speed; V3). +- `CurTime`/`PhysicsTimerTime` = the real clocks (R4-V5 fixed the remote clock; + reuse the same epoch-seconds source; the player uses `SimTimeSeconds`). +- `HandleUpdateTarget` = fan to the entity's `MoveToManager.HandleUpdateTarget` + (+ `PositionManager.HandleUpdateTarget` once V3 adds it). + +### V2 — wire TargetManager, retire AP-79 (behaviorally a no-op refactor) +Repoint MoveToManager's `setTarget`/`clearTarget`/`getTargetQuantum`/ +`setTargetQuantum` seams at `host.TargetManager`. Per tick, call +`host.TargetManager.HandleTargetting()` BEFORE `mtm.UseTime()` (retail +`UpdateObjectInternal` order) — in `TickRemoteMoveTo` (remotes, GameWindow.cs +~4469, both call sites ~9688/9911) and the player pre-Update block +(GameWindow.cs ~7994). **Every entity that can be a target must tick +HandleTargetting** — the creature-chase target is the player, so the player's +host MUST tick it (that's what pushes updates to the chasing creatures). +Delete the AP-79 fields (`RemoteMotion.TrackedTarget*`, the `_playerMoveToTarget*` +GameWindow fields) and the two manual poll blocks. **Delete the AP-79 register +row same commit.** Keep the 183-case/funnel/moveto suites green (unmodified). + +Behavioral-equivalence: the voyeur radius = MoveToManager's `set_target` radius +(0.5) = AP-79's `TrackedTargetRadius`; both deliver `HandleUpdateTarget(Ok)` +when the target drifts >radius; both deliver ExitWorld on despawn. Identical for +the moveto case. **VISUAL GATE**: a server-directed creature still chases the +player, and player auto-walk-to-object still tracks — no visible change, confirm +nothing broke. + +Lifecycle watchouts: (1) when a watcher despawns, its host removal must let the +target drop the dead voyeur (RemoveVoyeur on ClearTarget, or prune on send +failure). (2) The immediate-snapshot-on-subscribe (retail AddVoyeur) means the +watcher gets one `HandleUpdateTarget(Ok)` the instant it sets a target — matches +AP-79's `!FedOnce` first delivery. (3) HandleTargetting self-throttles to 0.5s; +the AP-79 poll ran every frame but only DELIVERED on drift — same effective +cadence for delivery, but a target moving fast between 0.5s ticks sends less +often. If chase feels choppier than AP-79, that's the throttle — retail-faithful, +but note it. + +### V3 — wire PositionManager (sticky), retire TS-39, apply mt-0 flags +Add a `PositionManager` to each host. Bind `MoveToManager.StickTo → +host.PositionManager.StickTo`, `MoveToManager.Unstick → host.PositionManager.UnStick`. +The load-bearing part: integrate `host.PositionManager.AdjustOffset(deltaFrame, +quantum)` into the per-frame body integration at the composed-delta chokepoint +(retail `UpdatePositionInternal` — in acdream that's where `RemoteMotionCombiner. +ComputeOffset` runs, GameWindow ~9716; sticky steer must ADD to the body's +per-tick motion). Per tick `host.PositionManager.UseTime()` AFTER +`mtm.UseTime()`. Apply the mt-0 wire flags (`UpdateMotion.cs` parses both, +unconsumed): `0x1 StickToObject` → `host.stick_to_object(guid)` → +`PositionManager.StickTo`; `0x2 StandingLongJump` → +`MotionInterpreter.StandingLongJump`. **Delete the TS-39 register row same +commit.** **VISUAL GATE**: a sticky scenario (server /follow-style sticky moveto +or a moving-platform stick) HOLDS the target instead of stopping-and-drifting. + +### V4 (capstone) — MovementManager facade + #164 + head-stance dispatch + docs +- Collapse per-entity `Motion`+`MoveTo` into a `MovementManager` owner + (structural; keep 183-case/funnel/moveto green UNMODIFIED). Optional if the + arc runs long — the retirements above don't need it. +- #164: action-replay Autonomous bit (params 0x1000 from per-action autonomy + flag, raw 305982) in `MotionInterpreter.MoveToInterpretedState` action loop. +- Head stance-change dispatch for mt 6-9 (the `RetailObserverTraceConformance + Tests.cs:33` "S3 wires the unpack-level style-on-change" exclusion — retail's + `unpack_movement` head does `InqStyle != wire-style → DoMotion(style)` + independent of movement type; acdream applies style only on the mt-0 path). +- Final register/ISSUES/roadmap/memory + successor handoff. + +## Open items carried +- **#167**: ConstraintManager leash-arming + the two unknown x87 constants + (`GetStart/MaxConstraintDistance`) — deferred; needs cdb/Ghidra. TS-35 stays + open (register corrected: the mechanism is the leash, not doorway-jamming). +- Verify against named-retail before treating as ground truth: the sticky/target + constants (StickyRadius 0.3 / StickyTime 1.0 / follow ×5 / fallback 15; + HandleTargetting 0.5s throttle / 10s staleness) — ACE values, cross-checked + against the mush but ACE flagged its own `// ref?` uncertainty on the + TargetInfo copy semantics (ported as copy — retail copy-constructs at every + fan-out). + +## Load-bearing lessons (from V1) +- The `Physics.PositionManager` name collision was a REAL compile break for any + file importing both namespaces, not cosmetic — the recon agent missed the + existing (misnamed) class entirely (grepped for retail semantics). Renaming it + was the "do it right" fix. Lesson: grep for the NAME too, not just the concept. +- ConstraintManager is NOT a general joint/constraint system — it's a narrow + server-position rubber-band leash (3 call sites total: SmartBox arm, teleport + disarm, jump-gate read). Don't over-scope it. +- The voyeur system is peer-to-peer intra-client: it needs EVERY entity to be a + host + tick HandleTargetting, or cross-entity delivery silently no-ops.