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:
Erik 2026-07-25 16:38:54 +02:00
parent f7ff9f4eea
commit d9446030e6
32 changed files with 2777 additions and 92 deletions

View 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;
}
}

View file

@ -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. " +

View file

@ -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>.

View file

@ -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;
}

View file

@ -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";

View 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);
}