using System.Numerics; using DatReaderWriter.Enums; namespace AcDream.Core.Physics; /// /// Integer-indexed shadow port of the retail BSP traversals in /// . The graph implementation remains authoritative /// until the Slice I differential and connected gates pass with zero /// mismatches. /// /// /// Named-retail oracle: BSPTREE::find_collisions 0x0053A440, /// BSPNODE::point_inside_cell_bsp 0x0053C1F0, /// BSPNODE::sphere_intersects_cell_bsp 0x0053C260, /// BSPNODE::sphere_intersects_poly 0x0053CA30, /// BSPNODE::sphere_intersects_solid 0x0053CAF0, /// BSPNODE::find_walkable 0x0053CC80, /// BSPNODE::sphere_intersects_solid_poly 0x0053CD50, and /// BSPLEAF::find_walkable 0x0053D6F0. Polygon math is shared /// deliberately with the graph oracle; only node and polygon storage changes. /// /// internal static class FlatBspQuery { private struct CollisionSphere { public Vector3 Center; public float Radius; public CollisionSphere(Vector3 center, float radius) { Center = center; Radius = radius; } } private static ReadOnlySpan Vertices( FlatPhysicsBsp tree, in FlatCollisionPolygon polygon) => tree.PolygonTable.Vertices.AsSpan( polygon.VertexRange.Start, polygon.VertexRange.Count); private static bool NodeIntersects( in FlatPhysicsBspNode node, CollisionSphere sphere) { Vector3 d = sphere.Center - node.BoundingSphere.Origin; float radius = sphere.Radius + node.BoundingSphere.Radius; return d.LengthSquared() < radius * radius; } private static bool PosHitsSphere( FlatPhysicsBsp tree, int polygonIndex, CollisionSphere sphere, Vector3 movement, ref Vector3 contactPoint, ref int hitPolygonIndex) { FlatCollisionPolygon polygon = tree.PolygonTable.Polygons[polygonIndex]; bool hit = BSPQuery.PolygonHitsSpherePrecise( polygon.Plane, Vertices(tree, polygon), sphere.Center, sphere.Radius, ref contactPoint); // Retail writes the polygon before the movement-direction cull. if (hit) hitPolygonIndex = polygonIndex; float moveDot = Vector3.Dot(movement, polygon.Plane.Normal); if (moveDot >= 0f) return false; return hit; } private static bool HitsSphere( FlatPhysicsBsp tree, int polygonIndex, CollisionSphere sphere) { FlatCollisionPolygon polygon = tree.PolygonTable.Polygons[polygonIndex]; Vector3 contactPoint = Vector3.Zero; return BSPQuery.PolygonHitsSpherePrecise( polygon.Plane, Vertices(tree, polygon), sphere.Center, sphere.Radius, ref contactPoint); } private static bool WalkableHitsSphere( FlatPhysicsBsp tree, int polygonIndex, SpherePath path, CollisionSphere sphere, Vector3 up) { FlatCollisionPolygon polygon = tree.PolygonTable.Polygons[polygonIndex]; float dp = Vector3.Dot(up, polygon.Plane.Normal); if (dp <= path.WalkableAllowance) return false; Vector3 contactPoint = Vector3.Zero; return BSPQuery.PolygonHitsSpherePrecise( polygon.Plane, Vertices(tree, polygon), sphere.Center, sphere.Radius, ref contactPoint); } private static bool CheckWalkable( FlatPhysicsBsp tree, int polygonIndex, CollisionSphere sphere, Vector3 up, bool small) { FlatCollisionPolygon polygon = tree.PolygonTable.Polygons[polygonIndex]; ReadOnlySpan vertices = Vertices(tree, polygon); float angleUp = Vector3.Dot(polygon.Plane.Normal, up); if (angleUp < PhysicsGlobals.EPSILON) return false; float angle = (Vector3.Dot(polygon.Plane.Normal, sphere.Center) + polygon.Plane.D) / angleUp; Vector3 center = sphere.Center - up * angle; float radiusSquared = sphere.Radius * sphere.Radius; if (small) radiusSquared *= 0.25f; int previousIndex = vertices.Length - 1; for (int i = 0; i < vertices.Length; i++) { Vector3 vertex = vertices[i]; Vector3 previousVertex = vertices[previousIndex]; previousIndex = i; Vector3 edge = vertex - previousVertex; Vector3 displacement = center - previousVertex; Vector3 cross = Vector3.Cross(polygon.Plane.Normal, edge); float difference = Vector3.Dot(displacement, cross); if (difference < 0f) { if (cross.LengthSquared() * radiusSquared < difference * difference) return false; float displacementAlongEdge = Vector3.Dot(displacement, edge); if (displacementAlongEdge >= 0f && displacementAlongEdge <= edge.LengthSquared()) { return true; } return false; } if (displacement.LengthSquared() <= radiusSquared) return true; } return true; } private static bool AdjustSphereToPlane( FlatPhysicsBsp tree, int polygonIndex, SpherePath path, ref CollisionSphere validPosition, Vector3 movement) { FlatCollisionPolygon polygon = tree.PolygonTable.Polygons[polygonIndex]; Vector3 inputCenter = validPosition.Center; float walkInterpolationBefore = path.WalkInterp; float positionDot = Vector3.Dot(validPosition.Center, polygon.Plane.Normal) + polygon.Plane.D; float movementDot = Vector3.Dot(movement, polygon.Plane.Normal); float distance; if (movementDot <= PhysicsGlobals.EPSILON) { if (movementDot >= -PhysicsGlobals.EPSILON) { LogPushBackAdjust( tree, polygonIndex, inputCenter, validPosition.Center, validPosition.Radius, walkInterpolationBefore, path.WalkInterp, positionDot, movementDot, 0f, applied: false); return false; } distance = positionDot - validPosition.Radius; } else { distance = -validPosition.Radius - positionDot; } float inverseDistance = distance / movementDot; float interpolation = (1f - inverseDistance) * path.WalkInterp; if (interpolation >= path.WalkInterp || interpolation < -0.5f) { LogPushBackAdjust( tree, polygonIndex, inputCenter, validPosition.Center, validPosition.Radius, walkInterpolationBefore, path.WalkInterp, positionDot, movementDot, inverseDistance, applied: false); return false; } validPosition.Center -= movement * inverseDistance; path.WalkInterp = interpolation; LogPushBackAdjust( tree, polygonIndex, inputCenter, validPosition.Center, validPosition.Radius, walkInterpolationBefore, path.WalkInterp, positionDot, movementDot, inverseDistance, applied: true); if (PhysicsDiagnostics.ProbePolyDumpEnabled) { PhysicsDiagnostics.LogPolyDump( path.CheckCellId, MaterializeResolvedPolygon(tree, polygonIndex)); } return true; } private static void LogPushBackAdjust( FlatPhysicsBsp tree, int polygonIndex, Vector3 inputCenter, Vector3 outputCenter, float radius, float walkInterpolationBefore, float walkInterpolationAfter, float positionDot, float movementDot, float inverseDistance, bool applied) { if (!PhysicsDiagnostics.ProbePushBackEnabled) return; PhysicsDiagnostics.LogPushBackAdjust( inputCenter, outputCenter, tree.PolygonTable.Polygons[polygonIndex].Plane, radius, walkInterpolationBefore, walkInterpolationAfter, positionDot, movementDot, inverseDistance, applied); } private static bool FindCrossedEdge( FlatPhysicsBsp tree, int polygonIndex, CollisionSphere sphere, Vector3 up, ref Vector3 normal) { FlatCollisionPolygon polygon = tree.PolygonTable.Polygons[polygonIndex]; if (!BSPQuery.FindCrossedEdge( polygon.Plane, Vertices(tree, polygon), sphere.Center, up, out Vector3 crossedNormal)) { return false; } normal = crossedNormal; return true; } private static Vector3 TransformNormal(Vector3 normal, Quaternion localToWorld) { Vector3 worldNormal = Vector3.Transform(normal, localToWorld); return worldNormal.LengthSquared() > PhysicsGlobals.EpsilonSq ? Vector3.Normalize(worldNormal) : Vector3.UnitZ; } private static Plane BuildWorldPlane( Vector3 worldNormal, ReadOnlySpan localVertices, Quaternion localToWorld, float scale, Vector3 worldOrigin) { float d = localVertices.Length > 0 ? -Vector3.Dot( worldNormal, Vector3.Transform(localVertices[0] * scale, localToWorld) + worldOrigin) : 0f; return new Plane(worldNormal, d); } private static void AdjustToPlacementPolygon( FlatPhysicsBsp tree, int polygonIndex, ref CollisionSphere validPosition, ref CollisionSphere validPosition2, bool hasValidPosition2, float radius, bool centerSolid, bool clearCell) { FlatCollisionPolygon polygon = tree.PolygonTable.Polygons[polygonIndex]; Vector3 moveDirection = Vector3.Zero; if (centerSolid) { moveDirection = polygon.Plane.Normal; } else { Vector3 up = Vector3.UnitZ; if (!FindCrossedEdge( tree, polygonIndex, validPosition, up, ref moveDirection)) { moveDirection = polygon.Plane.Normal; } } float distance = Vector3.Dot(validPosition.Center, polygon.Plane.Normal) + polygon.Plane.D; float pushAmount = radius - distance; if (pushAmount <= 0f) pushAmount = PhysicsGlobals.EPSILON; Vector3 offset = moveDirection * pushAmount; validPosition.Center += offset; if (hasValidPosition2) validPosition2.Center += offset; } private static float AdjustSphereToPolygon( FlatPhysicsBsp tree, int polygonIndex, CollisionSphere checkPosition, Vector3 currentPosition, Vector3 movement) { FlatCollisionPolygon polygon = tree.PolygonTable.Polygons[polygonIndex]; float positionDot = Vector3.Dot(currentPosition, polygon.Plane.Normal) + polygon.Plane.D; if (MathF.Abs(positionDot) < checkPosition.Radius) return 1f; float movementDot = Vector3.Dot(movement, polygon.Plane.Normal); if (MathF.Abs(movementDot) <= PhysicsGlobals.EPSILON) return 0f; float radius = positionDot < 0f ? -checkPosition.Radius : checkPosition.Radius; return (radius - positionDot) / movementDot; } /// /// Flat port of retail BSPNODE::point_inside_cell_bsp /// (0x0053C1F0). /// public static bool PointInsideCellBsp( FlatCellContainmentBsp tree, Vector3 point) { ArgumentNullException.ThrowIfNull(tree); return PointInsideCellBsp(tree, tree.RootIndex, point); } private static bool PointInsideCellBsp( FlatCellContainmentBsp tree, int nodeIndex, Vector3 point) { if (nodeIndex < 0) return true; FlatCellBspNode node = tree.Nodes[nodeIndex]; if (node.Type == BSPNodeType.Leaf) return true; float distance = Vector3.Dot(node.SplittingPlane.Normal, point) + node.SplittingPlane.D; if (distance >= 0f) { return node.PositiveChildIndex >= 0 ? PointInsideCellBsp(tree, node.PositiveChildIndex, point) : true; } return false; } /// /// Flat port of retail BSPNODE::sphere_intersects_cell_bsp /// (0x0053C260). /// public static bool SphereIntersectsCellBsp( FlatCellContainmentBsp tree, Vector3 center, float radius) { ArgumentNullException.ThrowIfNull(tree); return SphereIntersectsCellBsp(tree, tree.RootIndex, center, radius); } private static bool SphereIntersectsCellBsp( FlatCellContainmentBsp tree, int nodeIndex, Vector3 center, float radius) { if (nodeIndex < 0) return true; FlatCellBspNode node = tree.Nodes[nodeIndex]; if (node.Type == BSPNodeType.Leaf) return true; float distance = Vector3.Dot(node.SplittingPlane.Normal, center) + node.SplittingPlane.D; float expandedRadius = radius + 0.01f; if (distance < -expandedRadius) return false; return node.PositiveChildIndex >= 0 ? SphereIntersectsCellBsp( tree, node.PositiveChildIndex, center, radius) : true; } /// /// Flat static sphere/polygon overlap shadow query. /// public static bool SphereIntersectsPoly( FlatPhysicsBsp tree, Vector3 sphereCenter, float sphereRadius, out ushort hitPolygonId, out Vector3 hitNormal) { ArgumentNullException.ThrowIfNull(tree); hitPolygonId = 0; hitNormal = Vector3.Zero; if (tree.RootIndex < 0) return false; return SphereIntersectsPolyStaticRecurse( tree, tree.RootIndex, sphereCenter, sphereRadius, ref hitPolygonId, ref hitNormal); } private static bool SphereIntersectsPolyStaticRecurse( FlatPhysicsBsp tree, int nodeIndex, Vector3 center, float radius, ref ushort hitPolygonId, ref Vector3 hitNormal) { if (nodeIndex < 0) return false; FlatPhysicsBspNode node = tree.Nodes[nodeIndex]; Vector3 difference = center - node.BoundingSphere.Origin; float combinedRadius = radius + node.BoundingSphere.Radius; if (difference.LengthSquared() >= combinedRadius * combinedRadius) return false; if (node.Type == BSPNodeType.Leaf) { int end = node.PolygonIndexRange.EndExclusive; for (int i = node.PolygonIndexRange.Start; i < end; i++) { int polygonIndex = tree.PolygonIndexStream[i]; FlatCollisionPolygon polygon = tree.PolygonTable.Polygons[polygonIndex]; Vector3 contactPoint = Vector3.Zero; if (BSPQuery.PolygonHitsSpherePrecise( polygon.Plane, Vertices(tree, polygon), center, radius, ref contactPoint)) { hitPolygonId = polygon.Id; hitNormal = polygon.Plane.Normal; return true; } } return false; } float splitDistance = Vector3.Dot(node.SplittingPlane.Normal, center) + node.SplittingPlane.D; float reach = radius - PhysicsGlobals.EPSILON; if (splitDistance >= reach) { return SphereIntersectsPolyStaticRecurse( tree, node.PositiveChildIndex, center, radius, ref hitPolygonId, ref hitNormal); } if (splitDistance <= -reach) { return SphereIntersectsPolyStaticRecurse( tree, node.NegativeChildIndex, center, radius, ref hitPolygonId, ref hitNormal); } if (SphereIntersectsPolyStaticRecurse( tree, node.PositiveChildIndex, center, radius, ref hitPolygonId, ref hitNormal)) { return true; } return SphereIntersectsPolyStaticRecurse( tree, node.NegativeChildIndex, center, radius, ref hitPolygonId, ref hitNormal); } /// /// Flat swept-sphere BSP shadow query. /// public static bool SphereIntersectsPolyWithTime( FlatPhysicsBsp tree, Vector3 sphereCenter, float sphereRadius, Vector3 movement, out ushort hitPolygonId, out Vector3 hitNormal, out float hitTime) { ArgumentNullException.ThrowIfNull(tree); hitPolygonId = 0; hitNormal = Vector3.Zero; hitTime = float.MaxValue; if (tree.RootIndex < 0) return false; SphereIntersectsPolyWithTimeRecurse( tree, tree.RootIndex, sphereCenter, sphereRadius, movement, ref hitPolygonId, ref hitNormal, ref hitTime); return hitTime < float.MaxValue; } private static void SphereIntersectsPolyWithTimeRecurse( FlatPhysicsBsp tree, int nodeIndex, Vector3 center, float radius, Vector3 movement, ref ushort hitPolygonId, ref Vector3 hitNormal, ref float bestTime) { if (nodeIndex < 0) return; FlatPhysicsBspNode node = tree.Nodes[nodeIndex]; Vector3 difference = center - node.BoundingSphere.Origin; float combinedRadius = radius + node.BoundingSphere.Radius + movement.Length() + 0.1f; if (difference.LengthSquared() >= combinedRadius * combinedRadius) return; if (node.Type == BSPNodeType.Leaf) { int end = node.PolygonIndexRange.EndExclusive; for (int i = node.PolygonIndexRange.Start; i < end; i++) { int polygonIndex = tree.PolygonIndexStream[i]; FlatCollisionPolygon polygon = tree.PolygonTable.Polygons[polygonIndex]; if (Vector3.Dot(movement, polygon.Plane.Normal) >= 0f) continue; Vector3 contactPoint = Vector3.Zero; if (BSPQuery.PolygonHitsSpherePrecise( polygon.Plane, Vertices(tree, polygon), center, radius, ref contactPoint)) { if (0f < bestTime) { bestTime = 0f; hitPolygonId = polygon.Id; hitNormal = polygon.Plane.Normal; } continue; } Vector3 endCenter = center + movement; if (BSPQuery.PolygonHitsSpherePrecise( polygon.Plane, Vertices(tree, polygon), endCenter, radius, ref contactPoint) && 1f < bestTime) { bestTime = 1f; hitPolygonId = polygon.Id; hitNormal = polygon.Plane.Normal; } } return; } float splitDistance = Vector3.Dot(node.SplittingPlane.Normal, center) + node.SplittingPlane.D; float reach = radius + movement.Length(); if (splitDistance >= reach) { SphereIntersectsPolyWithTimeRecurse( tree, node.PositiveChildIndex, center, radius, movement, ref hitPolygonId, ref hitNormal, ref bestTime); return; } if (splitDistance <= -reach) { SphereIntersectsPolyWithTimeRecurse( tree, node.NegativeChildIndex, center, radius, movement, ref hitPolygonId, ref hitNormal, ref bestTime); return; } SphereIntersectsPolyWithTimeRecurse( tree, node.PositiveChildIndex, center, radius, movement, ref hitPolygonId, ref hitNormal, ref bestTime); SphereIntersectsPolyWithTimeRecurse( tree, node.NegativeChildIndex, center, radius, movement, ref hitPolygonId, ref hitNormal, ref bestTime); } private static bool SphereIntersectsPolyInternal( FlatPhysicsBsp tree, int nodeIndex, CollisionSphere sphere, Vector3 movement, ref int hitPolygonIndex, ref Vector3 contactPoint) { if (nodeIndex < 0) return false; FlatPhysicsBspNode node = tree.Nodes[nodeIndex]; if (!NodeIntersects(node, sphere)) return false; if (node.Type == BSPNodeType.Leaf) { if (node.PolygonIndexRange.Count == 0) return false; int end = node.PolygonIndexRange.EndExclusive; for (int i = node.PolygonIndexRange.Start; i < end; i++) { int polygonIndex = tree.PolygonIndexStream[i]; if (PosHitsSphere( tree, polygonIndex, sphere, movement, ref contactPoint, ref hitPolygonIndex)) { return true; } } return false; } float distance = Vector3.Dot(node.SplittingPlane.Normal, sphere.Center) + node.SplittingPlane.D; float reach = sphere.Radius - PhysicsGlobals.EPSILON; if (distance >= reach) { return SphereIntersectsPolyInternal( tree, node.PositiveChildIndex, sphere, movement, ref hitPolygonIndex, ref contactPoint); } if (distance <= -reach) { return SphereIntersectsPolyInternal( tree, node.NegativeChildIndex, sphere, movement, ref hitPolygonIndex, ref contactPoint); } if (node.PositiveChildIndex >= 0 && SphereIntersectsPolyInternal( tree, node.PositiveChildIndex, sphere, movement, ref hitPolygonIndex, ref contactPoint)) { return true; } if (node.NegativeChildIndex >= 0 && SphereIntersectsPolyInternal( tree, node.NegativeChildIndex, sphere, movement, ref hitPolygonIndex, ref contactPoint)) { return true; } return false; } private static void FindWalkableInternal( FlatPhysicsBsp tree, int nodeIndex, SpherePath path, ref CollisionSphere validPosition, Vector3 movement, Vector3 up, ref int hitPolygonIndex, ref ushort hitPolygonId, ref bool changed) { if (nodeIndex < 0) return; FlatPhysicsBspNode node = tree.Nodes[nodeIndex]; if (!NodeIntersects(node, validPosition)) return; if (node.Type == BSPNodeType.Leaf) { if (node.PolygonIndexRange.Count == 0) return; int end = node.PolygonIndexRange.EndExclusive; for (int i = node.PolygonIndexRange.Start; i < end; i++) { int polygonIndex = tree.PolygonIndexStream[i]; bool walkable = WalkableHitsSphere( tree, polygonIndex, path, validPosition, up); bool adjusted = walkable && AdjustSphereToPlane( tree, polygonIndex, path, ref validPosition, movement); if (walkable && adjusted) { changed = true; hitPolygonIndex = polygonIndex; hitPolygonId = tree.PolygonTable.Polygons[polygonIndex].Id; } } return; } float distance = Vector3.Dot(node.SplittingPlane.Normal, validPosition.Center) + node.SplittingPlane.D; float reach = validPosition.Radius - PhysicsGlobals.EPSILON; if (distance >= reach) { FindWalkableInternal( tree, node.PositiveChildIndex, path, ref validPosition, movement, up, ref hitPolygonIndex, ref hitPolygonId, ref changed); return; } if (distance <= -reach) { FindWalkableInternal( tree, node.NegativeChildIndex, path, ref validPosition, movement, up, ref hitPolygonIndex, ref hitPolygonId, ref changed); return; } FindWalkableInternal( tree, node.PositiveChildIndex, path, ref validPosition, movement, up, ref hitPolygonIndex, ref hitPolygonId, ref changed); FindWalkableInternal( tree, node.NegativeChildIndex, path, ref validPosition, movement, up, ref hitPolygonIndex, ref hitPolygonId, ref changed); } private static bool HitsWalkableInternal( FlatPhysicsBsp tree, int nodeIndex, SpherePath path, CollisionSphere sphere, Vector3 up) { if (nodeIndex < 0) return false; FlatPhysicsBspNode node = tree.Nodes[nodeIndex]; if (!NodeIntersects(node, sphere)) return false; if (node.Type == BSPNodeType.Leaf) { if (node.PolygonIndexRange.Count == 0) return false; int end = node.PolygonIndexRange.EndExclusive; for (int i = node.PolygonIndexRange.Start; i < end; i++) { int polygonIndex = tree.PolygonIndexStream[i]; if (WalkableHitsSphere(tree, polygonIndex, path, sphere, up) && CheckWalkable(tree, polygonIndex, sphere, up, small: true)) { return true; } } return false; } float distance = Vector3.Dot(node.SplittingPlane.Normal, sphere.Center) + node.SplittingPlane.D; float reach = sphere.Radius - PhysicsGlobals.EPSILON; if (distance >= reach) { return HitsWalkableInternal( tree, node.PositiveChildIndex, path, sphere, up); } if (distance <= -reach) { return HitsWalkableInternal( tree, node.NegativeChildIndex, path, sphere, up); } if (HitsWalkableInternal( tree, node.PositiveChildIndex, path, sphere, up)) { return true; } return HitsWalkableInternal( tree, node.NegativeChildIndex, path, sphere, up); } private static bool SphereIntersectsSolidInternal( FlatPhysicsBsp tree, int nodeIndex, CollisionSphere sphere, bool centerCheck) { if (nodeIndex < 0) return false; FlatPhysicsBspNode node = tree.Nodes[nodeIndex]; if (node.Type == BSPNodeType.Leaf) { if (node.PolygonIndexRange.Count == 0) return false; if (centerCheck && node.Solid != 0) { if (PhysicsDiagnostics.ProbePlacementFailEnabled) PhysicsDiagnostics.LastPlacementFailSolidLeaf = true; return true; } if (!NodeIntersects(node, sphere)) return false; int end = node.PolygonIndexRange.EndExclusive; for (int i = node.PolygonIndexRange.Start; i < end; i++) { int polygonIndex = tree.PolygonIndexStream[i]; if (!HitsSphere(tree, polygonIndex, sphere)) continue; RecordDiagnosticHit(tree, polygonIndex); if (PhysicsDiagnostics.ProbePlacementFailEnabled) { FlatCollisionPolygon polygon = tree.PolygonTable.Polygons[polygonIndex]; PhysicsDiagnostics.LastPlacementFailPolyId = polygon.Id; PhysicsDiagnostics.LastPlacementFailPolyNormal = polygon.Plane.Normal; PhysicsDiagnostics.LastPlacementFailPolyD = polygon.Plane.D; } return true; } return false; } if (!NodeIntersects(node, sphere)) return false; float distance = Vector3.Dot(node.SplittingPlane.Normal, sphere.Center) + node.SplittingPlane.D; float reach = sphere.Radius - PhysicsGlobals.EPSILON; if (distance >= reach) { return SphereIntersectsSolidInternal( tree, node.PositiveChildIndex, sphere, centerCheck); } if (distance <= -reach) { return SphereIntersectsSolidInternal( tree, node.NegativeChildIndex, sphere, centerCheck); } if (distance < 0f) { if (SphereIntersectsSolidInternal( tree, node.PositiveChildIndex, sphere, centerCheck: false)) { return true; } return SphereIntersectsSolidInternal( tree, node.NegativeChildIndex, sphere, centerCheck); } if (SphereIntersectsSolidInternal( tree, node.PositiveChildIndex, sphere, centerCheck)) { return true; } return SphereIntersectsSolidInternal( tree, node.NegativeChildIndex, sphere, centerCheck: false); } private static bool SphereIntersectsSolidPolygonInternal( FlatPhysicsBsp tree, int nodeIndex, CollisionSphere sphere, float radius, ref bool centerSolid, ref int hitPolygonIndex, bool centerCheck) { if (nodeIndex < 0) return centerSolid; FlatPhysicsBspNode node = tree.Nodes[nodeIndex]; if (node.Type == BSPNodeType.Leaf) { if (node.PolygonIndexRange.Count == 0) return false; if (centerCheck && node.Solid != 0) centerSolid = true; if (!NodeIntersects(node, sphere)) return centerSolid; int end = node.PolygonIndexRange.EndExclusive; for (int i = node.PolygonIndexRange.Start; i < end; i++) { int polygonIndex = tree.PolygonIndexStream[i]; if (HitsSphere(tree, polygonIndex, sphere)) { hitPolygonIndex = polygonIndex; return true; } } return centerSolid; } if (!NodeIntersects(node, sphere)) return centerSolid; float distance = Vector3.Dot(node.SplittingPlane.Normal, sphere.Center) + node.SplittingPlane.D; float reach = radius - PhysicsGlobals.EPSILON; if (distance >= reach) { return SphereIntersectsSolidPolygonInternal( tree, node.PositiveChildIndex, sphere, radius, ref centerSolid, ref hitPolygonIndex, centerCheck); } if (distance <= -reach) { return SphereIntersectsSolidPolygonInternal( tree, node.NegativeChildIndex, sphere, radius, ref centerSolid, ref hitPolygonIndex, centerCheck); } if (distance <= 0f) { SphereIntersectsSolidPolygonInternal( tree, node.NegativeChildIndex, sphere, radius, ref centerSolid, ref hitPolygonIndex, centerCheck); if (hitPolygonIndex >= 0) return centerSolid; return SphereIntersectsSolidPolygonInternal( tree, node.PositiveChildIndex, sphere, radius, ref centerSolid, ref hitPolygonIndex, centerCheck: false); } SphereIntersectsSolidPolygonInternal( tree, node.PositiveChildIndex, sphere, radius, ref centerSolid, ref hitPolygonIndex, centerCheck); if (hitPolygonIndex >= 0) return centerSolid; return SphereIntersectsSolidPolygonInternal( tree, node.NegativeChildIndex, sphere, radius, ref centerSolid, ref hitPolygonIndex, centerCheck: false); } private static bool AdjustToPlane( FlatPhysicsBsp tree, ref CollisionSphere checkPosition, Vector3 currentPosition, int hitPolygonIndex, Vector3 contactPoint) { Vector3 movement = checkPosition.Center - currentPosition; double clearTime = 0.0; double hitTime = 1.0; int iteration = 0; const int MaxIterations = 15; while (true) { float touchTime = AdjustSphereToPolygon( tree, hitPolygonIndex, checkPosition, currentPosition, movement); if (touchTime == 1f) break; checkPosition.Center = currentPosition + movement * touchTime; int nextHitPolygonIndex = -1; Vector3 nextContactPoint = Vector3.Zero; if (!SphereIntersectsPolyInternal( tree, tree.RootIndex, checkPosition, movement, ref nextHitPolygonIndex, ref nextContactPoint)) { clearTime = touchTime; break; } if (nextHitPolygonIndex >= 0) hitPolygonIndex = nextHitPolygonIndex; iteration++; hitTime = touchTime; if (iteration >= MaxIterations) return false; } while (iteration < MaxIterations) { double average = (clearTime + hitTime) * 0.5; checkPosition.Center = currentPosition + movement * (float)average; int nextHitPolygonIndex = -1; Vector3 nextContactPoint = Vector3.Zero; if (!SphereIntersectsPolyInternal( tree, tree.RootIndex, checkPosition, movement, ref nextHitPolygonIndex, ref nextContactPoint)) { clearTime = average; } else { hitTime = average; } if (hitTime - clearTime < 0.02) break; iteration++; } checkPosition.Center = currentPosition + movement * (float)clearTime; return true; } private static TransitionState CheckWalkableDispatch( FlatPhysicsBsp tree, SpherePath path, CollisionSphere checkPosition, Vector3 up) { CollisionSphere validPosition = checkPosition; return HitsWalkableInternal( tree, tree.RootIndex, path, validPosition, up) ? TransitionState.Collided : TransitionState.OK; } private static TransitionState StepSphereDown( FlatPhysicsBsp tree, Transition transition, CollisionSphere checkPosition, Vector3 up, float scale, Quaternion localToWorld = default, Vector3 worldOrigin = default) { if (localToWorld == default) localToWorld = Quaternion.Identity; SpherePath path = transition.SpherePath; CollisionInfo collisions = transition.CollisionInfo; float stepDownAmount = -(path.StepDownAmt * path.WalkInterp); Vector3 movement = up * stepDownAmount * (1f / scale); CollisionSphere validPosition = checkPosition; bool changed = false; int hitPolygonIndex = -1; ushort hitPolygonId = 0; FindWalkableInternal( tree, tree.RootIndex, path, ref validPosition, movement, up, ref hitPolygonIndex, ref hitPolygonId, ref changed); if (changed && hitPolygonIndex >= 0) { FlatCollisionPolygon hitPolygon = tree.PolygonTable.Polygons[hitPolygonIndex]; ReadOnlySpan vertices = Vertices(tree, hitPolygon); Vector3 adjusted = validPosition.Center - checkPosition.Center; Vector3 offset = Vector3.Transform(adjusted, localToWorld) * scale; path.AddOffsetToCheckPos(offset); Vector3 worldNormal = TransformNormal(hitPolygon.Plane.Normal, localToWorld); Plane worldPlane = BuildWorldPlane( worldNormal, vertices, localToWorld, scale, worldOrigin); collisions.SetContactPlane(worldPlane, path.CheckCellId, false); path.SetWalkableTransformed( worldPlane, vertices, localToWorld, scale, worldOrigin, Vector3.UnitZ); RecordDiagnosticHit(tree, hitPolygonIndex); return TransitionState.Adjusted; } return TransitionState.OK; } /// /// Flat shadow port of the retail find-walkable traversal. /// public static bool FindWalkableSphere( FlatPhysicsBsp tree, Transition transition, Vector3 sphereCenter, float sphereRadius, float probeDistance, Vector3 up, out int hitPolygonIndex, out ushort hitPolygonId, out Vector3 adjustedCenter) { ArgumentNullException.ThrowIfNull(tree); ArgumentNullException.ThrowIfNull(transition); CollisionSphere validPosition = new(sphereCenter, sphereRadius); adjustedCenter = validPosition.Center; hitPolygonIndex = -1; hitPolygonId = 0; if (tree.RootIndex < 0) return false; Vector3 movement = -up * probeDistance; bool changed = false; int polygonIndex = -1; ushort polygonId = 0; FindWalkableInternal( tree, tree.RootIndex, transition.SpherePath, ref validPosition, movement, up, ref polygonIndex, ref polygonId, ref changed); if (changed && polygonIndex >= 0) { hitPolygonIndex = polygonIndex; hitPolygonId = polygonId; adjustedCenter = validPosition.Center; return true; } return false; } private static TransitionState StepSphereUp( Transition transition, Vector3 collisionNormal, PhysicsEngine engine) { bool stepped = transition.DoStepUp(collisionNormal, engine); if (PhysicsDiagnostics.ProbeIndoorBspEnabled) { SpherePath path = transition.SpherePath; Console.WriteLine(FormattableString.Invariant( $"[stepsphereup] cell=0x{path.CheckCellId:X8} stepUpFlag={path.StepUp} stepDownFlag={path.StepDown} n=({collisionNormal.X:F2},{collisionNormal.Y:F2},{collisionNormal.Z:F2}) stepped={stepped} pos=({path.CheckPos.X:F3},{path.CheckPos.Y:F3},{path.CheckPos.Z:F3})")); } if (stepped) return TransitionState.OK; TransitionState slideResult = transition.SpherePath.StepUpSlide(transition); if (PhysicsDiagnostics.ProbeIndoorBspEnabled) { Console.WriteLine(FormattableString.Invariant( $"[stepsphereup] cell=0x{transition.SpherePath.CheckCellId:X8} → StepUpSlide={slideResult}")); } return slideResult; } private static TransitionState SlideSphere( Transition transition, Vector3 worldNormal) => transition.SlideSphereInternal( worldNormal, transition.SpherePath.GlobalCurrCenter[0].Origin); private static TransitionState CollideWithPoint( FlatPhysicsBsp tree, Transition transition, CollisionSphere checkPosition, Vector3 currentPosition, int hitPolygonIndex, Vector3 contactPoint, float scale, Quaternion localToWorld = default) { if (localToWorld == default) localToWorld = Quaternion.Identity; FlatCollisionPolygon hitPolygon = tree.PolygonTable.Polygons[hitPolygonIndex]; ObjectInfo objectInfo = transition.ObjectInfo; SpherePath path = transition.SpherePath; CollisionInfo collisions = transition.CollisionInfo; Vector3 collisionNormal = Vector3.Transform(hitPolygon.Plane.Normal, localToWorld); if ((objectInfo.State & ObjectInfoState.PerfectClip) == 0) { collisions.SetCollisionNormal(collisionNormal); RecordDiagnosticHit(tree, hitPolygonIndex); return TransitionState.Collided; } CollisionSphere validPosition = checkPosition; if (!AdjustToPlane( tree, ref validPosition, currentPosition, hitPolygonIndex, contactPoint)) { RecordDiagnosticHit(tree, hitPolygonIndex); return TransitionState.Collided; } collisions.SetCollisionNormal(collisionNormal); RecordDiagnosticHit(tree, hitPolygonIndex); Vector3 adjusted = validPosition.Center - checkPosition.Center; Vector3 offset = Vector3.Transform(adjusted, localToWorld) * scale; path.AddOffsetToCheckPos(offset); return TransitionState.Adjusted; } private static TransitionState NegativePolygonHitDispatch( FlatPhysicsBsp tree, SpherePath path, int hitPolygonIndex, bool stepUp, Quaternion localToWorld = default) { if (localToWorld == default) localToWorld = Quaternion.Identity; FlatCollisionPolygon hitPolygon = tree.PolygonTable.Polygons[hitPolygonIndex]; path.NegPolyHit = true; path.NegStepUp = stepUp; path.NegCollisionNormal = Vector3.Transform(hitPolygon.Plane.Normal, localToWorld); if (PhysicsDiagnostics.ProbeIndoorBspEnabled) { Vector3 normal = hitPolygon.Plane.Normal; Console.WriteLine(FormattableString.Invariant( $"[neg-poly] cell=0x{path.CheckCellId:X8} stepUp={stepUp} stepDownFlag={path.StepDown} poly=0x{hitPolygon.Id:X4} nLocal=({normal.X:F3},{normal.Y:F3},{normal.Z:F3}) sides={hitPolygon.SidesType} checkPos=({path.CheckPos.X:F3},{path.CheckPos.Y:F3},{path.CheckPos.Z:F3})")); } return TransitionState.OK; } private static TransitionState PlacementInsert( FlatPhysicsBsp tree, Transition transition, bool clearCell) { SpherePath path = transition.SpherePath; CollisionSphere sphere0 = new( path.LocalSphere[0].Origin, path.LocalSphere[0].Radius); float radius = sphere0.Radius; bool hasSphere1 = path.NumSphere > 1; CollisionSphere sphere1 = default; if (hasSphere1) { sphere1 = new CollisionSphere( path.LocalSphere[1].Origin, path.LocalSphere[1].Radius); } const int MaxIterations = 20; for (int iteration = 0; iteration < MaxIterations; iteration++) { bool centerSolid = false; int hitPolygonIndex = -1; if (SphereIntersectsSolidPolygonInternal( tree, tree.RootIndex, sphere0, radius, ref centerSolid, ref hitPolygonIndex, clearCell)) { if (hitPolygonIndex >= 0) { AdjustToPlacementPolygon( tree, hitPolygonIndex, ref sphere0, ref sphere1, hasSphere1, radius, centerSolid, clearCell); continue; } } else { if (hasSphere1) { centerSolid = false; hitPolygonIndex = -1; if (SphereIntersectsSolidPolygonInternal( tree, tree.RootIndex, sphere1, radius, ref centerSolid, ref hitPolygonIndex, clearCell)) { if (hitPolygonIndex >= 0) { AdjustToPlacementPolygon( tree, hitPolygonIndex, ref sphere1, ref sphere0, hasValidPosition2: true, radius, centerSolid, clearCell); continue; } } else { return PlacementInsertInner(sphere0, path, iteration); } } else { return PlacementInsertInner(sphere0, path, iteration); } } radius *= 2f; } return TransitionState.Collided; } private static TransitionState PlacementInsertInner( CollisionSphere sphere0, SpherePath path, int iteration) { if (iteration == 0) return TransitionState.OK; Vector3 adjustment = sphere0.Center - path.LocalSphere[0].Origin; path.AddOffsetToCheckPos(adjustment); return TransitionState.Adjusted; } /// /// Flat shadow port of retail BSPTREE::find_collisions /// (0x0053A440), preserving the graph path's six dispatch branches. /// public static TransitionState FindCollisions( FlatPhysicsBsp tree, Transition transition, DatReaderWriter.Types.Sphere localSphere, DatReaderWriter.Types.Sphere? localSphere1, Vector3 localCurrentCenter, Vector3 localSpaceZ, float scale, Quaternion localToWorld = default, PhysicsEngine? engine = null, Vector3 worldOrigin = default) { ArgumentNullException.ThrowIfNull(tree); ArgumentNullException.ThrowIfNull(transition); CollisionSphere sphere0 = new(localSphere.Origin, localSphere.Radius); bool hasSphere1 = localSphere1 is not null; CollisionSphere sphere1 = hasSphere1 ? new CollisionSphere(localSphere1!.Origin, localSphere1.Radius) : default; return FindCollisionsCore( tree, transition, sphere0, hasSphere1, sphere1, localCurrentCenter, localSpaceZ, scale, localToWorld, engine, worldOrigin); } internal static TransitionState FindCollisions( FlatPhysicsBsp tree, Transition transition, Vector3 localSphereCenter, float localSphereRadius, bool hasLocalSphere1, Vector3 localSphere1Center, float localSphere1Radius, Vector3 localCurrentCenter, Vector3 localSpaceZ, float scale, Quaternion localToWorld = default, PhysicsEngine? engine = null, Vector3 worldOrigin = default) { ArgumentNullException.ThrowIfNull(tree); ArgumentNullException.ThrowIfNull(transition); return FindCollisionsCore( tree, transition, new CollisionSphere(localSphereCenter, localSphereRadius), hasLocalSphere1, hasLocalSphere1 ? new CollisionSphere( localSphere1Center, localSphere1Radius) : default, localCurrentCenter, localSpaceZ, scale, localToWorld, engine, worldOrigin); } private static TransitionState FindCollisionsCore( FlatPhysicsBsp tree, Transition transition, CollisionSphere sphere0, bool hasSphere1, CollisionSphere sphere1, Vector3 localCurrentCenter, Vector3 localSpaceZ, float scale, Quaternion localToWorld, PhysicsEngine? engine, Vector3 worldOrigin) { if (tree.RootIndex < 0) return TransitionState.OK; if (localToWorld == default) localToWorld = Quaternion.Identity; SpherePath path = transition.SpherePath; CollisionInfo collisions = transition.CollisionInfo; ObjectInfo objectInfo = transition.ObjectInfo; Vector3 movement = sphere0.Center - localCurrentCenter; if (PhysicsDiagnostics.ProbePushBackEnabled) { PhysicsDiagnostics.LogPushBackDispatch( sphereCenter: sphere0.Center, movement, collide: path.Collide, insertType: (int)path.InsertType, objState: unchecked((int)objectInfo.State), walkInterpEntry: path.WalkInterp, returnState: -1); } Vector3 LocalToWorld(Vector3 value) => Vector3.Transform(value, localToWorld); // Path 1: Placement or obstruction-ethereal. if (path.InsertType == InsertType.Placement || path.ObstructionEthereal) { bool clearCell = !(path.BldgCheck && path.HitsInteriorCell); if (PhysicsDiagnostics.ProbePlacementFailEnabled) { PhysicsDiagnostics.LastPlacementFailPolyId = 0; PhysicsDiagnostics.LastPlacementFailSolidLeaf = false; } if (SphereIntersectsSolidInternal( tree, tree.RootIndex, sphere0, clearCell)) { if (PhysicsDiagnostics.ProbePlacementFailEnabled) { PhysicsDiagnostics.LogPlacementFail( "Path1.sphere0", sphere0.Center, sphere0.Radius, 0, path.CheckCellId, worldOrigin, objectInfo.Ethereal); } return TransitionState.Collided; } if (PhysicsDiagnostics.ProbePlacementFailEnabled) { PhysicsDiagnostics.LastPlacementFailPolyId = 0; PhysicsDiagnostics.LastPlacementFailSolidLeaf = false; } if (hasSphere1 && SphereIntersectsSolidInternal( tree, tree.RootIndex, sphere1, clearCell)) { if (PhysicsDiagnostics.ProbePlacementFailEnabled) { PhysicsDiagnostics.LogPlacementFail( "Path1.sphere1", sphere1.Center, sphere1.Radius, 1, path.CheckCellId, worldOrigin, objectInfo.Ethereal); } return TransitionState.Collided; } return TransitionState.OK; } // Path 2: CheckWalkable. if (path.CheckWalkable) { return CheckWalkableDispatch( tree, path, sphere0, localSpaceZ); } // Path 3: StepDown. if (path.StepDown) { return StepSphereDown( tree, transition, sphere0, localSpaceZ, scale, localToWorld, worldOrigin); } // Path 4: Collide / walkable landing. if (path.Collide) { CollisionSphere validPosition = sphere0; int hitPolygonIndex = -1; ushort hitPolygonId = 0; bool changed = false; FindWalkableInternal( tree, tree.RootIndex, path, ref validPosition, movement, localSpaceZ, ref hitPolygonIndex, ref hitPolygonId, ref changed); if (changed && hitPolygonIndex >= 0) { FlatCollisionPolygon hitPolygon = tree.PolygonTable.Polygons[hitPolygonIndex]; ReadOnlySpan vertices = Vertices(tree, hitPolygon); Vector3 localOffset = validPosition.Center - sphere0.Center; Vector3 worldOffset = LocalToWorld(localOffset) * scale; path.AddOffsetToCheckPos(worldOffset); Vector3 worldNormal = TransformNormal(hitPolygon.Plane.Normal, localToWorld); Plane worldPlane = BuildWorldPlane( worldNormal, vertices, localToWorld, scale, worldOrigin); collisions.SetContactPlane( worldPlane, path.CheckCellId, false); path.SetWalkableTransformed( worldPlane, vertices, localToWorld, scale, worldOrigin, Vector3.UnitZ); RecordDiagnosticHit(tree, hitPolygonIndex); return TransitionState.Adjusted; } return TransitionState.OK; } // Path 5: grounded contact. if ((objectInfo.State & ObjectInfoState.Contact) != 0) { int hitPolygonIndex0 = -1; Vector3 contact0 = Vector3.Zero; bool hit0 = SphereIntersectsPolyInternal( tree, tree.RootIndex, sphere0, movement, ref hitPolygonIndex0, ref contact0); if (hit0) { RecordDiagnosticHit(tree, hitPolygonIndex0); Vector3 worldNormal = LocalToWorld( tree.PolygonTable.Polygons[hitPolygonIndex0].Plane.Normal); if (engine is not null && !path.StepUp && !path.StepDown) return StepSphereUp(transition, worldNormal, engine); return SlideSphere(transition, worldNormal); } if (hasSphere1) { int hitPolygonIndex1 = -1; Vector3 contact1 = Vector3.Zero; bool hit1 = SphereIntersectsPolyInternal( tree, tree.RootIndex, sphere1, movement, ref hitPolygonIndex1, ref contact1); if (hit1) { RecordDiagnosticHit(tree, hitPolygonIndex1); Vector3 worldNormal = LocalToWorld( tree.PolygonTable.Polygons[hitPolygonIndex1].Plane.Normal); return SlideSphere(transition, worldNormal); } if (hitPolygonIndex1 >= 0) { RecordDiagnosticHit(tree, hitPolygonIndex1); NegativePolygonHitDispatch( tree, path, hitPolygonIndex1, stepUp: false, localToWorld); return TransitionState.OK; } if (hitPolygonIndex0 >= 0) { RecordDiagnosticHit(tree, hitPolygonIndex0); NegativePolygonHitDispatch( tree, path, hitPolygonIndex0, stepUp: true, localToWorld); return TransitionState.OK; } } return TransitionState.OK; } // Path 6: default airborne/path-clipped dispatch. int defaultHitPolygonIndex0 = -1; Vector3 defaultContact0 = Vector3.Zero; bool defaultHit0 = SphereIntersectsPolyInternal( tree, tree.RootIndex, sphere0, movement, ref defaultHitPolygonIndex0, ref defaultContact0); if (defaultHit0 || defaultHitPolygonIndex0 >= 0) { if ((objectInfo.State & ObjectInfoState.PathClipped) != 0) { return CollideWithPoint( tree, transition, sphere0, localCurrentCenter, defaultHitPolygonIndex0, defaultContact0, scale, localToWorld); } Vector3 worldNormal0 = LocalToWorld( tree.PolygonTable.Polygons[defaultHitPolygonIndex0].Plane.Normal); if (worldNormal0.Z < PhysicsGlobals.FloorZ) { Vector3 currentWorld = path.GlobalCurrCenter[0].Origin; Vector3 endWorld = path.GlobalSphere[0].Origin; Vector3 globalDelta = endWorld - currentWorld; float difference = Vector3.Dot(worldNormal0, globalDelta); if (difference < 0f) path.AddOffsetToCheckPos(-worldNormal0 * difference); collisions.SetCollisionNormal(worldNormal0); collisions.SetSlidingNormal(worldNormal0); RecordDiagnosticHit(tree, defaultHitPolygonIndex0); return TransitionState.Slid; } path.SetCollide(worldNormal0); path.WalkableAllowance = PhysicsGlobals.LandingZ; RecordDiagnosticHit(tree, defaultHitPolygonIndex0); return TransitionState.Adjusted; } if (hasSphere1) { int defaultHitPolygonIndex1 = -1; Vector3 defaultContact1 = Vector3.Zero; bool defaultHit1 = SphereIntersectsPolyInternal( tree, tree.RootIndex, sphere1, movement, ref defaultHitPolygonIndex1, ref defaultContact1); if (defaultHit1 || defaultHitPolygonIndex1 >= 0) { Vector3 worldNormal1 = LocalToWorld( tree.PolygonTable.Polygons[defaultHitPolygonIndex1].Plane.Normal); if (worldNormal1.Z < PhysicsGlobals.FloorZ) { Vector3 currentWorld = path.GlobalCurrCenter[0].Origin; Vector3 endWorld = path.GlobalSphere[0].Origin; Vector3 globalDelta = endWorld - currentWorld; float difference = Vector3.Dot(worldNormal1, globalDelta); if (difference < 0f) path.AddOffsetToCheckPos(-worldNormal1 * difference); collisions.SetCollisionNormal(worldNormal1); collisions.SetSlidingNormal(worldNormal1); RecordDiagnosticHit(tree, defaultHitPolygonIndex1); return TransitionState.Slid; } path.SetCollide(worldNormal1); path.WalkableAllowance = PhysicsGlobals.LandingZ; RecordDiagnosticHit(tree, defaultHitPolygonIndex1); return TransitionState.Adjusted; } } return TransitionState.OK; } private static void RecordDiagnosticHit( FlatPhysicsBsp tree, int polygonIndex) { if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled) { PhysicsDiagnostics.LastBspHitPoly = MaterializeResolvedPolygon(tree, polygonIndex); } } private static ResolvedPolygon MaterializeResolvedPolygon( FlatPhysicsBsp tree, int polygonIndex) { FlatCollisionPolygon polygon = tree.PolygonTable.Polygons[polygonIndex]; return new ResolvedPolygon { Id = polygon.Id, Plane = polygon.Plane, SidesType = polygon.SidesType, NumPoints = polygon.NumPoints, Vertices = Vertices(tree, polygon).ToArray(), }; } }