refactor(runtime): unify generation reset for direct hosts
Move canonical per-session teardown into one retryable Runtime transaction, reduce App reset to projection acknowledgements, and prove the same GameRuntime graph through deterministic no-window lifecycle, gameplay, portal, fault, reconnect, and isolation gates.\n\nCo-authored-by: Codex <noreply@openai.com>
This commit is contained in:
parent
7818494116
commit
a9a822f206
28 changed files with 2707 additions and 345 deletions
650
tests/AcDream.Runtime.Tests/NoWindowGameRuntimeHostTests.cs
Normal file
650
tests/AcDream.Runtime.Tests/NoWindowGameRuntimeHostTests.cs
Normal file
|
|
@ -0,0 +1,650 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Tests.Support;
|
||||
using AcDream.Runtime.World;
|
||||
|
||||
namespace AcDream.Runtime.Tests;
|
||||
|
||||
public sealed class NoWindowGameRuntimeHostTests
|
||||
{
|
||||
[Fact]
|
||||
public void RootHostRunsPopulatedGenerationWithoutPresentationAssembly()
|
||||
{
|
||||
var host = new NoWindowGameRuntimeHost();
|
||||
|
||||
RuntimeSessionStartResult started = host.Start();
|
||||
Assert.Equal(RuntimeSessionStartStatus.Connected, started.Status);
|
||||
Assert.Equal(0x50000001u, host.Runtime.PlayerIdentity.ServerGuid);
|
||||
Assert.True(host.Deliver(runtime =>
|
||||
{
|
||||
runtime.EntityObjects.RegisterEntity(
|
||||
Spawn(0x70000001u, 1));
|
||||
runtime.EntityObjects.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = 0x80000001u,
|
||||
Name = "fixture",
|
||||
ContainerId = runtime.PlayerIdentity.ServerGuid,
|
||||
});
|
||||
runtime.CommunicationOwner.Chat.OnSystemMessage(
|
||||
"fixture",
|
||||
1u);
|
||||
}));
|
||||
host.Advance(1d / 30d);
|
||||
RuntimeStateCheckpoint populated =
|
||||
host.Runtime.CaptureCheckpoint();
|
||||
Assert.Equal(1, populated.EntityCount);
|
||||
Assert.Equal(1, populated.InventoryObjectCount);
|
||||
Assert.Equal(1, populated.ChatCount);
|
||||
|
||||
RuntimeTeardownAcknowledgement stopped = host.Stop();
|
||||
|
||||
Assert.True(stopped.IsComplete);
|
||||
Assert.Equal(0u, host.Runtime.PlayerIdentity.ServerGuid);
|
||||
Assert.Equal(0, host.Runtime.Entities.Count);
|
||||
Assert.Equal(0, host.Runtime.Inventory.ObjectCount);
|
||||
Assert.False(host.Deliver(_ =>
|
||||
throw new InvalidOperationException("old route leaked")));
|
||||
Assert.False(host.Execute(_ =>
|
||||
throw new InvalidOperationException("old command leaked")));
|
||||
Assert.Equal(1, host.ProjectionRetirementCount);
|
||||
Assert.Equal(2, host.ProjectionDrainCount);
|
||||
Assert.Equal(2, host.ProjectionCompletionCount);
|
||||
|
||||
string[] loaded = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Select(static assembly =>
|
||||
assembly.GetName().Name ?? string.Empty)
|
||||
.ToArray();
|
||||
Assert.DoesNotContain(loaded, static name =>
|
||||
name.StartsWith(
|
||||
"AcDream.App",
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
|| name.StartsWith(
|
||||
"AcDream.UI.",
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
|| name.StartsWith(
|
||||
"Silk.NET",
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
|| name.StartsWith(
|
||||
"OpenAL",
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
|| name.StartsWith(
|
||||
"Arch",
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
|| name.StartsWith(
|
||||
"ImGui",
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
host.Dispose();
|
||||
Assert.True(host.Runtime.CaptureOwnership().IsConverged);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReconnectReusesRootButNotPriorGuidIncarnationOrRoutes()
|
||||
{
|
||||
const uint guid = 0x70000001u;
|
||||
var host = new NoWindowGameRuntimeHost();
|
||||
RuntimeSessionStartResult first = host.Start();
|
||||
RuntimeGenerationToken firstGeneration = first.Generation;
|
||||
Assert.True(host.Deliver(runtime =>
|
||||
runtime.EntityObjects.RegisterEntity(Spawn(guid, 1))));
|
||||
Assert.True(host.Runtime.EntityObjects.Entities.TryGetActive(
|
||||
guid,
|
||||
out RuntimeEntityRecord firstRecord));
|
||||
|
||||
RuntimeSessionStartResult second = host.Reconnect();
|
||||
RuntimeGenerationToken secondGeneration = second.Generation;
|
||||
|
||||
Assert.Equal(RuntimeSessionStartStatus.Connected, second.Status);
|
||||
Assert.NotEqual(firstGeneration, secondGeneration);
|
||||
Assert.False(host.Runtime.EntityObjects.Entities.TryGetActive(
|
||||
guid,
|
||||
out _));
|
||||
Assert.True(host.Deliver(runtime =>
|
||||
runtime.EntityObjects.RegisterEntity(Spawn(guid, 2))));
|
||||
Assert.True(host.Runtime.EntityObjects.Entities.TryGetActive(
|
||||
guid,
|
||||
out RuntimeEntityRecord secondRecord));
|
||||
Assert.NotSame(firstRecord, secondRecord);
|
||||
Assert.Equal((ushort)2, secondRecord.Incarnation);
|
||||
Assert.Equal(0, host.Runtime.EntityObjects.Entities
|
||||
.PendingTeardownCount);
|
||||
Assert.Contains(
|
||||
$"events-:{firstGeneration.Value}",
|
||||
host.LifecycleTrace);
|
||||
Assert.Contains(
|
||||
$"commands-:{firstGeneration.Value}",
|
||||
host.LifecycleTrace);
|
||||
|
||||
host.Dispose();
|
||||
Assert.True(host.Runtime.CaptureOwnership().IsConverged);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeterministicLifecycleUsesOneRootAcrossGameplayPortalAndReconnect()
|
||||
{
|
||||
const uint localGuid = 0x50000001u;
|
||||
const uint reusedGuid = 0x70000001u;
|
||||
const uint usableGuid = 0x70000002u;
|
||||
const uint containerGuid = 0x80000001u;
|
||||
const uint itemGuid = 0x80000002u;
|
||||
const string passwordMarker = "credential-must-not-appear";
|
||||
var host = new NoWindowGameRuntimeHost(
|
||||
password: passwordMarker);
|
||||
|
||||
RuntimeStateCheckpoint empty = host.CaptureCheckpoint();
|
||||
Assert.Equal(RuntimeLifecycleState.Constructed, empty.Lifecycle);
|
||||
Assert.Equal(0, empty.EntityCount);
|
||||
Assert.Equal(0, empty.InventoryObjectCount);
|
||||
Assert.Equal(0, empty.ChatCount);
|
||||
|
||||
RuntimeSessionStartResult first = host.Start();
|
||||
RuntimeGenerationToken firstGeneration = first.Generation;
|
||||
Assert.Equal(RuntimeSessionStartStatus.Connected, first.Status);
|
||||
Assert.True(host.Deliver(firstGeneration, runtime =>
|
||||
{
|
||||
runtime.EntityObjects.RegisterEntity(Spawn(localGuid, 1));
|
||||
runtime.EntityObjects.RegisterEntity(Spawn(reusedGuid, 1));
|
||||
runtime.EntityObjects.RegisterEntity(Spawn(usableGuid, 1));
|
||||
runtime.EntityObjects.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = containerGuid,
|
||||
Name = "fixture pack",
|
||||
ContainerId = localGuid,
|
||||
});
|
||||
runtime.EntityObjects.Objects.AddContainer(new Container
|
||||
{
|
||||
ObjectId = containerGuid,
|
||||
Capacity = 24,
|
||||
});
|
||||
runtime.EntityObjects.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = itemGuid,
|
||||
Name = "fixture item",
|
||||
ContainerId = localGuid,
|
||||
StackSize = 3,
|
||||
StackSizeMax = 10,
|
||||
});
|
||||
Assert.True(runtime.EntityObjects.Objects.ApplyServerMove(
|
||||
itemGuid,
|
||||
containerGuid,
|
||||
newWielderId: 0u,
|
||||
newSlot: 0));
|
||||
runtime.CommunicationOwner.Chat.OnSystemMessage(
|
||||
"fixture system",
|
||||
1u);
|
||||
runtime.CommunicationOwner.Chat.OnLocalSpeech(
|
||||
"Runtime",
|
||||
"fixture speech",
|
||||
localGuid,
|
||||
isRanged: false);
|
||||
runtime.CharacterOwner.LocalPlayer.OnVitalUpdate(
|
||||
vitalId: 1u,
|
||||
ranks: 100u,
|
||||
start: 100u,
|
||||
xp: 50u,
|
||||
current: 95u);
|
||||
runtime.EnvironmentOwner.Initialize(
|
||||
new RuntimeWorldEnvironmentDefinition(
|
||||
originOffsetTicks: 0d,
|
||||
sourceTickSize: 1d,
|
||||
lightTickSize: 1d,
|
||||
dayGroups: null));
|
||||
runtime.EnvironmentOwner.SynchronizeFromServer(1234d);
|
||||
}));
|
||||
|
||||
Assert.True(host.Move());
|
||||
Assert.True(host.Use(usableGuid));
|
||||
Assert.True(host.Attack());
|
||||
Assert.True(host.CastFixtureSpell());
|
||||
Assert.True(host.AdvanceProjectile());
|
||||
Assert.True(host.CompletePortal());
|
||||
host.Advance(1d / 30d);
|
||||
|
||||
RuntimeStateCheckpoint populated = host.CaptureCheckpoint();
|
||||
Assert.Equal(RuntimeLifecycleState.InWorld, populated.Lifecycle);
|
||||
Assert.Equal(4, populated.EntityCount);
|
||||
Assert.Equal(2, populated.InventoryObjectCount);
|
||||
Assert.Equal(1, populated.InventoryContainerCount);
|
||||
Assert.Equal(2, populated.ChatCount);
|
||||
Assert.Equal(1, populated.Character.LearnedSpellCount);
|
||||
Assert.True(populated.Movement.AutoRunActive);
|
||||
Assert.True(populated.Portal.Completed);
|
||||
Assert.True(populated.Portal.WorldViewportObserved);
|
||||
Assert.Equal(0, populated.TransitOwnership.HostProjectionCount);
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
"movement:run",
|
||||
$"use:{usableGuid:X8}",
|
||||
"attack:prepare",
|
||||
"attack:Medium:0.5",
|
||||
"cast:stop",
|
||||
"cast:1",
|
||||
"cast:busy",
|
||||
"projectile:ack",
|
||||
],
|
||||
host.GameplayTrace);
|
||||
RuntimeTraceEntry[] firstTrace = host.Trace.Entries
|
||||
.Where(entry =>
|
||||
entry.Stamp.Generation == firstGeneration)
|
||||
.ToArray();
|
||||
Assert.Contains(
|
||||
firstTrace,
|
||||
static entry => entry.Kind == RuntimeTraceKind.Lifecycle);
|
||||
Assert.Contains(
|
||||
firstTrace,
|
||||
static entry => entry.Kind == RuntimeTraceKind.Inventory
|
||||
&& entry.Code == (int)RuntimeInventoryChange.Moved);
|
||||
Assert.Contains(
|
||||
firstTrace,
|
||||
static entry => entry.Kind == RuntimeTraceKind.Movement);
|
||||
Assert.Contains(
|
||||
firstTrace,
|
||||
static entry => entry.Kind == RuntimeTraceKind.Command
|
||||
&& entry.Code >> 16
|
||||
== (int)RuntimeCommandDomain.Magic);
|
||||
Assert.Equal(
|
||||
5,
|
||||
firstTrace.Count(static entry =>
|
||||
entry.Kind == RuntimeTraceKind.Portal));
|
||||
AssertStrictlyOrdered(firstTrace);
|
||||
|
||||
Assert.True(host.Runtime.EntityObjects.Entities.TryGetActive(
|
||||
reusedGuid,
|
||||
out RuntimeEntityRecord firstRecord));
|
||||
RuntimeSessionStartResult second = host.Reconnect();
|
||||
RuntimeGenerationToken secondGeneration = second.Generation;
|
||||
Assert.Equal(RuntimeSessionStartStatus.Connected, second.Status);
|
||||
Assert.NotEqual(firstGeneration, secondGeneration);
|
||||
Assert.False(host.Deliver(
|
||||
firstGeneration,
|
||||
_ => throw new InvalidOperationException(
|
||||
"retired inbound route ran")));
|
||||
Assert.False(host.Execute(
|
||||
firstGeneration,
|
||||
_ => throw new InvalidOperationException(
|
||||
"retired command route ran")));
|
||||
|
||||
RuntimeStateCheckpoint reset = host.Runtime.CaptureCheckpoint();
|
||||
Assert.Equal(0, reset.EntityCount);
|
||||
Assert.Equal(0, reset.InventoryObjectCount);
|
||||
// Retail's end-character path resets active communication targets and
|
||||
// identity but does not issue ClearChatBuffer; bounded chat history is
|
||||
// therefore retained by this reusable root.
|
||||
Assert.Equal(2, reset.ChatCount);
|
||||
Assert.Equal(0, reset.Character.LearnedSpellCount);
|
||||
Assert.Equal(0u, reset.Actions.SelectedObjectId);
|
||||
Assert.Equal(RuntimePortalSnapshot.Idle, reset.Portal);
|
||||
Assert.False(reset.Movement.AutoRunActive);
|
||||
Assert.True(reset.EnvironmentOwnership.IsInitialized);
|
||||
|
||||
Assert.True(host.Deliver(secondGeneration, runtime =>
|
||||
runtime.EntityObjects.RegisterEntity(
|
||||
Spawn(reusedGuid, 2))));
|
||||
Assert.True(host.Runtime.EntityObjects.Entities.TryGetActive(
|
||||
reusedGuid,
|
||||
out RuntimeEntityRecord secondRecord));
|
||||
Assert.NotSame(firstRecord, secondRecord);
|
||||
Assert.Equal((ushort)2, secondRecord.Incarnation);
|
||||
RuntimeStateCheckpoint secondCheckpoint =
|
||||
host.CaptureCheckpoint();
|
||||
Assert.Equal(1, secondCheckpoint.EntityCount);
|
||||
Assert.Equal(0, secondCheckpoint.InventoryObjectCount);
|
||||
Assert.Equal(2, secondCheckpoint.ChatCount);
|
||||
|
||||
Assert.True(host.Stop().IsComplete);
|
||||
AssertStrictlyOrdered(
|
||||
host.Trace.Entries
|
||||
.Where(entry =>
|
||||
entry.Stamp.Generation == secondGeneration)
|
||||
.ToArray());
|
||||
Assert.DoesNotContain(
|
||||
host.LifecycleTrace.Concat(host.GameplayTrace),
|
||||
line => line.Contains(
|
||||
passwordMarker,
|
||||
StringComparison.Ordinal));
|
||||
|
||||
host.Dispose();
|
||||
Assert.True(host.Runtime.CaptureOwnership().IsConverged);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void PendingPortalCancellationRetiresHostAcknowledgementsBeforeReconnect(
|
||||
bool destinationReady)
|
||||
{
|
||||
var host = new NoWindowGameRuntimeHost();
|
||||
Assert.Equal(
|
||||
RuntimeSessionStartStatus.Connected,
|
||||
host.Start().Status);
|
||||
RuntimeWorldTransitState transit = host.Runtime.TransitOwner;
|
||||
const uint destinationCell = 0x8A020164u;
|
||||
const ushort sequence = 7;
|
||||
|
||||
Assert.True(transit.TryQueueTeleportStart(sequence));
|
||||
Assert.True(transit.ActivateQueuedTeleport());
|
||||
Assert.True(transit.OfferTeleportDestination(
|
||||
new RuntimeTeleportDestination(
|
||||
host.Runtime.PlayerIdentity.ServerGuid,
|
||||
InstanceSequence: 1,
|
||||
PositionSequence: 1,
|
||||
TeleportSequence: sequence,
|
||||
ForcePositionSequence: 1,
|
||||
Position: new Position(
|
||||
destinationCell,
|
||||
Vector3.Zero,
|
||||
Quaternion.Identity)),
|
||||
teleportTimestampAdvanced: true));
|
||||
Assert.True(transit.TryBeginPortalReveal(
|
||||
sequence,
|
||||
destinationCell,
|
||||
out long revealGeneration));
|
||||
Assert.True(transit.TryRegisterHostProjection(
|
||||
revealGeneration,
|
||||
destinationCell,
|
||||
out RuntimeWorldHostProjectionToken projection));
|
||||
Assert.True(transit.AcknowledgeHostProjection(
|
||||
new RuntimeWorldHostAcknowledgement(
|
||||
projection,
|
||||
RuntimeWorldHostAcknowledgementStage
|
||||
.ProjectionRegistered)));
|
||||
if (destinationReady)
|
||||
{
|
||||
Assert.True(transit.AcknowledgeDestinationReadiness(
|
||||
new RuntimeDestinationReadiness(
|
||||
revealGeneration,
|
||||
destinationCell,
|
||||
IsIndoor: true,
|
||||
IsUnhydratable: false,
|
||||
RequiredRenderRadius: 0,
|
||||
IsRenderNeighborhoodReady: true,
|
||||
AreCompositeTexturesReady: true,
|
||||
IsCollisionReady: true)));
|
||||
}
|
||||
|
||||
Assert.True(transit.Cancel(revealGeneration));
|
||||
DrainPendingPortal(transit, projection);
|
||||
Assert.Equal(0, transit.Ownership.HostProjectionCount);
|
||||
|
||||
RuntimeSessionStartResult reconnected = host.Reconnect();
|
||||
|
||||
Assert.Equal(
|
||||
RuntimeSessionStartStatus.Connected,
|
||||
reconnected.Status);
|
||||
Assert.Equal(RuntimePortalSnapshot.Idle, transit.Snapshot);
|
||||
host.Dispose();
|
||||
Assert.True(host.Runtime.CaptureOwnership().IsConverged);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConcurrentRootsKeepCredentialsClockStateAndFailuresIsolated()
|
||||
{
|
||||
const string firstPassword = "first-password-marker";
|
||||
const string secondPassword = "second-password-marker";
|
||||
const uint sharedGuid = 0x70000055u;
|
||||
var first = new NoWindowGameRuntimeHost(
|
||||
user: "first-user",
|
||||
password: firstPassword,
|
||||
characterId: 0x50000011u,
|
||||
characterName: "First");
|
||||
var second = new NoWindowGameRuntimeHost(
|
||||
user: "second-user",
|
||||
password: secondPassword,
|
||||
characterId: 0x50000022u,
|
||||
characterName: "Second");
|
||||
|
||||
Assert.Equal(
|
||||
RuntimeSessionStartStatus.Connected,
|
||||
first.Start().Status);
|
||||
Assert.Equal(
|
||||
RuntimeSessionStartStatus.Connected,
|
||||
second.Start().Status);
|
||||
Assert.True(first.Deliver(runtime =>
|
||||
{
|
||||
runtime.EntityObjects.RegisterEntity(Spawn(sharedGuid, 1));
|
||||
runtime.EntityObjects.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = 0x80000011u,
|
||||
Name = "first item",
|
||||
ContainerId = runtime.PlayerIdentity.ServerGuid,
|
||||
});
|
||||
runtime.CommunicationOwner.Chat.OnSystemMessage(
|
||||
"first chat",
|
||||
1u);
|
||||
}));
|
||||
Assert.True(second.Deliver(runtime =>
|
||||
{
|
||||
runtime.EntityObjects.RegisterEntity(Spawn(sharedGuid, 1));
|
||||
runtime.EntityObjects.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = 0x80000022u,
|
||||
Name = "second item",
|
||||
ContainerId = runtime.PlayerIdentity.ServerGuid,
|
||||
});
|
||||
runtime.CommunicationOwner.Chat.OnSystemMessage(
|
||||
"second chat",
|
||||
1u);
|
||||
}));
|
||||
Assert.True(first.CastFixtureSpell());
|
||||
Assert.True(second.CastFixtureSpell());
|
||||
Assert.True(first.AdvanceProjectile(0x70000011u));
|
||||
Assert.True(second.AdvanceProjectile(0x70000022u));
|
||||
Assert.True(first.CompletePortal(0x8A020164u, sequence: 1));
|
||||
Assert.True(second.CompletePortal(0x11340021u, sequence: 2));
|
||||
first.Advance(0.1d);
|
||||
first.Advance(0.2d);
|
||||
second.Advance(0.4d);
|
||||
|
||||
Assert.True(first.Runtime.EntityObjects.Entities.TryGetActive(
|
||||
sharedGuid,
|
||||
out RuntimeEntityRecord firstRecord));
|
||||
Assert.True(second.Runtime.EntityObjects.Entities.TryGetActive(
|
||||
sharedGuid,
|
||||
out RuntimeEntityRecord secondRecord));
|
||||
Assert.NotSame(firstRecord, secondRecord);
|
||||
Assert.Null(first.Runtime.EntityObjects.Objects.Get(0x80000022u));
|
||||
Assert.Null(second.Runtime.EntityObjects.Objects.Get(0x80000011u));
|
||||
Assert.Equal(
|
||||
"first chat",
|
||||
Assert.Single(
|
||||
first.Runtime.CommunicationOwner.Chat.Snapshot()).Text);
|
||||
Assert.Equal(
|
||||
"second chat",
|
||||
Assert.Single(
|
||||
second.Runtime.CommunicationOwner.Chat.Snapshot()).Text);
|
||||
Assert.Equal(2UL, first.Runtime.Clock.FrameNumber);
|
||||
Assert.Equal(1UL, second.Runtime.Clock.FrameNumber);
|
||||
Assert.NotEqual(
|
||||
first.Runtime.Clock.SimulationTimeSeconds,
|
||||
second.Runtime.Clock.SimulationTimeSeconds);
|
||||
Assert.Equal(
|
||||
0x8A020164u,
|
||||
first.Runtime.TransitOwner.Snapshot.DestinationCell);
|
||||
Assert.Equal(
|
||||
0x11340021u,
|
||||
second.Runtime.TransitOwner.Snapshot.DestinationCell);
|
||||
Assert.DoesNotContain(
|
||||
first.LifecycleTrace.Concat(first.GameplayTrace),
|
||||
line => line.Contains(
|
||||
firstPassword,
|
||||
StringComparison.Ordinal)
|
||||
|| line.Contains(
|
||||
secondPassword,
|
||||
StringComparison.Ordinal));
|
||||
Assert.DoesNotContain(
|
||||
second.LifecycleTrace.Concat(second.GameplayTrace),
|
||||
line => line.Contains(
|
||||
firstPassword,
|
||||
StringComparison.Ordinal)
|
||||
|| line.Contains(
|
||||
secondPassword,
|
||||
StringComparison.Ordinal));
|
||||
|
||||
first.Dispose();
|
||||
second.Dispose();
|
||||
Assert.True(first.Runtime.CaptureOwnership().IsConverged);
|
||||
Assert.True(second.Runtime.CaptureOwnership().IsConverged);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThrowingRootObserverIsContainedAndDoesNotBlockTeardown()
|
||||
{
|
||||
var host = new NoWindowGameRuntimeHost();
|
||||
using IDisposable failure =
|
||||
host.Runtime.Subscribe(new ThrowingObserver());
|
||||
Assert.Equal(
|
||||
RuntimeSessionStartStatus.Connected,
|
||||
host.Start().Status);
|
||||
|
||||
Assert.True(host.Deliver(runtime =>
|
||||
{
|
||||
runtime.EntityObjects.RegisterEntity(
|
||||
Spawn(0x70000077u, 1));
|
||||
runtime.CommunicationOwner.Chat.OnSystemMessage(
|
||||
"observer fixture",
|
||||
1u);
|
||||
}));
|
||||
Assert.True(host.Move());
|
||||
Assert.True(host.CompletePortal());
|
||||
Assert.True(
|
||||
host.Runtime.CaptureOwnership()
|
||||
.Events.DispatchFailureCount >= 4);
|
||||
|
||||
Assert.True(host.Stop().IsComplete);
|
||||
failure.Dispose();
|
||||
host.Dispose();
|
||||
Assert.True(host.Runtime.CaptureOwnership().IsConverged);
|
||||
}
|
||||
|
||||
private static void AssertStrictlyOrdered(
|
||||
IReadOnlyList<RuntimeTraceEntry> entries)
|
||||
{
|
||||
for (int index = 1; index < entries.Count; index++)
|
||||
{
|
||||
Assert.True(
|
||||
entries[index].Stamp.Sequence
|
||||
> entries[index - 1].Stamp.Sequence,
|
||||
$"trace sequence {entries[index].Stamp.Sequence} "
|
||||
+ $"did not follow {entries[index - 1].Stamp.Sequence}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrainPendingPortal(
|
||||
RuntimeWorldTransitState transit,
|
||||
RuntimeWorldHostProjectionToken projection)
|
||||
{
|
||||
while (transit.TryGetHostProjection(
|
||||
projection,
|
||||
out RuntimeWorldHostProjectionSnapshot pending))
|
||||
{
|
||||
RuntimeWorldHostAcknowledgementStage stages =
|
||||
pending.PendingAcknowledgements;
|
||||
RuntimeWorldHostAcknowledgementStage stage =
|
||||
(stages & RuntimeWorldHostAcknowledgementStage
|
||||
.SimulationReleaseProjected) != 0
|
||||
? RuntimeWorldHostAcknowledgementStage
|
||||
.SimulationReleaseProjected
|
||||
: (stages & RuntimeWorldHostAcknowledgementStage
|
||||
.DestinationReservationReleased) != 0
|
||||
? RuntimeWorldHostAcknowledgementStage
|
||||
.DestinationReservationReleased
|
||||
: RuntimeWorldHostAcknowledgementStage
|
||||
.TerminalProjected;
|
||||
Assert.NotEqual(
|
||||
RuntimeWorldHostAcknowledgementStage.None,
|
||||
stages);
|
||||
Assert.True(transit.AcknowledgeHostProjection(
|
||||
new RuntimeWorldHostAcknowledgement(
|
||||
projection,
|
||||
stage)));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ThrowingObserver : IRuntimeEventObserver
|
||||
{
|
||||
public void OnLifecycle(in RuntimeLifecycleDelta delta) =>
|
||||
throw new InvalidOperationException("observer");
|
||||
public void OnCommand(in RuntimeCommandDelta delta) =>
|
||||
throw new InvalidOperationException("observer");
|
||||
public void OnEntity(in RuntimeEntityDelta delta) =>
|
||||
throw new InvalidOperationException("observer");
|
||||
public void OnInventory(in RuntimeInventoryDelta delta) =>
|
||||
throw new InvalidOperationException("observer");
|
||||
public void OnChat(in RuntimeChatDelta delta) =>
|
||||
throw new InvalidOperationException("observer");
|
||||
public void OnMovement(in RuntimeMovementDelta delta) =>
|
||||
throw new InvalidOperationException("observer");
|
||||
public void OnPortal(in RuntimePortalDelta delta) =>
|
||||
throw new InvalidOperationException("observer");
|
||||
}
|
||||
|
||||
private static WorldSession.EntitySpawn Spawn(
|
||||
uint guid,
|
||||
ushort incarnation)
|
||||
{
|
||||
var position = new CreateObject.ServerPosition(
|
||||
0x01010001u,
|
||||
10f,
|
||||
10f,
|
||||
5f,
|
||||
1f,
|
||||
0f,
|
||||
0f,
|
||||
0f);
|
||||
var timestamps = new PhysicsTimestamps(
|
||||
Position: 1,
|
||||
Movement: 1,
|
||||
State: 1,
|
||||
Vector: 1,
|
||||
Teleport: 0,
|
||||
ServerControlledMove: 1,
|
||||
ForcePosition: 0,
|
||||
ObjDesc: 1,
|
||||
Instance: incarnation);
|
||||
var physics = new PhysicsSpawnData(
|
||||
RawState: (uint)PhysicsStateFlags.ReportCollisions,
|
||||
Position: position,
|
||||
Movement: null,
|
||||
AnimationFrame: null,
|
||||
SetupTableId: 0x02000001u,
|
||||
MotionTableId: null,
|
||||
SoundTableId: null,
|
||||
PhysicsScriptTableId: null,
|
||||
Parent: null,
|
||||
Children: null,
|
||||
Scale: null,
|
||||
Friction: null,
|
||||
Elasticity: null,
|
||||
Translucency: null,
|
||||
Velocity: null,
|
||||
Acceleration: null,
|
||||
AngularVelocity: null,
|
||||
DefaultScriptType: null,
|
||||
DefaultScriptIntensity: null,
|
||||
Timestamps: timestamps);
|
||||
return new WorldSession.EntitySpawn(
|
||||
guid,
|
||||
position,
|
||||
0x02000001u,
|
||||
Array.Empty<CreateObject.AnimPartChange>(),
|
||||
Array.Empty<CreateObject.TextureChange>(),
|
||||
Array.Empty<CreateObject.SubPaletteSwap>(),
|
||||
null,
|
||||
null,
|
||||
guid.ToString("X8"),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
PhysicsState: physics.RawState,
|
||||
InstanceSequence: incarnation,
|
||||
MovementSequence: 1,
|
||||
ServerControlSequence: 1,
|
||||
PositionSequence: 1,
|
||||
Physics: physics);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue