diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 353a5b1c..dc78cb5b 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -46,6 +46,50 @@ Copy this block when adding a new issue: --- +## #171 — Group melee: monsters interpenetrate + facing drifts (sticky melee unbound, arrival radii = 0) + +**Status:** OPEN — investigated + fix APPROVED 2026-07-04 (user: "Ok I approve"). +**Severity:** MEDIUM (visual — group combat feel vs retail) +**Filed:** 2026-07-04 (user report during the #170 visual gate: "some monsters were +partly inside other monsters while hitting me… some orientation of the monsters +are a bit off") +**Component:** remote entity, MoveTo/PositionManager, R5-V3 + +**Description:** in a pack melee, acdream attackers end up partly inside each +other with slightly stale facings vs retail on the same ACE. Two code-grounded +causes (both already-tracked R5-V3 scope), plus one server-side caveat: + +1. **Sticky melee is a no-op (register TS-39).** ACE arms every melee chase with + `Sticky|UseFinalHeading|MoveAway|FailWalk` + (`references/ACE/…/Monster_Navigation.cs:416`). Retail's sticky arrival hands + off to `PositionManager::StickTo` → `StickyManager::adjust_offset` + (0x00555430): per-tick 0.3 m edge-gap + facing tracking against the moving + target. Our `MoveToManager.BeginNextNode` invokes the `StickTo` seam — unbound + — so attackers complete-and-freeze at stale arrival poses until the next wire + re-arm. +2. **Arrival radii are zero.** ACE/retail arrive edge-to-edge + (`cylinder_distance`, own + target radii from the PartArray/setup — + `ACE PhysicsObj.MoveToObject` reads `GetRadius()/GetHeight()` from the TARGET). + acdream binds `getOwnRadius: () => 0f` (GameWindow `EnsureRemoteMotionBindings`, + the explicit R5-V3 pin) AND `RouteServerMoveTo` never sets + `MovementStruct.Radius/Height` → both radii 0 → every attacker closes ~one + body-radius deeper than retail → dogpile. +3. Caveat: ACE has no server-side monster-vs-monster avoidance — some overlap is + server-authoritative and shows on retail too. Acceptance = retail parity, not + zero overlap. + +Ruled out: the between-snap collision sweep (remotes run full +`ResolveWithTransition`; `CollisionExemption` keeps creature cylinders collidable +for non-viewer movers). + +**Approved fix (R5-V3):** port `StickyManager` + bind `PositionManager` +`StickTo`/`UnStick` seams + thread setup-derived cylsphere radii through the +MoveTo distance math. **SSOT:** `docs/research/2026-07-04-171-sticky-melee-handoff.md` ++ pickup `docs/research/2026-07-04-171-pickup-prompt.md`. + +**Acceptance:** side-by-side vs retail in a scamp pack: attackers hold separation ++ face the player while it strafes; user visual gate. + ## #170 — Remote creature chase+attack renders wrong vs retail (glide, over-frequency, uniform attack anims) **Status:** DONE (2026-07-04) — **user visual gate PASSED** ("looks good, as close diff --git a/docs/research/2026-07-04-171-pickup-prompt.md b/docs/research/2026-07-04-171-pickup-prompt.md new file mode 100644 index 00000000..fdebbae2 --- /dev/null +++ b/docs/research/2026-07-04-171-pickup-prompt.md @@ -0,0 +1,52 @@ +# Pickup prompt — #171 sticky melee / R5-V3 (paste into a fresh session) + +Read `docs/research/2026-07-04-171-sticky-melee-handoff.md` first — it is the +SSOT for this task (root causes, retail anchors, approved scope, gotchas). +Then implement R5-V3. + +**Context:** #170 (chase slide) is CLOSED — gate passed at `4cad626f`; do not +reopen that machinery. During its gate the user observed #171: in a pack +melee, monsters sit partly inside each other with slightly stale facings vs +retail on the same ACE. Investigated + **fix approved by the user 2026-07-04**. + +**The two causes (both R5-V3 scope, already register-tracked):** +1. Sticky melee is a no-op (TS-39): ACE arms melee chases with `Sticky`; + retail's arrival hands off to `PositionManager::StickTo` → + `StickyManager::adjust_offset` (0x00555430, per-tick 0.3 m gap + facing + tracking). Our `MoveToManager.StickTo`/`Unstick` seams are unbound — + attackers freeze at stale arrival poses. +2. Arrival radii are zero: `getOwnRadius: () => 0f` (the R5-V3 pin in + `EnsureRemoteMotionBindings`) + `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). + +**Do, in order:** +1. Grep the named decomp for `StickyManager::` + `PositionManager::` bodies + (adjust_offset 0x00555430; per-tick anchors already pinned: + `PositionManager::adjust_offset` @0x00512d0e inside UpdatePositionInternal, + `PositionManager::UseTime` @0x005159b3 in UpdateObjectInternal). + Pseudocode, then port verbatim; cross-check + `references/ACE/Source/ACE.Server/Physics/Managers/{StickyManager,PositionManager}.cs`. + Groundwork already in tree: `MoveToMath.CylinderDistanceNoZ` / + `GlobalToLocalVec` / `NormalizeCheckSmall`; `ConstraintManager` (R5-V1) is + the port-style reference. Do NOT arm ConstraintManager (#167 separate). +2. Bind `StickTo`/`UnStick` in `EnsureRemoteMotionBindings` AND the player + wiring; wire PositionManager into the tick at the retail-matching points. +3. Thread real cylsphere radii (own from the entity's Setup; target resolved + at `RouteServerMoveTo`'s MoveToObject branch). Sweep remote + player. +4. Same-commit register bookkeeping: retire TS-39, update the radius pin. +5. Extend `RemoteChaseEndToEndHarnessTests` with a sticky scenario (arrive → + target strafes → follower tracks gap + facing; UnStick on next + PerformMovement). Harness bodies MUST mirror the live RemoteMotion + construction (InWorld=true, RemoteWeenie — see RemoteChaseDrainBisectTests). +6. `dotnet build` + full `dotnet test` green → commit → launch for the user's + visual gate: pack melee side-by-side vs retail, strafe around the pack; + attackers should reshuffle + keep facing like retail. Acceptance is retail + PARITY — some overlap is ACE-server-side and shows on retail too. + +**Gotchas:** revert-prone remote-motion area — verbatim port, no redesign; +per-entity probes must print the guid (memory +`feedback_probe_identity_attribution`); PowerShell Tee logs are UTF-16 +(`tr -d '\000'` before grep); the user manages client lifecycle (graceful +close); both clients hit the same local ACE (127.0.0.1:9000). diff --git a/docs/research/2026-07-04-171-sticky-melee-handoff.md b/docs/research/2026-07-04-171-sticky-melee-handoff.md new file mode 100644 index 00000000..3fede6af --- /dev/null +++ b/docs/research/2026-07-04-171-sticky-melee-handoff.md @@ -0,0 +1,126 @@ +# 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).