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>
143 lines
8.9 KiB
Markdown
143 lines
8.9 KiB
Markdown
# 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.
|