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
|
|
@ -30,6 +30,7 @@ internal sealed record ContentEffectsAudioResult(
|
|||
MagicCatalog MagicCatalog,
|
||||
IAnimationLoader AnimationLoader,
|
||||
LiveEntityCollisionBuilder CollisionBuilder,
|
||||
LiveCollisionAssetPublisher CollisionAssets,
|
||||
EmitterDescRegistry EmitterRegistry,
|
||||
ParticleSystem ParticleSystem,
|
||||
ParticleHookSink ParticleSink,
|
||||
|
|
@ -95,6 +96,9 @@ internal interface IContentEffectsAudioCompositionFactory
|
|||
IDatReaderWriter dats,
|
||||
IAnimationLoader animationLoader,
|
||||
bool dumpMotionEnabled);
|
||||
LiveCollisionAssetPublisher CreateLiveCollisionAssetPublisher(
|
||||
PhysicsDataCache physicsData,
|
||||
IPreparedAssetSource preparedAssets);
|
||||
EmitterDescRegistry CreateEmitterRegistry(IDatReaderWriter dats);
|
||||
ParticleSystem CreateParticleSystem(EmitterDescRegistry emitters);
|
||||
ParticleHookSink CreateParticleSink(
|
||||
|
|
@ -165,6 +169,15 @@ internal sealed class RetailContentEffectsAudioCompositionFactory
|
|||
animationLoader,
|
||||
dumpMotionEnabled));
|
||||
|
||||
public LiveCollisionAssetPublisher CreateLiveCollisionAssetPublisher(
|
||||
PhysicsDataCache physicsData,
|
||||
IPreparedAssetSource preparedAssets) =>
|
||||
new(
|
||||
physicsData,
|
||||
preparedAssets as IPreparedCollisionSource
|
||||
?? throw new NotSupportedException(
|
||||
"Production prepared assets must expose collision data."));
|
||||
|
||||
public EmitterDescRegistry CreateEmitterRegistry(IDatReaderWriter dats) =>
|
||||
new(dats);
|
||||
|
||||
|
|
@ -332,6 +345,10 @@ internal sealed class ContentEffectsAudioCompositionPhase :
|
|||
_dependencies.DumpMotionEnabled);
|
||||
_publication.PublishLiveEntityCollisionBuilder(collision);
|
||||
Fault(ContentEffectsAudioCompositionPoint.CollisionBuilderPublished);
|
||||
LiveCollisionAssetPublisher collisionAssets =
|
||||
_factory.CreateLiveCollisionAssetPublisher(
|
||||
_dependencies.PhysicsDataCache,
|
||||
preparedAssets);
|
||||
|
||||
EmitterDescRegistry emitters = _factory.CreateEmitterRegistry(dats);
|
||||
_publication.PublishEmitterRegistry(emitters);
|
||||
|
|
@ -403,6 +420,7 @@ internal sealed class ContentEffectsAudioCompositionPhase :
|
|||
magic,
|
||||
animations,
|
||||
collision,
|
||||
collisionAssets,
|
||||
emitters,
|
||||
particles,
|
||||
particleSink,
|
||||
|
|
|
|||
|
|
@ -441,6 +441,9 @@ internal sealed class FrameRootCompositionPhase
|
|||
d.FrameProfiler,
|
||||
content.Dats,
|
||||
foundation.Residency,
|
||||
d.PhysicsEngine.DataCache
|
||||
?? throw new InvalidOperationException(
|
||||
"Lifecycle automation requires the canonical physics cache."),
|
||||
currentRenderSceneOracle,
|
||||
renderSceneShadowComparison,
|
||||
renderFrameProduct);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ using AcDream.App.Settings;
|
|||
using AcDream.App.Streaming;
|
||||
using AcDream.App.Update;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Content;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Chat;
|
||||
|
|
@ -252,11 +253,16 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
$"(window={2 * farRadius + 1}x{2 * farRadius + 1})");
|
||||
Fault(SessionPlayerCompositionPoint.StreamingRadiiResolved);
|
||||
|
||||
IPreparedCollisionSource preparedCollisions =
|
||||
content.PreparedAssets as IPreparedCollisionSource ??
|
||||
throw new NotSupportedException(
|
||||
"Production prepared assets must expose the matching " +
|
||||
"prepared-collision catalog.");
|
||||
var landblockBuildFactory = new LandblockBuildFactory(
|
||||
content.Dats,
|
||||
preparedCollisions,
|
||||
d.DatLock,
|
||||
world.TerrainBuild.HeightTable,
|
||||
d.PhysicsDataCache,
|
||||
d.Options.DumpSceneryZ);
|
||||
var streamerLease = scope.Acquire(
|
||||
"landblock streamer",
|
||||
|
|
@ -399,7 +405,7 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
d.Options,
|
||||
content.Dats,
|
||||
live.LiveEntities,
|
||||
d.PhysicsDataCache,
|
||||
content.CollisionAssets,
|
||||
content.AnimationLoader,
|
||||
live.EntitySpawnAdapter,
|
||||
world.Foundation.TextureCache,
|
||||
|
|
@ -689,7 +695,7 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
d.MotionBindings,
|
||||
content.Dats,
|
||||
d.DatLock,
|
||||
d.PhysicsDataCache,
|
||||
content.CollisionAssets,
|
||||
d.AnimatedEntities,
|
||||
localPlayerAnimation,
|
||||
localPlayerShadow,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using AcDream.App.Rendering.Residency;
|
|||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.UI.Testing;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.App.Diagnostics;
|
||||
|
||||
|
|
@ -81,6 +82,14 @@ internal sealed record WorldLifecycleResourceSnapshot(
|
|||
long DatObjectCacheHits,
|
||||
long DatObjectCacheMisses,
|
||||
long DatObjectCacheEvictions,
|
||||
int PhysicsGraphGfxObjs,
|
||||
int PhysicsGraphSetups,
|
||||
int PhysicsGraphCells,
|
||||
int PhysicsFlatGfxObjs,
|
||||
int PhysicsFlatSetups,
|
||||
int PhysicsFlatCells,
|
||||
int PhysicsFlatEnvCells,
|
||||
CollisionShadowStats CollisionShadow,
|
||||
StreamingWorkDiagnostics StreamingWork,
|
||||
ResidencySnapshot Residency,
|
||||
double Fps,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using AcDream.App.Rendering.Residency;
|
|||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Content;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Vfx;
|
||||
using DatReaderWriter.Lib.IO;
|
||||
|
||||
|
|
@ -39,6 +40,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
private readonly FrameProfiler _frameProfiler;
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly ResidencyManager _residency;
|
||||
private readonly PhysicsDataCache _physics;
|
||||
private readonly ICurrentRenderSceneOracleSnapshotSource?
|
||||
_renderSceneOracle;
|
||||
private readonly IRenderSceneShadowSnapshotSource?
|
||||
|
|
@ -63,6 +65,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
FrameProfiler frameProfiler,
|
||||
IDatReaderWriter dats,
|
||||
ResidencyManager residency,
|
||||
PhysicsDataCache physics,
|
||||
ICurrentRenderSceneOracleSnapshotSource? renderSceneOracle = null,
|
||||
IRenderSceneShadowSnapshotSource? renderSceneShadow = null,
|
||||
IRenderFrameProductSnapshotSource? renderFrameProduct = null)
|
||||
|
|
@ -92,6 +95,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||
_residency = residency
|
||||
?? throw new ArgumentNullException(nameof(residency));
|
||||
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
|
||||
_renderSceneOracle = renderSceneOracle;
|
||||
_renderSceneShadow = renderSceneShadow;
|
||||
_renderFrameProduct = renderFrameProduct;
|
||||
|
|
@ -184,6 +188,14 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
DatObjectCacheHits: datObjectCacheStats.Hits,
|
||||
DatObjectCacheMisses: datObjectCacheStats.Misses,
|
||||
DatObjectCacheEvictions: datObjectCacheStats.Evictions,
|
||||
PhysicsGraphGfxObjs: _physics.GfxObjCount,
|
||||
PhysicsGraphSetups: _physics.SetupCount,
|
||||
PhysicsGraphCells: _physics.CellStructCount,
|
||||
PhysicsFlatGfxObjs: _physics.FlatGfxObjCount,
|
||||
PhysicsFlatSetups: _physics.FlatSetupCount,
|
||||
PhysicsFlatCells: _physics.FlatCellStructCount,
|
||||
PhysicsFlatEnvCells: _physics.FlatEnvCellCount,
|
||||
CollisionShadow: _physics.CollisionShadowStats,
|
||||
StreamingWork: _streaming.WorkDiagnostics,
|
||||
Residency: residency,
|
||||
Fps: render.Fps,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ internal sealed class PlayerModeController :
|
|||
private readonly ILiveEntityMotionRuntimeBindings _motionBindings;
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly object _datLock;
|
||||
private readonly PhysicsDataCache _physicsDataCache;
|
||||
private readonly LiveCollisionAssetPublisher _collisionAssets;
|
||||
private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState> _animations;
|
||||
private readonly LocalPlayerAnimationController _animation;
|
||||
private readonly LocalPlayerShadowSynchronizer _shadow;
|
||||
|
|
@ -59,7 +59,7 @@ internal sealed class PlayerModeController :
|
|||
ILiveEntityMotionRuntimeBindings motionBindings,
|
||||
IDatReaderWriter dats,
|
||||
object datLock,
|
||||
PhysicsDataCache physicsDataCache,
|
||||
LiveCollisionAssetPublisher collisionAssets,
|
||||
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> animations,
|
||||
LocalPlayerAnimationController animation,
|
||||
LocalPlayerShadowSynchronizer shadow,
|
||||
|
|
@ -82,7 +82,8 @@ internal sealed class PlayerModeController :
|
|||
_motionBindings = motionBindings ?? throw new ArgumentNullException(nameof(motionBindings));
|
||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||
_datLock = datLock ?? throw new ArgumentNullException(nameof(datLock));
|
||||
_physicsDataCache = physicsDataCache ?? throw new ArgumentNullException(nameof(physicsDataCache));
|
||||
_collisionAssets = collisionAssets ??
|
||||
throw new ArgumentNullException(nameof(collisionAssets));
|
||||
_animations = animations ?? throw new ArgumentNullException(nameof(animations));
|
||||
_animation = animation ?? throw new ArgumentNullException(nameof(animation));
|
||||
_shadow = shadow ?? throw new ArgumentNullException(nameof(shadow));
|
||||
|
|
@ -536,7 +537,9 @@ internal sealed class PlayerModeController :
|
|||
setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(
|
||||
playerEntity.SourceGfxObjOrSetupId);
|
||||
if (setup is not null)
|
||||
_physicsDataCache.CacheSetup(playerEntity.SourceGfxObjOrSetupId, setup);
|
||||
_collisionAssets.CacheSetup(
|
||||
playerEntity.SourceGfxObjOrSetupId,
|
||||
setup);
|
||||
controller.StepUpHeight = setup is { StepUpHeight: > 0f }
|
||||
? setup.StepUpHeight
|
||||
: 0.4f;
|
||||
|
|
|
|||
68
src/AcDream.App/Physics/LiveCollisionAssetPublisher.cs
Normal file
68
src/AcDream.App/Physics/LiveCollisionAssetPublisher.cs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
using AcDream.Content;
|
||||
using AcDream.Core.Physics;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Publishes the graph and prepared collision views for DAT objects discovered
|
||||
/// through the live-object path. Streamed statics carry their prepared closure
|
||||
/// on <c>LandblockBuild</c>; live CreateObject/appearance data arrives outside
|
||||
/// that transaction and therefore uses this matching strict publisher.
|
||||
/// </summary>
|
||||
internal sealed class LiveCollisionAssetPublisher
|
||||
{
|
||||
private readonly PhysicsDataCache _cache;
|
||||
private readonly IPreparedCollisionSource _source;
|
||||
|
||||
public LiveCollisionAssetPublisher(
|
||||
PhysicsDataCache cache,
|
||||
IPreparedCollisionSource source)
|
||||
{
|
||||
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
|
||||
_source = source ?? throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
|
||||
public void CacheGfxObj(uint id, GfxObj gfxObj)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gfxObj);
|
||||
FlatGfxObjCollisionAsset? prepared =
|
||||
_cache.GetFlatGfxObj(id) is not null
|
||||
? null
|
||||
: Require(
|
||||
_source.ReadGfxObjCollision(id),
|
||||
"GfxObj collision",
|
||||
id);
|
||||
_cache.CacheGfxObj(id, gfxObj, prepared);
|
||||
}
|
||||
|
||||
public void CacheSetup(uint id, Setup setup)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(setup);
|
||||
FlatSetupCollision? prepared =
|
||||
_cache.GetFlatSetup(id) is not null
|
||||
? null
|
||||
: Require(
|
||||
_source.ReadSetupCollision(id),
|
||||
"Setup collision",
|
||||
id);
|
||||
_cache.CacheSetup(id, setup, prepared);
|
||||
}
|
||||
|
||||
private static T Require<T>(
|
||||
PreparedCollisionReadResult<T> result,
|
||||
string kind,
|
||||
uint id)
|
||||
where T : class
|
||||
{
|
||||
if (result.Status == PreparedAssetReadStatus.Loaded &&
|
||||
result.Data is not null)
|
||||
{
|
||||
return result.Data;
|
||||
}
|
||||
|
||||
throw new InvalidDataException(
|
||||
$"{kind} 0x{id:X8} is {result.Status}. " +
|
||||
"Live collision cannot publish only one representation.");
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
private readonly RuntimeOptions _options;
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly LiveEntityRuntime _runtime;
|
||||
private readonly PhysicsDataCache _physicsData;
|
||||
private readonly LiveCollisionAssetPublisher _collisionAssets;
|
||||
private readonly IAnimationLoader _animationLoader;
|
||||
private readonly EntitySpawnAdapter _spawnAdapter;
|
||||
private readonly TextureCache _textures;
|
||||
|
|
@ -62,7 +62,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
RuntimeOptions options,
|
||||
IDatReaderWriter dats,
|
||||
LiveEntityRuntime runtime,
|
||||
PhysicsDataCache physicsData,
|
||||
LiveCollisionAssetPublisher collisionAssets,
|
||||
IAnimationLoader animationLoader,
|
||||
EntitySpawnAdapter spawnAdapter,
|
||||
TextureCache textures,
|
||||
|
|
@ -82,7 +82,8 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
_physicsData = physicsData ?? throw new ArgumentNullException(nameof(physicsData));
|
||||
_collisionAssets = collisionAssets ??
|
||||
throw new ArgumentNullException(nameof(collisionAssets));
|
||||
_animationLoader = animationLoader ?? throw new ArgumentNullException(nameof(animationLoader));
|
||||
_spawnAdapter = spawnAdapter ?? throw new ArgumentNullException(nameof(spawnAdapter));
|
||||
_textures = textures ?? throw new ArgumentNullException(nameof(textures));
|
||||
|
|
@ -172,7 +173,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
|
||||
Setup? setup = _dats.Get<Setup>(canonicalSpawn.SetupTableId.Value);
|
||||
if (setup is not null)
|
||||
_physicsData.CacheSetup(canonicalSpawn.SetupTableId.Value, setup);
|
||||
_collisionAssets.CacheSetup(canonicalSpawn.SetupTableId.Value, setup);
|
||||
if (setup is null)
|
||||
{
|
||||
_missingSetup++;
|
||||
|
|
@ -282,7 +283,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
continue;
|
||||
}
|
||||
|
||||
_physicsData.CacheGfxObj(part.GfxObjId, gfx);
|
||||
_collisionAssets.CacheGfxObj(part.GfxObjId, gfx);
|
||||
if (dumpClothing)
|
||||
{
|
||||
var subMeshes = GfxObjMesh.Build(gfx, _dats);
|
||||
|
|
@ -430,7 +431,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
}
|
||||
|
||||
if (slotZeroGfx is not null)
|
||||
_physicsData.CacheGfxObj(slotZeroId, slotZeroGfx);
|
||||
_collisionAssets.CacheGfxObj(slotZeroId, slotZeroGfx);
|
||||
return slotZeroId;
|
||||
}
|
||||
|
||||
|
|
@ -512,7 +513,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
}
|
||||
continue;
|
||||
}
|
||||
_physicsData.CacheGfxObj(parts[partIndex].GfxObjId, gfx);
|
||||
_collisionAssets.CacheGfxObj(parts[partIndex].GfxObjId, gfx);
|
||||
|
||||
Dictionary<uint, uint>? resolved = null;
|
||||
foreach (var surfaceQid in gfx.Surfaces)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ namespace AcDream.App.Streaming;
|
|||
public sealed record LandblockBuild(
|
||||
LoadedLandblock Landblock,
|
||||
EnvCellLandblockBuild? EnvCells = null,
|
||||
LandblockBuildOrigin Origin = default)
|
||||
LandblockBuildOrigin Origin = default,
|
||||
LandblockCollisionBuild? Collisions = null)
|
||||
{
|
||||
public uint LandblockId => Landblock.LandblockId;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections.Immutable;
|
||||
using DatReaderWriter;
|
||||
using AcDream.Content;
|
||||
|
||||
|
|
@ -17,19 +18,21 @@ namespace AcDream.App.Streaming;
|
|||
public sealed class LandblockBuildFactory
|
||||
{
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly IPreparedCollisionSource _preparedCollisions;
|
||||
private readonly object _datLock;
|
||||
private readonly float[] _heightTable;
|
||||
private readonly AcDream.Core.Physics.PhysicsDataCache _physicsDataCache;
|
||||
private readonly bool _dumpSceneryZ;
|
||||
|
||||
public LandblockBuildFactory(
|
||||
IDatReaderWriter dats,
|
||||
IPreparedCollisionSource preparedCollisions,
|
||||
object datLock,
|
||||
float[] heightTable,
|
||||
AcDream.Core.Physics.PhysicsDataCache physicsDataCache,
|
||||
bool dumpSceneryZ = false)
|
||||
{
|
||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||
_preparedCollisions = preparedCollisions ??
|
||||
throw new ArgumentNullException(nameof(preparedCollisions));
|
||||
_datLock = datLock ?? throw new ArgumentNullException(nameof(datLock));
|
||||
ArgumentNullException.ThrowIfNull(heightTable);
|
||||
if (heightTable.Length < 256)
|
||||
|
|
@ -37,7 +40,6 @@ public sealed class LandblockBuildFactory
|
|||
"The retail terrain height table must contain at least 256 entries.",
|
||||
nameof(heightTable));
|
||||
_heightTable = (float[])heightTable.Clone();
|
||||
_physicsDataCache = physicsDataCache ?? throw new ArgumentNullException(nameof(physicsDataCache));
|
||||
_dumpSceneryZ = dumpSceneryZ;
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +75,7 @@ public sealed class LandblockBuildFactory
|
|||
// lock during a CreateObject flood) AND lock-HOLD (the intrinsic build
|
||||
// cost). Identical work in both branches; the probe branch only adds the
|
||||
// stopwatch + log. No behavior change when ProbeTeleportEnabled is false.
|
||||
LandblockBuild? build;
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeTeleportEnabled)
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
|
@ -80,17 +83,28 @@ public sealed class LandblockBuildFactory
|
|||
{
|
||||
long waitedMs = sw.ElapsedMilliseconds;
|
||||
sw.Restart();
|
||||
var built = BuildLocked(request);
|
||||
build = BuildLocked(request);
|
||||
AcDream.Core.Physics.PhysicsDiagnostics.LogTeleport(
|
||||
"BUILD", request.LandblockId,
|
||||
$"waited={waitedMs}ms held={sw.ElapsedMilliseconds}ms kind={request.Kind}");
|
||||
return built;
|
||||
}
|
||||
}
|
||||
lock (_datLock)
|
||||
else
|
||||
{
|
||||
return BuildLocked(request);
|
||||
lock (_datLock)
|
||||
build = BuildLocked(request);
|
||||
}
|
||||
|
||||
if (build is null ||
|
||||
request.Kind == LandblockStreamJobKind.LoadFar)
|
||||
{
|
||||
return build;
|
||||
}
|
||||
|
||||
return build with
|
||||
{
|
||||
Collisions = BuildPreparedCollisionClosure(build.Landblock),
|
||||
};
|
||||
}
|
||||
|
||||
private AcDream.App.Streaming.LandblockBuild? BuildLocked(
|
||||
|
|
@ -143,7 +157,6 @@ public sealed class LandblockBuildFactory
|
|||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(e.SourceGfxObjOrSetupId);
|
||||
if (gfx is not null)
|
||||
{
|
||||
_physicsDataCache.CacheGfxObj(e.SourceGfxObjOrSetupId, gfx);
|
||||
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
||||
if (pb is not null) stabBounds.Add(System.Numerics.Matrix4x4.Identity, pb.Value);
|
||||
meshRefs.Add(new AcDream.Core.World.MeshRef(
|
||||
|
|
@ -156,13 +169,11 @@ public sealed class LandblockBuildFactory
|
|||
var setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(e.SourceGfxObjOrSetupId);
|
||||
if (setup is not null)
|
||||
{
|
||||
_physicsDataCache.CacheSetup(e.SourceGfxObjOrSetupId, setup);
|
||||
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
|
||||
foreach (var mr in flat)
|
||||
{
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
|
||||
if (gfx is null) continue;
|
||||
_physicsDataCache.CacheGfxObj(mr.GfxObjId, gfx);
|
||||
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
||||
if (pb is not null) stabBounds.Add(mr.PartTransform, pb.Value);
|
||||
meshRefs.Add(mr);
|
||||
|
|
@ -293,6 +304,103 @@ public sealed class LandblockBuildFactory
|
|||
return new AcDream.Core.World.PhysicsDatBundle(lbInfo, envCells, environments, setups, gfxObjs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the exact prepared collision closure after the serialized DAT
|
||||
/// transaction completes. The immutable package is independently
|
||||
/// thread-safe, so these reads never extend the shared DAT-reader lock.
|
||||
/// Missing or corrupt prepared data fails the complete near-tier build;
|
||||
/// production must not publish a graph-only partial generation.
|
||||
/// </summary>
|
||||
private AcDream.Core.World.LandblockCollisionBuild
|
||||
BuildPreparedCollisionClosure(
|
||||
AcDream.Core.World.LoadedLandblock landblock)
|
||||
{
|
||||
AcDream.Core.World.PhysicsDatBundle dats =
|
||||
landblock.PhysicsDats ??
|
||||
AcDream.Core.World.PhysicsDatBundle.Empty;
|
||||
ImmutableArray<uint> gfxObjIds = [.. dats.GfxObjs.Keys.Order()];
|
||||
ImmutableArray<uint> setupIds = [.. dats.Setups.Keys.Order()];
|
||||
ImmutableArray<uint> envCellIds = [.. dats.EnvCells.Keys.Order()];
|
||||
var gfxObjs =
|
||||
ImmutableDictionary.CreateBuilder<
|
||||
uint,
|
||||
AcDream.Core.Physics.FlatGfxObjCollisionAsset>();
|
||||
var setups =
|
||||
ImmutableDictionary.CreateBuilder<
|
||||
uint,
|
||||
AcDream.Core.Physics.FlatSetupCollision>();
|
||||
var cellStructures =
|
||||
ImmutableDictionary.CreateBuilder<
|
||||
uint,
|
||||
AcDream.Core.Physics.FlatCellStructureCollisionAsset>();
|
||||
var envCells =
|
||||
ImmutableDictionary.CreateBuilder<
|
||||
uint,
|
||||
AcDream.Core.Physics.FlatEnvCellTopology>();
|
||||
|
||||
foreach (uint id in gfxObjIds)
|
||||
{
|
||||
gfxObjs.Add(
|
||||
id,
|
||||
RequirePrepared(
|
||||
_preparedCollisions.ReadGfxObjCollision(id),
|
||||
"GfxObj collision",
|
||||
id));
|
||||
}
|
||||
|
||||
foreach (uint id in setupIds)
|
||||
{
|
||||
setups.Add(
|
||||
id,
|
||||
RequirePrepared(
|
||||
_preparedCollisions.ReadSetupCollision(id),
|
||||
"Setup collision",
|
||||
id));
|
||||
}
|
||||
|
||||
foreach (uint id in envCellIds)
|
||||
{
|
||||
cellStructures.Add(
|
||||
id,
|
||||
RequirePrepared(
|
||||
_preparedCollisions.ReadCellStructureCollision(id),
|
||||
"CellStruct collision",
|
||||
id));
|
||||
envCells.Add(
|
||||
id,
|
||||
RequirePrepared(
|
||||
_preparedCollisions.ReadEnvCellTopology(id),
|
||||
"EnvCell topology",
|
||||
id));
|
||||
}
|
||||
|
||||
return new AcDream.Core.World.LandblockCollisionBuild(
|
||||
gfxObjs.ToImmutable(),
|
||||
setups.ToImmutable(),
|
||||
cellStructures.ToImmutable(),
|
||||
envCells.ToImmutable(),
|
||||
gfxObjIds,
|
||||
setupIds,
|
||||
envCellIds);
|
||||
}
|
||||
|
||||
private static T RequirePrepared<T>(
|
||||
PreparedCollisionReadResult<T> result,
|
||||
string kind,
|
||||
uint id)
|
||||
where T : class
|
||||
{
|
||||
if (result.Status == PreparedAssetReadStatus.Loaded &&
|
||||
result.Data is not null)
|
||||
{
|
||||
return result.Data;
|
||||
}
|
||||
|
||||
throw new InvalidDataException(
|
||||
$"{kind} 0x{id:X8} is {result.Status}. " +
|
||||
"The complete near-tier generation cannot be published.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase A.1 Task 8: generate scenery (trees, rocks, bushes) for a single
|
||||
/// landblock on the worker thread. Pure CPU — no GL calls.
|
||||
|
|
@ -363,7 +471,6 @@ public sealed class LandblockBuildFactory
|
|||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(spawn.ObjectId);
|
||||
if (gfx is not null)
|
||||
{
|
||||
_physicsDataCache.CacheGfxObj(spawn.ObjectId, gfx);
|
||||
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
||||
if (pb is not null) sceneryBounds.Add(scaleMat, pb.Value);
|
||||
meshRefs.Add(new AcDream.Core.World.MeshRef(spawn.ObjectId, scaleMat));
|
||||
|
|
@ -374,13 +481,11 @@ public sealed class LandblockBuildFactory
|
|||
var setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(spawn.ObjectId);
|
||||
if (setup is not null)
|
||||
{
|
||||
_physicsDataCache.CacheSetup(spawn.ObjectId, setup);
|
||||
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
|
||||
foreach (var mr in flat)
|
||||
{
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
|
||||
if (gfx is null) continue;
|
||||
_physicsDataCache.CacheGfxObj(mr.GfxObjId, gfx);
|
||||
// Compose: part's own transform, then the spawn's scale.
|
||||
var partXf = mr.PartTransform * scaleMat;
|
||||
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
||||
|
|
@ -687,7 +792,6 @@ public sealed class LandblockBuildFactory
|
|||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(stab.Id);
|
||||
if (gfx is not null)
|
||||
{
|
||||
_physicsDataCache.CacheGfxObj(stab.Id, gfx);
|
||||
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
||||
if (pb is not null) interiorBounds.Add(System.Numerics.Matrix4x4.Identity, pb.Value);
|
||||
meshRefs.Add(new AcDream.Core.World.MeshRef(stab.Id, System.Numerics.Matrix4x4.Identity));
|
||||
|
|
@ -702,7 +806,6 @@ public sealed class LandblockBuildFactory
|
|||
var setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(stab.Id);
|
||||
if (setup is not null)
|
||||
{
|
||||
_physicsDataCache.CacheSetup(stab.Id, setup);
|
||||
stabLightCount = setup.Lights.Count;
|
||||
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
|
||||
if (dumpStab)
|
||||
|
|
@ -727,7 +830,6 @@ public sealed class LandblockBuildFactory
|
|||
Console.WriteLine($"[dump-entity] HYDRATE src=0x{stab.Id:X8} cell=0x{envCellId:X8} part gfx=0x{mr.GfxObjId:X8} GFXOBJ-NULL -> part dropped");
|
||||
continue;
|
||||
}
|
||||
_physicsDataCache.CacheGfxObj(mr.GfxObjId, gfx);
|
||||
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
||||
if (pb is not null) interiorBounds.Add(mr.PartTransform, pb.Value);
|
||||
meshRefs.Add(mr);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ public sealed class LandblockPhysicsPublication
|
|||
internal int BuildingCursor { get; set; }
|
||||
internal bool BaseCommitted { get; set; }
|
||||
internal int GfxCursor { get; set; }
|
||||
internal uint[] SetupObjectIds { get; set; } = Array.Empty<uint>();
|
||||
internal int SetupCursor { get; set; }
|
||||
internal int PriorStaticCursor { get; set; }
|
||||
internal int StaticCursor { get; set; }
|
||||
internal int BspOwnerCount { get; set; }
|
||||
|
|
@ -199,7 +201,7 @@ public sealed class LandblockPhysicsPublisher
|
|||
build.Landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
|
||||
BuildingInfo[] buildings = datBundle.Info?.Buildings.ToArray()
|
||||
?? Array.Empty<BuildingInfo>();
|
||||
return new LandblockPhysicsPublication(
|
||||
var publication = new LandblockPhysicsPublication(
|
||||
_receiptOwner,
|
||||
build,
|
||||
origin,
|
||||
|
|
@ -207,6 +209,10 @@ public sealed class LandblockPhysicsPublisher
|
|||
buildings,
|
||||
_physicsEngine.ShadowObjects.CaptureStaticOwnersForLandblock(
|
||||
build.Landblock.LandblockId));
|
||||
publication.SetupObjectIds = build.Collisions is { } collisions
|
||||
? [.. collisions.SetupIds]
|
||||
: datBundle.Setups.Keys.Order().ToArray();
|
||||
return publication;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -223,6 +229,14 @@ public sealed class LandblockPhysicsPublisher
|
|||
|
||||
IReadOnlyList<WorldEntity> entities =
|
||||
publication.Build.Landblock.Entities;
|
||||
if (publication.Build.Collisions is { } collisions)
|
||||
{
|
||||
publication.GfxObjectIds = [.. collisions.GfxObjIds];
|
||||
publication.PreparationCursor = entities.Count;
|
||||
publication.PreparationCommitted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (publication.PreparationCursor < entities.Count)
|
||||
{
|
||||
WorldEntity entity = entities[publication.PreparationCursor];
|
||||
|
|
@ -389,10 +403,33 @@ public sealed class LandblockPhysicsPublisher
|
|||
long cacheStarted = Stopwatch.GetTimestamp();
|
||||
uint gfxObjectId = publication.GfxObjectIds[publication.GfxCursor];
|
||||
if (datBundle.GfxObjs.TryGetValue(gfxObjectId, out var gfx))
|
||||
_physicsDataCache.CacheGfxObj(gfxObjectId, gfx);
|
||||
{
|
||||
FlatGfxObjCollisionAsset? prepared = null;
|
||||
publication.Build.Collisions?.GfxObjs.TryGetValue(
|
||||
gfxObjectId,
|
||||
out prepared);
|
||||
_physicsDataCache.CacheGfxObj(
|
||||
gfxObjectId,
|
||||
gfx,
|
||||
prepared);
|
||||
}
|
||||
publication.GfxCursor++;
|
||||
_gfxCacheTicks += Stopwatch.GetTimestamp() - cacheStarted;
|
||||
}
|
||||
else if (publication.SetupCursor < publication.SetupObjectIds.Length)
|
||||
{
|
||||
uint setupId =
|
||||
publication.SetupObjectIds[publication.SetupCursor];
|
||||
if (datBundle.Setups.TryGetValue(setupId, out var setup))
|
||||
{
|
||||
FlatSetupCollision? prepared = null;
|
||||
publication.Build.Collisions?.Setups.TryGetValue(
|
||||
setupId,
|
||||
out prepared);
|
||||
_physicsDataCache.CacheSetup(setupId, setup, prepared);
|
||||
}
|
||||
publication.SetupCursor++;
|
||||
}
|
||||
else if (publication.PriorStaticCursor
|
||||
< publication.PriorStaticOwnerIds.Length)
|
||||
{
|
||||
|
|
@ -486,11 +523,21 @@ public sealed class LandblockPhysicsPublisher
|
|||
Matrix4x4.CreateFromQuaternion(rotation)
|
||||
* Matrix4x4.CreateTranslation(cellOriginWorld);
|
||||
|
||||
FlatCellStructureCollisionAsset? preparedStructure = null;
|
||||
FlatEnvCellTopology? preparedTopology = null;
|
||||
publication.Build.Collisions?.CellStructures.TryGetValue(
|
||||
envCellId,
|
||||
out preparedStructure);
|
||||
publication.Build.Collisions?.EnvCells.TryGetValue(
|
||||
envCellId,
|
||||
out preparedTopology);
|
||||
_physicsDataCache.CacheCellStruct(
|
||||
envCellId,
|
||||
envCell,
|
||||
cellStruct,
|
||||
physicsCellTransform);
|
||||
physicsCellTransform,
|
||||
preparedStructure,
|
||||
preparedTopology);
|
||||
|
||||
var worldVertices = new Dictionary<ushort, Vector3>(
|
||||
cellStruct.VertexArray.Vertices.Count);
|
||||
|
|
|
|||
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