test: replace streaming source freezes

This commit is contained in:
Erik 2026-08-18 15:14:41 +02:00
parent 5a33369074
commit 0ad2ee1cdf
6 changed files with 436 additions and 285 deletions

View file

@ -21,6 +21,69 @@ internal static class CompiledCallGraph
.ToDictionary(opCode => opCode.Value);
public static IReadOnlyList<CompiledCall> Read(MethodBase method)
=> ReadMethodReferences(method, includeDelegateTargets: false);
/// <summary>
/// Reads calls, object construction, and method references used to build
/// delegates. The latter lets lifetime tests inspect an operation manifest
/// without invoking its real process/window resources.
/// </summary>
public static IReadOnlyList<CompiledCall> ReadMethodReferences(MethodBase method) =>
ReadMethodReferences(method, includeDelegateTargets: true);
public static IReadOnlyList<CompiledCall> ReadDeclared(Type type)
{
ArgumentNullException.ThrowIfNull(type);
const BindingFlags flags = BindingFlags.Instance
| BindingFlags.Static
| BindingFlags.Public
| BindingFlags.NonPublic
| BindingFlags.DeclaredOnly;
return type.GetMethods(flags)
.Cast<MethodBase>()
.Concat(type.GetConstructors(flags))
.Where(method => method.GetMethodBody() is not null)
.SelectMany(Read)
.ToArray();
}
public static IReadOnlyList<string> ReadStringLiterals(MethodBase method)
{
ArgumentNullException.ThrowIfNull(method);
byte[] il = method.GetMethodBody()?.GetILAsByteArray()
?? throw new InvalidOperationException(
$"{method.DeclaringType?.FullName}.{method.Name} has no compiled body.");
var literals = new List<string>();
for (int cursor = 0; cursor < il.Length;)
{
OpCode opCode = ReadOpCode(il, ref cursor);
if (opCode == OpCodes.Ldstr)
{
int token = BitConverter.ToInt32(il, cursor);
literals.Add(method.Module.ResolveString(token));
}
cursor += OperandSize(opCode.OperandType, il, cursor);
}
return literals;
}
public static int IndexOf(
IReadOnlyList<CompiledCall> calls,
Type declaringType,
string methodName,
int startIndex = 0) =>
Enumerable.Range(startIndex, calls.Count - startIndex)
.FirstOrDefault(
index => calls[index].Target.DeclaringType == declaringType
&& calls[index].Target.Name == methodName,
-1);
private static IReadOnlyList<CompiledCall> ReadMethodReferences(
MethodBase method,
bool includeDelegateTargets)
{
ArgumentNullException.ThrowIfNull(method);
byte[] il = method.GetMethodBody()?.GetILAsByteArray()
@ -45,11 +108,13 @@ internal static class CompiledCallGraph
token,
declaringArguments,
methodArguments);
if (target is not null && opCode is { Value: var value }
&& value is 0x28 or 0x6F or 0x73)
{
bool invocation = opCode == OpCodes.Call
|| opCode == OpCodes.Callvirt
|| opCode == OpCodes.Newobj;
bool delegateTarget = includeDelegateTargets
&& (opCode == OpCodes.Ldftn || opCode == OpCodes.Ldvirtftn);
if (target is not null && (invocation || delegateTarget))
calls.Add(new CompiledCall(instructionOffset, target));
}
}
cursor += OperandSize(opCode.OperandType, il, cursor);
@ -58,17 +123,6 @@ internal static class CompiledCallGraph
return calls;
}
public static int IndexOf(
IReadOnlyList<CompiledCall> calls,
Type declaringType,
string methodName,
int startIndex = 0) =>
Enumerable.Range(startIndex, calls.Count - startIndex)
.FirstOrDefault(
index => calls[index].Target.DeclaringType == declaringType
&& calls[index].Target.Name == methodName,
-1);
private static OpCode ReadOpCode(byte[] il, ref int cursor)
{
byte first = il[cursor++];

View file

@ -1,7 +1,14 @@
using System.Reflection;
using AcDream.App.Composition;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Wb;
using AcDream.App.Streaming;
using AcDream.App.Tests.Architecture;
using AcDream.App.World;
using AcDream.Core.Terrain;
using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.Session;
using DatReaderWriter.DBObjs;
namespace AcDream.App.Tests.Streaming;
@ -252,179 +259,136 @@ public sealed class LandblockBuildOriginTests
}
[Fact]
public void BuildFactoryAndRenderPublisher_UseCapturedOriginWithoutGameWindowFacade()
public void ProductionCompositionOwnsPublishersWithoutGameWindowFacade()
{
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(
MethodInfo[] windowMethods = typeof(GameWindow).GetMethods(
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
string[] extractedMethodNames =
[
"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);
windowMethods,
method => extractedMethodNames.Contains(method.Name, StringComparer.Ordinal));
FieldInfo[] windowFields = typeof(GameWindow).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.Single(
windowFields,
field => field.FieldType == typeof(LandblockPresentationPipeline));
Assert.DoesNotContain(
"_landblockRenderPublisher",
gameWindowSource,
StringComparison.Ordinal);
windowFields,
field => field.FieldType == typeof(LandblockRenderPublisher)
|| field.FieldType == typeof(LandblockPhysicsPublisher)
|| field.FieldType == typeof(LandblockStaticPresentationPublisher));
IReadOnlyList<CompiledCall> windowCalls =
CompiledCallGraph.ReadDeclared(typeof(GameWindow));
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);
windowCalls,
call => call.Target.DeclaringType == typeof(LiveWorldOriginState)
&& call.Target.Name == nameof(LiveWorldOriginState.Recenter));
MethodInfo compose = typeof(LivePresentationCompositionPhase).GetMethod(
"CompletePresentation",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> composeCalls = CompiledCallGraph.Read(compose);
int renderPublisher = RequiredCallIndex(
composeCalls,
typeof(LandblockRenderPublisher),
".ctor");
int physicsPublisher = RequiredCallIndex(
composeCalls,
typeof(LandblockPhysicsPublisher),
".ctor");
int staticPublisher = RequiredCallIndex(
composeCalls,
typeof(LandblockStaticPresentationPublisher),
".ctor");
int pipeline = RequiredCallIndex(
composeCalls,
typeof(LandblockPresentationPipeline),
".ctor");
Assert.True(renderPublisher < physicsPublisher);
Assert.True(physicsPublisher < staticPublisher);
Assert.True(staticPublisher < pipeline);
// Captured-origin flow itself is behavioral: BuildFar exercises the
// real LandblockBuildFactory and BeginPublication exercises the real
// render publisher. This metadata check protects only production
// ownership and composition, which those fixtures cannot observe.
}
[Fact]
public void OriginRecenterWaitsForRetirementBeforeOriginAndDestinationCommits()
{
MethodInfo advance = typeof(StreamingOriginRecenterCoordinator).GetMethod(
nameof(StreamingOriginRecenterCoordinator.Advance))!;
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(advance);
int retirement = RequiredCallIndex(
calls,
typeof(StreamingController),
"IsOriginRecenterRetirementComplete");
int origin = RequiredCallIndex(
calls,
typeof(LiveWorldOriginState),
nameof(LiveWorldOriginState.Recenter));
int destination = RequiredCallIndex(
calls,
typeof(StreamingController),
"TryCommitOriginRecenter");
Assert.True(retirement < origin);
Assert.True(origin < destination);
}
[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);
MethodInfo manifest = typeof(GameWindowShutdownManifest).GetMethod(
nameof(GameWindowShutdownManifest.Create))!;
IReadOnlyList<string> labels = CompiledCallGraph.ReadStringLiterals(manifest);
AssertAppearsInOrder(
labels,
"host and session barriers",
"game runtime session",
"session dependents",
"streamer");
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);
IReadOnlyList<CompiledCall> references =
CompiledCallGraph.ReadMethodReferences(manifest);
int stopSession = RequiredCallIndex(
references,
typeof(GameRuntime),
nameof(GameRuntime.StopSession));
int streamerDispose = Enumerable.Range(stopSession + 1, references.Count - stopSession - 1)
.FirstOrDefault(
index => Calls(
references[index].Target,
typeof(LandblockStreamer),
nameof(LandblockStreamer.Dispose)),
-1);
Assert.True(streamerDispose > stopSession, "Missing later streamer-disposal operation.");
MethodInfo runtimeStop = typeof(GameRuntime).GetMethod(nameof(GameRuntime.StopSession))!;
IReadOnlyList<CompiledCall> runtimeCalls = CompiledCallGraph.Read(runtimeStop);
int sessionDispose = RequiredCallIndex(
runtimeCalls,
typeof(LiveSessionController),
nameof(LiveSessionController.Dispose));
int completionBarrier = RequiredCallIndex(
runtimeCalls,
typeof(LiveSessionController),
"get_IsDisposalComplete");
Assert.True(sessionDispose < completionBarrier);
Assert.Contains(
"The Runtime session shutdown was deferred by a re-entrant callback.",
runtime[disposalCompletionBarrier..],
StringComparison.Ordinal);
CompiledCallGraph.ReadStringLiterals(runtimeStop));
}
private static LandblockBuild EmptyBuild(uint landblockId, LandblockBuildOrigin origin) =>
@ -458,16 +422,37 @@ public sealed class LandblockBuildOriginTests
return results;
}
private static string FindRepoRoot()
private static int RequiredCallIndex(
IReadOnlyList<CompiledCall> calls,
Type declaringType,
string methodName)
{
string? dir = AppContext.BaseDirectory;
while (dir is not null)
{
if (File.Exists(Path.Combine(dir, "AcDream.slnx")))
return dir;
dir = Directory.GetParent(dir)?.FullName;
}
int index = CompiledCallGraph.IndexOf(calls, declaringType, methodName);
Assert.True(index >= 0, $"Missing compiled call: {declaringType.Name}.{methodName}");
return index;
}
throw new DirectoryNotFoundException("Could not locate AcDream.slnx.");
private static bool Calls(
MethodBase method,
Type declaringType,
string methodName) =>
method.GetMethodBody() is not null
&& CompiledCallGraph.IndexOf(
CompiledCallGraph.Read(method),
declaringType,
methodName) >= 0;
private static void AssertAppearsInOrder(
IReadOnlyList<string> values,
params string[] expected)
{
int cursor = -1;
foreach (string value in expected)
{
int next = Enumerable.Range(cursor + 1, values.Count - cursor - 1)
.FirstOrDefault(index => values[index] == value, -1);
Assert.True(next > cursor, $"Missing or out-of-order metadata string: {value}");
cursor = next;
}
}
}

