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

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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