fix(render): Phase A8.F — camera sweep uses retail moverFlags 0x5c (PathClipped hard-stop)

Code review found the probe passed ObjectInfoState.None; retail's
SmartBox::update_viewer calls init_object(player, 0x5c) =
IsViewer|PathClipped|FreeRotate|PerfectClip (pseudo-C :92864). PathClipped makes
the sweep hard-stop at first contact (TransitionTypes.cs:811) instead of
edge-sliding around corners (which would re-trigger the A8.F camera-cell
instability); IsViewer lets the eye pass through creatures, colliding only with
world geometry. Resolves the spec's slide-vs-stop open question. Also reset
CollideCamera in the Defaults_AreRetailValues baseline test (review: maintenance
trap). Spec §5.1/§11.1 synced.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-29 19:11:53 +02:00
parent 376e2c3578
commit fcea05f808
3 changed files with 27 additions and 11 deletions

View file

@ -144,9 +144,13 @@ public Vector3 SweepEye(Vector3 pivot, Vector3 desiredEye, uint cellId, uint sel
stepDownHeight: 0f, // no step-down / ground snap
isOnGround: false, // no contact-plane / walkable semantics
body: null, // no cross-frame contact-plane persistence
moverFlags: ObjectInfoState.None, // all targets collide; also keeps
// camera sweeps out of the #98
// IsPlayer capture filter
// Retail init_object(player, 0x5c) = IsViewer|PathClipped|FreeRotate|
// PerfectClip (pseudo-C :92864). PathClipped = hard-stop at first contact
// (the spring arm, not edge-slide); IsViewer = eye passes through creatures,
// colliding only with world geometry. Not IsPlayer → stays out of the #98
// capture filter.
moverFlags: ObjectInfoState.IsViewer | ObjectInfoState.PathClipped
| ObjectInfoState.FreeRotate | ObjectInfoState.PerfectClip,
movingEntityId: selfEntityId); // skip the player's own ShadowEntry
return r.Position + zoff; // r.Position = sp.CheckPos (path pt); + zoff = eye
}
@ -264,11 +268,13 @@ Compare `ACDREAM_CAMERA_COLLIDE=0` vs default to confirm the flag isolates the f
## 11. Open implementation questions (decide during the plan, not now)
1. **Slide vs hard-stop.** Reusing `ResolveWithTransition` gives the player
path's edge-slide (the eye glides along a wall rather than jittering). Read
retail's `find_valid_position` during implementation and confirm whether it
slides or hard-stops; match it. Both keep the eye out of walls, so this does
not change the architecture.
1. **Slide vs hard-stop — RESOLVED (hard-stop).** Retail's `update_viewer` calls
`init_object(player, 0x5c)`, and `0x5c` includes `PathClipped`, which makes the
transition HARD-STOP at first contact (`TransitionTypes.cs:811`) rather than
edge-slide. The probe therefore passes
`IsViewer | PathClipped | FreeRotate | PerfectClip` (see §5.1). Hard-stop also
avoids the eye sliding around a corner into the next room, which would re-trigger
the A8.F camera-cell instability this fix targets.
2. **`sphereHeight: 0f` — RESOLVED.** `SpherePath.InitPath` with height 0 yields a
single sphere (`NumSphere = 1`, `TransitionTypes.cs:534-537`). It also offsets
sphere0's center to `pathPos + (0,0,radius)` (foot-capsule convention) whereas