Move the canonical WorldSession generation, connect/enter/tick/stop transaction, inbound subscription owner, and retryable teardown acknowledgements into AcDream.Runtime. Keep App as a borrowing graphical host with a single inertable command projection and no mirrored session state. Validated by 79 Runtime tests, 3,776 App tests with three existing skips, the Release solution build, and 8,428 complete Release tests with five existing skips. Co-authored-by: Codex <codex@openai.com>
172 lines
5.9 KiB
C#
172 lines
5.9 KiB
C#
using AcDream.App.Composition;
|
|
using AcDream.App.Streaming;
|
|
using DatReaderWriter.DBObjs;
|
|
|
|
namespace AcDream.App.Tests.Composition;
|
|
|
|
public sealed class SessionPlayerCompositionTests
|
|
{
|
|
[Fact]
|
|
public void RuntimeBindingsReleaseInReverseAndRetryOnlyFailedEdges()
|
|
{
|
|
var calls = new List<string>();
|
|
var bindings = new SessionPlayerRuntimeBindings();
|
|
bindings.Adopt("first", new RetryBinding("first", calls, 0));
|
|
bindings.Adopt("second", new RetryBinding("second", calls, 1));
|
|
|
|
Assert.Throws<AggregateException>(bindings.Dispose);
|
|
Assert.Equal(["second", "first"], calls);
|
|
|
|
bindings.Dispose();
|
|
bindings.Dispose();
|
|
|
|
Assert.Equal(["second", "first", "second"], calls);
|
|
Assert.Throws<ObjectDisposedException>(() =>
|
|
bindings.Adopt("late", new RetryBinding("late", calls, 0)));
|
|
}
|
|
|
|
[Fact]
|
|
public void SpawnClaimClassifierPreservesIndoorBoundaryMemoAndReset()
|
|
{
|
|
int lookups = 0;
|
|
uint lastDid = 0;
|
|
var info = new LandBlockInfo { NumCells = 2 };
|
|
var classifier = new DatSpawnClaimHydrationClassifier(did =>
|
|
{
|
|
lookups++;
|
|
lastDid = did;
|
|
return info;
|
|
});
|
|
|
|
Assert.False(classifier.IsUnhydratable(0x123400FFu));
|
|
Assert.Equal(0, lookups);
|
|
Assert.False(classifier.IsUnhydratable(0x12340100u));
|
|
Assert.Equal(0x1234FFFEu, lastDid);
|
|
Assert.False(classifier.IsUnhydratable(0x12340100u));
|
|
Assert.Equal(1, lookups);
|
|
Assert.False(classifier.IsUnhydratable(0x12340101u));
|
|
Assert.True(classifier.IsUnhydratable(0x12340102u));
|
|
|
|
classifier.Reset();
|
|
Assert.True(classifier.IsUnhydratable(0x12340102u));
|
|
Assert.Equal(4, lookups);
|
|
}
|
|
|
|
[Fact]
|
|
public void MissingOrEmptyLandblockMakesIndoorClaimUnhydratable()
|
|
{
|
|
var missing = new DatSpawnClaimHydrationClassifier(_ => null);
|
|
var empty = new DatSpawnClaimHydrationClassifier(
|
|
_ => new LandBlockInfo { NumCells = 0 });
|
|
|
|
Assert.True(missing.IsUnhydratable(0x12340100u));
|
|
Assert.True(empty.IsUnhydratable(0x12340100u));
|
|
}
|
|
|
|
[Fact]
|
|
public void GameWindowUsesSessionPhaseAndContainsNoPhaseSevenBody()
|
|
{
|
|
string root = FindRepoRoot();
|
|
string window = File.ReadAllText(Path.Combine(
|
|
root,
|
|
"src",
|
|
"AcDream.App",
|
|
"Rendering",
|
|
"GameWindow.cs"));
|
|
string phase = File.ReadAllText(Path.Combine(
|
|
root,
|
|
"src",
|
|
"AcDream.App",
|
|
"Composition",
|
|
"SessionPlayerComposition.cs"));
|
|
|
|
Assert.Contains("new SessionPlayerCompositionPhase(", window);
|
|
Assert.DoesNotContain("LandblockStreamer.CreateForRequests(", window);
|
|
Assert.DoesNotContain("new AcDream.App.Net.LiveSessionController()", window);
|
|
Assert.DoesNotContain(
|
|
"new AcDream.App.Streaming.LocalPlayerTeleportController(",
|
|
window);
|
|
Assert.DoesNotContain("IsSpawnClaimUnhydratable", window);
|
|
Assert.Contains("LandblockStreamer.CreateForRequests(", phase);
|
|
Assert.Contains("new LiveSessionController()", phase);
|
|
Assert.Contains("new LocalPlayerTeleportController(", phase);
|
|
Assert.Contains("new DatSpawnClaimHydrationClassifier(", phase);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProductionPhaseStartsStreamerBeforeSessionAndTransfersPortalLast()
|
|
{
|
|
string phase = File.ReadAllText(Path.Combine(
|
|
FindRepoRoot(),
|
|
"src",
|
|
"AcDream.App",
|
|
"Composition",
|
|
"SessionPlayerComposition.cs"));
|
|
|
|
AssertAppearsInOrder(
|
|
phase,
|
|
"LandblockStreamer.CreateForRequests(",
|
|
"streamerLease.Resource.Start();",
|
|
"new StreamingController(",
|
|
"new WorldRevealCoordinator(",
|
|
"new LiveSessionController()",
|
|
"new LiveEntityHydrationController(",
|
|
"new LiveEntityNetworkUpdateController(",
|
|
"new GameplayInputFrameController(",
|
|
"new PlayerModeController(",
|
|
"d.PortalTunnelFallback.Transfer(",
|
|
"d.TeleportSink.BindOwned(localTeleport)",
|
|
"LiveSessionHost sessionHost = sessionRuntimeFactory.Create(",
|
|
"d.CombatModeCommands.BindOwned(combatCommand)",
|
|
"d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics)",
|
|
"GameplayInputActionRouter.Create(",
|
|
"gameplayActions.Attach();",
|
|
"_publication.PublishSessionPlayer(result);");
|
|
|
|
Assert.DoesNotContain("GameWindow", File.ReadAllText(Path.Combine(
|
|
FindRepoRoot(),
|
|
"src",
|
|
"AcDream.App",
|
|
"Net",
|
|
"LiveSessionRuntimeFactory.cs")), StringComparison.Ordinal);
|
|
}
|
|
|
|
private sealed class RetryBinding(
|
|
string name,
|
|
List<string> calls,
|
|
int failures) : IDisposable
|
|
{
|
|
private int _failures = failures;
|
|
|
|
public void Dispose()
|
|
{
|
|
calls.Add(name);
|
|
if (_failures-- > 0)
|
|
throw new InvalidOperationException("retry");
|
|
}
|
|
}
|
|
|
|
private static void AssertAppearsInOrder(string source, params string[] values)
|
|
{
|
|
int cursor = 0;
|
|
foreach (string value in values)
|
|
{
|
|
int found = source.IndexOf(value, cursor, StringComparison.Ordinal);
|
|
Assert.True(found >= 0, $"Missing expected source fragment: {value}");
|
|
cursor = found + value.Length;
|
|
}
|
|
}
|
|
|
|
private static string FindRepoRoot()
|
|
{
|
|
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.");
|
|
}
|
|
}
|