fix(phys): #111 - ground the validated claim via PHYSICS walkable polygons, not the CellSurface triangle soup
Two grounding selection rules failed against live ACE restores before the right one: (1) first-hit SampleFloorZ returned 0x171's 99.475 ceiling TOP face over its 94.0 floor (issue111-verify2.log) - the player committed onto the roof level, and the session's heartbeats poisoned ACE's save with z=99.475; (2) nearest-to-reference self-confirmed that poison (the reference SAT on the ceiling face, issue111-verify3.log). Root insight: ceiling/roof top faces are upward-facing and XY-projectable - geometrically indistinguishable from floors in the render-ish CellSurface soup. The PHYSICS walkable set (plane normal.Z >= PhysicsGlobals.FloorZ over the claim's Resolved cell-local polygons - retail BSPTREE::find_walkable's filter) contains only real floors: PhysicsEngine.WalkableFloorZNearest transforms into the cell frame, drops on each walkable plane under the XY, picks nearest the reference. Verified live (issue111-verify4.log): ACE restored the roof-poisoned (0xA9B40171, z=99.475); the snap validated the claim and grounded to z=94.000 - the first fully clean indoor login of the arc: [snap] claim=0xA9B40171 VALIDATED -> grounded to its walkable floor z=94.000 [cell-transit] 0x00000000 -> 0xA9B40171 pos=(155.525,12.416,94.000) Baseline: Core 1381 + 4 pre-existing #99 failures + 1 skip; App/UI/Net green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
5f1eb7c4b1
commit
5706e0e10a
2 changed files with 69 additions and 7 deletions
|
|
@ -76,6 +76,11 @@ public sealed class CellSurface
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Project (worldX, worldY) onto this cell's floor polygons and
|
/// Project (worldX, worldY) onto this cell's floor polygons and
|
||||||
/// return the Z. Returns null if outside all floor polygons.
|
/// return the Z. Returns null if outside all floor polygons.
|
||||||
|
/// ⚠️ Returns the FIRST triangle hit in list order — a cell's triangle
|
||||||
|
/// soup includes ceiling/roof top faces, so the result can be a surface
|
||||||
|
/// far above the actual floor (#111: 0xA9B40171 returned its 99.475
|
||||||
|
/// ceiling over its 94.0 floor). Use <see cref="SampleFloorZNearest"/>
|
||||||
|
/// when a reference height is known.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float? SampleFloorZ(float worldX, float worldY)
|
public float? SampleFloorZ(float worldX, float worldY)
|
||||||
{
|
{
|
||||||
|
|
@ -87,6 +92,11 @@ public sealed class CellSurface
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// (#111 note: a SampleFloorZNearest variant was tried and removed — even
|
||||||
|
// nearest-to-reference lands on ceiling faces when the reference itself
|
||||||
|
// sits on one. Placement snaps must ground via the PHYSICS walkable
|
||||||
|
// polygons instead: PhysicsEngine.WalkableFloorZNearest.)
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Test if (px, py) falls inside triangle (a, b, c) projected onto
|
/// Test if (px, py) falls inside triangle (a, b, c) projected onto
|
||||||
/// the XY plane. If inside, computes the barycentric Z interpolation
|
/// the XY plane. If inside, computes the barycentric Z interpolation
|
||||||
|
|
|
||||||
|
|
@ -430,6 +430,55 @@ public sealed class PhysicsEngine
|
||||||
/// green).
|
/// green).
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <summary>
|
||||||
|
/// #111: the walkable floor Z of <paramref name="cellId"/>'s PHYSICS
|
||||||
|
/// polygons under the world XY, nearest to <paramref name="referenceZ"/>.
|
||||||
|
/// Walkable = plane normal.Z ≥ <see cref="PhysicsGlobals.FloorZ"/> (retail
|
||||||
|
/// BSPTREE::find_walkable's filter) — ceilings/roof tops never qualify,
|
||||||
|
/// unlike the <see cref="CellSurface"/> triangle soup. Resolved polygons
|
||||||
|
/// are CELL-LOCAL: transform in, drop on the plane, transform out.
|
||||||
|
/// Returns null when the claim has no hydrated struct or no walkable
|
||||||
|
/// under the XY.
|
||||||
|
/// </summary>
|
||||||
|
private float? WalkableFloorZNearest(uint cellId, Vector3 worldPos, float referenceZ)
|
||||||
|
{
|
||||||
|
var cp = DataCache?.GetCellStruct(cellId);
|
||||||
|
if (cp is null) return null;
|
||||||
|
|
||||||
|
var local = Vector3.Transform(
|
||||||
|
new Vector3(worldPos.X, worldPos.Y, referenceZ), cp.InverseWorldTransform);
|
||||||
|
|
||||||
|
float? best = null;
|
||||||
|
float bestDist = float.MaxValue;
|
||||||
|
foreach (var kv in cp.Resolved)
|
||||||
|
{
|
||||||
|
var poly = kv.Value;
|
||||||
|
var n = poly.Plane.Normal;
|
||||||
|
if (n.Z < PhysicsGlobals.FloorZ) continue;
|
||||||
|
if (!PointInPolygonXY(poly.Vertices, local.X, local.Y)) continue;
|
||||||
|
// plane: n·p + d = 0 => z = -(n.x*x + n.y*y + d)/n.z
|
||||||
|
float lz = -(n.X * local.X + n.Y * local.Y + poly.Plane.D) / n.Z;
|
||||||
|
float wz = Vector3.Transform(new Vector3(local.X, local.Y, lz), cp.WorldTransform).Z;
|
||||||
|
float dist = MathF.Abs(wz - referenceZ);
|
||||||
|
if (dist < bestDist) { bestDist = dist; best = wz; }
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Even-odd XY-projection point-in-polygon test (cell-local frame).</summary>
|
||||||
|
private static bool PointInPolygonXY(IReadOnlyList<Vector3> verts, float x, float y)
|
||||||
|
{
|
||||||
|
bool inside = false;
|
||||||
|
for (int i = 0, j = verts.Count - 1; i < verts.Count; j = i++)
|
||||||
|
{
|
||||||
|
var vi = verts[i]; var vj = verts[j];
|
||||||
|
if ((vi.Y > y) != (vj.Y > y)
|
||||||
|
&& x < (vj.X - vi.X) * (y - vi.Y) / (vj.Y - vi.Y) + vi.X)
|
||||||
|
inside = !inside;
|
||||||
|
}
|
||||||
|
return inside;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// #107: does any loaded landblock carry a <see cref="CellSurface"/> for
|
/// #107: does any loaded landblock carry a <see cref="CellSurface"/> for
|
||||||
/// this cell id? Distinguishes "partially hydrated" (floor data present,
|
/// this cell id? Distinguishes "partially hydrated" (floor data present,
|
||||||
|
|
@ -636,16 +685,19 @@ public sealed class PhysicsEngine
|
||||||
// surface here (thresholds, stair lips) fall through to the legacy path.
|
// surface here (thresholds, stair lips) fall through to the legacy path.
|
||||||
if (snapDiag && adjustedFound && (cellId & 0xFFFFu) >= 0x0100u)
|
if (snapDiag && adjustedFound && (cellId & 0xFFFFu) >= 0x0100u)
|
||||||
{
|
{
|
||||||
CellSurface? claimSurface = null;
|
// Ground via the claim's PHYSICS WALKABLE polygons (normal.Z ≥
|
||||||
foreach (var c in physics.Cells)
|
// PhysicsGlobals.FloorZ), NOT the CellSurface triangle soup — the
|
||||||
{
|
// soup includes ceiling/roof TOP faces whose first-hit (99.475
|
||||||
if ((c.CellId & 0xFFFFu) == (cellId & 0xFFFFu)) { claimSurface = c; break; }
|
// over 0x171's 94.0 floor, issue111-verify2.log) and even
|
||||||
}
|
// nearest-to-reference (the poisoned reference SAT on the ceiling
|
||||||
float? claimFloorZ = claimSurface?.SampleFloorZ(candidatePos.X, candidatePos.Y);
|
// face, issue111-verify3.log) selections both land on non-floors.
|
||||||
|
// The walkable set contains only real floors (retail
|
||||||
|
// BSPTREE::find_walkable's polygon filter).
|
||||||
|
float? claimFloorZ = WalkableFloorZNearest(cellId, candidatePos, currentPos.Z);
|
||||||
if (claimFloorZ is not null)
|
if (claimFloorZ is not null)
|
||||||
{
|
{
|
||||||
Console.WriteLine(System.FormattableString.Invariant(
|
Console.WriteLine(System.FormattableString.Invariant(
|
||||||
$"[snap] claim=0x{cellId:X8} pos=({currentPos.X:F3},{currentPos.Y:F3},{currentPos.Z:F3}) VALIDATED -> grounded to its floor z={claimFloorZ.Value:F3}"));
|
$"[snap] claim=0x{cellId:X8} pos=({currentPos.X:F3},{currentPos.Y:F3},{currentPos.Z:F3}) VALIDATED -> grounded to its walkable floor z={claimFloorZ.Value:F3}"));
|
||||||
return new ResolveResult(
|
return new ResolveResult(
|
||||||
new Vector3(candidatePos.X, candidatePos.Y, claimFloorZ.Value),
|
new Vector3(candidatePos.X, candidatePos.Y, claimFloorZ.Value),
|
||||||
lbPrefix | (cellId & 0xFFFFu),
|
lbPrefix | (cellId & 0xFFFFu),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue