feat(streaming): shadow-publish flat collision assets
Carry one immutable prepared collision closure with each accepted near-tier generation and install graph plus flat views through the same retained publication receipt. Apply the same strict package-only rule to live entities, add exact sampled graph-authoritative comparison artifacts and lifecycle counters, and prove cancellation, demotion, rehydrate, revisit, teardown, reconnect, and the nine-stop route with 14,064 zero-mismatch samples. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
f7ff9f4eea
commit
d9446030e6
32 changed files with 2777 additions and 92 deletions
555
src/AcDream.Core/Physics/CollisionShadowVerifier.cs
Normal file
555
src/AcDream.Core/Physics/CollisionShadowVerifier.cs
Normal file
|
|
@ -0,0 +1,555 @@
|
|||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Text.Json;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
||||
public readonly record struct CollisionShadowStats(
|
||||
long Queries,
|
||||
long Samples,
|
||||
long Matches,
|
||||
long Mismatches,
|
||||
long Faults);
|
||||
|
||||
/// <summary>
|
||||
/// Slice I5 sampled graph/flat referee. The graph result always remains
|
||||
/// authoritative. One retained clone receives the flat query first; the
|
||||
/// canonical transition is then evaluated by the graph path and compared
|
||||
/// field-for-field with exact float bits.
|
||||
/// </summary>
|
||||
internal sealed class CollisionShadowVerifier
|
||||
{
|
||||
private readonly int _sampleEvery;
|
||||
private readonly string _artifactDirectory;
|
||||
private readonly Transition _shadowTransition = new();
|
||||
private long _queries;
|
||||
private long _samples;
|
||||
private long _matches;
|
||||
private long _mismatches;
|
||||
private long _faults;
|
||||
private bool _flatPass;
|
||||
|
||||
public CollisionShadowVerifier(
|
||||
int sampleEvery,
|
||||
string artifactDirectory)
|
||||
{
|
||||
if (sampleEvery <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(sampleEvery));
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(artifactDirectory);
|
||||
_sampleEvery = sampleEvery;
|
||||
_artifactDirectory = Path.GetFullPath(artifactDirectory);
|
||||
}
|
||||
|
||||
internal bool IsFlatPass => _flatPass;
|
||||
|
||||
internal CollisionShadowStats Stats => new(
|
||||
_queries,
|
||||
_samples,
|
||||
_matches,
|
||||
_mismatches,
|
||||
_faults);
|
||||
|
||||
internal bool TrySample(out long sample)
|
||||
{
|
||||
if (_flatPass)
|
||||
{
|
||||
sample = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
long query = ++_queries;
|
||||
if (query % _sampleEvery != 0)
|
||||
{
|
||||
sample = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
sample = ++_samples;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal Transition PrepareShadow(Transition source)
|
||||
{
|
||||
_shadowTransition.CopyFrom(source);
|
||||
return _shadowTransition;
|
||||
}
|
||||
|
||||
internal void BeginFlatPass()
|
||||
{
|
||||
if (_flatPass)
|
||||
throw new InvalidOperationException(
|
||||
"Collision shadow flat passes cannot overlap.");
|
||||
_flatPass = true;
|
||||
}
|
||||
|
||||
internal void EndFlatPass()
|
||||
{
|
||||
if (!_flatPass)
|
||||
throw new InvalidOperationException(
|
||||
"No collision shadow flat pass is active.");
|
||||
_flatPass = false;
|
||||
}
|
||||
|
||||
internal void RecordBoolean(
|
||||
long sample,
|
||||
string kind,
|
||||
uint sourceId,
|
||||
bool graph,
|
||||
bool flat,
|
||||
string input)
|
||||
{
|
||||
if (graph == flat)
|
||||
{
|
||||
_matches++;
|
||||
return;
|
||||
}
|
||||
|
||||
RecordMismatch(
|
||||
sample,
|
||||
kind,
|
||||
sourceId,
|
||||
"Result",
|
||||
graph.ToString(),
|
||||
flat.ToString(),
|
||||
input);
|
||||
}
|
||||
|
||||
internal void RecordSphere(
|
||||
long sample,
|
||||
string kind,
|
||||
uint sourceId,
|
||||
FlatCollisionSphere graph,
|
||||
FlatCollisionSphere flat)
|
||||
{
|
||||
if (Exact(graph.Origin, flat.Origin) &&
|
||||
Exact(graph.Radius, flat.Radius))
|
||||
{
|
||||
_matches++;
|
||||
return;
|
||||
}
|
||||
|
||||
RecordMismatch(
|
||||
sample,
|
||||
kind,
|
||||
sourceId,
|
||||
"RootBoundingSphere",
|
||||
Format(graph),
|
||||
Format(flat),
|
||||
string.Empty);
|
||||
}
|
||||
|
||||
internal void RecordTransition(
|
||||
long sample,
|
||||
string kind,
|
||||
uint sourceId,
|
||||
TransitionState graphState,
|
||||
TransitionState flatState,
|
||||
Transition graph,
|
||||
Transition flat,
|
||||
string input)
|
||||
{
|
||||
if (graphState != flatState)
|
||||
{
|
||||
RecordMismatch(
|
||||
sample,
|
||||
kind,
|
||||
sourceId,
|
||||
"TransitionState",
|
||||
graphState.ToString(),
|
||||
flatState.ToString(),
|
||||
input);
|
||||
return;
|
||||
}
|
||||
|
||||
if (TransitionExactComparer.TryFindDifference(
|
||||
graph,
|
||||
flat,
|
||||
out string path,
|
||||
out string graphValue,
|
||||
out string flatValue))
|
||||
{
|
||||
RecordMismatch(
|
||||
sample,
|
||||
kind,
|
||||
sourceId,
|
||||
path,
|
||||
graphValue,
|
||||
flatValue,
|
||||
input);
|
||||
return;
|
||||
}
|
||||
|
||||
_matches++;
|
||||
}
|
||||
|
||||
internal void RecordFault(
|
||||
long sample,
|
||||
string kind,
|
||||
uint sourceId,
|
||||
Exception fault,
|
||||
string input)
|
||||
{
|
||||
_faults++;
|
||||
WriteArtifact(new CollisionShadowMismatchArtifact(
|
||||
1,
|
||||
sample,
|
||||
kind,
|
||||
$"0x{sourceId:X8}",
|
||||
"FlatFault",
|
||||
"graph-authoritative",
|
||||
$"{fault.GetType().FullName}: {fault.Message}",
|
||||
input));
|
||||
}
|
||||
|
||||
private void RecordMismatch(
|
||||
long sample,
|
||||
string kind,
|
||||
uint sourceId,
|
||||
string difference,
|
||||
string graph,
|
||||
string flat,
|
||||
string input)
|
||||
{
|
||||
_mismatches++;
|
||||
WriteArtifact(new CollisionShadowMismatchArtifact(
|
||||
1,
|
||||
sample,
|
||||
kind,
|
||||
$"0x{sourceId:X8}",
|
||||
difference,
|
||||
graph,
|
||||
flat,
|
||||
input));
|
||||
}
|
||||
|
||||
private void WriteArtifact(CollisionShadowMismatchArtifact artifact)
|
||||
{
|
||||
Directory.CreateDirectory(_artifactDirectory);
|
||||
string path = Path.Combine(
|
||||
_artifactDirectory,
|
||||
FormattableString.Invariant(
|
||||
$"collision-shadow-{artifact.Sample:D8}.json"));
|
||||
string json = JsonSerializer.Serialize(
|
||||
artifact,
|
||||
CollisionShadowJsonContext.Default
|
||||
.CollisionShadowMismatchArtifact);
|
||||
File.WriteAllText(path, json);
|
||||
}
|
||||
|
||||
internal static string FormatInput(
|
||||
Vector3 center,
|
||||
float radius) =>
|
||||
$"center={Format(center)};radius={Bits(radius)}";
|
||||
|
||||
internal static string FormatInput(
|
||||
Vector3 center,
|
||||
float radius,
|
||||
bool hasSphere1,
|
||||
Vector3 sphere1Center,
|
||||
float sphere1Radius,
|
||||
Vector3 currentCenter,
|
||||
Vector3 localSpaceZ,
|
||||
float scale,
|
||||
Quaternion localToWorld,
|
||||
Vector3 worldOrigin) =>
|
||||
$"sphere0={Format(center)}/{Bits(radius)};" +
|
||||
$"sphere1={(hasSphere1 ? Format(sphere1Center) : "none")}/" +
|
||||
$"{Bits(sphere1Radius)};current={Format(currentCenter)};" +
|
||||
$"localZ={Format(localSpaceZ)};scale={Bits(scale)};" +
|
||||
$"rotation={Format(localToWorld)};origin={Format(worldOrigin)}";
|
||||
|
||||
private static string Format(FlatCollisionSphere sphere) =>
|
||||
$"{Format(sphere.Origin)}/{Bits(sphere.Radius)}";
|
||||
|
||||
private static string Format(Vector3 value) =>
|
||||
$"{Bits(value.X)},{Bits(value.Y)},{Bits(value.Z)}";
|
||||
|
||||
private static string Format(Quaternion value) =>
|
||||
$"{Bits(value.X)},{Bits(value.Y)},{Bits(value.Z)},{Bits(value.W)}";
|
||||
|
||||
private static string Bits(float value) =>
|
||||
$"0x{BitConverter.SingleToInt32Bits(value):X8}";
|
||||
|
||||
private static bool Exact(float left, float right) =>
|
||||
BitConverter.SingleToInt32Bits(left) ==
|
||||
BitConverter.SingleToInt32Bits(right);
|
||||
|
||||
private static bool Exact(Vector3 left, Vector3 right) =>
|
||||
Exact(left.X, right.X) &&
|
||||
Exact(left.Y, right.Y) &&
|
||||
Exact(left.Z, right.Z);
|
||||
}
|
||||
|
||||
internal sealed record CollisionShadowMismatchArtifact(
|
||||
int SchemaVersion,
|
||||
long Sample,
|
||||
string Kind,
|
||||
string SourceId,
|
||||
string Difference,
|
||||
string Graph,
|
||||
string Flat,
|
||||
string Input);
|
||||
|
||||
[System.Text.Json.Serialization.JsonSerializable(
|
||||
typeof(CollisionShadowMismatchArtifact))]
|
||||
internal sealed partial class CollisionShadowJsonContext :
|
||||
System.Text.Json.Serialization.JsonSerializerContext;
|
||||
|
||||
internal static class TransitionExactComparer
|
||||
{
|
||||
internal static bool TryFindDifference(
|
||||
Transition graph,
|
||||
Transition flat,
|
||||
out string path,
|
||||
out string graphValue,
|
||||
out string flatValue)
|
||||
{
|
||||
if (CompareObjectInfo(
|
||||
graph.ObjectInfo,
|
||||
flat.ObjectInfo,
|
||||
out path,
|
||||
out graphValue,
|
||||
out flatValue))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CompareSpherePath(
|
||||
graph.SpherePath,
|
||||
flat.SpherePath,
|
||||
out path,
|
||||
out graphValue,
|
||||
out flatValue))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return CompareCollisionInfo(
|
||||
graph.CollisionInfo,
|
||||
flat.CollisionInfo,
|
||||
out path,
|
||||
out graphValue,
|
||||
out flatValue);
|
||||
}
|
||||
|
||||
private static bool CompareObjectInfo(
|
||||
ObjectInfo a,
|
||||
ObjectInfo b,
|
||||
out string path,
|
||||
out string av,
|
||||
out string bv)
|
||||
{
|
||||
if (Different("ObjectInfo.State", a.State, b.State, out path, out av, out bv)) return true;
|
||||
if (Different("ObjectInfo.StepUpHeight", a.StepUpHeight, b.StepUpHeight, out path, out av, out bv)) return true;
|
||||
if (Different("ObjectInfo.StepDownHeight", a.StepDownHeight, b.StepDownHeight, out path, out av, out bv)) return true;
|
||||
if (Different("ObjectInfo.Ethereal", a.Ethereal, b.Ethereal, out path, out av, out bv)) return true;
|
||||
if (Different("ObjectInfo.StepDown", a.StepDown, b.StepDown, out path, out av, out bv)) return true;
|
||||
if (Different("ObjectInfo.Scale", a.Scale, b.Scale, out path, out av, out bv)) return true;
|
||||
if (Different("ObjectInfo.MoverPhysicsState", a.MoverPhysicsState, b.MoverPhysicsState, out path, out av, out bv)) return true;
|
||||
if (Different("ObjectInfo.TargetId", a.TargetId, b.TargetId, out path, out av, out bv)) return true;
|
||||
if (Different("ObjectInfo.SelfEntityId", a.SelfEntityId, b.SelfEntityId, out path, out av, out bv)) return true;
|
||||
if (Different("ObjectInfo.MoverHasGravity", a.MoverHasGravity, b.MoverHasGravity, out path, out av, out bv)) return true;
|
||||
if (Different("ObjectInfo.VelocityKilled", a.VelocityKilled, b.VelocityKilled, out path, out av, out bv)) return true;
|
||||
return Equal(out path, out av, out bv);
|
||||
}
|
||||
|
||||
private static bool CompareCollisionInfo(
|
||||
CollisionInfo a,
|
||||
CollisionInfo b,
|
||||
out string path,
|
||||
out string av,
|
||||
out string bv)
|
||||
{
|
||||
if (Different("CollisionInfo.ContactPlaneValid", a.ContactPlaneValid, b.ContactPlaneValid, out path, out av, out bv)) return true;
|
||||
if (Different("CollisionInfo.ContactPlane", a.ContactPlane, b.ContactPlane, out path, out av, out bv)) return true;
|
||||
if (Different("CollisionInfo.ContactPlaneCellId", a.ContactPlaneCellId, b.ContactPlaneCellId, out path, out av, out bv)) return true;
|
||||
if (Different("CollisionInfo.ContactPlaneIsWater", a.ContactPlaneIsWater, b.ContactPlaneIsWater, out path, out av, out bv)) return true;
|
||||
if (Different("CollisionInfo.LastKnownContactPlaneValid", a.LastKnownContactPlaneValid, b.LastKnownContactPlaneValid, out path, out av, out bv)) return true;
|
||||
if (Different("CollisionInfo.LastKnownContactPlane", a.LastKnownContactPlane, b.LastKnownContactPlane, out path, out av, out bv)) return true;
|
||||
if (Different("CollisionInfo.LastKnownContactPlaneCellId", a.LastKnownContactPlaneCellId, b.LastKnownContactPlaneCellId, out path, out av, out bv)) return true;
|
||||
if (Different("CollisionInfo.LastKnownContactPlaneIsWater", a.LastKnownContactPlaneIsWater, b.LastKnownContactPlaneIsWater, out path, out av, out bv)) return true;
|
||||
if (Different("CollisionInfo.SlidingNormalValid", a.SlidingNormalValid, b.SlidingNormalValid, out path, out av, out bv)) return true;
|
||||
if (Different("CollisionInfo.SlidingNormal", a.SlidingNormal, b.SlidingNormal, out path, out av, out bv)) return true;
|
||||
if (Different("CollisionInfo.CollisionNormalValid", a.CollisionNormalValid, b.CollisionNormalValid, out path, out av, out bv)) return true;
|
||||
if (Different("CollisionInfo.CollisionNormal", a.CollisionNormal, b.CollisionNormal, out path, out av, out bv)) return true;
|
||||
if (Different("CollisionInfo.CollidedWithEnvironment", a.CollidedWithEnvironment, b.CollidedWithEnvironment, out path, out av, out bv)) return true;
|
||||
if (Different("CollisionInfo.FramesStationaryFall", a.FramesStationaryFall, b.FramesStationaryFall, out path, out av, out bv)) return true;
|
||||
if (Different("CollisionInfo.AdjustOffset", a.AdjustOffset, b.AdjustOffset, out path, out av, out bv)) return true;
|
||||
if (Different("CollisionInfo.LastCollidedObjectGuid", a.LastCollidedObjectGuid, b.LastCollidedObjectGuid, out path, out av, out bv)) return true;
|
||||
if (Different("CollisionInfo.ContactPlaneWriteCount", a.ContactPlaneWriteCount, b.ContactPlaneWriteCount, out path, out av, out bv)) return true;
|
||||
if (DifferentList("CollisionInfo.CollideObjectGuids", a.CollideObjectGuids, b.CollideObjectGuids, out path, out av, out bv)) return true;
|
||||
return Equal(out path, out av, out bv);
|
||||
}
|
||||
|
||||
private static bool CompareSpherePath(
|
||||
SpherePath a,
|
||||
SpherePath b,
|
||||
out string path,
|
||||
out string av,
|
||||
out string bv)
|
||||
{
|
||||
if (Different("SpherePath.NumSphere", a.NumSphere, b.NumSphere, out path, out av, out bv)) return true;
|
||||
if (DifferentSpheres("SpherePath.LocalSphere", a.LocalSphere, b.LocalSphere, out path, out av, out bv)) return true;
|
||||
if (DifferentSpheres("SpherePath.GlobalSphere", a.GlobalSphere, b.GlobalSphere, out path, out av, out bv)) return true;
|
||||
if (DifferentSpheres("SpherePath.GlobalCurrCenter", a.GlobalCurrCenter, b.GlobalCurrCenter, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.BeginPos", a.BeginPos, b.BeginPos, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.EndPos", a.EndPos, b.EndPos, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.CurPos", a.CurPos, b.CurPos, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.CheckPos", a.CheckPos, b.CheckPos, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.BeginOrientation", a.BeginOrientation, b.BeginOrientation, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.EndOrientation", a.EndOrientation, b.EndOrientation, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.CurOrientation", a.CurOrientation, b.CurOrientation, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.CheckOrientation", a.CheckOrientation, b.CheckOrientation, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.CurCellId", a.CurCellId, b.CurCellId, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.CheckCellId", a.CheckCellId, b.CheckCellId, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.CarriedBlockOrigin", a.CarriedBlockOrigin, b.CarriedBlockOrigin, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.GlobalOffset", a.GlobalOffset, b.GlobalOffset, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.StepUp", a.StepUp, b.StepUp, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.StepUpNormal", a.StepUpNormal, b.StepUpNormal, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.Collide", a.Collide, b.Collide, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.StepDown", a.StepDown, b.StepDown, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.StepDownAmt", a.StepDownAmt, b.StepDownAmt, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.WalkInterp", a.WalkInterp, b.WalkInterp, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.WalkableValid", a.WalkableValid, b.WalkableValid, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.WalkablePlane", a.WalkablePlane, b.WalkablePlane, out path, out av, out bv)) return true;
|
||||
if (DifferentVectors("SpherePath.WalkableVertices", a.WalkableVertices, b.WalkableVertices, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.WalkableUp", a.WalkableUp, b.WalkableUp, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.WalkableAllowance", a.WalkableAllowance, b.WalkableAllowance, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.LastWalkableValid", a.LastWalkableValid, b.LastWalkableValid, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.LastWalkablePlane", a.LastWalkablePlane, b.LastWalkablePlane, out path, out av, out bv)) return true;
|
||||
if (DifferentVectors("SpherePath.LastWalkableVertices", a.LastWalkableVertices, b.LastWalkableVertices, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.LastWalkableUp", a.LastWalkableUp, b.LastWalkableUp, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.BackupCheckPos", a.BackupCheckPos, b.BackupCheckPos, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.BackupCheckCellId", a.BackupCheckCellId, b.BackupCheckCellId, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.NegPolyHit", a.NegPolyHit, b.NegPolyHit, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.NegStepUp", a.NegStepUp, b.NegStepUp, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.NegCollisionNormal", a.NegCollisionNormal, b.NegCollisionNormal, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.CheckWalkable", a.CheckWalkable, b.CheckWalkable, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.InsertType", a.InsertType, b.InsertType, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.PlacementAllowsSliding", a.PlacementAllowsSliding, b.PlacementAllowsSliding, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.ObstructionEthereal", a.ObstructionEthereal, b.ObstructionEthereal, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.BldgCheck", a.BldgCheck, b.BldgCheck, out path, out av, out bv)) return true;
|
||||
if (Different("SpherePath.HitsInteriorCell", a.HitsInteriorCell, b.HitsInteriorCell, out path, out av, out bv)) return true;
|
||||
if (DifferentList("SpherePath.CellCandidates", a.CellCandidates.OrderedIds, b.CellCandidates.OrderedIds, out path, out av, out bv)) return true;
|
||||
return Equal(out path, out av, out bv);
|
||||
}
|
||||
|
||||
private static bool DifferentSpheres(
|
||||
string pathPrefix,
|
||||
Sphere[] a,
|
||||
Sphere[] b,
|
||||
out string path,
|
||||
out string av,
|
||||
out string bv)
|
||||
{
|
||||
for (int i = 0; i < a.Length; i++)
|
||||
{
|
||||
if (Different($"{pathPrefix}[{i}].Origin", a[i].Origin, b[i].Origin, out path, out av, out bv)) return true;
|
||||
if (Different($"{pathPrefix}[{i}].Radius", a[i].Radius, b[i].Radius, out path, out av, out bv)) return true;
|
||||
}
|
||||
return Equal(out path, out av, out bv);
|
||||
}
|
||||
|
||||
private static bool DifferentVectors(
|
||||
string pathPrefix,
|
||||
Vector3[]? a,
|
||||
Vector3[]? b,
|
||||
out string path,
|
||||
out string av,
|
||||
out string bv)
|
||||
{
|
||||
if (a is null || b is null)
|
||||
{
|
||||
if (a is null && b is null)
|
||||
return Equal(out path, out av, out bv);
|
||||
return Difference(pathPrefix, a is null ? "null" : "array", b is null ? "null" : "array", out path, out av, out bv);
|
||||
}
|
||||
if (a.Length != b.Length)
|
||||
return Difference(pathPrefix + ".Length", a.Length.ToString(CultureInfo.InvariantCulture), b.Length.ToString(CultureInfo.InvariantCulture), out path, out av, out bv);
|
||||
for (int i = 0; i < a.Length; i++)
|
||||
if (Different($"{pathPrefix}[{i}]", a[i], b[i], out path, out av, out bv)) return true;
|
||||
return Equal(out path, out av, out bv);
|
||||
}
|
||||
|
||||
private static bool DifferentList<T>(
|
||||
string pathPrefix,
|
||||
IReadOnlyList<T> a,
|
||||
IReadOnlyList<T> b,
|
||||
out string path,
|
||||
out string av,
|
||||
out string bv)
|
||||
{
|
||||
if (a.Count != b.Count)
|
||||
return Difference(pathPrefix + ".Count", a.Count.ToString(CultureInfo.InvariantCulture), b.Count.ToString(CultureInfo.InvariantCulture), out path, out av, out bv);
|
||||
for (int i = 0; i < a.Count; i++)
|
||||
if (!EqualityComparer<T>.Default.Equals(a[i], b[i]))
|
||||
return Difference($"{pathPrefix}[{i}]", Format(a[i]), Format(b[i]), out path, out av, out bv);
|
||||
return Equal(out path, out av, out bv);
|
||||
}
|
||||
|
||||
private static bool Different<T>(
|
||||
string candidatePath,
|
||||
T a,
|
||||
T b,
|
||||
out string path,
|
||||
out string av,
|
||||
out string bv)
|
||||
{
|
||||
if (Exact(a, b))
|
||||
return Equal(out path, out av, out bv);
|
||||
return Difference(candidatePath, Format(a), Format(b), out path, out av, out bv);
|
||||
}
|
||||
|
||||
private static bool Exact<T>(T a, T b)
|
||||
{
|
||||
if (a is float af && b is float bf)
|
||||
return BitConverter.SingleToInt32Bits(af) == BitConverter.SingleToInt32Bits(bf);
|
||||
if (a is Vector3 av && b is Vector3 bv)
|
||||
return Exact(av.X, bv.X) && Exact(av.Y, bv.Y) && Exact(av.Z, bv.Z);
|
||||
if (a is Quaternion aq && b is Quaternion bq)
|
||||
return Exact(aq.X, bq.X) && Exact(aq.Y, bq.Y) &&
|
||||
Exact(aq.Z, bq.Z) && Exact(aq.W, bq.W);
|
||||
if (a is Plane ap && b is Plane bp)
|
||||
return Exact(ap.Normal, bp.Normal) && Exact(ap.D, bp.D);
|
||||
return EqualityComparer<T>.Default.Equals(a, b);
|
||||
}
|
||||
|
||||
private static string Format<T>(T value) => value switch
|
||||
{
|
||||
null => "null",
|
||||
float f => $"0x{BitConverter.SingleToInt32Bits(f):X8}",
|
||||
Vector3 v => $"{Format(v.X)},{Format(v.Y)},{Format(v.Z)}",
|
||||
Quaternion q => $"{Format(q.X)},{Format(q.Y)},{Format(q.Z)},{Format(q.W)}",
|
||||
Plane p => $"{Format(p.Normal)}/{Format(p.D)}",
|
||||
IFormattable formattable => formattable.ToString(
|
||||
null,
|
||||
CultureInfo.InvariantCulture),
|
||||
_ => value.ToString() ?? string.Empty,
|
||||
};
|
||||
|
||||
private static bool Difference(
|
||||
string candidatePath,
|
||||
string a,
|
||||
string b,
|
||||
out string path,
|
||||
out string av,
|
||||
out string bv)
|
||||
{
|
||||
path = candidatePath;
|
||||
av = a;
|
||||
bv = b;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool Equal(
|
||||
out string path,
|
||||
out string av,
|
||||
out string bv)
|
||||
{
|
||||
path = string.Empty;
|
||||
av = string.Empty;
|
||||
bv = string.Empty;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -23,49 +23,172 @@ internal static class CollisionTraversal
|
|||
PhysicsDataCache cache,
|
||||
CellPhysics cell)
|
||||
{
|
||||
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
||||
if (UseFlat(cache))
|
||||
{
|
||||
FlatCellContainmentBsp flat = cell.FlatContainmentBsp ??
|
||||
throw MissingFlat("cell containment");
|
||||
return flat.RootIndex >= 0;
|
||||
}
|
||||
|
||||
return cell.CellBSP?.Root is not null;
|
||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||
if (shadow is null || !shadow.TrySample(out long sample))
|
||||
return cell.CellBSP?.Root is not null;
|
||||
|
||||
bool flatResult = false;
|
||||
Exception? flatFault = null;
|
||||
shadow.BeginFlatPass();
|
||||
try
|
||||
{
|
||||
flatResult = (cell.FlatContainmentBsp ??
|
||||
throw MissingFlat("cell containment")).RootIndex >= 0;
|
||||
}
|
||||
catch (Exception fault)
|
||||
{
|
||||
flatFault = fault;
|
||||
}
|
||||
finally
|
||||
{
|
||||
shadow.EndFlatPass();
|
||||
}
|
||||
|
||||
bool graphResult = cell.CellBSP?.Root is not null;
|
||||
if (flatFault is null)
|
||||
{
|
||||
shadow.RecordBoolean(
|
||||
sample,
|
||||
"HasCellContainment",
|
||||
cell.SourceId,
|
||||
graphResult,
|
||||
flatResult,
|
||||
string.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
shadow.RecordFault(
|
||||
sample,
|
||||
"HasCellContainment",
|
||||
cell.SourceId,
|
||||
flatFault,
|
||||
string.Empty);
|
||||
}
|
||||
return graphResult;
|
||||
}
|
||||
|
||||
internal static bool HasPhysics(
|
||||
PhysicsDataCache cache,
|
||||
CellPhysics cell)
|
||||
{
|
||||
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
||||
if (UseFlat(cache))
|
||||
{
|
||||
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
||||
throw MissingFlat("cell physics");
|
||||
return flat.RootIndex >= 0;
|
||||
}
|
||||
|
||||
return cell.BSP?.Root is not null;
|
||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||
if (shadow is null || !shadow.TrySample(out long sample))
|
||||
return cell.BSP?.Root is not null;
|
||||
|
||||
bool flatResult = false;
|
||||
Exception? flatFault = null;
|
||||
shadow.BeginFlatPass();
|
||||
try
|
||||
{
|
||||
flatResult = (cell.FlatPhysicsBsp ??
|
||||
throw MissingFlat("cell physics")).RootIndex >= 0;
|
||||
}
|
||||
catch (Exception fault)
|
||||
{
|
||||
flatFault = fault;
|
||||
}
|
||||
finally
|
||||
{
|
||||
shadow.EndFlatPass();
|
||||
}
|
||||
|
||||
bool graphResult = cell.BSP?.Root is not null;
|
||||
if (flatFault is null)
|
||||
{
|
||||
shadow.RecordBoolean(
|
||||
sample,
|
||||
"HasCellPhysics",
|
||||
cell.SourceId,
|
||||
graphResult,
|
||||
flatResult,
|
||||
string.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
shadow.RecordFault(
|
||||
sample,
|
||||
"HasCellPhysics",
|
||||
cell.SourceId,
|
||||
flatFault,
|
||||
string.Empty);
|
||||
}
|
||||
return graphResult;
|
||||
}
|
||||
|
||||
internal static bool HasPhysics(
|
||||
PhysicsDataCache cache,
|
||||
GfxObjPhysics gfxObject)
|
||||
{
|
||||
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
||||
if (UseFlat(cache))
|
||||
{
|
||||
FlatPhysicsBsp flat = gfxObject.FlatPhysicsBsp ??
|
||||
throw MissingFlat("GfxObj physics");
|
||||
return flat.RootIndex >= 0;
|
||||
}
|
||||
|
||||
return gfxObject.BSP.Root is not null;
|
||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||
if (shadow is null || !shadow.TrySample(out long sample))
|
||||
return gfxObject.BSP.Root is not null;
|
||||
|
||||
bool flatResult = false;
|
||||
Exception? flatFault = null;
|
||||
shadow.BeginFlatPass();
|
||||
try
|
||||
{
|
||||
flatResult = (gfxObject.FlatPhysicsBsp ??
|
||||
throw MissingFlat("GfxObj physics")).RootIndex >= 0;
|
||||
}
|
||||
catch (Exception fault)
|
||||
{
|
||||
flatFault = fault;
|
||||
}
|
||||
finally
|
||||
{
|
||||
shadow.EndFlatPass();
|
||||
}
|
||||
|
||||
bool graphResult = gfxObject.BSP.Root is not null;
|
||||
if (flatFault is null)
|
||||
{
|
||||
shadow.RecordBoolean(
|
||||
sample,
|
||||
"HasGfxObjPhysics",
|
||||
gfxObject.SourceId,
|
||||
graphResult,
|
||||
flatResult,
|
||||
string.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
shadow.RecordFault(
|
||||
sample,
|
||||
"HasGfxObjPhysics",
|
||||
gfxObject.SourceId,
|
||||
flatFault,
|
||||
string.Empty);
|
||||
}
|
||||
return graphResult;
|
||||
}
|
||||
|
||||
internal static FlatCollisionSphere RootBoundingSphere(
|
||||
PhysicsDataCache cache,
|
||||
CellPhysics cell)
|
||||
{
|
||||
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
||||
if (UseFlat(cache))
|
||||
{
|
||||
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
||||
throw MissingFlat("cell physics");
|
||||
|
|
@ -73,7 +196,39 @@ internal static class CollisionTraversal
|
|||
}
|
||||
|
||||
DatReaderWriter.Types.Sphere sphere = cell.BSP!.Root!.BoundingSphere;
|
||||
return new FlatCollisionSphere(sphere.Origin, sphere.Radius);
|
||||
var graphResult =
|
||||
new FlatCollisionSphere(sphere.Origin, sphere.Radius);
|
||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||
if (shadow is null || !shadow.TrySample(out long sample))
|
||||
return graphResult;
|
||||
|
||||
try
|
||||
{
|
||||
shadow.BeginFlatPass();
|
||||
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
||||
throw MissingFlat("cell physics");
|
||||
FlatCollisionSphere flatResult =
|
||||
flat.Nodes[flat.RootIndex].BoundingSphere;
|
||||
shadow.EndFlatPass();
|
||||
shadow.RecordSphere(
|
||||
sample,
|
||||
"RootBoundingSphere",
|
||||
cell.SourceId,
|
||||
graphResult,
|
||||
flatResult);
|
||||
}
|
||||
catch (Exception fault)
|
||||
{
|
||||
if (shadow.IsFlatPass)
|
||||
shadow.EndFlatPass();
|
||||
shadow.RecordFault(
|
||||
sample,
|
||||
"RootBoundingSphere",
|
||||
cell.SourceId,
|
||||
fault,
|
||||
string.Empty);
|
||||
}
|
||||
return graphResult;
|
||||
}
|
||||
|
||||
internal static bool PointInsideCell(
|
||||
|
|
@ -81,14 +236,63 @@ internal static class CollisionTraversal
|
|||
CellPhysics cell,
|
||||
Vector3 localPoint)
|
||||
{
|
||||
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
||||
if (UseFlat(cache))
|
||||
{
|
||||
FlatCellContainmentBsp flat = cell.FlatContainmentBsp ??
|
||||
throw MissingFlat("cell containment");
|
||||
return FlatBspQuery.PointInsideCellBsp(flat, localPoint);
|
||||
}
|
||||
|
||||
return BSPQuery.PointInsideCellBsp(cell.CellBSP?.Root, localPoint);
|
||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||
if (shadow is null || !shadow.TrySample(out long sample))
|
||||
return BSPQuery.PointInsideCellBsp(
|
||||
cell.CellBSP?.Root,
|
||||
localPoint);
|
||||
|
||||
bool flatResult = false;
|
||||
Exception? flatFault = null;
|
||||
shadow.BeginFlatPass();
|
||||
try
|
||||
{
|
||||
flatResult = FlatBspQuery.PointInsideCellBsp(
|
||||
cell.FlatContainmentBsp ??
|
||||
throw MissingFlat("cell containment"),
|
||||
localPoint);
|
||||
}
|
||||
catch (Exception fault)
|
||||
{
|
||||
flatFault = fault;
|
||||
}
|
||||
finally
|
||||
{
|
||||
shadow.EndFlatPass();
|
||||
}
|
||||
bool graphResult = BSPQuery.PointInsideCellBsp(
|
||||
cell.CellBSP?.Root,
|
||||
localPoint);
|
||||
string input = CollisionShadowVerifier.FormatInput(
|
||||
localPoint,
|
||||
0f);
|
||||
if (flatFault is null)
|
||||
{
|
||||
shadow.RecordBoolean(
|
||||
sample,
|
||||
"PointInsideCell",
|
||||
cell.SourceId,
|
||||
graphResult,
|
||||
flatResult,
|
||||
input);
|
||||
}
|
||||
else
|
||||
{
|
||||
shadow.RecordFault(
|
||||
sample,
|
||||
"PointInsideCell",
|
||||
cell.SourceId,
|
||||
flatFault,
|
||||
input);
|
||||
}
|
||||
return graphResult;
|
||||
}
|
||||
|
||||
internal static bool SphereIntersectsCell(
|
||||
|
|
@ -97,7 +301,7 @@ internal static class CollisionTraversal
|
|||
Vector3 localCenter,
|
||||
float radius)
|
||||
{
|
||||
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
||||
if (UseFlat(cache))
|
||||
{
|
||||
FlatCellContainmentBsp flat = cell.FlatContainmentBsp ??
|
||||
throw MissingFlat("cell containment");
|
||||
|
|
@ -107,10 +311,61 @@ internal static class CollisionTraversal
|
|||
radius);
|
||||
}
|
||||
|
||||
return BSPQuery.SphereIntersectsCellBsp(
|
||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||
if (shadow is null || !shadow.TrySample(out long sample))
|
||||
{
|
||||
return BSPQuery.SphereIntersectsCellBsp(
|
||||
cell.CellBSP?.Root,
|
||||
localCenter,
|
||||
radius);
|
||||
}
|
||||
|
||||
bool flatResult = false;
|
||||
Exception? flatFault = null;
|
||||
shadow.BeginFlatPass();
|
||||
try
|
||||
{
|
||||
flatResult = FlatBspQuery.SphereIntersectsCellBsp(
|
||||
cell.FlatContainmentBsp ??
|
||||
throw MissingFlat("cell containment"),
|
||||
localCenter,
|
||||
radius);
|
||||
}
|
||||
catch (Exception fault)
|
||||
{
|
||||
flatFault = fault;
|
||||
}
|
||||
finally
|
||||
{
|
||||
shadow.EndFlatPass();
|
||||
}
|
||||
bool graphResult = BSPQuery.SphereIntersectsCellBsp(
|
||||
cell.CellBSP?.Root,
|
||||
localCenter,
|
||||
radius);
|
||||
string input = CollisionShadowVerifier.FormatInput(
|
||||
localCenter,
|
||||
radius);
|
||||
if (flatFault is null)
|
||||
{
|
||||
shadow.RecordBoolean(
|
||||
sample,
|
||||
"SphereIntersectsCell",
|
||||
cell.SourceId,
|
||||
graphResult,
|
||||
flatResult,
|
||||
input);
|
||||
}
|
||||
else
|
||||
{
|
||||
shadow.RecordFault(
|
||||
sample,
|
||||
"SphereIntersectsCell",
|
||||
cell.SourceId,
|
||||
flatFault,
|
||||
input);
|
||||
}
|
||||
return graphResult;
|
||||
}
|
||||
|
||||
internal static TransitionState FindCollisions(
|
||||
|
|
@ -129,7 +384,7 @@ internal static class CollisionTraversal
|
|||
PhysicsEngine? engine,
|
||||
Vector3 worldOrigin)
|
||||
{
|
||||
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
||||
if (UseFlat(cache))
|
||||
{
|
||||
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
||||
throw MissingFlat("cell physics");
|
||||
|
|
@ -149,7 +404,58 @@ internal static class CollisionTraversal
|
|||
worldOrigin);
|
||||
}
|
||||
|
||||
return BSPQuery.FindCollisions(
|
||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||
if (shadow is null || !shadow.TrySample(out long sample))
|
||||
{
|
||||
return BSPQuery.FindCollisions(
|
||||
cell.BSP?.Root,
|
||||
cell.Resolved,
|
||||
transition,
|
||||
localSphereCenter,
|
||||
localSphereRadius,
|
||||
hasLocalSphere1,
|
||||
localSphere1Center,
|
||||
localSphere1Radius,
|
||||
localCurrentCenter,
|
||||
localSpaceZ,
|
||||
scale,
|
||||
localToWorld,
|
||||
engine,
|
||||
worldOrigin);
|
||||
}
|
||||
|
||||
Transition flatTransition = shadow.PrepareShadow(transition);
|
||||
TransitionState flatState = TransitionState.Invalid;
|
||||
Exception? flatFault = null;
|
||||
shadow.BeginFlatPass();
|
||||
try
|
||||
{
|
||||
flatState = FlatBspQuery.FindCollisions(
|
||||
cell.FlatPhysicsBsp ??
|
||||
throw MissingFlat("cell physics"),
|
||||
flatTransition,
|
||||
localSphereCenter,
|
||||
localSphereRadius,
|
||||
hasLocalSphere1,
|
||||
localSphere1Center,
|
||||
localSphere1Radius,
|
||||
localCurrentCenter,
|
||||
localSpaceZ,
|
||||
scale,
|
||||
localToWorld,
|
||||
engine,
|
||||
worldOrigin);
|
||||
}
|
||||
catch (Exception fault)
|
||||
{
|
||||
flatFault = fault;
|
||||
}
|
||||
finally
|
||||
{
|
||||
shadow.EndFlatPass();
|
||||
}
|
||||
|
||||
TransitionState graphState = BSPQuery.FindCollisions(
|
||||
cell.BSP?.Root,
|
||||
cell.Resolved,
|
||||
transition,
|
||||
|
|
@ -164,6 +470,39 @@ internal static class CollisionTraversal
|
|||
localToWorld,
|
||||
engine,
|
||||
worldOrigin);
|
||||
string input = CollisionShadowVerifier.FormatInput(
|
||||
localSphereCenter,
|
||||
localSphereRadius,
|
||||
hasLocalSphere1,
|
||||
localSphere1Center,
|
||||
localSphere1Radius,
|
||||
localCurrentCenter,
|
||||
localSpaceZ,
|
||||
scale,
|
||||
localToWorld,
|
||||
worldOrigin);
|
||||
if (flatFault is null)
|
||||
{
|
||||
shadow.RecordTransition(
|
||||
sample,
|
||||
"FindCellCollisions",
|
||||
cell.SourceId,
|
||||
graphState,
|
||||
flatState,
|
||||
transition,
|
||||
flatTransition,
|
||||
input);
|
||||
}
|
||||
else
|
||||
{
|
||||
shadow.RecordFault(
|
||||
sample,
|
||||
"FindCellCollisions",
|
||||
cell.SourceId,
|
||||
flatFault,
|
||||
input);
|
||||
}
|
||||
return graphState;
|
||||
}
|
||||
|
||||
internal static TransitionState FindCollisions(
|
||||
|
|
@ -182,7 +521,7 @@ internal static class CollisionTraversal
|
|||
PhysicsEngine? engine,
|
||||
Vector3 worldOrigin)
|
||||
{
|
||||
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
||||
if (UseFlat(cache))
|
||||
{
|
||||
FlatPhysicsBsp flat = gfxObject.FlatPhysicsBsp ??
|
||||
throw MissingFlat("GfxObj physics");
|
||||
|
|
@ -202,7 +541,58 @@ internal static class CollisionTraversal
|
|||
worldOrigin);
|
||||
}
|
||||
|
||||
return BSPQuery.FindCollisions(
|
||||
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||
if (shadow is null || !shadow.TrySample(out long sample))
|
||||
{
|
||||
return BSPQuery.FindCollisions(
|
||||
gfxObject.BSP.Root,
|
||||
gfxObject.Resolved,
|
||||
transition,
|
||||
localSphereCenter,
|
||||
localSphereRadius,
|
||||
hasLocalSphere1,
|
||||
localSphere1Center,
|
||||
localSphere1Radius,
|
||||
localCurrentCenter,
|
||||
localSpaceZ,
|
||||
scale,
|
||||
localToWorld,
|
||||
engine,
|
||||
worldOrigin);
|
||||
}
|
||||
|
||||
Transition flatTransition = shadow.PrepareShadow(transition);
|
||||
TransitionState flatState = TransitionState.Invalid;
|
||||
Exception? flatFault = null;
|
||||
shadow.BeginFlatPass();
|
||||
try
|
||||
{
|
||||
flatState = FlatBspQuery.FindCollisions(
|
||||
gfxObject.FlatPhysicsBsp ??
|
||||
throw MissingFlat("GfxObj physics"),
|
||||
flatTransition,
|
||||
localSphereCenter,
|
||||
localSphereRadius,
|
||||
hasLocalSphere1,
|
||||
localSphere1Center,
|
||||
localSphere1Radius,
|
||||
localCurrentCenter,
|
||||
localSpaceZ,
|
||||
scale,
|
||||
localToWorld,
|
||||
engine,
|
||||
worldOrigin);
|
||||
}
|
||||
catch (Exception fault)
|
||||
{
|
||||
flatFault = fault;
|
||||
}
|
||||
finally
|
||||
{
|
||||
shadow.EndFlatPass();
|
||||
}
|
||||
|
||||
TransitionState graphState = BSPQuery.FindCollisions(
|
||||
gfxObject.BSP.Root,
|
||||
gfxObject.Resolved,
|
||||
transition,
|
||||
|
|
@ -217,8 +607,45 @@ internal static class CollisionTraversal
|
|||
localToWorld,
|
||||
engine,
|
||||
worldOrigin);
|
||||
string input = CollisionShadowVerifier.FormatInput(
|
||||
localSphereCenter,
|
||||
localSphereRadius,
|
||||
hasLocalSphere1,
|
||||
localSphere1Center,
|
||||
localSphere1Radius,
|
||||
localCurrentCenter,
|
||||
localSpaceZ,
|
||||
scale,
|
||||
localToWorld,
|
||||
worldOrigin);
|
||||
if (flatFault is null)
|
||||
{
|
||||
shadow.RecordTransition(
|
||||
sample,
|
||||
"FindGfxObjCollisions",
|
||||
gfxObject.SourceId,
|
||||
graphState,
|
||||
flatState,
|
||||
transition,
|
||||
flatTransition,
|
||||
input);
|
||||
}
|
||||
else
|
||||
{
|
||||
shadow.RecordFault(
|
||||
sample,
|
||||
"FindGfxObjCollisions",
|
||||
gfxObject.SourceId,
|
||||
flatFault,
|
||||
input);
|
||||
}
|
||||
return graphState;
|
||||
}
|
||||
|
||||
private static bool UseFlat(PhysicsDataCache cache) =>
|
||||
cache.CollisionTraversalMode == CollisionTraversalMode.Flat ||
|
||||
cache.CollisionShadow?.IsFlatPass == true;
|
||||
|
||||
private static InvalidOperationException MissingFlat(string kind) =>
|
||||
new(
|
||||
$"Flat collision traversal requires a prepared {kind} asset. " +
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@ namespace AcDream.Core.Physics;
|
|||
|
||||
/// <summary>
|
||||
/// Thread-safe cache of physics-relevant data extracted from GfxObj and Setup
|
||||
/// dat objects during streaming. Populated by the streaming worker thread;
|
||||
/// read by the physics engine on the game/render thread. ConcurrentDictionary
|
||||
/// makes cross-thread access safe without a global lock.
|
||||
/// dat objects during streaming and live-object materialization. Prepared
|
||||
/// payloads are built/read off the hot path; one update-thread publisher
|
||||
/// installs each graph/flat pair. ConcurrentDictionary keeps diagnostics and
|
||||
/// tooling reads safe without a global lock.
|
||||
/// </summary>
|
||||
public sealed class PhysicsDataCache
|
||||
{
|
||||
|
|
@ -21,6 +22,29 @@ public sealed class PhysicsDataCache
|
|||
private readonly ConcurrentDictionary<uint, GfxObjVisualBounds> _visualBounds = new();
|
||||
private readonly ConcurrentDictionary<uint, SetupPhysics> _setup = new();
|
||||
private readonly ConcurrentDictionary<uint, CellPhysics> _cellStruct = new();
|
||||
private readonly ConcurrentDictionary<uint, FlatGfxObjCollisionAsset>
|
||||
_flatGfxObj = new();
|
||||
private readonly ConcurrentDictionary<uint, FlatSetupCollision>
|
||||
_flatSetup = new();
|
||||
private readonly ConcurrentDictionary<uint, FlatCellStructureCollisionAsset>
|
||||
_flatCellStruct = new();
|
||||
private readonly ConcurrentDictionary<uint, FlatEnvCellTopology>
|
||||
_flatEnvCell = new();
|
||||
|
||||
public PhysicsDataCache()
|
||||
{
|
||||
if (PhysicsDiagnostics.CollisionShadowSampleEvery > 0)
|
||||
{
|
||||
CollisionShadow = new CollisionShadowVerifier(
|
||||
PhysicsDiagnostics.CollisionShadowSampleEvery,
|
||||
PhysicsDiagnostics.CollisionShadowArtifactDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
internal CollisionShadowVerifier? CollisionShadow { get; set; }
|
||||
|
||||
public CollisionShadowStats CollisionShadowStats =>
|
||||
CollisionShadow?.Stats ?? default;
|
||||
|
||||
/// <summary>
|
||||
/// Slice I graph/flat differential selector. Production remains on the
|
||||
|
|
@ -55,8 +79,14 @@ public sealed class PhysicsDataCache
|
|||
/// user collide with decorative meshes that don't have a CylSphere or
|
||||
/// per-part BSP.
|
||||
/// </summary>
|
||||
public void CacheGfxObj(uint gfxObjId, GfxObj gfxObj)
|
||||
public void CacheGfxObj(
|
||||
uint gfxObjId,
|
||||
GfxObj gfxObj,
|
||||
FlatGfxObjCollisionAsset? prepared = null)
|
||||
{
|
||||
if (prepared is not null)
|
||||
_flatGfxObj.TryAdd(gfxObjId, prepared);
|
||||
|
||||
// Always cache a visual AABB from the mesh vertices — this is cheap
|
||||
// and fed by the mesh data that's already loaded. It serves as the
|
||||
// fallback collision shape for pure-visual entities.
|
||||
|
|
@ -65,18 +95,25 @@ public sealed class PhysicsDataCache
|
|||
_visualBounds[gfxObjId] = ComputeVisualBounds(gfxObj.VertexArray);
|
||||
}
|
||||
|
||||
if (_gfxObj.ContainsKey(gfxObjId)) return;
|
||||
if (_gfxObj.TryGetValue(gfxObjId, out GfxObjPhysics? existing))
|
||||
{
|
||||
if (prepared is not null)
|
||||
existing.FlatPhysicsBsp ??= prepared.PhysicsBsp;
|
||||
return;
|
||||
}
|
||||
if (!gfxObj.Flags.HasFlag(GfxObjFlags.HasPhysics)) return;
|
||||
if (gfxObj.PhysicsBSP?.Root is null) return;
|
||||
if (gfxObj.VertexArray is null) return;
|
||||
|
||||
var physics = new GfxObjPhysics
|
||||
{
|
||||
SourceId = gfxObjId,
|
||||
BSP = gfxObj.PhysicsBSP,
|
||||
PhysicsPolygons = gfxObj.PhysicsPolygons,
|
||||
BoundingSphere = gfxObj.PhysicsBSP.Root.BoundingSphere,
|
||||
Vertices = gfxObj.VertexArray,
|
||||
Resolved = ResolvePolygons(gfxObj.PhysicsPolygons, gfxObj.VertexArray),
|
||||
FlatPhysicsBsp = prepared?.PhysicsBsp,
|
||||
};
|
||||
_gfxObj[gfxObjId] = physics;
|
||||
|
||||
|
|
@ -157,17 +194,30 @@ public sealed class PhysicsDataCache
|
|||
/// Extract and cache the collision shape data from a Setup.
|
||||
/// No-ops if the id is already cached.
|
||||
/// </summary>
|
||||
public void CacheSetup(uint setupId, Setup setup)
|
||||
public void CacheSetup(
|
||||
uint setupId,
|
||||
Setup setup,
|
||||
FlatSetupCollision? prepared = null)
|
||||
{
|
||||
if (_setup.ContainsKey(setupId)) return;
|
||||
if (prepared is not null)
|
||||
_flatSetup.TryAdd(setupId, prepared);
|
||||
|
||||
if (_setup.TryGetValue(setupId, out SetupPhysics? existing))
|
||||
{
|
||||
if (prepared is not null)
|
||||
existing.FlatCollision ??= prepared;
|
||||
return;
|
||||
}
|
||||
_setup[setupId] = new SetupPhysics
|
||||
{
|
||||
SourceId = setupId,
|
||||
CylSpheres = setup.CylSpheres ?? new(),
|
||||
Spheres = setup.Spheres ?? new(),
|
||||
Height = setup.Height,
|
||||
Radius = setup.Radius,
|
||||
StepUpHeight = setup.StepUpHeight,
|
||||
StepDownHeight = setup.StepDownHeight,
|
||||
FlatCollision = prepared,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -176,9 +226,19 @@ public sealed class PhysicsDataCache
|
|||
/// (indoor room geometry). No-ops if the id is already cached or the
|
||||
/// CellStruct has no physics BSP.
|
||||
/// </summary>
|
||||
public void CacheCellStruct(uint envCellId, DatReaderWriter.DBObjs.EnvCell envCell,
|
||||
CellStruct cellStruct, Matrix4x4 worldTransform)
|
||||
public void CacheCellStruct(
|
||||
uint envCellId,
|
||||
DatReaderWriter.DBObjs.EnvCell envCell,
|
||||
CellStruct cellStruct,
|
||||
Matrix4x4 worldTransform,
|
||||
FlatCellStructureCollisionAsset? preparedStructure = null,
|
||||
FlatEnvCellTopology? preparedTopology = null)
|
||||
{
|
||||
if (preparedStructure is not null)
|
||||
_flatCellStruct.TryAdd(envCellId, preparedStructure);
|
||||
if (preparedTopology is not null)
|
||||
_flatEnvCell.TryAdd(envCellId, preparedTopology);
|
||||
|
||||
// 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.
|
||||
if (!CellGraph.Contains(envCellId))
|
||||
|
|
@ -216,12 +276,17 @@ public sealed class PhysicsDataCache
|
|||
|
||||
var cellPhysics = new CellPhysics
|
||||
{
|
||||
SourceId = envCellId,
|
||||
BSP = cellStruct.PhysicsBSP,
|
||||
PhysicsPolygons = cellStruct.PhysicsPolygons,
|
||||
Vertices = cellStruct.VertexArray,
|
||||
WorldTransform = worldTransform,
|
||||
InverseWorldTransform = inverseTransform,
|
||||
Resolved = resolved,
|
||||
FlatPhysicsBsp = preparedStructure?.PhysicsBsp,
|
||||
FlatContainmentBsp = preparedStructure?.ContainmentBsp,
|
||||
FlatPortalPolygons = preparedStructure?.PortalPolygons,
|
||||
FlatTopology = preparedTopology,
|
||||
// ── Phase 2 portal fields ──
|
||||
CellBSP = cellStruct.CellBSP,
|
||||
Portals = portals,
|
||||
|
|
@ -394,9 +459,21 @@ public sealed class PhysicsDataCache
|
|||
|
||||
public SetupPhysics? GetSetup(uint id) => _setup.TryGetValue(id, out var p) ? p : null;
|
||||
public CellPhysics? GetCellStruct(uint id) => _cellStruct.TryGetValue(id, out var p) ? p : null;
|
||||
public FlatGfxObjCollisionAsset? GetFlatGfxObj(uint id) =>
|
||||
_flatGfxObj.TryGetValue(id, out var value) ? value : null;
|
||||
public FlatSetupCollision? GetFlatSetup(uint id) =>
|
||||
_flatSetup.TryGetValue(id, out var value) ? value : null;
|
||||
public FlatCellStructureCollisionAsset? GetFlatCellStruct(uint id) =>
|
||||
_flatCellStruct.TryGetValue(id, out var value) ? value : null;
|
||||
public FlatEnvCellTopology? GetFlatEnvCell(uint id) =>
|
||||
_flatEnvCell.TryGetValue(id, out var value) ? value : null;
|
||||
public int GfxObjCount => _gfxObj.Count;
|
||||
public int SetupCount => _setup.Count;
|
||||
public int CellStructCount => _cellStruct.Count;
|
||||
public int FlatGfxObjCount => _flatGfxObj.Count;
|
||||
public int FlatSetupCount => _flatSetup.Count;
|
||||
public int FlatCellStructCount => _flatCellStruct.Count;
|
||||
public int FlatEnvCellCount => _flatEnvCell.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Indoor walking Phase 1 (2026-05-19). Snapshot of currently-cached
|
||||
|
|
@ -483,6 +560,12 @@ public sealed class PhysicsDataCache
|
|||
foreach (var key in _cellStruct.Keys)
|
||||
if ((key & 0xFFFF0000u) == prefix)
|
||||
_cellStruct.TryRemove(key, out _);
|
||||
foreach (var key in _flatCellStruct.Keys)
|
||||
if ((key & 0xFFFF0000u) == prefix)
|
||||
_flatCellStruct.TryRemove(key, out _);
|
||||
foreach (var key in _flatEnvCell.Keys)
|
||||
if ((key & 0xFFFF0000u) == prefix)
|
||||
_flatEnvCell.TryRemove(key, out _);
|
||||
}
|
||||
|
||||
public BuildingPhysics? GetBuilding(uint landcellId)
|
||||
|
|
@ -539,6 +622,7 @@ public sealed class ResolvedPolygon
|
|||
/// <summary>Cached physics data for a single GfxObj part.</summary>
|
||||
public sealed class GfxObjPhysics
|
||||
{
|
||||
public uint SourceId { get; init; }
|
||||
public required PhysicsBSPTree BSP { get; init; }
|
||||
public required Dictionary<ushort, Polygon> PhysicsPolygons { get; init; }
|
||||
public Sphere? BoundingSphere { get; init; }
|
||||
|
|
@ -551,21 +635,24 @@ public sealed class GfxObjPhysics
|
|||
public required Dictionary<ushort, ResolvedPolygon> Resolved { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Prepared integer-indexed shadow representation. Optional until Slice I5
|
||||
/// dual publication is complete.
|
||||
/// Prepared integer-indexed representation. Slice I5 guarantees this for
|
||||
/// production-published collision objects; graph-only test fixtures may
|
||||
/// omit it.
|
||||
/// </summary>
|
||||
public FlatPhysicsBsp? FlatPhysicsBsp { get; init; }
|
||||
public FlatPhysicsBsp? FlatPhysicsBsp { get; internal set; }
|
||||
}
|
||||
|
||||
/// <summary>Cached collision shape data for a Setup (character/creature capsule).</summary>
|
||||
public sealed class SetupPhysics
|
||||
{
|
||||
public uint SourceId { get; init; }
|
||||
public List<CylSphere> CylSpheres { get; init; } = new();
|
||||
public List<Sphere> Spheres { get; init; } = new();
|
||||
public float Height { get; init; }
|
||||
public float Radius { get; init; }
|
||||
public float StepUpHeight { get; init; }
|
||||
public float StepDownHeight { get; init; }
|
||||
public FlatSetupCollision? FlatCollision { get; internal set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -575,6 +662,7 @@ public sealed class SetupPhysics
|
|||
/// </summary>
|
||||
public sealed class CellPhysics
|
||||
{
|
||||
public uint SourceId { get; init; }
|
||||
/// <summary>
|
||||
/// The physics BSP tree for this cell. Nullable so that test fixtures
|
||||
/// can construct a <see cref="CellPhysics"/> from <see cref="Resolved"/>
|
||||
|
|
@ -593,7 +681,9 @@ public sealed class CellPhysics
|
|||
public required Dictionary<ushort, ResolvedPolygon> Resolved { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Prepared integer-indexed physics BSP shadow. Optional until Slice I5.
|
||||
/// Prepared integer-indexed physics BSP. Slice I5 guarantees this for
|
||||
/// production-published collision cells; graph-only test fixtures may
|
||||
/// omit it.
|
||||
/// </summary>
|
||||
public FlatPhysicsBsp? FlatPhysicsBsp { get; init; }
|
||||
|
||||
|
|
@ -610,11 +700,23 @@ public sealed class CellPhysics
|
|||
public DatReaderWriter.Types.CellBSPTree? CellBSP { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Prepared integer-indexed cell-containment BSP shadow. Optional until
|
||||
/// Slice I5.
|
||||
/// Prepared integer-indexed cell-containment BSP. Slice I5 guarantees this
|
||||
/// for production-published cells; graph-only test fixtures may omit it.
|
||||
/// </summary>
|
||||
public FlatCellContainmentBsp? FlatContainmentBsp { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Prepared visible-polygon table addressed by
|
||||
/// <see cref="FlatEnvCellTopology.Portals"/>.
|
||||
/// </summary>
|
||||
public FlatPolygonTable? FlatPortalPolygons { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Prepared per-EnvCell portal/visibility state. Placement remains on this
|
||||
/// runtime record's world transforms.
|
||||
/// </summary>
|
||||
public FlatEnvCellTopology? FlatTopology { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Portal connections to neighbouring cells, in cell-local space.
|
||||
/// Default: empty list. Source: <c>envCell.CellPortals</c>.
|
||||
|
|
|
|||
|
|
@ -21,6 +21,27 @@ namespace AcDream.Core.Physics;
|
|||
/// </summary>
|
||||
public static class PhysicsDiagnostics
|
||||
{
|
||||
/// <summary>
|
||||
/// Slice I5 graph/flat referee cadence. Zero disables the diagnostic;
|
||||
/// N samples every Nth collision-traversal entry. The graph path remains
|
||||
/// authoritative regardless of the comparison result.
|
||||
/// </summary>
|
||||
public static int CollisionShadowSampleEvery { get; set; } =
|
||||
ParsePositiveInt(
|
||||
Environment.GetEnvironmentVariable(
|
||||
"ACDREAM_COLLISION_SHADOW_EVERY"));
|
||||
|
||||
/// <summary>
|
||||
/// Directory for deterministic Slice I5 mismatch artifacts.
|
||||
/// </summary>
|
||||
public static string CollisionShadowArtifactDirectory { get; set; } =
|
||||
Environment.GetEnvironmentVariable(
|
||||
"ACDREAM_COLLISION_SHADOW_DIR")
|
||||
?? Path.Combine(
|
||||
Environment.CurrentDirectory,
|
||||
".test-out",
|
||||
"collision-shadow");
|
||||
|
||||
/// <summary>
|
||||
/// When true, <see cref="PhysicsEngine.ResolveWithTransition"/> emits
|
||||
/// one structured <c>[resolve]</c> line per call: input + target +
|
||||
|
|
@ -962,4 +983,14 @@ public static class PhysicsDiagnostics
|
|||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
private static int ParsePositiveInt(string? value) =>
|
||||
int.TryParse(
|
||||
value,
|
||||
System.Globalization.NumberStyles.None,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out int parsed)
|
||||
&& parsed > 0
|
||||
? parsed
|
||||
: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,6 +182,22 @@ public sealed class ObjectInfo
|
|||
MoverHasGravity = false;
|
||||
VelocityKilled = false;
|
||||
}
|
||||
|
||||
internal void CopyFrom(ObjectInfo source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
State = source.State;
|
||||
StepUpHeight = source.StepUpHeight;
|
||||
StepDownHeight = source.StepDownHeight;
|
||||
Ethereal = source.Ethereal;
|
||||
StepDown = source.StepDown;
|
||||
Scale = source.Scale;
|
||||
MoverPhysicsState = source.MoverPhysicsState;
|
||||
TargetId = source.TargetId;
|
||||
SelfEntityId = source.SelfEntityId;
|
||||
MoverHasGravity = source.MoverHasGravity;
|
||||
VelocityKilled = source.VelocityKilled;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -398,6 +414,30 @@ public sealed class CollisionInfo
|
|||
LastCollidedObjectGuid = null;
|
||||
ContactPlaneWriteCount = 0;
|
||||
}
|
||||
|
||||
internal void CopyFrom(CollisionInfo source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
_contactPlaneValid = source._contactPlaneValid;
|
||||
_contactPlane = source._contactPlane;
|
||||
_contactPlaneCellId = source._contactPlaneCellId;
|
||||
_contactPlaneIsWater = source._contactPlaneIsWater;
|
||||
_lastKnownContactPlaneValid = source._lastKnownContactPlaneValid;
|
||||
_lastKnownContactPlane = source._lastKnownContactPlane;
|
||||
_lastKnownContactPlaneCellId = source._lastKnownContactPlaneCellId;
|
||||
_lastKnownContactPlaneIsWater = source._lastKnownContactPlaneIsWater;
|
||||
SlidingNormalValid = source.SlidingNormalValid;
|
||||
SlidingNormal = source.SlidingNormal;
|
||||
CollisionNormalValid = source.CollisionNormalValid;
|
||||
CollisionNormal = source.CollisionNormal;
|
||||
CollidedWithEnvironment = source.CollidedWithEnvironment;
|
||||
FramesStationaryFall = source.FramesStationaryFall;
|
||||
AdjustOffset = source.AdjustOffset;
|
||||
CollideObjectGuids.Clear();
|
||||
CollideObjectGuids.AddRange(source.CollideObjectGuids);
|
||||
LastCollidedObjectGuid = source.LastCollidedObjectGuid;
|
||||
ContactPlaneWriteCount = source.ContactPlaneWriteCount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -774,6 +814,65 @@ public sealed class SpherePath
|
|||
OrderedCellScratch.ResetForReuse();
|
||||
}
|
||||
|
||||
internal void CopyFrom(SpherePath source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
NumSphere = source.NumSphere;
|
||||
CopySphereArray(source.LocalSphere, LocalSphere);
|
||||
CopySphereArray(source.GlobalSphere, GlobalSphere);
|
||||
CopySphereArray(source.GlobalCurrCenter, GlobalCurrCenter);
|
||||
BeginPos = source.BeginPos;
|
||||
EndPos = source.EndPos;
|
||||
CurPos = source.CurPos;
|
||||
CheckPos = source.CheckPos;
|
||||
BeginOrientation = source.BeginOrientation;
|
||||
EndOrientation = source.EndOrientation;
|
||||
CurOrientation = source.CurOrientation;
|
||||
CheckOrientation = source.CheckOrientation;
|
||||
CurCellId = source.CurCellId;
|
||||
CheckCellId = source.CheckCellId;
|
||||
CarriedBlockOrigin = source.CarriedBlockOrigin;
|
||||
GlobalOffset = source.GlobalOffset;
|
||||
StepUp = source.StepUp;
|
||||
StepUpNormal = source.StepUpNormal;
|
||||
Collide = source.Collide;
|
||||
StepDown = source.StepDown;
|
||||
StepDownAmt = source.StepDownAmt;
|
||||
WalkInterp = source.WalkInterp;
|
||||
WalkableValid = source.WalkableValid;
|
||||
WalkablePlane = source.WalkablePlane;
|
||||
WalkableVertices = source.WalkableVertices is null
|
||||
? null
|
||||
: CopyExact(
|
||||
source.WalkableVertices,
|
||||
ref _walkableVertexStorage);
|
||||
WalkableUp = source.WalkableUp;
|
||||
WalkableAllowance = source.WalkableAllowance;
|
||||
LastWalkableValid = source.LastWalkableValid;
|
||||
LastWalkablePlane = source.LastWalkablePlane;
|
||||
LastWalkableVertices = source.LastWalkableVertices is null
|
||||
? null
|
||||
: CopyExact(
|
||||
source.LastWalkableVertices,
|
||||
ref _lastWalkableVertexStorage);
|
||||
LastWalkableUp = source.LastWalkableUp;
|
||||
BackupCheckPos = source.BackupCheckPos;
|
||||
BackupCheckCellId = source.BackupCheckCellId;
|
||||
NegPolyHit = source.NegPolyHit;
|
||||
NegStepUp = source.NegStepUp;
|
||||
NegCollisionNormal = source.NegCollisionNormal;
|
||||
CheckWalkable = source.CheckWalkable;
|
||||
InsertType = source.InsertType;
|
||||
PlacementAllowsSliding = source.PlacementAllowsSliding;
|
||||
ObstructionEthereal = source.ObstructionEthereal;
|
||||
BldgCheck = source.BldgCheck;
|
||||
HitsInteriorCell = source.HitsInteriorCell;
|
||||
CellCandidates.Clear();
|
||||
foreach (uint id in source.CellCandidates)
|
||||
CellCandidates.Add(id);
|
||||
OrderedCellScratch.ResetForReuse();
|
||||
}
|
||||
|
||||
private static Vector3[] CopyExact(
|
||||
ReadOnlySpan<Vector3> source,
|
||||
ref Vector3[]? storage)
|
||||
|
|
@ -800,6 +899,15 @@ public sealed class SpherePath
|
|||
}
|
||||
}
|
||||
|
||||
private static void CopySphereArray(Sphere[] source, Sphere[] destination)
|
||||
{
|
||||
for (int i = 0; i < destination.Length; i++)
|
||||
{
|
||||
destination[i].Origin = source[i].Origin;
|
||||
destination[i].Radius = source[i].Radius;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Slide fallback when step-up fails. Clears the contact-plane state that
|
||||
/// caused the step-up attempt and runs the full sphere-slide computation
|
||||
|
|
@ -958,6 +1066,14 @@ public sealed class Transition
|
|||
CollisionInfo.ResetForReuse();
|
||||
}
|
||||
|
||||
internal void CopyFrom(Transition source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
ObjectInfo.CopyFrom(source.ObjectInfo);
|
||||
SpherePath.CopyFrom(source.SpherePath);
|
||||
CollisionInfo.CopyFrom(source.CollisionInfo);
|
||||
}
|
||||
|
||||
private static bool DumpEdgeSlideEnabled =>
|
||||
Environment.GetEnvironmentVariable("ACDREAM_DUMP_EDGE_SLIDE") == "1";
|
||||
|
||||
|
|
|
|||
29
src/AcDream.Core/World/LandblockCollisionBuild.cs
Normal file
29
src/AcDream.Core/World/LandblockCollisionBuild.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
using System.Collections.Immutable;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.Core.World;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable prepared-collision closure carried by one near-tier landblock
|
||||
/// build. Keys remain the original DAT source IDs. Cell-structure geometry and
|
||||
/// per-EnvCell topology stay separate so package aliases retain one reusable
|
||||
/// geometry identity without conflating cell placement or portal state.
|
||||
/// </summary>
|
||||
public sealed record LandblockCollisionBuild(
|
||||
ImmutableDictionary<uint, FlatGfxObjCollisionAsset> GfxObjs,
|
||||
ImmutableDictionary<uint, FlatSetupCollision> Setups,
|
||||
ImmutableDictionary<uint, FlatCellStructureCollisionAsset> CellStructures,
|
||||
ImmutableDictionary<uint, FlatEnvCellTopology> EnvCells,
|
||||
ImmutableArray<uint> GfxObjIds,
|
||||
ImmutableArray<uint> SetupIds,
|
||||
ImmutableArray<uint> EnvCellIds)
|
||||
{
|
||||
public static readonly LandblockCollisionBuild Empty = new(
|
||||
ImmutableDictionary<uint, FlatGfxObjCollisionAsset>.Empty,
|
||||
ImmutableDictionary<uint, FlatSetupCollision>.Empty,
|
||||
ImmutableDictionary<uint, FlatCellStructureCollisionAsset>.Empty,
|
||||
ImmutableDictionary<uint, FlatEnvCellTopology>.Empty,
|
||||
ImmutableArray<uint>.Empty,
|
||||
ImmutableArray<uint>.Empty,
|
||||
ImmutableArray<uint>.Empty);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue