acdream/tests/AcDream.App.Tests/Runtime/CurrentGameRuntimeAdapterTests.cs
Erik b298f99f91 refactor(runtime): own canonical action state
Move selection, combat, and interaction target mode under one Runtime owner; make plugins, retained UI, session routing, and typed runtime views borrow its exact children; and add failure-safe reset, instance isolation, source ownership, and normalized checkpoint coverage without changing retail ordering.

Co-authored-by: Codex <codex@openai.com>
2026-07-26 10:44:09 +02:00

1192 lines
42 KiB
C#

using System.Net;
using System.Numerics;
using AcDream.App.Combat;
using AcDream.App.Input;
using AcDream.App.Interaction;
using AcDream.App.Net;
using AcDream.App.Runtime;
using AcDream.App.Streaming;
using AcDream.App.UI;
using AcDream.App.Update;
using AcDream.App.World;
using AcDream.Core.Chat;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.Selection;
using AcDream.Core.Social;
using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Session;
using AcDream.UI.Abstractions;
using AcDream.UI.Abstractions.Input;
namespace AcDream.App.Tests.Runtime;
public sealed class CurrentGameRuntimeAdapterTests
{
[Fact]
public void AdapterBorrowsCurrentOwnersAndCommandsExecuteAtPressTime()
{
using var harness = new Harness();
var trace = new RuntimeTraceRecorder();
using IDisposable subscription = harness.Runtime.Subscribe(trace);
RuntimeGenerationToken initial = harness.Runtime.Generation;
RuntimeSessionStartResult start = harness.Runtime.Session.Start(initial);
Assert.Equal(RuntimeSessionStartStatus.Connected, start.Status);
Assert.Equal(new RuntimeGenerationToken(1), start.Generation);
Assert.Equal(Harness.PlayerGuid, harness.Identity.ServerGuid);
Assert.Equal(RuntimeLifecycleState.InWorld, harness.Runtime.Lifecycle.State);
Assert.Same(harness.Actions.View, harness.Runtime.Actions);
LiveEntityRecord liveRecord =
harness.Entities.RegisterAndMaterializeProjection(Spawn(
Harness.TargetGuid,
instance: 3,
cell: 0x12340001u));
Assert.NotNull(liveRecord.WorldEntity);
var item = new ClientObject
{
ObjectId = Harness.TargetGuid,
Name = "Runtime fixture",
StackSize = 4,
Value = 25,
ContainerId = Harness.PlayerGuid,
ContainerSlot = 2,
};
harness.Objects.AddOrUpdate(item);
harness.Chat.OnSystemMessage("runtime parity", 0x1Au);
harness.WorldReveal.Begin(WorldRevealKind.Portal, 0x12340001u);
WorldRevealReadinessSnapshot readiness =
harness.WorldReveal.PrepareAndEvaluate(0x12340001u);
Assert.True(readiness.IsReady);
harness.Clock.Advance(new UpdateFrameInput(0.125));
RuntimeGenerationToken generation = harness.Runtime.Generation;
RuntimeCommandResult selection = harness.Runtime.Selection.Execute(
generation,
RuntimeSelectionCommand.SelectClosestHostile);
RuntimeCommandResult movement =
((IGameRuntimeCommands)harness.Runtime).Movement.Execute(
generation,
RuntimeMovementCommand.ToggleRunLock);
RuntimeCommandResult combat = harness.Runtime.Combat.Execute(
generation,
RuntimeCombatCommand.ToggleMode);
RuntimeCommandResult chat =
((IGameRuntimeCommands)harness.Runtime).Chat.Execute(
generation,
new RuntimeChatCommand(
RuntimeChatChannel.General,
"hello runtime"));
RuntimeCommandResult portal =
((IGameRuntimeCommands)harness.Runtime).Portal.Execute(
generation,
RuntimePortalCommand.RecallLifestone);
Assert.True(selection.Accepted);
Assert.True(movement.Accepted);
Assert.True(combat.Accepted);
Assert.True(chat.Accepted);
Assert.True(portal.Accepted);
Assert.Equal(Harness.TargetGuid, harness.Selection.SelectedObjectId);
Assert.True(harness.MovementInput.AutoRunActive);
Assert.Equal(1, harness.Combat.ToggleCount);
Assert.Contains(
harness.Commands.Published,
static command => command is SendChatCmd
{
Channel: ChatChannelKind.General,
Text: "hello runtime",
});
Assert.Contains(
harness.Commands.Published,
static command => command is ExecuteClientCommandCmd
{
Command: ClientCommandId.LifestoneRecall,
});
RuntimeStateCheckpoint checkpoint = harness.Runtime.CaptureCheckpoint();
Assert.Equal(1, checkpoint.EntityCount);
Assert.Equal(1, checkpoint.InventoryObjectCount);
Assert.Equal(1, checkpoint.ChatCount);
Assert.Equal(1L, checkpoint.ChatRevision);
Assert.Equal(1UL, checkpoint.FrameNumber);
Assert.Equal(
Harness.TargetGuid,
checkpoint.Actions.SelectedObjectId);
Assert.Equal(1, checkpoint.Actions.SelectionRevision);
Assert.Equal(RuntimePortalKind.Portal, checkpoint.Portal.Kind);
Assert.Equal(0x12340001u, checkpoint.Portal.DestinationCell);
Assert.True(checkpoint.Portal.IsReady);
Assert.True(harness.Runtime.Entities.TryGet(
Harness.TargetGuid,
out RuntimeEntitySnapshot entity));
Assert.Equal((ushort)3, entity.Identity.Incarnation);
Assert.True(harness.Runtime.Inventory.TryGet(
Harness.TargetGuid,
out RuntimeInventoryItemSnapshot inventory));
Assert.Equal((ushort)3, inventory.Incarnation);
Assert.Equal(4, inventory.StackSize);
RuntimeTraceKind[] kinds = trace.Entries
.Select(static entry => entry.Kind)
.ToArray();
Assert.Equal(RuntimeTraceKind.Command, kinds[0]);
Assert.Equal(RuntimeTraceKind.Lifecycle, kinds[1]);
Assert.Contains(RuntimeTraceKind.Inventory, kinds);
Assert.Contains(RuntimeTraceKind.Chat, kinds);
Assert.Equal(
[
RuntimeCommandDomain.Session,
RuntimeCommandDomain.Selection,
RuntimeCommandDomain.Movement,
RuntimeCommandDomain.Combat,
RuntimeCommandDomain.Chat,
RuntimeCommandDomain.Portal,
],
trace.Entries
.Where(static entry => entry.Kind == RuntimeTraceKind.Command)
.Select(static entry =>
(RuntimeCommandDomain)(entry.Code >> 16))
.ToArray());
}
[Fact]
public void GenerationGateRejectsStaleCommandsAndStopAcknowledgesTeardown()
{
using var harness = new Harness();
RuntimeSessionStartResult start =
harness.Runtime.Session.Start(harness.Runtime.Generation);
Assert.Equal(RuntimeSessionStartStatus.Connected, start.Status);
RuntimeGenerationToken active = harness.Runtime.Generation;
RuntimeTeardownAcknowledgement stopped =
harness.Runtime.Session.Stop(active);
RuntimeCommandResult stale = harness.Runtime.Combat.Execute(
active,
RuntimeCombatCommand.ToggleMode);
Assert.True(stopped.IsComplete);
Assert.Equal(active, stopped.RetiredGeneration);
Assert.Equal(new RuntimeGenerationToken(2), stopped.CurrentGeneration);
Assert.Equal(RuntimeCommandStatus.StaleGeneration, stale.Status);
Assert.Equal(0, harness.Combat.ToggleCount);
Assert.Equal(RuntimeLifecycleState.Stopped, harness.Runtime.Lifecycle.State);
}
[Fact]
public void DirectAndGraphicalHosts_ProduceIdenticalEntityObjectTrace()
{
WorldSession.EntitySpawn spawn =
Spawn(Harness.TargetGuid, instance: 7, cell: 0x12340001u);
var direct = new RuntimeEntityObjectLifetime();
direct.BindEventContext(static () => default, static () => 0UL);
var directTrace = new EntityObjectTrace();
using IDisposable directSubscription =
direct.Events.Subscribe(directTrace);
RuntimeEntityRegistrationResult directRegistration =
direct.RegisterEntity(spawn);
RuntimeEntityRecord directCanonical =
Assert.IsType<RuntimeEntityRecord>(directRegistration.Canonical);
direct.Objects.AddOrUpdate(Object(spawn));
direct.Objects.MoveItem(
spawn.Guid,
Harness.PlayerGuid,
newSlot: 3);
var objDesc = new ObjDescEvent.Parsed(
spawn.Guid,
new CreateObject.ModelData(
0x04000001u,
Array.Empty<CreateObject.SubPaletteSwap>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.AnimPartChange>()),
spawn.InstanceSequence,
ObjDescSequence: 2);
Assert.True(direct.TryApplyObjDesc(
objDesc,
acknowledgeProjection: null,
out _));
var motion = new WorldSession.EntityMotionUpdate(
spawn.Guid,
new CreateObject.ServerMotionState(0x3D, 0x11),
spawn.InstanceSequence,
MovementSequence: 2,
ServerControlSequence: 2,
IsAutonomous: false);
Assert.True(direct.TryApplyMotion(
motion,
retainPayload: true,
acknowledgeProjection: null,
out _,
out _));
var vector = new VectorUpdate.Parsed(
spawn.Guid,
new Vector3(1f, 2f, 3f),
new Vector3(0f, 0f, 0.25f),
spawn.InstanceSequence,
VectorSequence: 2);
Assert.True(direct.TryApplyVector(
vector,
acknowledgeProjection: null,
out _));
var state = new SetState.Parsed(
spawn.Guid,
(uint)(PhysicsStateFlags.ReportCollisions
| PhysicsStateFlags.Hidden),
spawn.InstanceSequence,
StateSequence: 2);
Assert.True(direct.TryApplyState(
state,
acknowledgeProjection: null,
out _,
out _));
Assert.True(direct.CommitChildNoDraw(
directCanonical,
noDraw: true));
Assert.True(direct.CommitChildNoDraw(
directCanonical,
noDraw: false));
WorldSession.EntityPositionUpdate position =
Position(spawn.Guid, spawn.InstanceSequence);
Assert.True(direct.TryApplyPosition(
position,
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
projectionRequiresTeleportHook: false,
acknowledgeProjection: null,
out _,
out _,
out _));
Assert.True(direct.CommitRebucket(
directCanonical,
0x12360001u,
0x1236FFFFu));
Assert.True(direct.TryApplyPickup(
new PickupEvent.Parsed(
spawn.Guid,
spawn.InstanceSequence,
PositionSequence: 3),
acknowledgeProjection: null,
out _));
Assert.True(direct.TryAcceptDelete(
new DeleteObject.Parsed(spawn.Guid, spawn.InstanceSequence),
isLocalPlayer: false,
removeRetainedObject: true,
out RuntimeEntityDeleteAcceptance directDelete));
direct.CompleteAcceptedDelete(directDelete);
Assert.Null(direct.RetireCanonicalOnly(directCanonical));
using var graphical = new Harness();
var graphicalTrace = new EntityObjectTrace();
using IDisposable graphicalSubscription =
graphical.EntityObjects.Events.Subscribe(graphicalTrace);
_ = graphical.Entities.RegisterAndMaterializeProjection(spawn);
graphical.Objects.AddOrUpdate(Object(spawn));
graphical.Objects.MoveItem(
spawn.Guid,
Harness.PlayerGuid,
newSlot: 3);
Assert.True(graphical.Entities.TryApplyObjDesc(
objDesc,
out _));
Assert.True(graphical.Entities.TryApplyMotion(
motion,
retainPayload: true,
out _,
out _));
Assert.True(graphical.Entities.TryApplyVector(vector, out _));
Assert.True(graphical.Entities.TryApplyState(state, out _, out _));
Assert.True(graphical.Entities.SetAttachedChildNoDraw(
spawn.Guid,
noDraw: true));
Assert.True(graphical.Entities.SetAttachedChildNoDraw(
spawn.Guid,
noDraw: false));
Assert.True(graphical.Entities.TryApplyPosition(
position,
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
out _,
out _,
out _));
Assert.True(graphical.Entities.RebucketLiveEntity(
spawn.Guid,
0x12360001u));
Assert.True(graphical.Entities.TryApplyPickup(
new PickupEvent.Parsed(
spawn.Guid,
spawn.InstanceSequence,
PositionSequence: 3),
out _));
Assert.True(graphical.Entities.UnregisterLiveEntity(
new DeleteObject.Parsed(spawn.Guid, spawn.InstanceSequence),
isLocalPlayer: false,
removeRetainedObject: true));
Assert.Equal(directTrace.Entries, graphicalTrace.Entries);
Assert.Equal(0, direct.Entities.Count);
Assert.Equal(0, direct.Objects.ObjectCount);
Assert.Equal(0, graphical.EntityObjects.Entities.Count);
Assert.Equal(0, graphical.Objects.ObjectCount);
}
[Fact]
public void GraphicalObserverFailure_DoesNotStarveLaterObserverOrOwner()
{
using var harness = new Harness();
var throwing = new ThrowingEntityObserver();
var recording = new RuntimeTraceRecorder();
IDisposable first = harness.Runtime.Subscribe(throwing);
IDisposable second = harness.Runtime.Subscribe(recording);
LiveEntityRecord record =
harness.Entities.RegisterAndMaterializeProjection(Spawn(
Harness.TargetGuid,
instance: 4,
cell: 0x12340001u));
Assert.True(harness.EntityObjects.Entities.IsCurrent(record.Canonical));
Assert.Contains(
recording.Entries,
entry => entry.Kind == RuntimeTraceKind.Entity
&& entry.PrimaryObjectId == Harness.TargetGuid);
Assert.Equal(1, harness.EntityObjects.Events.SubscriberCount);
Assert.Equal(0, harness.EntityObjects.Events.DispatchFailureCount);
first.Dispose();
second.Dispose();
Assert.Equal(0, harness.EntityObjects.Events.SubscriberCount);
}
[Fact]
public void AdapterBorrowsExactJ4ViewsAndCheckpointState()
{
using var harness = new Harness();
harness.Character.Options.Replace(0x04000000u, 0x00948700u);
harness.Character.MovementSkills.Update(200, 175);
harness.Character.Spellbook.OnSpellLearned(42u);
harness.InventoryState.Shortcuts.Load(
[
new ShortcutEntry(1, 0x80000001u, 0u),
]);
harness.Communication.Friends.Apply(new FriendsUpdate(
FriendsUpdateType.Full,
[
new FriendEntry(
0x50000003u,
"Borrowed",
true,
false,
[],
[]),
]));
Assert.Same(harness.Character.View, harness.Runtime.Character);
Assert.Same(
harness.InventoryState.View,
harness.Runtime.InventoryState);
Assert.Same(
harness.Communication.SocialView,
harness.Runtime.Social);
RuntimeStateCheckpoint checkpoint =
harness.Runtime.CaptureCheckpoint();
Assert.Equal(1, checkpoint.Character.LearnedSpellCount);
Assert.True(checkpoint.Character.MovementSkills.IsComplete);
Assert.Equal(1, checkpoint.InventoryState.ShortcutCount);
Assert.Equal(1, checkpoint.Social.FriendCount);
}
[Fact]
public void J4StateCommandsAreGenerationGatedAndReachCurrentTransport()
{
using var harness = new Harness();
var trace = new RuntimeTraceRecorder();
using IDisposable subscription = harness.Runtime.Subscribe(trace);
_ = harness.Runtime.Session.Start(harness.Runtime.Generation);
RuntimeGenerationToken generation = harness.Runtime.Generation;
IGameRuntimeCommands commands = harness.Runtime;
RuntimeCommandResult shortcut = commands.InventoryState.AddShortcut(
generation,
new RuntimeShortcutCommand(2, 0x80000001u, 0u));
RuntimeCommandResult favorite = commands.Spellbook.AddFavorite(
generation,
tabIndex: 0,
position: 0,
spellId: 42u);
RuntimeCommandResult filter = commands.Spellbook.SetFilter(
generation,
0x3FFEu);
RuntimeCommandResult desired = commands.Spellbook.SetDesiredComponent(
generation,
0x68000001u,
10u);
RuntimeCommandResult advancement = commands.Character.Advance(
generation,
new RuntimeAdvancementCommand(
RuntimeAdvancementKind.Skill,
StatId: 6u,
Cost: 500u));
RuntimeCommandResult options = commands.Character.SetOptions1(
generation,
0x50C4A54Au);
RuntimeCommandResult friend = commands.Social.Execute(
generation,
new RuntimeFriendCommand(
RuntimeFriendCommandKind.Add,
Name: "Runtime Friend"));
RuntimeCommandResult squelch = commands.Social.Execute(
generation,
new RuntimeSquelchCommand(
RuntimeSquelchScope.Global,
Add: true,
MessageType: 3u));
Assert.All(
new[]
{
shortcut,
favorite,
filter,
desired,
advancement,
options,
friend,
squelch,
},
static result => Assert.True(result.Accepted));
Assert.Contains(
harness.Commands.Published,
static command => command is AddShortcutRuntimeCmd);
Assert.Contains(
harness.Commands.Published,
static command => command is AddFavoriteRuntimeCmd);
Assert.Contains(
harness.Commands.Published,
static command => command is SetSpellbookFilterRuntimeCmd);
Assert.Contains(
harness.Commands.Published,
static command => command is SetDesiredComponentRuntimeCmd);
Assert.Contains(
harness.Commands.Published,
static command => command is RaiseSkillRuntimeCmd);
Assert.Contains(
harness.Commands.Published,
static command => command is SetCharacterOptionsRuntimeCmd);
Assert.Contains(
harness.Commands.Published,
static command => command is AddFriendRuntimeCmd);
Assert.Contains(
harness.Commands.Published,
static command => command is ModifyGlobalSquelchRuntimeCmd);
Assert.Contains(
trace.Entries,
static entry => entry.Kind == RuntimeTraceKind.Command
&& (entry.Code >> 16)
== (int)RuntimeCommandDomain.InventoryState);
Assert.Contains(
trace.Entries,
static entry => entry.Kind == RuntimeTraceKind.Command
&& (entry.Code >> 16)
== (int)RuntimeCommandDomain.Spellbook);
Assert.Contains(
trace.Entries,
static entry => entry.Kind == RuntimeTraceKind.Command
&& (entry.Code >> 16)
== (int)RuntimeCommandDomain.Character);
Assert.Contains(
trace.Entries,
static entry => entry.Kind == RuntimeTraceKind.Command
&& (entry.Code >> 16)
== (int)RuntimeCommandDomain.Social);
Assert.True(harness.InventoryState.View.TryGetShortcut(
2,
out RuntimeShortcutSnapshot storedShortcut));
Assert.Equal(0x80000001u, storedShortcut.ObjectId);
Assert.True(harness.Character.View.TryGetFavorite(
0,
0,
out uint storedFavorite));
Assert.Equal(42u, storedFavorite);
Assert.Equal(0x3FFEu, harness.Character.Spellbook.SpellbookFilters);
Assert.True(harness.Character.View.TryGetDesiredComponent(
0x68000001u,
out uint storedDesired));
Assert.Equal(10u, storedDesired);
int published = harness.Commands.Published.Count;
RuntimeCommandResult stale = commands.Character.SetOptions1(
new RuntimeGenerationToken(generation.Value - 1),
0u);
Assert.Equal(RuntimeCommandStatus.StaleGeneration, stale.Status);
Assert.Equal(published, harness.Commands.Published.Count);
}
[Fact]
public void GraphicalAndNoWindowJ4CommandsProduceIdenticalCanonicalState()
{
using var directEntities = new RuntimeEntityObjectLifetime();
using var directInventory =
new RuntimeInventoryState(directEntities);
using var directCharacter = new RuntimeCharacterState();
using var harness = new Harness();
_ = harness.Runtime.Session.Start(harness.Runtime.Generation);
RuntimeGenerationToken generation = harness.Runtime.Generation;
IGameRuntimeCommands graphical = harness.Runtime;
var directOutbound = new List<string>();
var shortcut = new ShortcutEntry(2, 0x80000001u, 0u);
Assert.True(directInventory.TryAddShortcut(
shortcut,
() => directOutbound.Add("add-shortcut")));
Assert.True(directCharacter.TryAddFavorite(
0,
0,
42u,
() => directOutbound.Add("add-favorite")));
directCharacter.SetSpellbookFilter(
0x3FFEu,
() => directOutbound.Add("filter"));
Assert.True(directCharacter.TrySetDesiredComponent(
0x68000001u,
10u,
() => directOutbound.Add("desired")));
Assert.True(graphical.InventoryState.AddShortcut(
generation,
new RuntimeShortcutCommand(
shortcut.Index,
shortcut.ObjectId,
shortcut.SpellId)).Accepted);
Assert.True(graphical.Spellbook.AddFavorite(
generation,
0,
0,
42u).Accepted);
Assert.True(graphical.Spellbook.SetFilter(
generation,
0x3FFEu).Accepted);
Assert.True(graphical.Spellbook.SetDesiredComponent(
generation,
0x68000001u,
10u).Accepted);
Assert.Equal(
directInventory.View.Snapshot,
harness.InventoryState.View.Snapshot);
Assert.Equal(
directCharacter.View.Snapshot,
harness.Character.View.Snapshot);
Assert.Equal(
directInventory.Shortcuts.Items,
harness.InventoryState.Shortcuts.Items);
Assert.Equal(
directCharacter.Spellbook.GetFavorites(0),
harness.Character.Spellbook.GetFavorites(0));
Assert.Equal(
["add-shortcut", "add-favorite", "filter", "desired"],
directOutbound);
Assert.True(directInventory.TryRemoveShortcut(
2,
() => directOutbound.Add("remove-shortcut")));
Assert.True(directCharacter.TryRemoveFavorite(
0,
42u,
() => directOutbound.Add("remove-favorite")));
directCharacter.ClearDesiredComponents(
() => directOutbound.Add("clear-desired"));
Assert.True(graphical.InventoryState.RemoveShortcut(
generation,
2).Accepted);
Assert.True(graphical.Spellbook.RemoveFavorite(
generation,
0,
42u).Accepted);
Assert.True(graphical.Spellbook.ClearDesiredComponents(
generation).Accepted);
Assert.Equal(
directInventory.View.Snapshot,
harness.InventoryState.View.Snapshot);
Assert.Equal(
directCharacter.View.Snapshot,
harness.Character.View.Snapshot);
Assert.Empty(harness.InventoryState.Shortcuts.Items);
Assert.Empty(harness.Character.Spellbook.GetFavorites(0));
Assert.Empty(harness.Character.Spellbook.DesiredComponents);
}
private static ClientObject Object(WorldSession.EntitySpawn spawn) => new()
{
ObjectId = spawn.Guid,
Name = spawn.Name ?? string.Empty,
StackSize = 4,
Value = 25,
ContainerId = Harness.PlayerGuid,
ContainerSlot = 2,
};
private static WorldSession.EntityPositionUpdate Position(
uint guid,
ushort instance) =>
new(
guid,
new CreateObject.ServerPosition(
0x12350001u,
30f,
40f,
8f,
1f,
0f,
0f,
0f),
Velocity: new Vector3(1f, 2f, 3f),
PlacementId: null,
IsGrounded: true,
InstanceSequence: instance,
PositionSequence: 2,
TeleportSequence: 0,
ForcePositionSequence: 0);
private sealed class Harness : IDisposable
{
public const uint PlayerGuid = 0x50000002u;
public const uint TargetGuid = 0x70000001u;
private readonly ItemInteractionController _items;
private readonly LiveSessionController _session;
public Harness()
{
Identity = new LocalPlayerIdentityState();
EntityObjects = new RuntimeEntityObjectLifetime();
InventoryState = new RuntimeInventoryState(EntityObjects);
Character = new RuntimeCharacterState();
Entities = new LiveEntityRuntime(
new GpuWorldState(),
new NoopEntityResources(),
EntityObjects);
Objects = EntityObjects.Objects;
Communication = new RuntimeCommunicationState();
Actions = new RuntimeActionState();
MovementInput = new DispatcherMovementInputSource();
GameplayInput = new GameplayInputFrameController(
dispatcher: null,
MovementInput,
mouseLook: null,
new NoopCombatInput());
Combat = new RecordingCombatCommand();
Clock = new UpdateFrameClock();
WorldReveal = new WorldRevealCoordinator(
static (_, _) => true,
static _ => true,
static (_, _) => true,
static () => true,
static (_, _) => { },
static () => { },
static _ => false);
_items = new ItemInteractionController(
Objects,
InventoryState.Transactions,
Actions.Interaction,
() => PlayerGuid,
sendUse: null,
sendUseWithTarget: null,
sendWield: null,
sendDrop: null);
var query = new SelectionQuery(TargetGuid);
var selectionController = new SelectionInteractionController(
Selection,
query,
_items,
new SelectionTransport(() => _session?.IsInWorld == true),
new NoopInteractionMovement());
Options = LiveOptions();
Commands = new RecordingCommandRouting();
Transport = new TestTransport();
_session = new LiveSessionController(
new SessionOperations(Transport));
Host = CreateHost(_session, Commands, Identity);
Runtime = new CurrentGameRuntimeAdapter(
_session,
Host,
Commands,
Identity,
Entities,
EntityObjects,
InventoryState,
Character,
Communication,
Actions,
new LocalPlayerControllerSlot(),
WorldReveal,
Clock,
selectionController,
GameplayInput,
Combat);
}
public RuntimeOptions Options { get; }
public LocalPlayerIdentityState Identity { get; }
public RuntimeEntityObjectLifetime EntityObjects { get; }
public RuntimeInventoryState InventoryState { get; }
public RuntimeCharacterState Character { get; }
public LiveEntityRuntime Entities { get; }
public ClientObjectTable Objects { get; }
public RuntimeCommunicationState Communication { get; }
public ChatLog Chat => Communication.Chat;
public RuntimeActionState Actions { get; }
public SelectionState Selection => Actions.Selection;
public DispatcherMovementInputSource MovementInput { get; }
public GameplayInputFrameController GameplayInput { get; }
public RecordingCombatCommand Combat { get; }
public UpdateFrameClock Clock { get; }
public WorldRevealCoordinator WorldReveal { get; }
public RecordingCommandRouting Commands { get; }
public TestTransport Transport { get; }
public LiveSessionHost Host { get; }
public CurrentGameRuntimeAdapter Runtime { get; }
public void Dispose()
{
Runtime.Dispose();
Character.Dispose();
InventoryState.Dispose();
Communication.Dispose();
_session.Dispose();
_items.Dispose();
Actions.Dispose();
Entities.Clear();
}
}
private static LiveSessionHost CreateHost(
LiveSessionController controller,
RecordingCommandRouting commands,
LocalPlayerIdentityState identity)
{
Action noop = static () => { };
var reset = new LiveSessionResetBindings
{
MouseCapture = noop,
PlayerPresentation = noop,
TeleportTransit = noop,
SessionDialogs = noop,
ChatCommandTargets = noop,
SettingsCharacterContext = noop,
EquippedChildren = noop,
ExternalContainer = noop,
InteractionAndSelection = noop,
InventoryTransactions = noop,
SelectionPresentation = noop,
ObjectTable = noop,
Spellbook = noop,
MagicRuntime = noop,
CombatAttack = noop,
CombatState = noop,
ItemMana = noop,
LocalPlayer = noop,
Friends = noop,
Squelch = noop,
TurbineChat = noop,
ParticleVisibility = noop,
InboundEventFifo = noop,
LiveLiveness = noop,
LiveRuntime = noop,
RenderSceneProjection = noop,
SessionIdentity = noop,
RemoteTeleport = noop,
NetworkEffects = noop,
AnimationHookFrames = noop,
LivePresentation = noop,
RemoteMovementDiagnostics = noop,
};
return new LiveSessionHost(
controller,
new LiveSessionHostBindings(
new LiveSessionRoutingFactories(
_ => new NoopEventRouting(),
_ => commands),
LiveSessionResetManifest.Create(reset).Execute,
new LiveSessionSelectionBindings(
id => identity.ServerGuid = id,
_ => { },
_ => { },
_ => { },
_ => { },
() => { }),
new LiveSessionEnteredWorldBindings(
_ => { },
() => { },
() => { },
_ => { },
() => { }),
(_, _, _) => { },
() => { }),
new LiveSessionConnectOptions(
true,
"127.0.0.1",
9000,
"user",
"password"));
}
private static RuntimeOptions LiveOptions()
{
var environment = new Dictionary<string, string?>
{
["ACDREAM_LIVE"] = "1",
["ACDREAM_TEST_HOST"] = "127.0.0.1",
["ACDREAM_TEST_PORT"] = "9000",
["ACDREAM_TEST_USER"] = "user",
["ACDREAM_TEST_PASS"] = "password",
};
return RuntimeOptions.Parse("dat", environment.GetValueOrDefault);
}
private static WorldSession.EntitySpawn Spawn(
uint guid,
ushort instance,
uint cell)
{
var position = new CreateObject.ServerPosition(
cell,
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: instance);
var physics = new PhysicsSpawnData(
RawState: (uint)PhysicsStateFlags.ReportCollisions,
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: 0x02000001u,
MotionTableId: 0x09000001u,
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,
"runtime fixture",
null,
null,
0x09000001u,
PhysicsState: (uint)PhysicsStateFlags.ReportCollisions,
InstanceSequence: instance,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
private sealed class SessionOperations(TestTransport transport)
: ILiveSessionOperations
{
public IPEndPoint ResolveEndpoint(string host, int port) =>
new(IPAddress.Loopback, port);
public WorldSession CreateSession(IPEndPoint endpoint) =>
new(endpoint, transport);
public void Connect(WorldSession session, string user, string password)
{
}
public CharacterList.Parsed GetCharacters(WorldSession session) =>
new(
0u,
[
new CharacterList.Character(
0x50000001u,
"Unavailable",
30u),
new CharacterList.Character(
Harness.PlayerGuid,
"Runtime",
0u),
],
[],
11,
"Runtime",
true,
true);
public void EnterWorld(WorldSession session, int activeCharacterIndex)
{
}
public void Tick(WorldSession session)
{
}
public void DisposeSession(WorldSession session) => session.Dispose();
}
internal sealed class TestTransport : IWorldSessionTransport
{
public List<byte[]> Sent { get; } = [];
public void Send(ReadOnlySpan<byte> datagram)
{
Sent.Add(datagram.ToArray());
}
public void Send(IPEndPoint remote, ReadOnlySpan<byte> datagram)
{
Sent.Add(datagram.ToArray());
}
public int Receive(
Span<byte> destination,
TimeSpan timeout,
out IPEndPoint? from)
{
from = null;
return -1;
}
public ValueTask<NetReceiveResult> ReceiveAsync(
Memory<byte> destination,
CancellationToken cancellationToken) =>
throw new OperationCanceledException(cancellationToken);
public void Dispose()
{
}
}
private sealed class NoopEventRouting : ILiveSessionEventRouting
{
public void Attach()
{
}
public void Dispose()
{
}
}
internal sealed class RecordingCommandRouting
: ILiveSessionCommandRouting,
ICommandBus
{
private bool _active;
public List<object> Published { get; } = [];
public void Activate() => _active = true;
public void Publish<T>(T command) where T : notnull
{
if (_active)
Published.Add(command);
}
public void Dispose() => _active = false;
}
internal sealed class RecordingCombatCommand : ILiveCombatModeCommand
{
public int ToggleCount { get; private set; }
public void Toggle() => ToggleCount++;
}
private sealed class NoopCombatInput : ICombatInputFrameController
{
public void Tick()
{
}
public void HandleMovementInput(
InputAction action,
ActivationType activation)
{
}
public bool HandleInputAction(
InputAction action,
ActivationType activation) => false;
}
private sealed class EntityObjectTrace : IRuntimeEntityObjectObserver
{
public List<string> Entries { get; } = [];
public void OnEntity(in RuntimeEntityDelta delta) =>
Entries.Add(
$"E:{delta.Stamp.Sequence}:{delta.Change}:"
+ $"{delta.Entity.Identity.ServerGuid:X8}:"
+ $"{delta.Entity.Identity.LocalEntityId}:"
+ $"{delta.Entity.Identity.Incarnation}:"
+ $"{delta.Entity.CellId:X8}:"
+ $"{delta.Entity.PhysicsState:X8}:"
+ $"{delta.Entity.Position?.ObjCellId:X8}:"
+ $"{delta.Entity.Position?.Frame.Origin.X}:"
+ $"{delta.Entity.Position?.Frame.Origin.Y}:"
+ $"{delta.Entity.Position?.Frame.Origin.Z}");
public void OnInventory(in RuntimeInventoryDelta delta) =>
Entries.Add(
$"I:{delta.Stamp.Sequence}:{delta.Change}:"
+ $"{delta.Item.ObjectId:X8}:"
+ $"{delta.Item.Incarnation}:"
+ $"{delta.Item.ContainerId:X8}:"
+ $"{delta.Item.ContainerSlot}:"
+ $"{delta.Item.StackSize}:"
+ $"{delta.Item.Value}");
}
private sealed class ThrowingEntityObserver : IRuntimeEventObserver
{
public void OnLifecycle(in RuntimeLifecycleDelta delta)
{
}
public void OnCommand(in RuntimeCommandDelta delta)
{
}
public void OnEntity(in RuntimeEntityDelta delta) =>
throw new InvalidOperationException("observer");
public void OnInventory(in RuntimeInventoryDelta delta)
{
}
public void OnChat(in RuntimeChatDelta delta)
{
}
public void OnMovement(in RuntimeMovementDelta delta)
{
}
public void OnPortal(in RuntimePortalDelta delta)
{
}
}
private sealed class SelectionQuery(uint target) : IWorldSelectionQuery
{
public uint? PickAtCursor(bool includeSelf) => target;
public uint? PickAt(float mouseX, float mouseY, bool includeSelf) => target;
public void BeginLightingPulse(uint serverGuid)
{
}
public bool TryCaptureIdentity(uint serverGuid, out uint localEntityId)
{
localEntityId = 1u;
return serverGuid == target;
}
public bool IsCurrent(uint serverGuid, uint localEntityId) =>
serverGuid == target && localEntityId == 1u;
public string Describe(uint serverGuid) => "Runtime target";
public bool IsCreature(uint serverGuid) => serverGuid == target;
public bool IsHostileMonster(uint serverGuid) => serverGuid == target;
public ClosestCombatTarget? FindClosestHostileMonster() =>
new(target, DistanceSquared: 4f);
public bool IsUseable(uint serverGuid) => serverGuid == target;
public bool IsPickupable(uint serverGuid) => false;
public bool TryGetApproach(
uint serverGuid,
out InteractionApproach approach)
{
approach = default;
return false;
}
public Vector3? GetCombatCameraTargetPoint(uint serverGuid) => null;
}
private sealed class SelectionTransport(Func<bool> isInWorld)
: ISelectionInteractionTransport
{
public bool IsInWorld => isInWorld();
public bool TrySendUse(uint serverGuid, out uint sequence)
{
sequence = 1u;
return IsInWorld;
}
public bool TrySendPickup(
uint itemGuid,
uint destinationContainerId,
int placement,
out uint sequence)
{
sequence = 1u;
return IsInWorld;
}
}
private sealed class NoopInteractionMovement
: IPlayerInteractionMovementSink
{
public bool BeginApproach(
InteractionApproach approach,
Action<PlayerApproachToken>? armAfterCancel = null) => false;
}
private sealed class NoopEntityResources
: ILiveEntityResourceLifecycle
{
public void Register(WorldEntity entity)
{
}
public void Unregister(WorldEntity entity)
{
}
}
}