Make prepared flat BSP data authoritative for gameplay while retaining the parsed graph only as an exact sampled referee. Fail production publication when collision package data is genuinely absent, keep idempotent already-cached publication valid, and move cell membership, floor lookup, camera diagnostics, and live/static shape bounds onto the flat representation. Validated by 8,402 Release tests, a strict dense-Arwic connected gate with 46,309/46,309 exact referee matches, and graceful shutdown. Co-authored-by: Codex <codex@openai.com>
114 lines
6.6 KiB
C#
114 lines
6.6 KiB
C#
using System.Numerics;
|
|
using AcDream.Core.Physics;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
/// <summary>
|
|
/// <see cref="ICameraCollisionProbe"/> backed by the player's swept-sphere
|
|
/// engine. Ports retail's <c>SmartBox::update_viewer</c> (0x00453ce0): sweep
|
|
/// the 0.3 m <c>viewer_sphere</c> from the head-pivot to the desired eye via a
|
|
/// <c>CTransition</c> and use the stopped position. Reusing
|
|
/// <see cref="PhysicsEngine.ResolveWithTransition"/> collides against indoor
|
|
/// cell walls (<c>FindEnvCollisions</c>) AND outdoor/baked GfxObj shells
|
|
/// (<c>FindObjCollisions</c>) in one faithful path.
|
|
/// </summary>
|
|
public sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe
|
|
{
|
|
/// <summary>Retail <c>viewer_sphere</c> radius (acclient :93314).</summary>
|
|
public const float ViewerSphereRadius = 0.3f;
|
|
|
|
private readonly PhysicsEngine _physics;
|
|
|
|
public PhysicsCameraCollisionProbe(PhysicsEngine physics) => _physics = physics;
|
|
|
|
public CameraSweepResult SweepEye(Vector3 pivot, Vector3 desiredEye, uint cellId, uint selfEntityId, Vector3 playerPos)
|
|
{
|
|
// update_viewer: player->cell == 0 → set_viewer(player_pos, 1), viewer_cell = null
|
|
// (acclient_2013_pseudo_c.txt:92775). No cell to sweep against → snap to the player.
|
|
if (cellId == 0) return new CameraSweepResult(playerPos, 0u);
|
|
|
|
// === Start cell (pc:92824-92844) ===
|
|
// Indoor (objcell_id >= 0x100): seat the sweep's start cell at the head-PIVOT via
|
|
// CPhysicsObj::AdjustPosition (pc:92832) — the head can sit in a different cell than
|
|
// the feet (the cellar lip: feet in the low connector, head up at floor level). On
|
|
// failure retail falls back to player->cell. Outdoor: cell = player->cell (no AdjustPosition).
|
|
uint startCell = cellId;
|
|
if ((cellId & 0xFFFFu) >= 0x0100u)
|
|
{
|
|
var (pivotCell, found) = _physics.AdjustPosition(cellId, pivot);
|
|
if (found) startCell = pivotCell;
|
|
}
|
|
|
|
// === Sweep the viewer_sphere pivot → sought-eye from the start cell (pc:92860-92868) ===
|
|
// SpherePath.InitPath puts sphere0's center at pathPos + (0,0,radius) (the player
|
|
// foot-capsule convention). Retail's viewer_sphere center is (0,0,0), so shift the
|
|
// path DOWN by the radius to make the SPHERE CENTER travel pivot→eye, then add it back.
|
|
Vector3 begin = ToSpherePath(pivot, ViewerSphereRadius);
|
|
Vector3 end = ToSpherePath(desiredEye, ViewerSphereRadius);
|
|
|
|
var r = _physics.ResolveWithTransition(
|
|
currentPos: begin,
|
|
targetPos: end,
|
|
cellId: startCell,
|
|
sphereRadius: ViewerSphereRadius,
|
|
sphereHeight: 0f, // single sphere (no head sphere)
|
|
stepUpHeight: 0f, // no step-up for a camera
|
|
stepDownHeight: 0f, // no step-down / ground snap
|
|
isOnGround: false, // no contact-plane / walkable semantics
|
|
body: null, // no cross-frame persistence
|
|
// Retail SmartBox::update_viewer calls init_object(player, 0x5c) =
|
|
// IsViewer | PathClipped | FreeRotate | PerfectClip (acclient
|
|
// 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
|
|
|
|
Vector3 eye = FromSpherePath(r.Position, ViewerSphereRadius);
|
|
|
|
// [flap-sweep] camera-collision probe (ACDREAM_PROBE_FLAP), paired with the
|
|
// builder's [flap]/[flap-cam]. start = the pivot-seated start cell (vs cell = the
|
|
// player feet cell); ok = the sweep found a valid position (find_valid_position != 0).
|
|
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled)
|
|
{
|
|
var cp = _physics.DataCache?.GetCellStruct(startCell);
|
|
string bsp = cp?.FlatPhysicsBsp is null
|
|
? "noflatbsp"
|
|
: (cp.FlatPhysicsBsp.RootIndex < 0 ? "noroot" : "ok");
|
|
float desiredBack = Vector3.Distance(pivot, desiredEye);
|
|
float eyeBack = Vector3.Distance(pivot, eye);
|
|
System.Console.WriteLine(
|
|
$"[flap-sweep] cell=0x{cellId:X8} start=0x{startCell:X8} ok={r.Ok} resolved={(cp is not null ? "Y" : "n")} bsp={bsp} " +
|
|
$"desiredBack={desiredBack:F2} eyeBack={eyeBack:F2} pulledIn={desiredBack - eyeBack:F2} " +
|
|
$"in=({desiredEye.X:F6},{desiredEye.Y:F6},{desiredEye.Z:F6}) out=({eye.X:F6},{eye.Y:F6},{eye.Z:F6}) " +
|
|
$"viewerCell=0x{r.CellId:X8} collNormValid={r.CollisionNormalValid}");
|
|
}
|
|
|
|
// success: set_viewer(curr_pos, 0); viewer_cell = sphere_path.curr_cell (pc:92870-92871).
|
|
// Graph-tracked, no AABB/grace.
|
|
if (r.Ok) return new CameraSweepResult(eye, r.CellId);
|
|
|
|
// === Fallback 1 (pc:92878-92883): AdjustPosition at the sought eye ===
|
|
// The sweep found no valid position; try to seat the eye at its own cell.
|
|
// (Seed with the player cell — acdream's camera doesn't track the sought-eye's
|
|
// cell separately; the eye is near the player so its stab-list is the right one.)
|
|
var (eyeCell, eyeFound) = _physics.AdjustPosition(cellId, desiredEye);
|
|
if (eyeFound) return new CameraSweepResult(desiredEye, eyeCell);
|
|
|
|
// === Fallback 2 (pc:92886-92887): set_viewer(player_pos), viewer_cell = null ===
|
|
return new CameraSweepResult(playerPos, 0u);
|
|
}
|
|
|
|
/// <summary>Eye/pivot point → InitPath path point (subtract the sphere-center offset).</summary>
|
|
internal static Vector3 ToSpherePath(Vector3 spherePoint, float radius)
|
|
=> spherePoint - new Vector3(0f, 0f, radius);
|
|
|
|
/// <summary>InitPath path point → eye point (add the sphere-center offset back).</summary>
|
|
internal static Vector3 FromSpherePath(Vector3 pathPoint, float radius)
|
|
=> pathPoint + new Vector3(0f, 0f, radius);
|
|
}
|