acdream/tests/AcDream.App.Tests/Composition/SessionPlayerCompositionTests.cs

251 lines
9.8 KiB
C#

using System.Reflection;
using AcDream.App.Combat;
using AcDream.App.Composition;
using AcDream.App.Diagnostics;
using AcDream.App.Input;
using AcDream.App.Net;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Streaming;
using AcDream.App.Tests.Architecture;
using AcDream.App.UI;
using AcDream.App.World;
using AcDream.Core.Net.Messages;
using AcDream.Runtime.Session;
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()
{
IReadOnlyList<CompiledCall> windowCalls =
CompiledCallGraph.ReadDeclared(typeof(GameWindow));
Assert.Single(
windowCalls,
call => call.Target.DeclaringType
== typeof(SessionPlayerCompositionPhase)
&& call.Target.IsConstructor);
Assert.DoesNotContain(
windowCalls,
call => call.Target.DeclaringType == typeof(LandblockStreamer)
&& call.Target.Name == nameof(LandblockStreamer.CreateForRequests));
Assert.DoesNotContain(
windowCalls,
call => call.Target.IsConstructor
&& call.Target.DeclaringType is { } type
&& (type == typeof(LiveSessionController)
|| type == typeof(LocalPlayerTeleportController)
|| type == typeof(DatSpawnClaimHydrationClassifier)));
IReadOnlyList<FieldInfo> phaseFields =
typeof(SessionPlayerCompositionPhase).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.DoesNotContain(
phaseFields,
field => field.FieldType == typeof(GameWindow));
}
[Fact]
public void ProductionPhaseStartsStreamerBeforeSessionAndTransfersPortalLast()
{
MethodInfo composeCore = RequiredMethod(
typeof(SessionPlayerCompositionPhase),
"ComposeCore");
IReadOnlyList<CompiledCall> composeCalls =
CompiledCallGraph.Read(composeCore);
AssertCallOrder(
composeCalls,
(typeof(LandblockStreamer), nameof(LandblockStreamer.Start)),
(typeof(StreamingController), ".ctor"),
(typeof(WorldRevealCoordinator), ".ctor"),
(typeof(SessionPlayerCompositionPhase), "CompleteSessionPlayer"));
IEnumerable<MethodBase> streamerFactories =
CompiledCallGraph.ReadMethodReferences(composeCore)
.Select(call => call.Target)
.Where(method => method.GetMethodBody() is not null);
MethodBase streamerFactory = Assert.Single(
streamerFactories,
method => CompiledCallGraph.Read(method).Any(call =>
call.Target.DeclaringType == typeof(LandblockStreamer)
&& call.Target.Name
== nameof(LandblockStreamer.CreateForRequests)));
Assert.Contains(
CompiledCallGraph.Read(streamerFactory),
call => call.Target.DeclaringType == typeof(LandblockStreamer)
&& call.Target.Name == nameof(LandblockStreamer.CreateForRequests));
MethodInfo complete = RequiredMethod(
typeof(SessionPlayerCompositionPhase),
"CompleteSessionPlayer");
IReadOnlyList<CompiledCall> completeCalls =
CompiledCallGraph.Read(complete);
AssertCallOrder(
completeCalls,
(typeof(LiveEntityHydrationController), ".ctor"),
(typeof(LiveEntityNetworkUpdateController), ".ctor"),
(typeof(GameplayInputFrameController), ".ctor"),
(typeof(PlayerModeController), ".ctor"),
(typeof(TransferableResourceSlot<>), "Transfer"),
(typeof(DeferredLocalPlayerTeleportNetworkSink), "BindOwned"),
(typeof(LiveSessionRuntimeFactory), nameof(LiveSessionRuntimeFactory.Create)),
(typeof(LiveCombatModeCommandSlot), "BindOwned"),
(typeof(RuntimeDiagnosticCommandSlot), "BindOwned"),
(typeof(GameplayInputActionRouter), nameof(GameplayInputActionRouter.Create)),
(typeof(GameplayInputActionRouter), nameof(GameplayInputActionRouter.Attach)),
(typeof(IGameWindowSessionPlayerPublication), "PublishSessionPlayer"));
Assert.DoesNotContain(
typeof(LiveSessionRuntimeFactory).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic),
field => field.FieldType == typeof(GameWindow));
}
[Fact]
public void GraphicalCompositionPausesOnlyWhenCharacterSelectorIsAbsent()
{
MethodInfo complete = RequiredMethod(
typeof(SessionPlayerCompositionPhase),
"CompleteSessionPlayer");
IReadOnlyList<CompiledCall> sessionCalls =
CompiledCallGraph.Read(complete);
Assert.Contains(
sessionCalls,
call => call.Target.DeclaringType == typeof(RuntimeOptions)
&& call.Target.Name == "get_LiveCharacterSelector");
Assert.Contains(
sessionCalls,
call => call.Target.DeclaringType == typeof(LiveSessionConnectOptions)
&& call.Target.IsConstructor);
Assert.DoesNotContain(
sessionCalls,
call => call.Target.DeclaringType == typeof(CharacterList)
&& call.Target.Name == nameof(CharacterList.TrySelectFirstAvailable));
MethodInfo retainedUi = typeof(RetailInteractionRetainedUiCompositionFactory)
.GetMethod(nameof(
RetailInteractionRetainedUiCompositionFactory.CreateRetainedUi))!;
IReadOnlyList<CompiledCall> uiCalls = CompiledCallGraph.Read(retainedUi);
Assert.Contains(
uiCalls,
call => call.Target.DeclaringType == typeof(RuntimeOptions)
&& call.Target.Name == "get_LiveCharacterSelector");
Assert.Contains(
uiCalls,
call => call.Target.DeclaringType
== typeof(CharacterSelectionRuntimeBindings)
&& call.Target.IsConstructor);
}
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 MethodInfo RequiredMethod(Type type, string name) =>
type.GetMethod(
name,
BindingFlags.Instance | BindingFlags.Static
| BindingFlags.Public | BindingFlags.NonPublic)
?? throw new MissingMethodException(type.FullName, name);
private static void AssertCallOrder(
IReadOnlyList<CompiledCall> calls,
params (Type DeclaringType, string MethodName)[] expected)
{
int cursor = -1;
foreach ((Type declaringType, string methodName) in expected)
{
int found = calls
.Select((call, index) => (call, index))
.FirstOrDefault(
pair => pair.index > cursor
&& MatchesType(pair.call.Target.DeclaringType, declaringType)
&& pair.call.Target.Name == methodName,
defaultValue: (default, -1))
.index;
Assert.True(
found > cursor,
$"Missing compiled edge after index {cursor}: "
+ $"{declaringType.FullName}.{methodName}");
cursor = found;
}
}
private static bool MatchesType(Type? actual, Type expected) =>
actual == expected
|| expected.IsGenericTypeDefinition
&& actual?.IsGenericType == true
&& actual.GetGenericTypeDefinition() == expected;
}