acdream/docs/research/2026-07-04-171-sticky-melee-handoff.md
Erik d413ac2a29 docs(#171): file the group-melee interpenetration issue + R5-V3 handoff/pickup
User report during the #170 gate: pack-melee monsters partly inside each
other with slightly stale facings vs retail on the same ACE. Investigation
(report-only) pinned two code-grounded causes, both already-tracked R5-V3
scope, fix APPROVED by the user:

1. TS-39 — sticky melee is a no-op: ACE arms melee chases with Sticky
   (Monster_Navigation.cs:416); retail's arrival hands off to
   PositionManager::StickTo -> StickyManager::adjust_offset (0x00555430,
   per-tick 0.3m edge gap + facing tracking); acdream's StickTo/Unstick
   seams are unbound so attackers freeze at stale arrival poses.
2. Arrival radii are zero: getOwnRadius () => 0f (the R5-V3 pin) and
   RouteServerMoveTo never sets MovementStruct.Radius/Height — retail/ACE
   arrive edge-to-edge with setup-derived radii (ACE PhysicsObj.MoveToObject
   reads the TARGET's PartArray radius/height).

Ruled out: the between-snap collision sweep (remotes run full
ResolveWithTransition; CollisionExemption keeps creatures collidable).
Caveat recorded: ACE has no server-side monster avoidance — acceptance is
retail PARITY, not zero overlap.

ISSUES #171 filed; handoff docs/research/2026-07-04-171-sticky-melee-handoff.md
+ pickup docs/research/2026-07-04-171-pickup-prompt.md; memory index updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 22:38:58 +02:00

7.4 KiB

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_objectPositionManager::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).