docs: R5-V1 landed — wiring handoff, register TS-35 correction, #167, roadmap

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 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-03 19:42:04 +02:00
parent 3d89446d98
commit 2b5e8a6738
4 changed files with 181 additions and 4 deletions

View file

@ -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 ## #160 — Remote moveto: run animation pace vs actual movement speed mismatch
**Status:** CLOSED (2026-07-03, `41006e79`, user-verified same session). **Status:** CLOSED (2026-07-03, `41006e79`, user-verified same session).

View file

@ -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-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-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-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-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 | | 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 |

View file

@ -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; whole B.6 auto-walk DELETED; the P1 autonomous-echo gate ported verbatim;
TS-36 bound — input/jump/teleport cancel movetos through the retail 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 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 plan doc). R2+R3+R4 visual pass PASSED 2026-07-03 (#161#163 closed;
pass, then R5 (MovementManager facade + StickyManager + ConstraintManager #164#166 filed). **R5-V1 SHIPPED 2026-07-03 `3d89446d`** — PositionManager
+ full TargetManager — retires TS-39/AP-79's adapter). 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).** - **L.2g — Inbound motion interpretation (remote-entity CMotionInterp funnel).**
ACTIVE 2026-07-02. Port retail's inbound motion pipeline verbatim for ALL ACTIVE 2026-07-02. Port retail's inbound motion pipeline verbatim for ALL
remote entities (players, NPCs, monsters — one funnel, user-approved): remote entities (players, NPCs, monsters — one funnel, user-approved):

View file

@ -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<uint, EntityPhysicsHost> _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.