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

@ -1,5 +1,6 @@
using System.Reflection;
using System.Numerics;
using System.Collections.Immutable;
using AcDream.App.Streaming;
using AcDream.Content;
using AcDream.Core.Physics;
@ -32,6 +33,7 @@ public sealed class LandblockBuildFactoryTests
Assert.Empty(result.Landblock.Entities);
Assert.Same(PhysicsDatBundle.Empty, result.Landblock.PhysicsDats);
Assert.Null(result.EnvCells);
Assert.Null(result.Collisions);
Assert.Equal(HoltburgOrigin, result.Origin);
Assert.Equal([(typeof(LandBlock), LandblockId)], proxy.Reads);
}
@ -81,6 +83,40 @@ public sealed class LandblockBuildFactoryTests
var physics = Assert.IsType<PhysicsDatBundle>(result.Landblock.PhysicsDats);
Assert.Null(physics.Info);
Assert.Empty(physics.EnvCells);
Assert.NotNull(result.Collisions);
Assert.Empty(result.Collisions.GfxObjs);
Assert.Empty(result.Collisions.Setups);
Assert.Empty(result.Collisions.CellStructures);
Assert.Empty(result.Collisions.EnvCells);
}
[Fact]
public void NearPreparedFaultRejectsWholeGenerationWhileFarNeverReadsCollision()
{
var dat = CreateDat(out RecordingDatProxy proxy);
AddNearFixture(proxy, LandblockId, environmentId: 1);
var source = new TestPreparedCollisionSource(
PreparedAssetReadStatus.Missing);
var factory = new LandblockBuildFactory(
dat,
source,
new object(),
new float[256]);
LandblockBuild? far = factory.Build(
Request(LandblockStreamJobKind.LoadFar));
Assert.NotNull(far);
Assert.Null(far.Collisions);
Assert.Equal(0, source.Reads);
InvalidDataException failure =
Assert.Throws<InvalidDataException>(() =>
factory.Build(Request(LandblockStreamJobKind.LoadNear)));
Assert.Contains(
"complete near-tier generation",
failure.Message,
StringComparison.Ordinal);
Assert.True(source.Reads > 0);
}
[Fact]
@ -103,6 +139,9 @@ public sealed class LandblockBuildFactoryTests
var physics = Assert.IsType<PhysicsDatBundle>(result.Landblock.PhysicsDats);
Assert.Single(physics.EnvCells);
Assert.DoesNotContain(missingCell, physics.EnvCells.Keys);
Assert.Single(result.Collisions!.CellStructures);
Assert.Single(result.Collisions.EnvCells);
Assert.DoesNotContain(missingCell, result.Collisions.EnvCells.Keys);
}
[Fact]
@ -215,9 +254,9 @@ public sealed class LandblockBuildFactoryTests
var heights = Enumerable.Range(0, 256).Select(value => (float)value).ToArray();
var factory = new LandblockBuildFactory(
dat,
TestPreparedCollisionSource.Instance,
new object(),
heights,
new PhysicsDataCache());
heights);
heights[42] = -1f;
var snapshot = Assert.IsType<float[]>(typeof(LandblockBuildFactory)
@ -234,9 +273,9 @@ public sealed class LandblockBuildFactoryTests
Assert.Throws<ArgumentException>(() => new LandblockBuildFactory(
dat,
TestPreparedCollisionSource.Instance,
new object(),
new float[255],
new PhysicsDataCache()));
new float[255]));
}
[Fact]
@ -273,6 +312,7 @@ public sealed class LandblockBuildFactoryTests
dependency.FullName?.Contains("LiveWorldOrigin", StringComparison.Ordinal) == true);
Assert.DoesNotContain(dependencyTypes, dependency =>
dependency.Namespace?.StartsWith("Silk.NET", StringComparison.Ordinal) == true);
Assert.DoesNotContain(typeof(PhysicsDataCache), dependencyTypes);
}
[Fact]
@ -288,11 +328,15 @@ public sealed class LandblockBuildFactoryTests
float[]? heights = region?.LandDefs.LandHeightTable;
Assert.NotNull(heights);
Assert.True(heights.Length >= 256);
string? pakPath = ResolvePreparedPackagePath(datDirectory);
if (pakPath is null)
throw SkipException.ForSkip("Installed acdream.pak is required.");
using var prepared = new PakPreparedAssetSource(pakPath, bounded);
var factory = new LandblockBuildFactory(
bounded,
prepared,
new object(),
heights,
new PhysicsDataCache());
heights);
LandblockBuildRequest request = Request(LandblockStreamJobKind.LoadNear, generation: 4);
LandblockBuild? first = factory.Build(request);
@ -302,6 +346,7 @@ public sealed class LandblockBuildFactoryTests
Assert.NotNull(second);
Assert.Equal(HoltburgOrigin, first.Origin);
Assert.NotNull(first.EnvCells);
Assert.NotNull(first.Collisions);
Assert.NotEmpty(first.Landblock.Entities);
AssertEntityBuildsEqual(first.Landblock.Entities, second.Landblock.Entities);
Assert.Equal(
@ -335,6 +380,12 @@ public sealed class LandblockBuildFactoryTests
PhysicsDatBundle physics = Assert.IsType<PhysicsDatBundle>(
first.Landblock.PhysicsDats);
Assert.NotNull(physics.Info);
Assert.Equal(physics.EnvCells.Keys.Order(), first.Collisions.EnvCells.Keys.Order());
Assert.Equal(
physics.EnvCells.Keys.Order(),
first.Collisions.CellStructures.Keys.Order());
Assert.Equal(physics.Setups.Keys.Order(), first.Collisions.Setups.Keys.Order());
Assert.Equal(physics.GfxObjs.Keys.Order(), first.Collisions.GfxObjs.Keys.Order());
foreach (WorldEntity entity in first.Landblock.Entities)
{
if ((entity.SourceGfxObjOrSetupId & 0xFF000000u) == 0x02000000u)
@ -345,7 +396,11 @@ public sealed class LandblockBuildFactoryTests
}
private static LandblockBuildFactory Factory(IDatReaderWriter dat, object gate) =>
new(dat, gate, new float[256], new PhysicsDataCache());
new(
dat,
TestPreparedCollisionSource.Instance,
gate,
new float[256]);
private static LandblockBuildRequest Request(
LandblockStreamJobKind kind,
@ -444,6 +499,26 @@ public sealed class LandblockBuildFactoryTests
return Directory.Exists(documents) ? documents : null;
}
private static string? ResolvePreparedPackagePath(string datDirectory)
{
string? configured =
System.Environment.GetEnvironmentVariable("ACDREAM_PAK_PATH");
if (!string.IsNullOrWhiteSpace(configured) && File.Exists(configured))
return configured;
string besideDats = Path.Combine(datDirectory, "acdream.pak");
if (File.Exists(besideDats))
return besideDats;
string documents = Path.Combine(
System.Environment.GetFolderPath(
System.Environment.SpecialFolder.UserProfile),
"Documents",
"Asheron's Call",
"acdream.pak");
return File.Exists(documents) ? documents : null;
}
private class RecordingDatProxy : DispatchProxy
{
private readonly Dictionary<(Type Type, uint Id), object> _objects = new();
@ -506,4 +581,97 @@ public sealed class LandblockBuildFactoryTests
return null;
}
}
private sealed class TestPreparedCollisionSource :
IPreparedCollisionSource
{
private static readonly FlatPhysicsBsp EmptyPhysics = new(
-1,
ImmutableArray<FlatPhysicsBspNode>.Empty,
ImmutableArray<int>.Empty,
FlatPolygonTable.Empty);
private static readonly FlatCellStructureCollisionAsset EmptyCell =
new(
EmptyPhysics,
new FlatCellContainmentBsp(
-1,
ImmutableArray<FlatCellBspNode>.Empty),
FlatPolygonTable.Empty);
private static readonly FlatEnvCellTopology EmptyTopology = new(
ImmutableArray<FlatEnvCellPortal>.Empty,
ImmutableArray<uint>.Empty,
seenOutside: false);
private readonly PreparedAssetReadStatus _status;
internal TestPreparedCollisionSource(
PreparedAssetReadStatus status =
PreparedAssetReadStatus.Loaded)
{
_status = status;
}
internal static TestPreparedCollisionSource Instance { get; } = new();
internal int Reads { get; private set; }
public PreparedAssetPresence ProbeCollision(
AcDream.Content.Pak.PakAssetType type,
uint sourceFileId) =>
PreparedAssetPresence.Available;
public PreparedCollisionReadResult<FlatGfxObjCollisionAsset>
ReadGfxObjCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
Result(
new FlatGfxObjCollisionAsset(
EmptyPhysics,
null,
null));
public PreparedCollisionReadResult<FlatSetupCollision>
ReadSetupCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
Result(
new FlatSetupCollision(
ImmutableArray<FlatCollisionCylinder>.Empty,
ImmutableArray<FlatCollisionSphere>.Empty,
0f,
0f,
0f,
0f));
public PreparedCollisionReadResult<FlatCellStructureCollisionAsset>
ReadCellStructureCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
Result(EmptyCell);
public PreparedCollisionReadResult<FlatEnvCellTopology>
ReadEnvCellTopology(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
Result(EmptyTopology);
public PreparedCollisionSourceStats CollisionStats => default;
public void Dispose()
{
}
private PreparedCollisionReadResult<T> Result<T>(T value)
where T : class
{
Reads++;
return _status switch
{
PreparedAssetReadStatus.Loaded =>
PreparedCollisionReadResult<T>.Loaded(value),
PreparedAssetReadStatus.Missing =>
PreparedCollisionReadResult<T>.Missing,
_ => PreparedCollisionReadResult<T>.Corrupt,
};
}
}
}