acdream/docs/research/2026-07-03-r5-managers/r5-wiring-handoff.md
Erik 5bd2b8bc8b fix(#171): R5-V3 — bind sticky melee (StickyManager live) + real arrival radii
Group-melee interpenetration + facing drift: the R5-V1-ported
StickyManager/PositionManager were Core-only — the StickTo/Unstick seams
were unbound and every arrival radius was 0, so ACE's Sticky|UseSpheres
melee chases closed ~one body-radius too deep and froze at stale arrival
poses until the next wire re-arm. Retires TS-39.

Wiring (anchors re-verified against the named decomp this session):
- EntityPhysicsHost owns a PositionManager; HandleUpdateTarget fans
  MoveToManager then PositionManager (CPhysicsObj::HandleUpdateTarget
  0x00512bc0 order).
- Seams bound remote + player: MoveToManager.StickTo (BeginNextNode
  sticky arrival @0x00529d3a), Unstick (PerformMovement head), and
  MotionInterpreter.UnstickFromObject (UM funnel head, 0x0050eaea).
- AdjustOffset at the retail UpdatePositionInternal slot (@0x00512d0e):
  NPC branch composes pre-sweep (steer is swept by ResolveWithTransition);
  remote-player branch chains the combiner offset through the shared
  delta frame (the interp stage) so sticky OVERWRITES when armed
  (0x00555430 assigns m_fOrigin, not accumulates); player inside the
  30 Hz physics quantum before UpdatePhysicsInternal.
- UseTime (the 1 s lease watchdog) at the UpdateObjectInternal tail
  (@0x005159b3): unconditional per remote; player gated on the physics
  tick (retail's MinQuantum gate skips UseTime too).
- Real setup cylsphere radii (CPartArray::GetRadius/GetHeight
  0x005180a0/0x005180b0 = setup radius/height x ObjScale from the spawn
  record): own via EnsureRemoteMotionBindings + player wiring; target via
  RouteServerMoveTo AND the speculative use-walk install (retail resolves
  the target PartArray at EVERY MoveToObject site — ACE PhysicsObj.cs:951).
- Teardown parity: exit_world (0x00514e60) UnStick + ClearTarget before
  the ExitWorld notify; player teleport fires teleport_hook's tail
  (UnStick in SetPosition + EntityPhysicsHost.NotifyTeleported =
  ClearTarget + NotifyVoyeurOfEvent(Teleported) @0x00514f1b) so mobs
  stuck to the player drop their sticks on a recall.
- SERVERVEL arbitration also yields to a stuck entity (same starvation
  class as the #170 fix — sticky owns the between-snap translation).
- StickyManager.UseTime aligned to retail's strict > deadline
  (0x00555626; ACE >): two V1 tests had pinned the >= edge — corrected.

Register: TS-39 deleted; TS-41 narrowed (stickyArmed gate); TS-43 added
(remote teleport_hook gap — self-corrects within the 1 s lease); AP-23
narrowed (real radii at the speculative site; only the use-radius
buckets remain invented).

Conformance: 2 new full-stack sticky scenarios in
RemoteChaseEndToEndHarnessTests (arrive -> stick -> strafing-target
gap+facing track -> lease expiry; unstick-on-rearm -> re-stick).
Full suite 4038 green.

Pre-commit adversarial diff review (3 lenses + per-finding refuters)
confirmed and fixed 4 findings: ObjScale-dead radius read, player
UseTime order inversion, missing teleport voyeur notify, speculative-
site radius asymmetry.

Awaiting the user visual gate: pack melee side-by-side vs retail
(attackers reshuffle + keep facing; some overlap is ACE-server-side).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 23:46:17 +02:00

143 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
> **STATUS 2026-07-04: SHIPPED (as the #171 fix), awaiting the visual gate**
> — sticky seams bound (StickTo/Unstick/UnstickFromObject, remote + player),
> AdjustOffset at the UpdatePositionInternal slot (combiner chained as the
> interp stage in the remote-player branch; pre-sweep compose in the NPC
> branch + player physics tick), UseTime at the UpdateObjectInternal tail,
> real setup cylsphere radii threaded, exit-world/teleport teardown (remote
> teleport gap = TS-43), SERVERVEL yields to a stuck entity (TS-41). TS-39
> retired. **The mt-0 wire flags (0x1 StickToObject / 0x2 StandingLongJump)
> were NOT part of the user-approved #171 scope — they move to V4.**
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.