perf(physics): cut production traversal to flat assets
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>
This commit is contained in:
parent
d9446030e6
commit
068a06518d
13 changed files with 743 additions and 43 deletions
|
|
@ -43,8 +43,15 @@ internal sealed class LiveEntityCollisionBuilder
|
||||||
PhysicsDataCache physicsData,
|
PhysicsDataCache physicsData,
|
||||||
LiveEntityDefaultPoseResolver defaultPose)
|
LiveEntityDefaultPoseResolver defaultPose)
|
||||||
: this(
|
: this(
|
||||||
id => physicsData.GetGfxObj(id)?.BSP?.Root is not null,
|
id => physicsData.GetGfxObj(id)?.FlatPhysicsBsp?.RootIndex >= 0,
|
||||||
id => physicsData.GetGfxObj(id)?.BoundingSphere?.Radius,
|
id =>
|
||||||
|
{
|
||||||
|
FlatPhysicsBsp? flat =
|
||||||
|
physicsData.GetGfxObj(id)?.FlatPhysicsBsp;
|
||||||
|
return flat is { RootIndex: >= 0 }
|
||||||
|
? flat.Nodes[flat.RootIndex].BoundingSphere.Radius
|
||||||
|
: null;
|
||||||
|
},
|
||||||
defaultPose)
|
defaultPose)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(physicsData);
|
ArgumentNullException.ThrowIfNull(physicsData);
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,8 @@ public sealed class GameWindow :
|
||||||
// Task 4: physics data cache — BSP trees + collision shapes extracted from
|
// Task 4: physics data cache — BSP trees + collision shapes extracted from
|
||||||
// GfxObj/Setup dats during streaming. Populated on the worker thread;
|
// GfxObj/Setup dats during streaming. Populated on the worker thread;
|
||||||
// ConcurrentDictionary inside makes cross-thread access safe.
|
// ConcurrentDictionary inside makes cross-thread access safe.
|
||||||
private readonly AcDream.Core.Physics.PhysicsDataCache _physicsDataCache = new();
|
private readonly AcDream.Core.Physics.PhysicsDataCache _physicsDataCache =
|
||||||
|
AcDream.Core.Physics.PhysicsDataCache.CreateProduction();
|
||||||
|
|
||||||
// #184 Slice 2a: the per-remote dead-reckoning tick, extracted out of the
|
// #184 Slice 2a: the per-remote dead-reckoning tick, extracted out of the
|
||||||
// >10k-line TickAnimations (Code Structure Rule 1). Reusable setup/motion
|
// >10k-line TickAnimations (Code Structure Rule 1). Reusable setup/motion
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,9 @@ public sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe
|
||||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled)
|
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled)
|
||||||
{
|
{
|
||||||
var cp = _physics.DataCache?.GetCellStruct(startCell);
|
var cp = _physics.DataCache?.GetCellStruct(startCell);
|
||||||
string bsp = cp?.BSP is null ? "nobsp" : (cp.BSP.Root is null ? "noroot" : "ok");
|
string bsp = cp?.FlatPhysicsBsp is null
|
||||||
|
? "noflatbsp"
|
||||||
|
: (cp.FlatPhysicsBsp.RootIndex < 0 ? "noroot" : "ok");
|
||||||
float desiredBack = Vector3.Distance(pivot, desiredEye);
|
float desiredBack = Vector3.Distance(pivot, desiredEye);
|
||||||
float eyeBack = Vector3.Distance(pivot, eye);
|
float eyeBack = Vector3.Distance(pivot, eye);
|
||||||
System.Console.WriteLine(
|
System.Console.WriteLine(
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,9 @@ public readonly record struct CollisionShadowStats(
|
||||||
long Faults);
|
long Faults);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Slice I5 sampled graph/flat referee. The graph result always remains
|
/// Slice I5/I6 sampled graph/flat referee. The authority is selected by
|
||||||
/// authoritative. One retained clone receives the flat query first; the
|
/// <see cref="PhysicsDataCache.CollisionTraversalMode"/>. One retained clone
|
||||||
/// canonical transition is then evaluated by the graph path and compared
|
/// receives the non-authoritative query and the two results are compared
|
||||||
/// field-for-field with exact float bits.
|
/// field-for-field with exact float bits.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class CollisionShadowVerifier
|
internal sealed class CollisionShadowVerifier
|
||||||
|
|
@ -29,6 +29,7 @@ internal sealed class CollisionShadowVerifier
|
||||||
private long _mismatches;
|
private long _mismatches;
|
||||||
private long _faults;
|
private long _faults;
|
||||||
private bool _flatPass;
|
private bool _flatPass;
|
||||||
|
private bool _graphPass;
|
||||||
|
|
||||||
public CollisionShadowVerifier(
|
public CollisionShadowVerifier(
|
||||||
int sampleEvery,
|
int sampleEvery,
|
||||||
|
|
@ -42,6 +43,7 @@ internal sealed class CollisionShadowVerifier
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool IsFlatPass => _flatPass;
|
internal bool IsFlatPass => _flatPass;
|
||||||
|
internal bool IsGraphPass => _graphPass;
|
||||||
|
|
||||||
internal CollisionShadowStats Stats => new(
|
internal CollisionShadowStats Stats => new(
|
||||||
_queries,
|
_queries,
|
||||||
|
|
@ -52,7 +54,7 @@ internal sealed class CollisionShadowVerifier
|
||||||
|
|
||||||
internal bool TrySample(out long sample)
|
internal bool TrySample(out long sample)
|
||||||
{
|
{
|
||||||
if (_flatPass)
|
if (_flatPass || _graphPass)
|
||||||
{
|
{
|
||||||
sample = 0;
|
sample = 0;
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -77,9 +79,9 @@ internal sealed class CollisionShadowVerifier
|
||||||
|
|
||||||
internal void BeginFlatPass()
|
internal void BeginFlatPass()
|
||||||
{
|
{
|
||||||
if (_flatPass)
|
if (_flatPass || _graphPass)
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"Collision shadow flat passes cannot overlap.");
|
"Collision shadow passes cannot overlap.");
|
||||||
_flatPass = true;
|
_flatPass = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,6 +93,22 @@ internal sealed class CollisionShadowVerifier
|
||||||
_flatPass = false;
|
_flatPass = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void BeginGraphPass()
|
||||||
|
{
|
||||||
|
if (_flatPass || _graphPass)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Collision shadow passes cannot overlap.");
|
||||||
|
_graphPass = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void EndGraphPass()
|
||||||
|
{
|
||||||
|
if (!_graphPass)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"No collision shadow graph pass is active.");
|
||||||
|
_graphPass = false;
|
||||||
|
}
|
||||||
|
|
||||||
internal void RecordBoolean(
|
internal void RecordBoolean(
|
||||||
long sample,
|
long sample,
|
||||||
string kind,
|
string kind,
|
||||||
|
|
@ -188,7 +206,8 @@ internal sealed class CollisionShadowVerifier
|
||||||
string kind,
|
string kind,
|
||||||
uint sourceId,
|
uint sourceId,
|
||||||
Exception fault,
|
Exception fault,
|
||||||
string input)
|
string input,
|
||||||
|
string authority = "graph")
|
||||||
{
|
{
|
||||||
_faults++;
|
_faults++;
|
||||||
WriteArtifact(new CollisionShadowMismatchArtifact(
|
WriteArtifact(new CollisionShadowMismatchArtifact(
|
||||||
|
|
@ -196,8 +215,8 @@ internal sealed class CollisionShadowVerifier
|
||||||
sample,
|
sample,
|
||||||
kind,
|
kind,
|
||||||
$"0x{sourceId:X8}",
|
$"0x{sourceId:X8}",
|
||||||
"FlatFault",
|
authority == "flat" ? "GraphFault" : "FlatFault",
|
||||||
"graph-authoritative",
|
$"{authority}-authoritative",
|
||||||
$"{fault.GetType().FullName}: {fault.Message}",
|
$"{fault.GetType().FullName}: {fault.Message}",
|
||||||
input));
|
input));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ using System.Numerics;
|
||||||
namespace AcDream.Core.Physics;
|
namespace AcDream.Core.Physics;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Temporary Slice I referee mode. Graph remains production-authoritative;
|
/// Temporary Slice I6 authority/referee selector. Gameplay uses
|
||||||
/// Flat is enabled only by differential tests until dual publication and the
|
/// <see cref="Flat"/>; <see cref="Graph"/> remains only for the old-oracle
|
||||||
/// connected shadow gate complete.
|
/// differential fixtures until the accepted cutover deletes it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal enum CollisionTraversalMode
|
internal enum CollisionTraversalMode
|
||||||
{
|
{
|
||||||
|
|
@ -14,8 +14,8 @@ internal enum CollisionTraversalMode
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Narrow representation switch used while the graph and flat collision
|
/// Narrow representation switch used while flat production and the graph
|
||||||
/// worlds coexist. I6 removes the graph branch after acceptance.
|
/// referee coexist. I6 removes the graph branch after acceptance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class CollisionTraversal
|
internal static class CollisionTraversal
|
||||||
{
|
{
|
||||||
|
|
@ -27,7 +27,49 @@ internal static class CollisionTraversal
|
||||||
{
|
{
|
||||||
FlatCellContainmentBsp flat = cell.FlatContainmentBsp ??
|
FlatCellContainmentBsp flat = cell.FlatContainmentBsp ??
|
||||||
throw MissingFlat("cell containment");
|
throw MissingFlat("cell containment");
|
||||||
return flat.RootIndex >= 0;
|
bool flatAuthorityResult = flat.RootIndex >= 0;
|
||||||
|
CollisionShadowVerifier? flatShadow = cache.CollisionShadow;
|
||||||
|
if (flatShadow is null ||
|
||||||
|
!flatShadow.TrySample(out long flatAuthoritySample))
|
||||||
|
return flatAuthorityResult;
|
||||||
|
|
||||||
|
bool graphRefereeResult = false;
|
||||||
|
Exception? graphRefereeFault = null;
|
||||||
|
flatShadow.BeginGraphPass();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
graphRefereeResult = cell.CellBSP?.Root is not null;
|
||||||
|
}
|
||||||
|
catch (Exception fault)
|
||||||
|
{
|
||||||
|
graphRefereeFault = fault;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
flatShadow.EndGraphPass();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (graphRefereeFault is null)
|
||||||
|
{
|
||||||
|
flatShadow.RecordBoolean(
|
||||||
|
flatAuthoritySample,
|
||||||
|
"HasCellContainment",
|
||||||
|
cell.SourceId,
|
||||||
|
graphRefereeResult,
|
||||||
|
flatAuthorityResult,
|
||||||
|
string.Empty);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
flatShadow.RecordFault(
|
||||||
|
flatAuthoritySample,
|
||||||
|
"HasCellContainment",
|
||||||
|
cell.SourceId,
|
||||||
|
graphRefereeFault,
|
||||||
|
string.Empty,
|
||||||
|
"flat");
|
||||||
|
}
|
||||||
|
return flatAuthorityResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||||
|
|
@ -82,7 +124,49 @@ internal static class CollisionTraversal
|
||||||
{
|
{
|
||||||
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
||||||
throw MissingFlat("cell physics");
|
throw MissingFlat("cell physics");
|
||||||
return flat.RootIndex >= 0;
|
bool flatAuthorityResult = flat.RootIndex >= 0;
|
||||||
|
CollisionShadowVerifier? flatShadow = cache.CollisionShadow;
|
||||||
|
if (flatShadow is null ||
|
||||||
|
!flatShadow.TrySample(out long flatAuthoritySample))
|
||||||
|
return flatAuthorityResult;
|
||||||
|
|
||||||
|
bool graphRefereeResult = false;
|
||||||
|
Exception? graphRefereeFault = null;
|
||||||
|
flatShadow.BeginGraphPass();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
graphRefereeResult = cell.BSP?.Root is not null;
|
||||||
|
}
|
||||||
|
catch (Exception fault)
|
||||||
|
{
|
||||||
|
graphRefereeFault = fault;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
flatShadow.EndGraphPass();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (graphRefereeFault is null)
|
||||||
|
{
|
||||||
|
flatShadow.RecordBoolean(
|
||||||
|
flatAuthoritySample,
|
||||||
|
"HasCellPhysics",
|
||||||
|
cell.SourceId,
|
||||||
|
graphRefereeResult,
|
||||||
|
flatAuthorityResult,
|
||||||
|
string.Empty);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
flatShadow.RecordFault(
|
||||||
|
flatAuthoritySample,
|
||||||
|
"HasCellPhysics",
|
||||||
|
cell.SourceId,
|
||||||
|
graphRefereeFault,
|
||||||
|
string.Empty,
|
||||||
|
"flat");
|
||||||
|
}
|
||||||
|
return flatAuthorityResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||||
|
|
@ -137,7 +221,49 @@ internal static class CollisionTraversal
|
||||||
{
|
{
|
||||||
FlatPhysicsBsp flat = gfxObject.FlatPhysicsBsp ??
|
FlatPhysicsBsp flat = gfxObject.FlatPhysicsBsp ??
|
||||||
throw MissingFlat("GfxObj physics");
|
throw MissingFlat("GfxObj physics");
|
||||||
return flat.RootIndex >= 0;
|
bool flatAuthorityResult = flat.RootIndex >= 0;
|
||||||
|
CollisionShadowVerifier? flatShadow = cache.CollisionShadow;
|
||||||
|
if (flatShadow is null ||
|
||||||
|
!flatShadow.TrySample(out long flatAuthoritySample))
|
||||||
|
return flatAuthorityResult;
|
||||||
|
|
||||||
|
bool graphRefereeResult = false;
|
||||||
|
Exception? graphRefereeFault = null;
|
||||||
|
flatShadow.BeginGraphPass();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
graphRefereeResult = gfxObject.BSP.Root is not null;
|
||||||
|
}
|
||||||
|
catch (Exception fault)
|
||||||
|
{
|
||||||
|
graphRefereeFault = fault;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
flatShadow.EndGraphPass();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (graphRefereeFault is null)
|
||||||
|
{
|
||||||
|
flatShadow.RecordBoolean(
|
||||||
|
flatAuthoritySample,
|
||||||
|
"HasGfxObjPhysics",
|
||||||
|
gfxObject.SourceId,
|
||||||
|
graphRefereeResult,
|
||||||
|
flatAuthorityResult,
|
||||||
|
string.Empty);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
flatShadow.RecordFault(
|
||||||
|
flatAuthoritySample,
|
||||||
|
"HasGfxObjPhysics",
|
||||||
|
gfxObject.SourceId,
|
||||||
|
graphRefereeFault,
|
||||||
|
string.Empty,
|
||||||
|
"flat");
|
||||||
|
}
|
||||||
|
return flatAuthorityResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||||
|
|
@ -192,7 +318,43 @@ internal static class CollisionTraversal
|
||||||
{
|
{
|
||||||
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
||||||
throw MissingFlat("cell physics");
|
throw MissingFlat("cell physics");
|
||||||
return flat.Nodes[flat.RootIndex].BoundingSphere;
|
FlatCollisionSphere flatAuthorityResult =
|
||||||
|
flat.Nodes[flat.RootIndex].BoundingSphere;
|
||||||
|
CollisionShadowVerifier? flatShadow = cache.CollisionShadow;
|
||||||
|
if (flatShadow is null ||
|
||||||
|
!flatShadow.TrySample(out long flatAuthoritySample))
|
||||||
|
return flatAuthorityResult;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
flatShadow.BeginGraphPass();
|
||||||
|
DatReaderWriter.Types.Sphere graphSphere =
|
||||||
|
cell.BSP!.Root!.BoundingSphere;
|
||||||
|
var graphRefereeResult =
|
||||||
|
new FlatCollisionSphere(
|
||||||
|
graphSphere.Origin,
|
||||||
|
graphSphere.Radius);
|
||||||
|
flatShadow.EndGraphPass();
|
||||||
|
flatShadow.RecordSphere(
|
||||||
|
flatAuthoritySample,
|
||||||
|
"RootBoundingSphere",
|
||||||
|
cell.SourceId,
|
||||||
|
graphRefereeResult,
|
||||||
|
flatAuthorityResult);
|
||||||
|
}
|
||||||
|
catch (Exception fault)
|
||||||
|
{
|
||||||
|
if (flatShadow.IsGraphPass)
|
||||||
|
flatShadow.EndGraphPass();
|
||||||
|
flatShadow.RecordFault(
|
||||||
|
flatAuthoritySample,
|
||||||
|
"RootBoundingSphere",
|
||||||
|
cell.SourceId,
|
||||||
|
fault,
|
||||||
|
string.Empty,
|
||||||
|
"flat");
|
||||||
|
}
|
||||||
|
return flatAuthorityResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
DatReaderWriter.Types.Sphere sphere = cell.BSP!.Root!.BoundingSphere;
|
DatReaderWriter.Types.Sphere sphere = cell.BSP!.Root!.BoundingSphere;
|
||||||
|
|
@ -240,7 +402,54 @@ internal static class CollisionTraversal
|
||||||
{
|
{
|
||||||
FlatCellContainmentBsp flat = cell.FlatContainmentBsp ??
|
FlatCellContainmentBsp flat = cell.FlatContainmentBsp ??
|
||||||
throw MissingFlat("cell containment");
|
throw MissingFlat("cell containment");
|
||||||
return FlatBspQuery.PointInsideCellBsp(flat, localPoint);
|
bool flatAuthorityResult =
|
||||||
|
FlatBspQuery.PointInsideCellBsp(flat, localPoint);
|
||||||
|
CollisionShadowVerifier? flatShadow = cache.CollisionShadow;
|
||||||
|
if (flatShadow is null ||
|
||||||
|
!flatShadow.TrySample(out long flatAuthoritySample))
|
||||||
|
return flatAuthorityResult;
|
||||||
|
|
||||||
|
bool graphRefereeResult = false;
|
||||||
|
Exception? graphRefereeFault = null;
|
||||||
|
flatShadow.BeginGraphPass();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
graphRefereeResult = BSPQuery.PointInsideCellBsp(
|
||||||
|
cell.CellBSP?.Root,
|
||||||
|
localPoint);
|
||||||
|
}
|
||||||
|
catch (Exception fault)
|
||||||
|
{
|
||||||
|
graphRefereeFault = fault;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
flatShadow.EndGraphPass();
|
||||||
|
}
|
||||||
|
string flatAuthorityInput = CollisionShadowVerifier.FormatInput(
|
||||||
|
localPoint,
|
||||||
|
0f);
|
||||||
|
if (graphRefereeFault is null)
|
||||||
|
{
|
||||||
|
flatShadow.RecordBoolean(
|
||||||
|
flatAuthoritySample,
|
||||||
|
"PointInsideCell",
|
||||||
|
cell.SourceId,
|
||||||
|
graphRefereeResult,
|
||||||
|
flatAuthorityResult,
|
||||||
|
flatAuthorityInput);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
flatShadow.RecordFault(
|
||||||
|
flatAuthoritySample,
|
||||||
|
"PointInsideCell",
|
||||||
|
cell.SourceId,
|
||||||
|
graphRefereeFault,
|
||||||
|
flatAuthorityInput,
|
||||||
|
"flat");
|
||||||
|
}
|
||||||
|
return flatAuthorityResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||||
|
|
@ -305,10 +514,57 @@ internal static class CollisionTraversal
|
||||||
{
|
{
|
||||||
FlatCellContainmentBsp flat = cell.FlatContainmentBsp ??
|
FlatCellContainmentBsp flat = cell.FlatContainmentBsp ??
|
||||||
throw MissingFlat("cell containment");
|
throw MissingFlat("cell containment");
|
||||||
return FlatBspQuery.SphereIntersectsCellBsp(
|
bool flatAuthorityResult = FlatBspQuery.SphereIntersectsCellBsp(
|
||||||
flat,
|
flat,
|
||||||
localCenter,
|
localCenter,
|
||||||
radius);
|
radius);
|
||||||
|
CollisionShadowVerifier? flatShadow = cache.CollisionShadow;
|
||||||
|
if (flatShadow is null ||
|
||||||
|
!flatShadow.TrySample(out long flatAuthoritySample))
|
||||||
|
return flatAuthorityResult;
|
||||||
|
|
||||||
|
bool graphRefereeResult = false;
|
||||||
|
Exception? graphRefereeFault = null;
|
||||||
|
flatShadow.BeginGraphPass();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
graphRefereeResult = BSPQuery.SphereIntersectsCellBsp(
|
||||||
|
cell.CellBSP?.Root,
|
||||||
|
localCenter,
|
||||||
|
radius);
|
||||||
|
}
|
||||||
|
catch (Exception fault)
|
||||||
|
{
|
||||||
|
graphRefereeFault = fault;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
flatShadow.EndGraphPass();
|
||||||
|
}
|
||||||
|
string flatAuthorityInput = CollisionShadowVerifier.FormatInput(
|
||||||
|
localCenter,
|
||||||
|
radius);
|
||||||
|
if (graphRefereeFault is null)
|
||||||
|
{
|
||||||
|
flatShadow.RecordBoolean(
|
||||||
|
flatAuthoritySample,
|
||||||
|
"SphereIntersectsCell",
|
||||||
|
cell.SourceId,
|
||||||
|
graphRefereeResult,
|
||||||
|
flatAuthorityResult,
|
||||||
|
flatAuthorityInput);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
flatShadow.RecordFault(
|
||||||
|
flatAuthoritySample,
|
||||||
|
"SphereIntersectsCell",
|
||||||
|
cell.SourceId,
|
||||||
|
graphRefereeFault,
|
||||||
|
flatAuthorityInput,
|
||||||
|
"flat");
|
||||||
|
}
|
||||||
|
return flatAuthorityResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||||
|
|
@ -386,9 +642,17 @@ internal static class CollisionTraversal
|
||||||
{
|
{
|
||||||
if (UseFlat(cache))
|
if (UseFlat(cache))
|
||||||
{
|
{
|
||||||
|
CollisionShadowVerifier? flatShadow = cache.CollisionShadow;
|
||||||
|
long flatAuthoritySample = 0;
|
||||||
|
bool sampled =
|
||||||
|
flatShadow is not null &&
|
||||||
|
flatShadow.TrySample(out flatAuthoritySample);
|
||||||
|
Transition? graphTransition = sampled
|
||||||
|
? flatShadow!.PrepareShadow(transition)
|
||||||
|
: null;
|
||||||
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
||||||
throw MissingFlat("cell physics");
|
throw MissingFlat("cell physics");
|
||||||
return FlatBspQuery.FindCollisions(
|
TransitionState flatAuthorityState = FlatBspQuery.FindCollisions(
|
||||||
flat,
|
flat,
|
||||||
transition,
|
transition,
|
||||||
localSphereCenter,
|
localSphereCenter,
|
||||||
|
|
@ -402,6 +666,73 @@ internal static class CollisionTraversal
|
||||||
localToWorld,
|
localToWorld,
|
||||||
engine,
|
engine,
|
||||||
worldOrigin);
|
worldOrigin);
|
||||||
|
if (!sampled)
|
||||||
|
return flatAuthorityState;
|
||||||
|
|
||||||
|
TransitionState graphRefereeState = TransitionState.Invalid;
|
||||||
|
Exception? graphRefereeFault = null;
|
||||||
|
flatShadow!.BeginGraphPass();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
graphRefereeState = BSPQuery.FindCollisions(
|
||||||
|
cell.BSP?.Root,
|
||||||
|
cell.Resolved,
|
||||||
|
graphTransition!,
|
||||||
|
localSphereCenter,
|
||||||
|
localSphereRadius,
|
||||||
|
hasLocalSphere1,
|
||||||
|
localSphere1Center,
|
||||||
|
localSphere1Radius,
|
||||||
|
localCurrentCenter,
|
||||||
|
localSpaceZ,
|
||||||
|
scale,
|
||||||
|
localToWorld,
|
||||||
|
engine,
|
||||||
|
worldOrigin);
|
||||||
|
}
|
||||||
|
catch (Exception fault)
|
||||||
|
{
|
||||||
|
graphRefereeFault = fault;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
flatShadow.EndGraphPass();
|
||||||
|
}
|
||||||
|
|
||||||
|
string flatAuthorityInput = CollisionShadowVerifier.FormatInput(
|
||||||
|
localSphereCenter,
|
||||||
|
localSphereRadius,
|
||||||
|
hasLocalSphere1,
|
||||||
|
localSphere1Center,
|
||||||
|
localSphere1Radius,
|
||||||
|
localCurrentCenter,
|
||||||
|
localSpaceZ,
|
||||||
|
scale,
|
||||||
|
localToWorld,
|
||||||
|
worldOrigin);
|
||||||
|
if (graphRefereeFault is null)
|
||||||
|
{
|
||||||
|
flatShadow.RecordTransition(
|
||||||
|
flatAuthoritySample,
|
||||||
|
"FindCellCollisions",
|
||||||
|
cell.SourceId,
|
||||||
|
graphRefereeState,
|
||||||
|
flatAuthorityState,
|
||||||
|
graphTransition!,
|
||||||
|
transition,
|
||||||
|
flatAuthorityInput);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
flatShadow.RecordFault(
|
||||||
|
flatAuthoritySample,
|
||||||
|
"FindCellCollisions",
|
||||||
|
cell.SourceId,
|
||||||
|
graphRefereeFault,
|
||||||
|
flatAuthorityInput,
|
||||||
|
"flat");
|
||||||
|
}
|
||||||
|
return flatAuthorityState;
|
||||||
}
|
}
|
||||||
|
|
||||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||||
|
|
@ -523,9 +854,17 @@ internal static class CollisionTraversal
|
||||||
{
|
{
|
||||||
if (UseFlat(cache))
|
if (UseFlat(cache))
|
||||||
{
|
{
|
||||||
|
CollisionShadowVerifier? flatShadow = cache.CollisionShadow;
|
||||||
|
long flatAuthoritySample = 0;
|
||||||
|
bool sampled =
|
||||||
|
flatShadow is not null &&
|
||||||
|
flatShadow.TrySample(out flatAuthoritySample);
|
||||||
|
Transition? graphTransition = sampled
|
||||||
|
? flatShadow!.PrepareShadow(transition)
|
||||||
|
: null;
|
||||||
FlatPhysicsBsp flat = gfxObject.FlatPhysicsBsp ??
|
FlatPhysicsBsp flat = gfxObject.FlatPhysicsBsp ??
|
||||||
throw MissingFlat("GfxObj physics");
|
throw MissingFlat("GfxObj physics");
|
||||||
return FlatBspQuery.FindCollisions(
|
TransitionState flatAuthorityState = FlatBspQuery.FindCollisions(
|
||||||
flat,
|
flat,
|
||||||
transition,
|
transition,
|
||||||
localSphereCenter,
|
localSphereCenter,
|
||||||
|
|
@ -539,6 +878,73 @@ internal static class CollisionTraversal
|
||||||
localToWorld,
|
localToWorld,
|
||||||
engine,
|
engine,
|
||||||
worldOrigin);
|
worldOrigin);
|
||||||
|
if (!sampled)
|
||||||
|
return flatAuthorityState;
|
||||||
|
|
||||||
|
TransitionState graphRefereeState = TransitionState.Invalid;
|
||||||
|
Exception? graphRefereeFault = null;
|
||||||
|
flatShadow!.BeginGraphPass();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
graphRefereeState = BSPQuery.FindCollisions(
|
||||||
|
gfxObject.BSP.Root,
|
||||||
|
gfxObject.Resolved,
|
||||||
|
graphTransition!,
|
||||||
|
localSphereCenter,
|
||||||
|
localSphereRadius,
|
||||||
|
hasLocalSphere1,
|
||||||
|
localSphere1Center,
|
||||||
|
localSphere1Radius,
|
||||||
|
localCurrentCenter,
|
||||||
|
localSpaceZ,
|
||||||
|
scale,
|
||||||
|
localToWorld,
|
||||||
|
engine,
|
||||||
|
worldOrigin);
|
||||||
|
}
|
||||||
|
catch (Exception fault)
|
||||||
|
{
|
||||||
|
graphRefereeFault = fault;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
flatShadow.EndGraphPass();
|
||||||
|
}
|
||||||
|
|
||||||
|
string flatAuthorityInput = CollisionShadowVerifier.FormatInput(
|
||||||
|
localSphereCenter,
|
||||||
|
localSphereRadius,
|
||||||
|
hasLocalSphere1,
|
||||||
|
localSphere1Center,
|
||||||
|
localSphere1Radius,
|
||||||
|
localCurrentCenter,
|
||||||
|
localSpaceZ,
|
||||||
|
scale,
|
||||||
|
localToWorld,
|
||||||
|
worldOrigin);
|
||||||
|
if (graphRefereeFault is null)
|
||||||
|
{
|
||||||
|
flatShadow.RecordTransition(
|
||||||
|
flatAuthoritySample,
|
||||||
|
"FindGfxObjCollisions",
|
||||||
|
gfxObject.SourceId,
|
||||||
|
graphRefereeState,
|
||||||
|
flatAuthorityState,
|
||||||
|
graphTransition!,
|
||||||
|
transition,
|
||||||
|
flatAuthorityInput);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
flatShadow.RecordFault(
|
||||||
|
flatAuthoritySample,
|
||||||
|
"FindGfxObjCollisions",
|
||||||
|
gfxObject.SourceId,
|
||||||
|
graphRefereeFault,
|
||||||
|
flatAuthorityInput,
|
||||||
|
"flat");
|
||||||
|
}
|
||||||
|
return flatAuthorityState;
|
||||||
}
|
}
|
||||||
|
|
||||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||||
|
|
@ -642,9 +1048,15 @@ internal static class CollisionTraversal
|
||||||
return graphState;
|
return graphState;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool UseFlat(PhysicsDataCache cache) =>
|
private static bool UseFlat(PhysicsDataCache cache)
|
||||||
cache.CollisionTraversalMode == CollisionTraversalMode.Flat ||
|
{
|
||||||
cache.CollisionShadow?.IsFlatPass == true;
|
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||||
|
if (shadow?.IsGraphPass == true)
|
||||||
|
return false;
|
||||||
|
if (shadow?.IsFlatPass == true)
|
||||||
|
return true;
|
||||||
|
return cache.CollisionTraversalMode == CollisionTraversalMode.Flat;
|
||||||
|
}
|
||||||
|
|
||||||
private static InvalidOperationException MissingFlat(string kind) =>
|
private static InvalidOperationException MissingFlat(string kind) =>
|
||||||
new(
|
new(
|
||||||
|
|
|
||||||
|
|
@ -143,8 +143,8 @@ public readonly record struct FlatPhysicsBspNode(
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Immutable, deterministic pre-order representation of a retail physics BSP.
|
/// Immutable, deterministic pre-order representation of a retail physics BSP.
|
||||||
/// It changes source storage only; traversal order and behavior remain defined
|
/// It changes source storage only; traversal order and behavior remain the
|
||||||
/// by the graph oracle until Slice I6.
|
/// retail port pinned by the graph differential oracle.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class FlatPhysicsBsp
|
public sealed class FlatPhysicsBsp
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,13 @@ namespace AcDream.Core.Physics;
|
||||||
/// Thread-safe cache of physics-relevant data extracted from GfxObj and Setup
|
/// Thread-safe cache of physics-relevant data extracted from GfxObj and Setup
|
||||||
/// dat objects during streaming and live-object materialization. Prepared
|
/// dat objects during streaming and live-object materialization. Prepared
|
||||||
/// payloads are built/read off the hot path; one update-thread publisher
|
/// payloads are built/read off the hot path; one update-thread publisher
|
||||||
/// installs each graph/flat pair. ConcurrentDictionary keeps diagnostics and
|
/// installs prepared production records and, during the I6 acceptance window,
|
||||||
/// tooling reads safe without a global lock.
|
/// their graph referee. ConcurrentDictionary keeps diagnostics and tooling
|
||||||
|
/// reads safe without a global lock.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PhysicsDataCache
|
public sealed class PhysicsDataCache
|
||||||
{
|
{
|
||||||
|
private readonly bool _requirePreparedCollision;
|
||||||
private readonly ConcurrentDictionary<uint, GfxObjPhysics> _gfxObj = new();
|
private readonly ConcurrentDictionary<uint, GfxObjPhysics> _gfxObj = new();
|
||||||
private readonly ConcurrentDictionary<uint, GfxObjVisualBounds> _visualBounds = new();
|
private readonly ConcurrentDictionary<uint, GfxObjVisualBounds> _visualBounds = new();
|
||||||
private readonly ConcurrentDictionary<uint, SetupPhysics> _setup = new();
|
private readonly ConcurrentDictionary<uint, SetupPhysics> _setup = new();
|
||||||
|
|
@ -32,7 +34,13 @@ public sealed class PhysicsDataCache
|
||||||
_flatEnvCell = new();
|
_flatEnvCell = new();
|
||||||
|
|
||||||
public PhysicsDataCache()
|
public PhysicsDataCache()
|
||||||
|
: this(requirePreparedCollision: false)
|
||||||
{
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private PhysicsDataCache(bool requirePreparedCollision)
|
||||||
|
{
|
||||||
|
_requirePreparedCollision = requirePreparedCollision;
|
||||||
if (PhysicsDiagnostics.CollisionShadowSampleEvery > 0)
|
if (PhysicsDiagnostics.CollisionShadowSampleEvery > 0)
|
||||||
{
|
{
|
||||||
CollisionShadow = new CollisionShadowVerifier(
|
CollisionShadow = new CollisionShadowVerifier(
|
||||||
|
|
@ -41,15 +49,29 @@ public sealed class PhysicsDataCache
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the gameplay cache with Slice I6's prepared flat collision
|
||||||
|
/// representation authoritative. The public constructor deliberately
|
||||||
|
/// remains the graph-oracle fixture seam until the I6 referee is removed.
|
||||||
|
/// </summary>
|
||||||
|
public static PhysicsDataCache CreateProduction()
|
||||||
|
{
|
||||||
|
var cache = new PhysicsDataCache(requirePreparedCollision: true)
|
||||||
|
{
|
||||||
|
CollisionTraversalMode = CollisionTraversalMode.Flat,
|
||||||
|
};
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
internal CollisionShadowVerifier? CollisionShadow { get; set; }
|
internal CollisionShadowVerifier? CollisionShadow { get; set; }
|
||||||
|
|
||||||
public CollisionShadowStats CollisionShadowStats =>
|
public CollisionShadowStats CollisionShadowStats =>
|
||||||
CollisionShadow?.Stats ?? default;
|
CollisionShadow?.Stats ?? default;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Slice I graph/flat differential selector. Production remains on the
|
/// Slice I graph/flat differential selector. Production is flat-authoritative
|
||||||
/// graph oracle until I6; tests use the flat value to run the same complete
|
/// from I6 onward. Graph remains an explicit fixture/oracle mode until the
|
||||||
/// resolver from cloned inputs.
|
/// referee is removed after acceptance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal CollisionTraversalMode CollisionTraversalMode { get; set; } =
|
internal CollisionTraversalMode CollisionTraversalMode { get; set; } =
|
||||||
CollisionTraversalMode.Graph;
|
CollisionTraversalMode.Graph;
|
||||||
|
|
@ -84,6 +106,15 @@ public sealed class PhysicsDataCache
|
||||||
GfxObj gfxObj,
|
GfxObj gfxObj,
|
||||||
FlatGfxObjCollisionAsset? prepared = null)
|
FlatGfxObjCollisionAsset? prepared = null)
|
||||||
{
|
{
|
||||||
|
if (_requirePreparedCollision &&
|
||||||
|
gfxObj.Flags.HasFlag(GfxObjFlags.HasPhysics) &&
|
||||||
|
gfxObj.PhysicsBSP?.Root is not null &&
|
||||||
|
prepared?.PhysicsBsp is null &&
|
||||||
|
!_flatGfxObj.ContainsKey(gfxObjId))
|
||||||
|
{
|
||||||
|
throw MissingPreparedCollision("GfxObj", gfxObjId);
|
||||||
|
}
|
||||||
|
|
||||||
if (prepared is not null)
|
if (prepared is not null)
|
||||||
_flatGfxObj.TryAdd(gfxObjId, prepared);
|
_flatGfxObj.TryAdd(gfxObjId, prepared);
|
||||||
|
|
||||||
|
|
@ -199,6 +230,11 @@ public sealed class PhysicsDataCache
|
||||||
Setup setup,
|
Setup setup,
|
||||||
FlatSetupCollision? prepared = null)
|
FlatSetupCollision? prepared = null)
|
||||||
{
|
{
|
||||||
|
if (_requirePreparedCollision &&
|
||||||
|
prepared is null &&
|
||||||
|
!_flatSetup.ContainsKey(setupId))
|
||||||
|
throw MissingPreparedCollision("Setup", setupId);
|
||||||
|
|
||||||
if (prepared is not null)
|
if (prepared is not null)
|
||||||
_flatSetup.TryAdd(setupId, prepared);
|
_flatSetup.TryAdd(setupId, prepared);
|
||||||
|
|
||||||
|
|
@ -234,6 +270,15 @@ public sealed class PhysicsDataCache
|
||||||
FlatCellStructureCollisionAsset? preparedStructure = null,
|
FlatCellStructureCollisionAsset? preparedStructure = null,
|
||||||
FlatEnvCellTopology? preparedTopology = null)
|
FlatEnvCellTopology? preparedTopology = null)
|
||||||
{
|
{
|
||||||
|
if (_requirePreparedCollision &&
|
||||||
|
preparedStructure is null &&
|
||||||
|
!_flatCellStruct.ContainsKey(envCellId))
|
||||||
|
throw MissingPreparedCollision("CellStruct", envCellId);
|
||||||
|
if (_requirePreparedCollision &&
|
||||||
|
preparedTopology is null &&
|
||||||
|
!_flatEnvCell.ContainsKey(envCellId))
|
||||||
|
throw MissingPreparedCollision("EnvCell topology", envCellId);
|
||||||
|
|
||||||
if (preparedStructure is not null)
|
if (preparedStructure is not null)
|
||||||
_flatCellStruct.TryAdd(envCellId, preparedStructure);
|
_flatCellStruct.TryAdd(envCellId, preparedStructure);
|
||||||
if (preparedTopology is not null)
|
if (preparedTopology is not null)
|
||||||
|
|
@ -242,7 +287,14 @@ public sealed class PhysicsDataCache
|
||||||
// UCG Stage 1: register in the unified graph for ALL cells — before the
|
// UCG Stage 1: register in the unified graph for ALL cells — before the
|
||||||
// idempotency + null-BSP guards below, so BSP-less cells are still included.
|
// idempotency + null-BSP guards below, so BSP-less cells are still included.
|
||||||
if (!CellGraph.Contains(envCellId))
|
if (!CellGraph.Contains(envCellId))
|
||||||
CellGraph.Add(UcgEnvCell.FromDat(envCellId, envCell, cellStruct, worldTransform));
|
{
|
||||||
|
CellGraph.Add(UcgEnvCell.FromDat(
|
||||||
|
envCellId,
|
||||||
|
envCell,
|
||||||
|
cellStruct,
|
||||||
|
worldTransform,
|
||||||
|
preparedStructure?.ContainmentBsp));
|
||||||
|
}
|
||||||
|
|
||||||
if (_cellStruct.ContainsKey(envCellId)) return;
|
if (_cellStruct.ContainsKey(envCellId)) return;
|
||||||
if (cellStruct.PhysicsBSP?.Root is null) return;
|
if (cellStruct.PhysicsBSP?.Root is null) return;
|
||||||
|
|
@ -455,6 +507,13 @@ public sealed class PhysicsDataCache
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static InvalidOperationException MissingPreparedCollision(
|
||||||
|
string kind,
|
||||||
|
uint sourceId) =>
|
||||||
|
new(
|
||||||
|
$"Production {kind} 0x{sourceId:X8} has no prepared collision asset. " +
|
||||||
|
"Gameplay must not extract or fall back to a parsed DAT graph.");
|
||||||
|
|
||||||
public GfxObjPhysics? GetGfxObj(uint id) => _gfxObj.TryGetValue(id, out var p) ? p : null;
|
public GfxObjPhysics? GetGfxObj(uint id) => _gfxObj.TryGetValue(id, out var p) ? p : null;
|
||||||
|
|
||||||
public SetupPhysics? GetSetup(uint id) => _setup.TryGetValue(id, out var p) ? p : null;
|
public SetupPhysics? GetSetup(uint id) => _setup.TryGetValue(id, out var p) ? p : null;
|
||||||
|
|
|
||||||
|
|
@ -509,6 +509,39 @@ public sealed class PhysicsEngine
|
||||||
|
|
||||||
float? best = null;
|
float? best = null;
|
||||||
float bestDist = float.MaxValue;
|
float bestDist = float.MaxValue;
|
||||||
|
FlatPhysicsBsp? flat = cp.FlatPhysicsBsp;
|
||||||
|
if (flat is not null)
|
||||||
|
{
|
||||||
|
FlatPolygonTable table = flat.PolygonTable;
|
||||||
|
for (int i = 0; i < table.Polygons.Length; i++)
|
||||||
|
{
|
||||||
|
FlatCollisionPolygon poly = table.Polygons[i];
|
||||||
|
Vector3 n = poly.Plane.Normal;
|
||||||
|
if (n.Z < PhysicsGlobals.FloorZ) continue;
|
||||||
|
if (!PointInPolygonXY(table, poly.VertexRange, local.X, local.Y))
|
||||||
|
continue;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DataCache!.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Production CellStruct 0x{cellId:X8} has no prepared physics BSP.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit graph-oracle fixture path. Production returns above.
|
||||||
foreach (var kv in cp.Resolved)
|
foreach (var kv in cp.Resolved)
|
||||||
{
|
{
|
||||||
var poly = kv.Value;
|
var poly = kv.Value;
|
||||||
|
|
@ -524,6 +557,28 @@ public sealed class PhysicsEngine
|
||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool PointInPolygonXY(
|
||||||
|
FlatPolygonTable table,
|
||||||
|
FlatIndexRange range,
|
||||||
|
float x,
|
||||||
|
float y)
|
||||||
|
{
|
||||||
|
bool inside = false;
|
||||||
|
int end = range.EndExclusive;
|
||||||
|
for (int i = range.Start, j = end - 1; i < end; j = i++)
|
||||||
|
{
|
||||||
|
Vector3 vi = table.Vertices[i];
|
||||||
|
Vector3 vj = table.Vertices[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>Even-odd XY-projection point-in-polygon test (cell-local frame).</summary>
|
/// <summary>Even-odd XY-projection point-in-polygon test (cell-local frame).</summary>
|
||||||
private static bool PointInPolygonXY(IReadOnlyList<Vector3> verts, float x, float y)
|
private static bool PointInPolygonXY(IReadOnlyList<Vector3> verts, float x, float y)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,11 @@ public static class ShadowShapeBuilder
|
||||||
foreach (var meshRef in meshRefs)
|
foreach (var meshRef in meshRefs)
|
||||||
{
|
{
|
||||||
var phys = getGfxObj(meshRef.GfxObjId);
|
var phys = getGfxObj(meshRef.GfxObjId);
|
||||||
if (phys?.BSP?.Root is null) continue; // no physics BSP → nothing to collide
|
if (phys is null) continue;
|
||||||
|
FlatPhysicsBsp? flat = phys.FlatPhysicsBsp;
|
||||||
|
bool hasFlat = flat is { RootIndex: >= 0 };
|
||||||
|
if (!hasFlat && phys.BSP?.Root is null)
|
||||||
|
continue; // graph-only fixture seam until I6 referee removal
|
||||||
|
|
||||||
// PartTransform is root-relative; decompose to local pos/rot/scale.
|
// PartTransform is root-relative; decompose to local pos/rot/scale.
|
||||||
if (!Matrix4x4.Decompose(meshRef.PartTransform,
|
if (!Matrix4x4.Decompose(meshRef.PartTransform,
|
||||||
|
|
@ -205,7 +209,9 @@ public static class ShadowShapeBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
float partScale = pScale.X > 0f ? pScale.X : 1f; // AC objects are uniformly scaled
|
float partScale = pScale.X > 0f ? pScale.X : 1f; // AC objects are uniformly scaled
|
||||||
float localRadius = phys.BoundingSphere?.Radius ?? 1f;
|
float localRadius = hasFlat
|
||||||
|
? flat!.Nodes[flat.RootIndex].BoundingSphere.Radius
|
||||||
|
: phys.BoundingSphere?.Radius ?? 1f;
|
||||||
|
|
||||||
shapes.Add(new ShadowShape(
|
shapes.Add(new ShadowShape(
|
||||||
GfxObjId: meshRef.GfxObjId,
|
GfxObjId: meshRef.GfxObjId,
|
||||||
|
|
|
||||||
|
|
@ -13,19 +13,30 @@ public sealed class EnvCell : ObjCell
|
||||||
/// <summary>Cell-containment BSP (retail CellStruct.CellBSP). Null => AABB fallback.</summary>
|
/// <summary>Cell-containment BSP (retail CellStruct.CellBSP). Null => AABB fallback.</summary>
|
||||||
public CellBSPTree? ContainmentBsp { get; }
|
public CellBSPTree? ContainmentBsp { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepared production containment tree. Slice I6 makes this path
|
||||||
|
/// authoritative while <see cref="ContainmentBsp"/> remains only as the
|
||||||
|
/// temporary graph referee/test fixture seam.
|
||||||
|
/// </summary>
|
||||||
|
public FlatCellContainmentBsp? FlatContainmentBsp { get; }
|
||||||
|
|
||||||
public EnvCell(uint id, Matrix4x4 worldTransform, Matrix4x4 inverseWorldTransform,
|
public EnvCell(uint id, Matrix4x4 worldTransform, Matrix4x4 inverseWorldTransform,
|
||||||
Vector3 localBoundsMin, Vector3 localBoundsMax,
|
Vector3 localBoundsMin, Vector3 localBoundsMax,
|
||||||
IReadOnlyList<CellPortal> portals, IReadOnlyList<uint> stabList,
|
IReadOnlyList<CellPortal> portals, IReadOnlyList<uint> stabList,
|
||||||
bool seenOutside, CellBSPTree? containmentBsp)
|
bool seenOutside, CellBSPTree? containmentBsp,
|
||||||
|
FlatCellContainmentBsp? flatContainmentBsp = null)
|
||||||
: base(id, worldTransform, inverseWorldTransform, localBoundsMin, localBoundsMax,
|
: base(id, worldTransform, inverseWorldTransform, localBoundsMin, localBoundsMax,
|
||||||
portals, stabList, seenOutside)
|
portals, stabList, seenOutside)
|
||||||
{
|
{
|
||||||
ContainmentBsp = containmentBsp;
|
ContainmentBsp = containmentBsp;
|
||||||
|
FlatContainmentBsp = flatContainmentBsp;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool PointInCell(Vector3 worldPoint)
|
public override bool PointInCell(Vector3 worldPoint)
|
||||||
{
|
{
|
||||||
var local = Vector3.Transform(worldPoint, InverseWorldTransform);
|
var local = Vector3.Transform(worldPoint, InverseWorldTransform);
|
||||||
|
if (FlatContainmentBsp is not null)
|
||||||
|
return FlatBspQuery.PointInsideCellBsp(FlatContainmentBsp, local);
|
||||||
if (ContainmentBsp?.Root is not null)
|
if (ContainmentBsp?.Root is not null)
|
||||||
return BSPQuery.PointInsideCellBsp(ContainmentBsp.Root, local); // BSPQuery.cs:1034
|
return BSPQuery.PointInsideCellBsp(ContainmentBsp.Root, local); // BSPQuery.cs:1034
|
||||||
return local.X >= LocalBoundsMin.X && local.X <= LocalBoundsMax.X
|
return local.X >= LocalBoundsMin.X && local.X <= LocalBoundsMax.X
|
||||||
|
|
@ -40,7 +51,8 @@ public sealed class EnvCell : ObjCell
|
||||||
/// (no +2 cm render lift).
|
/// (no +2 cm render lift).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static EnvCell FromDat(uint id, DatReaderWriter.DBObjs.EnvCell datCell,
|
public static EnvCell FromDat(uint id, DatReaderWriter.DBObjs.EnvCell datCell,
|
||||||
CellStruct cellStruct, Matrix4x4 worldTransform)
|
CellStruct cellStruct, Matrix4x4 worldTransform,
|
||||||
|
FlatCellContainmentBsp? flatContainmentBsp = null)
|
||||||
{
|
{
|
||||||
Matrix4x4.Invert(worldTransform, out var inverse);
|
Matrix4x4.Invert(worldTransform, out var inverse);
|
||||||
|
|
||||||
|
|
@ -72,7 +84,7 @@ public sealed class EnvCell : ObjCell
|
||||||
bool seenOutside = datCell.Flags.HasFlag(EnvCellFlags.SeenOutside);
|
bool seenOutside = datCell.Flags.HasFlag(EnvCellFlags.SeenOutside);
|
||||||
|
|
||||||
return new EnvCell(id, worldTransform, inverse, min, max, portals, stab,
|
return new EnvCell(id, worldTransform, inverse, min, max, portals, stab,
|
||||||
seenOutside, cellStruct.CellBSP);
|
seenOutside, cellStruct.CellBSP, flatContainmentBsp);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<Vector3> ResolvePortalPolygon(CellStruct cellStruct, ushort polygonId)
|
private static IReadOnlyList<Vector3> ResolvePortalPolygon(CellStruct cellStruct, ushort polygonId)
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,36 @@ public sealed class CollisionShadowVerifierTests
|
||||||
Assert.Equal("False", root.GetProperty("Flat").GetString());
|
Assert.Equal("False", root.GetProperty("Flat").GetString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FlatAuthority_MismatchKeepsFlatResultAndUsesGraphOnlyAsReferee()
|
||||||
|
{
|
||||||
|
string directory = NewArtifactDirectory();
|
||||||
|
PhysicsDataCache cache = CacheWithShadow(directory);
|
||||||
|
cache.CollisionTraversalMode = CollisionTraversalMode.Flat;
|
||||||
|
CellPhysics matching = MatchingCell(0xA9B4_0100u);
|
||||||
|
CellPhysics mismatched = WithFlatPhysics(
|
||||||
|
matching,
|
||||||
|
new FlatPhysicsBsp(
|
||||||
|
-1,
|
||||||
|
ImmutableArray<FlatPhysicsBspNode>.Empty,
|
||||||
|
ImmutableArray<int>.Empty,
|
||||||
|
FlatPolygonTable.Empty));
|
||||||
|
|
||||||
|
bool flatResult = CollisionTraversal.HasPhysics(cache, mismatched);
|
||||||
|
|
||||||
|
Assert.False(flatResult);
|
||||||
|
CollisionShadowStats stats = cache.CollisionShadowStats;
|
||||||
|
Assert.Equal(1, stats.Queries);
|
||||||
|
Assert.Equal(1, stats.Samples);
|
||||||
|
Assert.Equal(1, stats.Mismatches);
|
||||||
|
Assert.Equal(0, stats.Faults);
|
||||||
|
string artifact = Assert.Single(Directory.EnumerateFiles(directory));
|
||||||
|
using JsonDocument json = JsonDocument.Parse(File.ReadAllText(artifact));
|
||||||
|
JsonElement root = json.RootElement;
|
||||||
|
Assert.Equal("True", root.GetProperty("Graph").GetString());
|
||||||
|
Assert.Equal("False", root.GetProperty("Flat").GetString());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MissingFlat_IsRecordedAsFaultAndGraphRemainsAuthoritative()
|
public void MissingFlat_IsRecordedAsFaultAndGraphRemainsAuthoritative()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Tests.Physics;
|
||||||
|
|
||||||
|
public sealed class PhysicsDataCacheProductionTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CreateProduction_SelectsFlatTraversal()
|
||||||
|
{
|
||||||
|
PhysicsDataCache cache = PhysicsDataCache.CreateProduction();
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
CollisionTraversalMode.Flat,
|
||||||
|
cache.CollisionTraversalMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProductionSetupPublication_RejectsMissingPreparedAsset()
|
||||||
|
{
|
||||||
|
PhysicsDataCache cache = PhysicsDataCache.CreateProduction();
|
||||||
|
|
||||||
|
InvalidOperationException fault = Assert.Throws<InvalidOperationException>(
|
||||||
|
() => cache.CacheSetup(0x0200_0001u, new Setup()));
|
||||||
|
|
||||||
|
Assert.Contains(
|
||||||
|
"no prepared collision asset",
|
||||||
|
fault.Message,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GraphOracleFixture_StillAcceptsSourceSetup()
|
||||||
|
{
|
||||||
|
var cache = new PhysicsDataCache();
|
||||||
|
|
||||||
|
cache.CacheSetup(0x0200_0001u, new Setup());
|
||||||
|
|
||||||
|
Assert.NotNull(cache.GetSetup(0x0200_0001u));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProductionSetupPublication_AllowsIdempotentCachedFlatAsset()
|
||||||
|
{
|
||||||
|
PhysicsDataCache cache = PhysicsDataCache.CreateProduction();
|
||||||
|
var setup = new Setup();
|
||||||
|
var prepared = new FlatSetupCollision(
|
||||||
|
ImmutableArray<FlatCollisionCylinder>.Empty,
|
||||||
|
ImmutableArray<FlatCollisionSphere>.Empty,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0f);
|
||||||
|
|
||||||
|
cache.CacheSetup(0x0200_0001u, setup, prepared);
|
||||||
|
cache.CacheSetup(0x0200_0001u, setup);
|
||||||
|
|
||||||
|
Assert.Same(prepared, cache.GetFlatSetup(0x0200_0001u));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.World.Cells;
|
using AcDream.Core.World.Cells;
|
||||||
|
using DatReaderWriter.Enums;
|
||||||
|
using DatReaderWriter.Types;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using UcgCellPortal = AcDream.Core.World.Cells.CellPortal;
|
||||||
|
|
||||||
namespace AcDream.Core.Tests.World.Cells;
|
namespace AcDream.Core.Tests.World.Cells;
|
||||||
|
|
||||||
|
|
@ -11,7 +15,7 @@ public class EnvCellTests
|
||||||
var t = transform ?? Matrix4x4.Identity;
|
var t = transform ?? Matrix4x4.Identity;
|
||||||
Matrix4x4.Invert(t, out var inv);
|
Matrix4x4.Invert(t, out var inv);
|
||||||
return new EnvCell(0xA9B40174u, t, inv, min, max,
|
return new EnvCell(0xA9B40174u, t, inv, min, max,
|
||||||
System.Array.Empty<CellPortal>(), System.Array.Empty<uint>(),
|
System.Array.Empty<UcgCellPortal>(), System.Array.Empty<uint>(),
|
||||||
seenOutside: false, containmentBsp: null);
|
seenOutside: false, containmentBsp: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,4 +34,36 @@ public class EnvCellTests
|
||||||
Assert.True(c.PointInCell(new Vector3(105,5,5)));
|
Assert.True(c.PointInCell(new Vector3(105,5,5)));
|
||||||
Assert.False(c.PointInCell(new Vector3(5,5,5)));
|
Assert.False(c.PointInCell(new Vector3(5,5,5)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PointInCell_PreparedContainmentIsAuthoritative()
|
||||||
|
{
|
||||||
|
var graphRoot = new CellBSPNode
|
||||||
|
{
|
||||||
|
Type = BSPNodeType.BPIn,
|
||||||
|
SplittingPlane = new Plane(Vector3.UnitX, -1f),
|
||||||
|
PosNode = new CellBSPNode { Type = BSPNodeType.Leaf },
|
||||||
|
};
|
||||||
|
var flatRoot = new CellBSPNode
|
||||||
|
{
|
||||||
|
Type = BSPNodeType.Leaf,
|
||||||
|
LeafIndex = 1,
|
||||||
|
};
|
||||||
|
FlatCellContainmentBsp flat =
|
||||||
|
FlatCollisionAssetBuilder.FlattenCellContainmentBsp(flatRoot);
|
||||||
|
var cell = new EnvCell(
|
||||||
|
0xA9B4_0174u,
|
||||||
|
Matrix4x4.Identity,
|
||||||
|
Matrix4x4.Identity,
|
||||||
|
-Vector3.One,
|
||||||
|
Vector3.One,
|
||||||
|
Array.Empty<UcgCellPortal>(),
|
||||||
|
Array.Empty<uint>(),
|
||||||
|
seenOutside: false,
|
||||||
|
containmentBsp: new CellBSPTree { Root = graphRoot },
|
||||||
|
flatContainmentBsp: flat);
|
||||||
|
|
||||||
|
Assert.True(cell.PointInCell(Vector3.Zero));
|
||||||
|
Assert.False(BSPQuery.PointInsideCellBsp(graphRoot, Vector3.Zero));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue