LandblockStreamer.HandleJob's near-payload check is "fail loud in Debug builds and strip in Release" (its own comment, with a Debug.Assert at the check). FarLoad_StripsEnvCellsAndPhysicsEvenWhenEntityListIsAlreadyEmpty feeds a deliberately-buggy far factory to verify the Release strip — so under Debug the assert fires, the test host's listener turns it into an exception, and the job publishes Failed BY DESIGN. The test asserted the Release outcome unconditionally and therefore failed on every full Debug App run (found 2026-08-13 during the #385 session; every campaign gate runs Release, which is why it never surfaced). It now asserts the Failed result + assert message under DEBUG and the strip under Release. Verified green in both configs; full Debug App suite 4,937/3 skips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
469 lines
17 KiB
C#
469 lines
17 KiB
C#
using AcDream.App.Rendering.Wb;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.Core.Terrain;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter.DBObjs;
|
|
|
|
namespace AcDream.App.Tests.Streaming;
|
|
|
|
public sealed class LandblockBuildOriginTests
|
|
{
|
|
private const int SpinTimeoutMs = 2000;
|
|
private const int SpinStepMs = 10;
|
|
|
|
[Fact]
|
|
public void ExplicitMapZeroOrigin_IsDistinctFromUnspecifiedCompatibilityOrigin()
|
|
{
|
|
var mapZero = new LandblockBuildOrigin(0, 0);
|
|
|
|
Assert.True(mapZero.IsSpecified);
|
|
Assert.False(default(LandblockBuildOrigin).IsSpecified);
|
|
Assert.NotEqual(default, mapZero);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RequestAwareLoad_CarriesExplicitMapZeroOriginThroughFactoryAndCompletion()
|
|
{
|
|
const uint landblockId = 0x0000FFFFu;
|
|
var capturedOrigin = new LandblockBuildOrigin(0, 0);
|
|
LandblockBuildRequest? observedRequest = null;
|
|
|
|
using var streamer = LandblockStreamer.CreateForRequests(
|
|
loadLandblock: request =>
|
|
{
|
|
observedRequest = request;
|
|
return EmptyBuild(request.LandblockId, request.Origin);
|
|
},
|
|
buildMeshOrNull: (_, _) => EmptyMesh());
|
|
streamer.EnqueueLoad(new LandblockBuildRequest(
|
|
landblockId,
|
|
LandblockStreamJobKind.LoadNear,
|
|
Generation: 77,
|
|
capturedOrigin));
|
|
streamer.Start();
|
|
|
|
var loaded = Assert.IsType<LandblockStreamResult.Loaded>(
|
|
await DrainFirstAsync(streamer));
|
|
|
|
Assert.Equal(
|
|
new LandblockBuildRequest(
|
|
landblockId,
|
|
LandblockStreamJobKind.LoadNear,
|
|
77,
|
|
capturedOrigin),
|
|
observedRequest);
|
|
Assert.Equal(capturedOrigin, loaded.Build.Origin);
|
|
Assert.Equal(77ul, loaded.Generation);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RequestAwareLoad_WhenFactoryReturnsUnspecifiedOrigin_FailsAtMapZero()
|
|
{
|
|
using var streamer = LandblockStreamer.CreateForRequests(
|
|
loadLandblock: request => EmptyBuild(request.LandblockId, default));
|
|
streamer.EnqueueLoad(new LandblockBuildRequest(
|
|
0x0000FFFFu,
|
|
LandblockStreamJobKind.LoadNear,
|
|
Generation: 78,
|
|
new LandblockBuildOrigin(0, 0)));
|
|
streamer.Start();
|
|
|
|
var failed = Assert.IsType<LandblockStreamResult.Failed>(
|
|
await DrainFirstAsync(streamer));
|
|
|
|
Assert.Contains("origin", failed.Error, StringComparison.OrdinalIgnoreCase);
|
|
Assert.Equal(78ul, failed.Generation);
|
|
}
|
|
|
|
[Fact]
|
|
public void CompatibilityLoader_RejectsEvenExplicitMapZeroOriginBeforeQueueing()
|
|
{
|
|
using var streamer = new LandblockStreamer(loadLandblock: _ => null);
|
|
|
|
Assert.Throws<InvalidOperationException>(() => streamer.EnqueueLoad(
|
|
new LandblockBuildRequest(
|
|
0x0000FFFFu,
|
|
LandblockStreamJobKind.LoadNear,
|
|
Generation: 79,
|
|
new LandblockBuildOrigin(0, 0))));
|
|
}
|
|
|
|
[Fact]
|
|
public void RequestAwareLoader_RejectsOriginlessCompatibilityEnqueue()
|
|
{
|
|
using var streamer = LandblockStreamer.CreateForRequests(
|
|
loadLandblock: request => EmptyBuild(request.LandblockId, request.Origin));
|
|
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
streamer.EnqueueLoad(0xA9B4FFFFu));
|
|
}
|
|
|
|
[Fact]
|
|
public void RequestAwareLoader_RejectsDirectRequestWithUnspecifiedOrigin()
|
|
{
|
|
using var streamer = LandblockStreamer.CreateForRequests(
|
|
loadLandblock: request => EmptyBuild(request.LandblockId, request.Origin));
|
|
|
|
Assert.Throws<InvalidOperationException>(() => streamer.EnqueueLoad(
|
|
new LandblockBuildRequest(
|
|
0xA9B4FFFFu,
|
|
LandblockStreamJobKind.LoadNear,
|
|
Generation: 80,
|
|
Origin: default)));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TwoQueuedLoads_RetainTheirDistinctOriginAndGeneration()
|
|
{
|
|
var observed = new List<LandblockBuildRequest>();
|
|
var firstOrigin = new LandblockBuildOrigin(0xA9, 0xB4);
|
|
var secondOrigin = new LandblockBuildOrigin(0x71, 0xEC);
|
|
using var streamer = LandblockStreamer.CreateForRequests(
|
|
loadLandblock: request =>
|
|
{
|
|
observed.Add(request);
|
|
return EmptyBuild(request.LandblockId, request.Origin);
|
|
},
|
|
buildMeshOrNull: (_, _) => EmptyMesh());
|
|
|
|
streamer.EnqueueLoad(new LandblockBuildRequest(
|
|
0xA9B4FFFFu,
|
|
LandblockStreamJobKind.LoadNear,
|
|
Generation: 80,
|
|
firstOrigin));
|
|
streamer.EnqueueLoad(new LandblockBuildRequest(
|
|
0x71ECFFFFu,
|
|
LandblockStreamJobKind.LoadNear,
|
|
Generation: 81,
|
|
secondOrigin));
|
|
streamer.Start();
|
|
|
|
IReadOnlyList<LandblockStreamResult> completions =
|
|
await DrainCountAsync(streamer, 2);
|
|
|
|
Assert.Equal([firstOrigin, secondOrigin], observed.Select(request => request.Origin));
|
|
Assert.Equal([80ul, 81ul], completions.Select(result => result.Generation));
|
|
Assert.Equal(
|
|
[firstOrigin, secondOrigin],
|
|
completions
|
|
.Cast<LandblockStreamResult.Loaded>()
|
|
.Select(result => result.Build.Origin));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task QueuedPromotion_SupersedesOldFarWithItsOwnOriginAndGeneration()
|
|
{
|
|
var observed = new List<LandblockBuildRequest>();
|
|
var oldOrigin = new LandblockBuildOrigin(0xA9, 0xB4);
|
|
var newOrigin = new LandblockBuildOrigin(0x71, 0xEC);
|
|
const uint landblockId = 0x71ECFFFFu;
|
|
using var streamer = LandblockStreamer.CreateForRequests(
|
|
loadLandblock: request =>
|
|
{
|
|
observed.Add(request);
|
|
return EmptyBuild(request.LandblockId, request.Origin);
|
|
},
|
|
buildMeshOrNull: (_, _) => EmptyMesh());
|
|
|
|
streamer.EnqueueLoad(new LandblockBuildRequest(
|
|
landblockId,
|
|
LandblockStreamJobKind.LoadFar,
|
|
Generation: 82,
|
|
oldOrigin));
|
|
streamer.EnqueueLoad(new LandblockBuildRequest(
|
|
landblockId,
|
|
LandblockStreamJobKind.PromoteToNear,
|
|
Generation: 83,
|
|
newOrigin));
|
|
streamer.Start();
|
|
|
|
var promoted = Assert.IsType<LandblockStreamResult.Promoted>(
|
|
await DrainFirstAsync(streamer));
|
|
|
|
Assert.Equal([new LandblockBuildRequest(
|
|
landblockId,
|
|
LandblockStreamJobKind.PromoteToNear,
|
|
83,
|
|
newOrigin)], observed);
|
|
Assert.Equal(newOrigin, promoted.Build.Origin);
|
|
Assert.Equal(83ul, promoted.Generation);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FarLoad_StripsEnvCellsAndPhysicsEvenWhenEntityListIsAlreadyEmpty()
|
|
{
|
|
const uint landblockId = 0xA9B4FFFFu;
|
|
var origin = new LandblockBuildOrigin(0xA9, 0xB4);
|
|
var physics = new PhysicsDatBundle(
|
|
new LandBlockInfo(),
|
|
new Dictionary<uint, EnvCell>(),
|
|
new Dictionary<uint, DatReaderWriter.DBObjs.Environment>(),
|
|
new Dictionary<uint, Setup>(),
|
|
new Dictionary<uint, GfxObj>());
|
|
var envCells = new EnvCellLandblockBuild(
|
|
landblockId,
|
|
Array.Empty<AcDream.App.Rendering.LoadedCell>(),
|
|
Array.Empty<EnvCellShellPlacement>());
|
|
using var streamer = LandblockStreamer.CreateForRequests(
|
|
loadLandblock: request => new LandblockBuild(
|
|
new LoadedLandblock(
|
|
request.LandblockId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>(),
|
|
physics),
|
|
envCells,
|
|
request.Origin),
|
|
buildMeshOrNull: (_, _) => EmptyMesh());
|
|
streamer.EnqueueLoad(new LandblockBuildRequest(
|
|
landblockId,
|
|
LandblockStreamJobKind.LoadFar,
|
|
Generation: 84,
|
|
origin));
|
|
streamer.Start();
|
|
|
|
LandblockStreamResult result = await DrainFirstAsync(streamer);
|
|
|
|
#if DEBUG
|
|
// LandblockStreamer.HandleJob's near-payload check is "fail loud in
|
|
// Debug builds and strip in Release" (its own comment): the
|
|
// Debug.Assert fires on this deliberately-buggy factory, the test
|
|
// host's trace listener turns it into an exception, and the job's
|
|
// catch-all publishes it as Failed. That IS the designed Debug
|
|
// outcome — this test previously asserted the Release strip
|
|
// unconditionally and so failed on every full Debug run
|
|
// (found 2026-08-13; the campaign gates all run Release).
|
|
var failed = Assert.IsType<LandblockStreamResult.Failed>(result);
|
|
Assert.Equal(landblockId, failed.LandblockId);
|
|
Assert.Equal(84ul, failed.Generation);
|
|
Assert.Contains("Far-tier factory returned Near payload", failed.Error);
|
|
#else
|
|
var loaded = Assert.IsType<LandblockStreamResult.Loaded>(result);
|
|
|
|
Assert.Equal(LandblockStreamTier.Far, loaded.Tier);
|
|
Assert.Empty(loaded.Landblock.Entities);
|
|
Assert.Same(PhysicsDatBundle.Empty, loaded.Landblock.PhysicsDats);
|
|
Assert.Null(loaded.Build.EnvCells);
|
|
Assert.Equal(origin, loaded.Build.Origin);
|
|
#endif
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildFactoryAndRenderPublisher_UseCapturedOriginWithoutGameWindowFacade()
|
|
{
|
|
string root = FindRepoRoot();
|
|
string gameWindowSource = File.ReadAllText(Path.Combine(
|
|
root,
|
|
"src",
|
|
"AcDream.App",
|
|
"Rendering",
|
|
"GameWindow.cs"));
|
|
string livePresentationSource = File.ReadAllText(Path.Combine(
|
|
root,
|
|
"src",
|
|
"AcDream.App",
|
|
"Composition",
|
|
"LivePresentationComposition.cs"));
|
|
string buildSource = File.ReadAllText(Path.Combine(
|
|
root,
|
|
"src",
|
|
"AcDream.App",
|
|
"Streaming",
|
|
"LandblockBuildFactory.cs"));
|
|
string renderPublisherSource = File.ReadAllText(Path.Combine(
|
|
root,
|
|
"src",
|
|
"AcDream.App",
|
|
"Streaming",
|
|
"LandblockRenderPublisher.cs"));
|
|
string recenterSource = File.ReadAllText(Path.Combine(
|
|
root,
|
|
"src",
|
|
"AcDream.App",
|
|
"Streaming",
|
|
"StreamingOriginRecenterCoordinator.cs"));
|
|
Assert.Contains("request.Origin", buildSource, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("_liveCenterX", buildSource, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("_liveCenterY", buildSource, StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"BuildLandblockForStreaming",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"BuildSceneryEntitiesForStreaming",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"BuildInteriorEntitiesForStreaming",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"BuildPhysicsDatBundle",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"ApplyLoadedTerrain",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"PublishLandblockStaticLightingBeforeCollision",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"_landblockPhysicsPublisher!.RemoveLandblock",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"_landblockRenderPublisher",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"_landblockPhysicsPublisher",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"_landblockStaticPresentationPublisher",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"_landblockPresentationPipeline",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"new LandblockRenderPublisher(",
|
|
livePresentationSource,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"new LandblockPhysicsPublisher(",
|
|
livePresentationSource,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"new LandblockStaticPresentationPublisher(",
|
|
livePresentationSource,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain("applyTerrain:", gameWindowSource, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("demoteNearLayer:", gameWindowSource, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("retirementCoordinator:", gameWindowSource, StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"_liveWorldOrigin.Recenter(lbX, lbY)",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
int retirementBarrier = recenterSource.IndexOf(
|
|
"IsOriginRecenterRetirementComplete()",
|
|
StringComparison.Ordinal);
|
|
int originCommit = recenterSource.IndexOf(
|
|
"_origin.Recenter(",
|
|
StringComparison.Ordinal);
|
|
int destinationCommit = recenterSource.IndexOf(
|
|
"_streaming.TryCommitOriginRecenter(",
|
|
StringComparison.Ordinal);
|
|
Assert.True(retirementBarrier >= 0);
|
|
Assert.True(originCommit > retirementBarrier);
|
|
Assert.True(destinationCommit > originCommit);
|
|
Assert.Contains("ComputeOrigin(landblockId, build.Origin)", renderPublisherSource, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("_liveCenterX", renderPublisherSource, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("_liveCenterY", renderPublisherSource, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void GameWindowShutdownKeepsStreamerAliveUntilSessionResetConverges()
|
|
{
|
|
string source = File.ReadAllText(Path.Combine(
|
|
FindRepoRoot(),
|
|
"src",
|
|
"AcDream.App",
|
|
"Rendering",
|
|
"GameWindowLifetime.cs"));
|
|
int sessionStage = source.IndexOf(
|
|
"new ResourceShutdownStage(\"host and session barriers\"",
|
|
StringComparison.Ordinal);
|
|
Assert.True(sessionStage >= 0);
|
|
|
|
int sessionOperation = source.IndexOf(
|
|
"Hard(\"game runtime session\", ingress.Runtime.StopSession)",
|
|
sessionStage,
|
|
StringComparison.Ordinal);
|
|
Assert.True(sessionOperation > sessionStage);
|
|
|
|
int dependentStage = source.IndexOf(
|
|
"new ResourceShutdownStage(\"session dependents\"",
|
|
sessionOperation,
|
|
StringComparison.Ordinal);
|
|
Assert.True(dependentStage > sessionOperation);
|
|
|
|
int streamerDispose = source.IndexOf(
|
|
"Hard(\"streamer\", () => live.Streamer?.Dispose())",
|
|
dependentStage,
|
|
StringComparison.Ordinal);
|
|
Assert.True(streamerDispose > dependentStage);
|
|
|
|
string runtime = File.ReadAllText(Path.Combine(
|
|
FindRepoRoot(),
|
|
"src",
|
|
"AcDream.Runtime",
|
|
"GameRuntime.cs"));
|
|
int helper = runtime.IndexOf(
|
|
"public void StopSession()",
|
|
StringComparison.Ordinal);
|
|
Assert.True(helper >= 0);
|
|
int sessionDispose = runtime.IndexOf(
|
|
"Session.Dispose();",
|
|
helper,
|
|
StringComparison.Ordinal);
|
|
Assert.True(sessionDispose > helper);
|
|
|
|
int disposalCompletionBarrier = runtime.IndexOf(
|
|
"if (!Session.IsDisposalComplete)",
|
|
sessionDispose,
|
|
StringComparison.Ordinal);
|
|
Assert.True(disposalCompletionBarrier > sessionDispose);
|
|
|
|
Assert.Contains(
|
|
"The Runtime session shutdown was deferred by a re-entrant callback.",
|
|
runtime[disposalCompletionBarrier..],
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
private static LandblockBuild EmptyBuild(uint landblockId, LandblockBuildOrigin origin) =>
|
|
new(
|
|
new LoadedLandblock(
|
|
landblockId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()),
|
|
Origin: origin);
|
|
|
|
private static LandblockMeshData EmptyMesh() =>
|
|
new(Array.Empty<TerrainVertex>(), Array.Empty<uint>());
|
|
|
|
private static async Task<LandblockStreamResult> DrainFirstAsync(
|
|
LandblockStreamer streamer) =>
|
|
(await DrainCountAsync(streamer, 1))[0];
|
|
|
|
private static async Task<IReadOnlyList<LandblockStreamResult>> DrainCountAsync(
|
|
LandblockStreamer streamer,
|
|
int count)
|
|
{
|
|
var results = new List<LandblockStreamResult>(count);
|
|
for (int i = 0; i < SpinTimeoutMs / SpinStepMs && results.Count < count; i++)
|
|
{
|
|
results.AddRange(streamer.DrainCompletions(count - results.Count));
|
|
if (results.Count < count)
|
|
await Task.Delay(SpinStepMs);
|
|
}
|
|
|
|
Assert.Equal(count, results.Count);
|
|
return results;
|
|
}
|
|
|
|
private static string FindRepoRoot()
|
|
{
|
|
string? dir = AppContext.BaseDirectory;
|
|
while (dir is not null)
|
|
{
|
|
if (File.Exists(Path.Combine(dir, "AcDream.slnx")))
|
|
return dir;
|
|
dir = Directory.GetParent(dir)?.FullName;
|
|
}
|
|
|
|
throw new DirectoryNotFoundException("Could not locate AcDream.slnx.");
|
|
}
|
|
}
|