View file

@ -1,7 +1,9 @@
using System.Collections.Immutable;
using System.Numerics;
using System.Reflection;
using AcDream.App.Rendering;
using AcDream.App.Streaming;
using AcDream.App.Tests.Architecture;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
@ -1047,34 +1049,63 @@ public sealed class LandblockPhysicsPublisherTests
[Fact]
public void GameWindow_HasNoLandblockPhysicsPublicationBodies()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
IReadOnlyList<CompiledCall> windowCalls =
CompiledCallGraph.ReadDeclared(typeof(GameWindow));
AssertNoCall(windowCalls, typeof(PhysicsDataCache), nameof(PhysicsDataCache.CacheCellStruct));
AssertNoCall(windowCalls, typeof(PhysicsDataCache), nameof(PhysicsDataCache.CacheBuilding));
AssertNoCall(
windowCalls,
typeof(ShadowShapeBuilder),
nameof(ShadowShapeBuilder.FromLandblockBspParts));
AssertNoCall(
windowCalls,
typeof(ShadowObjectRegistry),
nameof(ShadowObjectRegistry.RefloodLandblock));
AssertNoCall(
windowCalls,
typeof(PhysicsEngine),
"DemoteLandblockToTerrain");
AssertNoCall(windowCalls, typeof(PhysicsEngine), "RemoveLandblock");
Assert.DoesNotContain("_physicsDataCache.CacheCellStruct", source, StringComparison.Ordinal);
Assert.DoesNotContain("_physicsDataCache.CacheBuilding", source, StringComparison.Ordinal);
Assert.DoesNotContain("ShadowShapeBuilder.FromLandblockBspParts", source, StringComparison.Ordinal);
Assert.DoesNotContain("ShadowObjects.RefloodLandblock", source, StringComparison.Ordinal);
Assert.DoesNotContain("_physicsEngine.DemoteLandblockToTerrain", source, StringComparison.Ordinal);
Assert.DoesNotContain("_physicsEngine.RemoveLandblock", source, StringComparison.Ordinal);
string publisherSource = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Streaming",
"LandblockPhysicsPublisher.cs"));
Assert.DoesNotContain("_physicsDataCache.RemoveCellsForLandblock", publisherSource, StringComparison.Ordinal);
Assert.DoesNotContain("_physicsDataCache.RemoveBuildingsForLandblock", publisherSource, StringComparison.Ordinal);
Assert.DoesNotContain("_physicsEngine.ShadowObjects.Reflood", publisherSource, StringComparison.Ordinal);
Assert.Contains("CommitCollisionGeneration(", publisherSource, StringComparison.Ordinal);
Assert.DoesNotContain(
"RestartCollisionRetainedOwnerCapture(",
publisherSource,
StringComparison.Ordinal);
IReadOnlyList<CompiledCall> publisherCalls =
CompiledCallGraph.ReadDeclared(typeof(LandblockPhysicsPublisher));
MethodInfo advance = typeof(LandblockPhysicsPublisher).GetMethod(
"AdvanceBeginOne",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> advanceCalls = CompiledCallGraph.Read(advance);
int stagedCache = CompiledCallGraph.IndexOf(
advanceCalls,
typeof(LandblockPhysicsPublication),
"get_StagingCache");
int removeCells = CompiledCallGraph.IndexOf(
advanceCalls,
typeof(PhysicsDataCache),
nameof(PhysicsDataCache.RemoveCellsForLandblock));
int removeBuildings = CompiledCallGraph.IndexOf(
advanceCalls,
typeof(PhysicsDataCache),
nameof(PhysicsDataCache.RemoveBuildingsForLandblock));
int secondStagedCache = CompiledCallGraph.IndexOf(
advanceCalls,
typeof(LandblockPhysicsPublication),
"get_StagingCache",
removeCells + 1);
Assert.True(stagedCache >= 0);
Assert.True(stagedCache < removeCells);
Assert.True(removeCells < secondStagedCache);
Assert.True(secondStagedCache < removeBuildings);
AssertNoCall(
publisherCalls,
typeof(ShadowObjectRegistry),
nameof(ShadowObjectRegistry.RefloodLandblock));
Assert.Contains(
publisherCalls,
call => call.Target.DeclaringType == typeof(RuntimePhysicsState)
&& call.Target.Name == "CommitCollisionGeneration");
AssertNoCall(
publisherCalls,
typeof(RuntimePhysicsState),
"RestartCollisionRetainedOwnerCapture");
}
private static void Publish(
@ -1500,15 +1531,14 @@ public sealed class LandblockPhysicsPublisherTests
Height = new byte[81],
};
private static string FindRepoRoot()
private static void AssertNoCall(
IReadOnlyList<CompiledCall> calls,
Type declaringType,
string methodName)
{
string? directory = AppContext.BaseDirectory;
while (directory is not null)
{
if (File.Exists(Path.Combine(directory, "AcDream.slnx")))
return directory;
directory = Directory.GetParent(directory)?.FullName;
}
throw new DirectoryNotFoundException("Could not locate repository root.");
Assert.DoesNotContain(
calls,
call => call.Target.DeclaringType == declaringType
&& call.Target.Name == methodName);
}
}

