# Handoff — #171 group-melee interpenetration + facing drift (R5-V3: StickyManager + arrival radii) — 2026-07-04 Fresh session picks up HERE for #171. Worktree `vigorous-joliot-f0c3ad`, branch `claude/vigorous-joliot-f0c3ad`, tree CLEAN after the #170 close-out (`4cad626f`). The fix scope below is USER-APPROVED ("Ok I approve", 2026-07-04). Pickup prompt: `docs/research/2026-07-04-171-pickup-prompt.md`. ## Symptom (user report, #170 gate session) In a pack melee vs the player, acdream monsters end up **partly inside each other** with **slightly stale facings**, vs retail on the same local ACE. The #170 chase itself (turn → sustained run → plant → swing) is CLOSED and gate-passed — do not reopen that machinery. ## Root causes (code-grounded, investigated 2026-07-04) 1. **Sticky melee is a no-op — register TS-39.** ACE arms EVERY melee chase with `Sticky | UseFinalHeading | MoveAway | FailWalk` (`references/ACE/Source/ACE.Server/WorldObjects/Monster_Navigation.cs:406-419`). The retail arrival chain: `MoveToManager::BeginNextNode` (sticky bit set, queue empty) reads `SoughtObjectId/Radius/Height` BEFORE `CleanUp` zeroes them (that ordering IS already ported — `MoveToManager.cs` ~700-714) and calls `PositionManager::StickTo(tlid, radius, height)`; from then on `StickyManager::adjust_offset` (0x00555430) runs per tick and holds a ~0.3 m edge gap + facing against the moving target. Every `MovementManager::PerformMovement` head calls `unstick_from_object` → `PositionManager::UnStick` (our `MoveToManager.PerformMovement:414` invokes the seam). acdream's `StickTo`/`Unstick` seams are UNBOUND (GameWindow `EnsureRemoteMotionBindings` + the player wiring bind neither) → attackers complete-and-freeze at stale arrival poses until the next wire re-arm. This is the direct cause of BOTH observed symptoms. 2. **Arrival radii are zero.** Retail/ACE arrive EDGE-TO-EDGE: ACE `PhysicsObj.MoveToObject` (`references/ACE/…/Physics/PhysicsObj.cs:936-956`) reads the TARGET's `PartArray.GetRadius()/GetHeight()` and passes them into `MoveToObject_Internal`; the mover's own radius feeds `GetCurrentDistance`'s `cylinder_distance` when `UseSpheres` (ACE default TRUE, `DistanceToObject` default 0.6). acdream: - `GameWindow.EnsureRemoteMotionBindings` binds `getOwnRadius/getOwnHeight : () => 0f` — the comment IS the R5-V3 pin ("setup cylsphere radius lands with R5-V3"); - `GameWindow.RouteServerMoveTo` never sets `MovementStruct.Radius/Height` (target radii) → 0. Net: every attacker closes ~one body-radius deeper than retail → dogpile. 3. **Server-side caveat:** ACE has no monster-vs-monster avoidance; UP hard-snaps plant bodies where ACE says (retail hard-snaps identically). Some overlap is server-authoritative and visible on retail too — **acceptance is retail parity, not zero overlap.** Ruled out: the between-snap collision sweep. Remotes run the full `ResolveWithTransition` (GameWindow ~10100) and `CollisionExemption` keeps creature cylinders collidable for non-viewer movers. ## Approved fix scope (= the R5-V3 slice, roadmap-aligned) 1. **Port `StickyManager` verbatim** (grep `docs/research/named-retail/ acclient_2013_pseudo_c.txt` for `StickyManager::` first — `StickTo`, `UnStick`, `adjust_offset` 0x00555430, `HandleUpdateTarget`; cross-check `references/ACE/Source/ACE.Server/Physics/Managers/StickyManager.cs`). Groundwork ALREADY in tree (`MoveToMath`): `CylinderDistanceNoZ` (signed — the adjust_offset gap math), `GlobalToLocalVec`, `NormalizeCheckSmall` — all documented as adjust_offset consumers. `ConstraintManager` (the sibling PositionManager member) was ported R5-V1 (TS-35, unarmed — #167 is SEPARATE, do not arm it here). 2. **PositionManager**: the facade that owns Sticky + Constraint (`references/ACE/…/Managers/PositionManager.cs`). Retail per-tick anchors pinned this session from the named decomp: - `PositionManager::adjust_offset` called inside `CPhysicsObj::UpdatePositionInternal` @0x00512d0e (BEFORE process_hooks); - `PositionManager::UseTime` called in `UpdateObjectInternal` @0x005159b3 (AFTER `CPartArray::HandleMovement`). Wire the acdream equivalents at the matching points of the remote tick (GameWindow `TickAnimations`) and the player path. 3. **Bind the seams**: `MoveToManager.StickTo` → PositionManager.StickTo; `MoveToManager.Unstick` → PositionManager.UnStick — in `EnsureRemoteMotionBindings` AND the player wiring (`EnterPlayerModeNow` area, ~13128). The TargetManager voyeur fan-out already delivers target info; the R5-V2 comment at GameWindow ~4327 says exactly this: "PositionManager sticky joins the fan-out in V3" — StickyManager is a voyeur consumer in retail (its own HandleUpdateTarget). 4. **Thread real radii**: own radius/height from the entity's Setup (the spawn path already reads `setup.Radius`/`setup.Height` for collision registration, GameWindow ~4187); target radius/height at the `RouteServerMoveTo` MoveToObject branch (resolve the target entity's setup — retail reads the TARGET object, not the wire). Sweep BOTH the remote and player MoveToManager bindings. 5. **Register bookkeeping (same commit):** retire TS-39; retire/narrow the radius pin note; new rows for any adaptation introduced. 6. **Conformance:** extend `RemoteChaseEndToEndHarnessTests` with a sticky scenario — arm a sticky MoveToObject, arrive, then MOVE the target sideways: assert the follower tracks (gap ≈ stick distance, facing follows) and `UnStick` fires on the next PerformMovement head. The harness already models the full tick order; add a PositionManager step where retail has it. ## Gotchas - **Revert-prone area** (remote motion). Follow the workflow: grep named decomp FIRST, pseudocode, port verbatim, ACE cross-check, harness, then wire. No redesigns; the two prior #170 reverts were redesigns. - The harness gotcha from #170: any new harness body MUST replicate the live `RemoteMotion` construction (`InWorld=true`, `RemoteWeenie`, Contact|OnWalkable|Active) or the TS-40 guard strips links and everything wedges (`RemoteChaseDrainBisectTests` documents the shape). - Per-entity probes must print the guid (memory: `feedback_probe_identity_attribution` — the #170 misread). - `StickyManager::adjust_offset` sign semantics: inside the gap the signed distance goes NEGATIVE and the per-tick delta inverts (backs off) — ACE StickyManager.cs:156, already noted on `MoveToMath.CylinderDistanceNoZ`. - Do NOT arm ConstraintManager (#167 — two unknown x87 constants). - Visual gate at the end: pack melee side-by-side vs retail; the user strafes around the pack; attackers should reshuffle + keep facing like retail. ## Where things are (quick map) - `MoveToManager.cs`: `PerformMovement:411` (Unstick head), `BeginNextNode` sticky arrival ~700-714, `GetCurrentDistance` (UseSpheres cylinder math). - `MoveToMath.cs`: `CylinderDistance`, `CylinderDistanceNoZ`, `GlobalToLocalVec`, `NormalizeCheckSmall`. - `GameWindow.cs`: `EnsureRemoteMotionBindings` ~4251 (seams + radius 0f pin), `RouteServerMoveTo` ~4457 (MovementStruct — Radius/Height unset), remote tick `TickAnimations` (PositionManager wiring points), player wiring ~13128. - `ConstraintManager.cs` (R5-V1) — PositionManager sibling, reference for the port style. - Register rows: TS-39 (sticky seams), TS-35 (ConstraintManager), TS-41/42 (#170 rows, adjacent machinery).