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 stepDownHeight: 0f, // no step-down / ground snap
isOnGround: false, // no contact-plane / walkable semantics isOnGround: false, // no contact-plane / walkable semantics
body: null, // no cross-frame contact-plane persistence body: null, // no cross-frame contact-plane persistence
moverFlags: ObjectInfoState.None, // all targets collide; also keeps // Retail init_object(player, 0x5c) = IsViewer|PathClipped|FreeRotate|
// camera sweeps out of the #98 // PerfectClip (pseudo-C :92864). PathClipped = hard-stop at first contact
// IsPlayer capture filter // (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 movingEntityId: selfEntityId); // skip the player's own ShadowEntry
return r.Position + zoff; // r.Position = sp.CheckPos (path pt); + zoff = eye 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) ## 11. Open implementation questions (decide during the plan, not now)
1. **Slide vs hard-stop.** Reusing `ResolveWithTransition` gives the player 1. **Slide vs hard-stop — RESOLVED (hard-stop).** Retail's `update_viewer` calls
path's edge-slide (the eye glides along a wall rather than jittering). Read `init_object(player, 0x5c)`, and `0x5c` includes `PathClipped`, which makes the
retail's `find_valid_position` during implementation and confirm whether it transition HARD-STOP at first contact (`TransitionTypes.cs:811`) rather than
slides or hard-stops; match it. Both keep the eye out of walls, so this does edge-slide. The probe therefore passes
not change the architecture. `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 2. **`sphereHeight: 0f` — RESOLVED.** `SpherePath.InitPath` with height 0 yields a
single sphere (`NumSphere = 1`, `TransitionTypes.cs:534-537`). It also offsets single sphere (`NumSphere = 1`, `TransitionTypes.cs:534-537`). It also offsets
sphere0's center to `pathPos + (0,0,radius)` (foot-capsule convention) whereas sphere0's center to `pathPos + (0,0,radius)` (foot-capsule convention) whereas

View file

@ -43,9 +43,17 @@ public sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe
stepDownHeight: 0f, // no step-down / ground snap stepDownHeight: 0f, // no step-down / ground snap
isOnGround: false, // no contact-plane / walkable semantics isOnGround: false, // no contact-plane / walkable semantics
body: null, // no cross-frame persistence body: null, // no cross-frame persistence
moverFlags: ObjectInfoState.None, // all targets collide; also keeps // Retail SmartBox::update_viewer calls init_object(player, 0x5c) =
// camera sweeps out of the #98 // IsViewer | PathClipped | FreeRotate | PerfectClip (acclient
// IsPlayer capture filter // pseudo-C :92864; enum TransitionTypes.cs:24-33). PathClipped makes
// the sweep HARD-STOP at first contact (TransitionTypes.cs:811) — the
// spring-arm pull-in, not the player's edge-slide. IsViewer lets the
// eye pass through creatures, colliding only with world geometry
// (CollisionExemption.cs:83-85). FreeRotate/PerfectClip are no-ops in
// acdream today but set to match retail's exact value. NOT IsPlayer
// (0x100), so camera sweeps stay out of the #98 capture filter.
moverFlags: ObjectInfoState.IsViewer | ObjectInfoState.PathClipped
| ObjectInfoState.FreeRotate | ObjectInfoState.PerfectClip,
movingEntityId: selfEntityId); // skip the player's own ShadowEntry movingEntityId: selfEntityId); // skip the player's own ShadowEntry
return FromSpherePath(r.Position, ViewerSphereRadius); return FromSpherePath(r.Position, ViewerSphereRadius);

View file

@ -21,6 +21,7 @@ public class CameraDiagnosticsTests
CameraDiagnostics.CameraAdjustmentSpeed = 40.0f; CameraDiagnostics.CameraAdjustmentSpeed = 40.0f;
CameraDiagnostics.AlignToSlope = true; CameraDiagnostics.AlignToSlope = true;
CameraDiagnostics.UseRetailChaseCamera = true; CameraDiagnostics.UseRetailChaseCamera = true;
CameraDiagnostics.CollideCamera = true;
Assert.Equal(0.45f, CameraDiagnostics.TranslationStiffness); Assert.Equal(0.45f, CameraDiagnostics.TranslationStiffness);
Assert.Equal(0.45f, CameraDiagnostics.RotationStiffness); Assert.Equal(0.45f, CameraDiagnostics.RotationStiffness);
@ -31,6 +32,7 @@ public class CameraDiagnosticsTests
// legacy camera remains opt-in via the DebugPanel toggle until // legacy camera remains opt-in via the DebugPanel toggle until
// the follow-up deletion commit. // the follow-up deletion commit.
Assert.True(CameraDiagnostics.UseRetailChaseCamera); Assert.True(CameraDiagnostics.UseRetailChaseCamera);
Assert.True(CameraDiagnostics.CollideCamera);
} }
[Fact] [Fact]