View file

@ -4,6 +4,7 @@ using System.Collections.Immutable;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Wb;
using AcDream.App.Streaming;
using AcDream.App.Tests.Architecture;
using AcDream.Core.Terrain;
using AcDream.Core.World;
using DatReaderWriter.DBObjs;
@ -306,18 +307,27 @@ public sealed class LandblockRenderPublisherTests
[Fact]
public void GameWindow_HasNoDirectRenderPublicationBodies()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
IReadOnlyList<CompiledCall> calls =
CompiledCallGraph.ReadDeclared(typeof(GameWindow));
Assert.DoesNotContain(
calls,
call => call.Target.DeclaringType == typeof(TerrainModernRenderer)
&& call.Target.Name == nameof(TerrainModernRenderer.AddLandblockWithMesh));
Assert.DoesNotContain(
calls,
call => call.Target.DeclaringType == typeof(CellVisibility)
&& call.Target.Name == nameof(CellVisibility.CommitLandblock));
Assert.DoesNotContain(
calls,
call => call.Target.DeclaringType == typeof(EnvCellRenderer)
&& call.Target.Name == nameof(EnvCellRenderer.CommitLandblock));
Assert.DoesNotContain("_terrain.AddLandblockWithMesh", source, StringComparison.Ordinal);
Assert.DoesNotContain("_cellVisibility.CommitLandblock", source, StringComparison.Ordinal);
Assert.DoesNotContain("_buildingRegistries", source, StringComparison.Ordinal);
Assert.DoesNotContain("_envCellRenderer?.CommitLandblock", source, StringComparison.Ordinal);
Assert.DoesNotContain("EnsureEnvCellMeshesAfterPin", source, StringComparison.Ordinal);
Assert.Null(typeof(GameWindow).GetMethod(
"EnsureEnvCellMeshesAfterPin",
BindingFlags.Instance | BindingFlags.NonPublic));
Assert.DoesNotContain(
typeof(GameWindow).GetFields(BindingFlags.Instance | BindingFlags.NonPublic),
field => field.Name == "_buildingRegistries");
}
private static LandblockRenderPublisher Publisher(
@ -406,18 +416,6 @@ public sealed class LandblockRenderPublisherTests
private static LandblockMeshData EmptyMesh() =>
new(Array.Empty<TerrainVertex>(), Array.Empty<uint>());
private static string FindRepoRoot()
{
string? directory = AppContext.BaseDirectory;
while (directory is not null)
{
if (File.Exists(Path.Combine(directory, "AcDream.slnx")))
return directory;
directory = Directory.GetParent(directory)?.FullName;
}
throw new DirectoryNotFoundException("Could not locate repository root.");
}
private sealed class RecordingEnvCellPublisher :
IEnvCellLandblockPublisher
{

View file

@ -1,7 +1,11 @@
using System.Collections;
using System.Reflection;
using AcDream.App.Composition;
using AcDream.App.Input;
using AcDream.App.Net;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Tests.Architecture;
using AcDream.App.World;
using AcDream.Core.Net;
using AcDream.Core.Physics;
@ -213,58 +217,74 @@ public sealed class GameWindowLiveEntityCompositionTests
field => typeof(Delegate).IsAssignableFrom(field.FieldType)
&& !field.Name.Contains("DiagnosticSink", StringComparison.Ordinal));
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(), "src", "AcDream.App", "Rendering", "GameWindow.cs"));
string livePresentation = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"LivePresentationComposition.cs"));
FieldInfo[] windowFields = typeof(GameWindow).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.Equal(
typeof(LiveEntityAnimationRuntimeView<LiveEntityAnimationState>),
Assert.Single(windowFields, field => field.Name == "_animatedEntities").FieldType);
Assert.DoesNotContain(
"new LiveEntityAnimationRuntimeView<LiveEntityAnimationState>(() =>",
source,
StringComparison.Ordinal);
Assert.DoesNotContain(
"LiveEntityRemoteMotionRuntimeView",
source,
StringComparison.Ordinal);
Assert.Contains("new DatProjectileSetupResolver", livePresentation);
windowFields,
field => field.FieldType.Name.Contains(
"LiveEntityRemoteMotionRuntimeView",
StringComparison.Ordinal));
MethodInfo compose = typeof(LivePresentationCompositionPhase).GetMethod(
"ComposeCore",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(compose);
int resolver = RequiredCallIndex(
calls,
typeof(DatProjectileSetupResolver),
".ctor");
int controller = RequiredCallIndex(
calls,
typeof(ProjectileController),
".ctor");
Assert.True(resolver < controller);
}
[Fact]
public void SessionReset_ClosesEveryStreamingReadinessOwner()
public void SessionReset_CallsPlayerModeNetworkAndWorldOriginOwners()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(), "src", "AcDream.App", "Net", "LiveSessionRuntimeFactory.cs"));
MethodInfo resetPlayer = typeof(LiveSessionRuntimeFactory).GetMethod(
"ResetPlayerPresentation",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> playerCalls = CompiledCallGraph.Read(resetPlayer);
Assert.True(RequiredCallIndex(
playerCalls,
typeof(PlayerModeController),
nameof(PlayerModeController.ResetSession)) >= 0);
Assert.Contains("_interaction.PlayerMode.ResetSession();", source,
StringComparison.Ordinal);
string playerModeSource = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Input",
"PlayerModeController.cs"));
Assert.Contains("_mode.ResetSession();", playerModeSource,
StringComparison.Ordinal);
Assert.Contains(
"_world.NetworkUpdates.ResetSessionState();",
source,
StringComparison.Ordinal);
Assert.Contains("_player.WorldOrigin.Reset();", source, StringComparison.Ordinal);
MethodInfo resetMode = typeof(PlayerModeController).GetMethod(
nameof(PlayerModeController.ResetSession))!;
IReadOnlyList<CompiledCall> modeCalls = CompiledCallGraph.Read(resetMode);
Assert.True(RequiredCallIndex(
modeCalls,
typeof(LocalPlayerModeState),
nameof(LocalPlayerModeState.ResetSession)) >= 0);
MethodInfo resetIdentity = typeof(LiveSessionRuntimeFactory).GetMethod(
"ResetIdentityPresentation",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> identityCalls = CompiledCallGraph.Read(resetIdentity);
int network = RequiredCallIndex(
identityCalls,
typeof(LiveEntityNetworkUpdateController),
nameof(LiveEntityNetworkUpdateController.ResetSessionState));
int origin = RequiredCallIndex(
identityCalls,
typeof(LiveWorldOriginState),
nameof(LiveWorldOriginState.Reset));
Assert.True(network < origin);
}
private static string FindRepoRoot()
private static int RequiredCallIndex(
IReadOnlyList<CompiledCall> calls,
Type declaringType,
string methodName)
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
return directory.FullName;
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
int index = CompiledCallGraph.IndexOf(calls, declaringType, methodName);
Assert.True(index >= 0, $"Missing compiled call: {declaringType.Name}.{methodName}");
return index;
}
}